Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

grpc-js: support adding/removing services on started server #1614

Merged
merged 3 commits into from
Oct 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 20 additions & 5 deletions packages/grpc-js/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,6 @@ export class Server {
service: ServiceDefinition,
implementation: UntypedServiceImplementation
): void {
if (this.started === true) {
throw new Error("Can't add a service to a started server.");
}

if (
service === null ||
typeof service !== 'object' ||
Expand Down Expand Up @@ -212,6 +208,21 @@ export class Server {
});
}

removeService(service: ServiceDefinition): void {
if (
service === null ||
typeof service !== 'object'
) {
throw new Error('removeService() requires object as argument');
}

const serviceKeys = Object.keys(service);
serviceKeys.forEach((name) => {
const attrs = service[name];
this.unregister(attrs.path);
});
}

bind(port: string, creds: ServerCredentials): void {
throw new Error('Not implemented. Use bindAsync() instead');
}
Expand Down Expand Up @@ -462,6 +473,10 @@ export class Server {
return true;
}

unregister(name: string): boolean {
return this.handlers.delete(name);
}

start(): void {
if (
this.http2ServerList.length === 0 ||
Expand Down Expand Up @@ -637,7 +652,7 @@ async function handleUnary<RequestType, ResponseType>(
if (request === undefined || call.cancelled) {
return;
}

const emitter = new ServerUnaryCallImpl<RequestType, ResponseType>(
call,
metadata,
Expand Down
122 changes: 119 additions & 3 deletions packages/grpc-js/test/test-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ describe('Server', () => {
});
});

it('fails if the server has been started', done => {
it('succeeds after server has been started', done => {
const server = new Server();

server.bindAsync(
Expand All @@ -211,15 +211,131 @@ describe('Server', () => {
(err, port) => {
assert.ifError(err);
server.start();
assert.throws(() => {
assert.doesNotThrow(() => {
server.addService(mathServiceAttrs, dummyImpls);
}, /Can't add a service to a started server\./);
});
server.tryShutdown(done);
}
);
});
});

describe('removeService', () => {
let server: Server;
let client: ServiceClient;

const mathProtoFile = path.join(__dirname, 'fixtures', 'math.proto');
const mathClient = (loadProtoFile(mathProtoFile).math as any).Math;
const mathServiceAttrs = mathClient.service;
const dummyImpls = { div() {}, divMany() {}, fib() {}, sum() {} };

beforeEach(done => {
server = new Server();
server.addService(mathServiceAttrs, dummyImpls);
server.bindAsync(
'localhost:0',
ServerCredentials.createInsecure(),
(err, port) => {
assert.ifError(err);
client = new mathClient(
`localhost:${port}`,
grpc.credentials.createInsecure()
);
server.start();
done();
}
);
});

afterEach(done => {
client.close();
server.tryShutdown(done);
});

it('succeeds with a single service by removing all method handlers', done => {
server.removeService(mathServiceAttrs);

let methodsVerifiedCount = 0;
const methodsToVerify = Object.keys(mathServiceAttrs);

const assertFailsWithUnimplementedError = (error: ServiceError) => {
assert(error);
assert.strictEqual(error.code, grpc.status.UNIMPLEMENTED);
methodsVerifiedCount++;
if (methodsVerifiedCount === methodsToVerify.length) {
done();
}
};

methodsToVerify.forEach((method) => {
const call = client[method]({}, assertFailsWithUnimplementedError); // for unary
call.on('error', assertFailsWithUnimplementedError); // for streamed
});
});

it('fails for non-object service definition argument', () => {
assert.throws(() => {
server.removeService('upsie' as any)
}, /removeService.*requires object as argument/
);
});
});

describe('unregister', () => {

let server: Server;
let client: ServiceClient;

const mathProtoFile = path.join(__dirname, 'fixtures', 'math.proto');
const mathClient = (loadProtoFile(mathProtoFile).math as any).Math;
const mathServiceAttrs = mathClient.service;

beforeEach(done => {
server = new Server();
server.addService(mathServiceAttrs, {
div(call: ServerUnaryCall<any, any>, callback: sendUnaryData<any>) {
callback(null, {quotient: '42'});
},
});
server.bindAsync(
'localhost:0',
ServerCredentials.createInsecure(),
(err, port) => {
assert.ifError(err);
client = new mathClient(
`localhost:${port}`,
grpc.credentials.createInsecure()
);
server.start();
done();
}
);
});

afterEach(done => {
client.close();
server.tryShutdown(done);
});

it('removes handler by name and returns true', done => {
const name = mathServiceAttrs['Div'].path;
assert.strictEqual(server.unregister(name), true, 'Server#unregister should return true on success');

client.div(
{ divisor: 4, dividend: 3 },
(error: ServiceError, response: any) => {
assert(error);
assert.strictEqual(error.code, grpc.status.UNIMPLEMENTED);
done();
}
);
});

it('returns false for unknown handler', () => {
assert.strictEqual(server.unregister('noOneHere'), false, 'Server#unregister should return false on failure');
});
});

it('throws when unimplemented methods are called', () => {
const server = new Server();

Expand Down