From c5ee5592706615285c9cf5d6114e5e4670a8d59b Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Wed, 22 Feb 2017 17:21:39 -0800 Subject: [PATCH 01/11] Add client interceptor support to NodeJS --- NODEJS-CLIENT-INTERCEPTORS.md | 354 ++++++++++++++++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 NODEJS-CLIENT-INTERCEPTORS.md diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/NODEJS-CLIENT-INTERCEPTORS.md new file mode 100644 index 000000000..cf3ea0779 --- /dev/null +++ b/NODEJS-CLIENT-INTERCEPTORS.md @@ -0,0 +1,354 @@ +NodeJS Client Interceptors +---- +* Author(s): David Vroom-Duke (dduke@netflix.com), William Thurston (wthurston@netflix.com), David Liu (dliu@netflix.com), Howard Yuan (hyuan@netflix.com), Eran Landau (elandau@netflix.com) +* Approver: a11r +* Status: Draft +* Implemented in: Java, Go +* Last updated: 2017-02-17 +* Discussion at: (filled after thread exists) + +## Abstract + +NodeJS gRPC clients will present an interceptor interface. Consumers of these clients can provide an ordered list of +interceptors with methods to be executed on any outbound or inbound operation. The NodeJS client interceptor framework +API will provide similar functionality as existing gRPC interceptor implementations. + +## Background + +Some gRPC language implementations currently have client interceptor support and some do not. The NodeJS implementation +does not have client interceptor support, which makes the application of cross-cutting concerns difficult. + +### Related Proposals: + +None + +## Proposal + +### Interceptor API +Interceptors must implement `interceptCall`: + +```javascript +/** + * @param {Function} callConstructor A constructor for a grpc.Call proxy object + * @param {object} options + * @param {MethodDescriptor} options.method_descriptor A container of properties describing the call (see below) + * @param {grpc.ChannelCredentials} [options.credentials] Credentials used to authenticate with the server + * @param {number} [options.deadline] The deadline for the call + * @param {string} [options.host] The server to call + * @param {grpc.Call} [options.parent] The parent call + * @param {number} [options.propagate_flags] Propagation flags for the underlying grpc.Call + * @return {object} A grpc.Call proxy + */ +interceptCall(callConstructor, options) +``` + +The `interceptCall` method provides interceptors an opportunity to return a replacement for the interceptor's +`grpc.Call` proxy (which may include interceptor methods). An `InterceptorBuilder` will be provided to easily construct +an interceptor. + +`interceptCall` must return an object with a `grpc.Call` proxy interface. The proxy interface is identical to the +underlying `grpc.Call` interface, except for the addition of a `context` argument to the `startBatch` method. +Returning `callConstructor(options)` will satisfy the contract (but provide no interceptor functionality). + +Most interceptors will use the `ForwardingCall` constructor to create a call proxy with interceptor methods. Returning +`new ForwardingCall(callConstructor(options), outboundInterceptor)` will cause the call to trigger any interceptor +methods in the `outboundInterceptor` (described below). + +A short-circuiting interceptor can return a custom `grpc.Call` proxy which skips any downstream interceptors. A simple +example is: +```javascript +{ + interceptCall: function(callConstructor, options) { + var call = callConstructor(options); + var _originalStartBatch = call.startBatch; + call.startBatch = function(batch, context, cb) { + var cachedResponse = _getCachedResponse(batch); + if (cachedResponse) { + cb(null, { + read: cachedResponse, + metadata: {}, + status: { + code: grpc.status.OK, + metadata: {} + } + }); + } else { + _originalStartBatch(batch, context, cb); + } + }; + return call; + } +} +``` +The `grpc.Call` proxy interface is identical to the `grpc.Call` interface except for the `startBatch` signature. A +`context` argument has been added to carry values needed to serialize/deserialize messages. + +```javascript +/** +* @param {object} client_batch +* @param {object} context +* @param {Function} callback +*/ +startBatch(client_batch, context, callback) + +``` + +The `options` argument to `interceptCall` includes all the options that go into the base `grpc.Call` constructor, plus +an additional `MethodDescriptor` object. The `MethodDescriptor` is a container for properties of the call which are +used internally and may also be useful to consumers: + +```javascript +/** + * @param {string} name The name of the call, i.e. 'myCall' + * @param {string} path The full path of the call, i.e. '/MyService/MyCall' + * @param {MethodType} method_type One of four types: MethodType.UNARY, MethodType.CLIENT_STREAMING, + * MethodType.SERVER_STREAMING, MethodType.BIDI_STREAMING + * @param {Function} serialize The function used to serialize a message + * @param {Function} deserialize The function used to deserialize a message + * @constructor + */ +MethodDescriptor(name, path, method_type, serialize, deserialize) +``` + +An outbound interceptor is a POJO which implements zero or more of the following methods: + +```javascript +/** + * An interceptor method called before an outbound call has started. + * @param {Metadata} metadata The call's outbound metadata (request headers). + * @param {function} next A callback which continues the gRPC interceptor chain. + * The first argument to next is required: a Metadata object. + * The second argument to next is optional: an inbound interceptor (described below) + */ +start(metadata, next) + +/** + * An interceptor method called prior to every outbound message. + * @param {object} A protobuf message + * @param {function} next A callback which continues the gRPC interceptor chain, called with the message to send. + */ +sendMessage(message, next) + +/** + * An interceptor method called when the outbound stream closes (after the message is sent). + * @param {function} next A callback which continues the gRPC interceptor chain. + */ +halfClose(next) + +/** + * An interceptor method called when the stream is canceled from the client. + * @param {message} string|null A cancel message if provided + * @param {function} next A callback which continues the gRPC interceptor chain. + */ +cancel(message, next) + +``` + +An `OutboundInterceptorBuilder` will be provided to easily construct an outbound interceptor. + +An inbound interceptor is an object passed to the `start` method's `next` callback with zero or more of the following +methods: + +```javascript + +/** + * An inbound interceptor method triggered when metadata is received. + * @param {Metadata} metadata The metadata received (response headers). + * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the metadata to respond with. + */ +onReceiveMetadata(metadata, next) + +/** + * An inbound interceptor method triggered when each message is received. + * @param {object} message The protobuf message received. + * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the message to respond with. + */ +onReceiveMessage(message, next) + +/** + * An inbound interceptor method triggered when status is received. + * @param {object} status The status received. + * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the status to respond with. + */ +onReceiveStatus(status, next) +``` + +An `InboundInterceptorBuilder` will be provided to easily construct an inbound interceptor. + +To intercept errors, implement the `onReceiveStatus` method and test for `status.code !== grpc.status.OK`. + +To intercept trailers, examine `status.metadata` in the `onReceiveStatus` method. + +This is a full implementation of all interceptor methods using the proposed interceptor builders: + +```javascript +(new InterceptorBuilder()).withInterceptCall(function(callConstructor, options) { + const outboundInterceptor = (new OutboundInterceptorBuilder()) + .withStart(function(metadata, next) { + const inboundInterceptor = (new InboundInterceptorBuilder()) + .withOnReceiveMetadata(function(metadata, next) { + next(metadata); + }) + .withOnReceiveMessage(function(message, next) { + next(message); + }) + .withOnReceiveStatus(function(status, next) { + next(status); + }).build(); + next(metadata, inboundInterceptor); + }) + .withSendMessage(function(message, next) { + next(messasge); + }) + .withHalfClose(function(next) { + next(); + }) + .withCancel(function(message, next) { + next(); + }).build(); + const call = callConstructor(options); + return new ForwardingCall(call, outboundInterceptor); +}).build(); +``` +Outbound and inbound interceptors do not need to implement all methods. + +### Use + +Interceptors can be configured during client construction, or on individual invocations of gRPC calls. + +#### At Client Construction + +An `InterceptorProvider` type will be provided for computing the association of interceptors with gRPC calls +dynamically. + +```javascript +/** +* @param {Function} getInterceptorForMethod A filter method which accepts a MethodDescriptor and returns +* `undefined` (when no interceptor should be associated) or a single interceptor object. +* @constructor +*/ +InterceptorProvider(getInterceptorForMethod) + +``` + +An array of InterceptorProviders can be passed in the `options` parameter when constructing a client: +```javascript +var interceptor_providers = [ + new InterceptorProvider(function(method_descriptor) { + if (method_descriptor.method_type === MethodType.UNARY) { + return unary_interceptor; + } + }), + new InterceptorProvider(function(method_descriptor) { + if (method_descriptor.method_type === MethodType.SERVER_STREAMING) { + return streaming_interceptor; + } + }) +]; +var constructor_options = { + interceptor_providers: interceptor_providers +}; +var client = new InterceptingClient('localhost:8080', credentials, constructor_options); +``` + +#### At Call Invocation + +If an array of interceptors is provided at call invocation via `options.interceptors`, the call will ignore all +interceptors provided via the client constructor. + +Example: +```javascript +client.unaryCall(message, { interceptors: [ myInterceptor ] }, callback); +``` + +Alternatively, an array of InterceptorProviders can be passed at call invocation (which will also supersede constructor +options): + +```javascript +client.unaryCall(message, { interceptor_providers: [ myInterceptorProvider ] }, callback); +``` + +The framework will throw an error if both `interceptors` and `interceptor_providers` are provided in the invocation +options. + +### Internal implementation + +#### gRPC Operations +A new `client_interceptors` module will be added and the `getCall` method in `client.js` will be modified to use it. +The interceptor module will produce a proxy for the `grpc.Call` object which triggers any interceptors provided on the +corresponding events. + +In the invocation of a `grpc.Call`'s `startBatch`, a set of gRPC opTypes are passed. This is the mapping of the +outbound opTypes to the interceptor methods they will trigger: + +``` +grpc.opType.SEND_INITIAL_METADATA -> start +grpc.opType.SEND_MESSAGE -> sendMessage +grpc.opType.SEND_CLOSE_FROM_CLIENT -> halfClose +``` +On the inbound side, these opTypes will map to these interceptor methods when responses are received from the server: +``` +grpc.opType.RECV_INITIAL_METADATA -> onReceiveMetadata +grpc.opType.RECV_MESSAGE -> onReceiveMessage +grpc.opType.RECV_STATUS_ON_CLIENT -> onReceiveStatus +``` + +Additional, `cancel()` and `cancelWithStatus()` are modified to trigger any `cancel()` interceptors. + +#### Interceptor Order +Interceptors nest in the order provided. Providing an interceptors array like +`[interceptorA, interceptorB, interceptorC]` will produce this execution graph: +``` +interceptorA outbound -> + interceptorB outbound -> + interceptorC outbound -> + underlying grpc.Call -> + interceptorC inbound -> + interceptorB inbound -> +interceptorA inbound +``` +All the appropriate interceptor methods on a given interceptor fire before the next interceptor is evaluated. (i.e. +interceptorA's `start` and `sendMessage` would fire before interceptorB's `start` and `sendMessage`) + +#### Serialization and Deserialization +To provide interceptors with useful message objects, we will move all serialization and deserialization to a common +`startBatch` wrapper. This allows us to intercept the `startBatch` method and trigger outbound interceptor methods +before serialization happens. On the inbound side, we will deserialize the messages before triggering interceptor +methods. + +We also box/unbox Metadata objects in the same way so interceptors get useful Metadata. + +To do this requires passing certain values through the `grpc.Call` proxies via a new `context` parameter to +`startBatch`. + +## Rationale + +### Abstraction level +Interceptors are implemented by wrapping the `grpc.Call#startBatch` method. This level of abstraction avoids coupling +interceptors with much of the NodeJS client code since `startBatch` is the common entry point for all gRPC operations. + +- If we intercepted calls at a higher level, the interceptor framework would have to account for the different types +of RPCs (unary, clientStream, serverStream, bidirectional) and could not provide the same interceptor granularity +that other gRPC language interceptors provide. +- We could intercept calls at a lower level (`call.cc`), but no additional interceptor granularity would be provided. + +### Invocation-specific and client-wide interceptor configuration +Clients can be configured with interceptor providers at construction time, or interceptors can be passed to individual +call invocations. This allows configuration flexibility: a service author can provide a wrapped client constructor +which configures the interceptors as desired, and consumers of the client can override any interceptor configuration +at invocation time. If any interceptors are passed at invocation, all interceptors attached to the call during +construction are ignored. + +## Implementation + +The implementation will include: + +- A `client_interceptors.js` module with the logic for producing a `grpc.Call` proxy. +- Modifying the `client.js` module to use the `client_interceptors` module. +- Tests which exercise the client interceptor methods for all four RPC types. +- Markdown documentation with example interceptors. + +All steps will be implemented by Netflix engineers. + +An implementation of server interceptors is not in scope. A separate proposal for server interceptors using similar +concepts and interfaces will be provided at a later date. From 6e45f536ed87fd78a016391e169aeca98914e14d Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Thu, 23 Feb 2017 13:50:47 -0800 Subject: [PATCH 02/11] Add grpc-io discussion link --- NODEJS-CLIENT-INTERCEPTORS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/NODEJS-CLIENT-INTERCEPTORS.md index cf3ea0779..e3ff612df 100644 --- a/NODEJS-CLIENT-INTERCEPTORS.md +++ b/NODEJS-CLIENT-INTERCEPTORS.md @@ -5,7 +5,8 @@ NodeJS Client Interceptors * Status: Draft * Implemented in: Java, Go * Last updated: 2017-02-17 -* Discussion at: (filled after thread exists) +* Discussion at: + https://stash.corp.netflix.com/projects/CPIE/repos/nf-eureka2-registration/pull-requests/13/overview ## Abstract From 8c14fa343b5a8767dc28f24c9889d9f3714572a7 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Thu, 9 Mar 2017 10:41:49 -0800 Subject: [PATCH 03/11] Update proposal with API changes (listeners) This removes the batch-handling API and makes additions to address the short-circuiting and delay use cases. --- NODEJS-CLIENT-INTERCEPTORS.md | 388 +++++++++++++++++++++++++--------- 1 file changed, 288 insertions(+), 100 deletions(-) diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/NODEJS-CLIENT-INTERCEPTORS.md index e3ff612df..62c23ae4a 100644 --- a/NODEJS-CLIENT-INTERCEPTORS.md +++ b/NODEJS-CLIENT-INTERCEPTORS.md @@ -4,9 +4,8 @@ NodeJS Client Interceptors * Approver: a11r * Status: Draft * Implemented in: Java, Go -* Last updated: 2017-02-17 -* Discussion at: - https://stash.corp.netflix.com/projects/CPIE/repos/nf-eureka2-registration/pull-requests/13/overview +* Last updated: 2017-03-09 +* Discussion at: https://groups.google.com/forum/#!topic/grpc-io/LxT1JjN33Q4 ## Abstract @@ -30,7 +29,6 @@ Interceptors must implement `interceptCall`: ```javascript /** - * @param {Function} callConstructor A constructor for a grpc.Call proxy object * @param {object} options * @param {MethodDescriptor} options.method_descriptor A container of properties describing the call (see below) * @param {grpc.ChannelCredentials} [options.credentials] Credentials used to authenticate with the server @@ -38,72 +36,49 @@ Interceptors must implement `interceptCall`: * @param {string} [options.host] The server to call * @param {grpc.Call} [options.parent] The parent call * @param {number} [options.propagate_flags] Propagation flags for the underlying grpc.Call - * @return {object} A grpc.Call proxy + * @return {InterceptingCall} */ -interceptCall(callConstructor, options) +interceptCall(options) ``` -The `interceptCall` method provides interceptors an opportunity to return a replacement for the interceptor's -`grpc.Call` proxy (which may include interceptor methods). An `InterceptorBuilder` will be provided to easily construct -an interceptor. +The `interceptCall` method allows developers to modify the call options, store any call-scoped values needed, and define +interceptor methods. An `InterceptorBuilder` will be provided to easily construct an interceptor. -`interceptCall` must return an object with a `grpc.Call` proxy interface. The proxy interface is identical to the -underlying `grpc.Call` interface, except for the addition of a `context` argument to the `startBatch` method. -Returning `callConstructor(options)` will satisfy the contract (but provide no interceptor functionality). +`interceptCall` must return an `InterceptingCall` object. An `InterceptingCall` is a container for the call options +and an optional `caller` object. Returning `new InterceptingCall(options)` will satisfy the contract (but provide no +interceptor functionality). -Most interceptors will use the `ForwardingCall` constructor to create a call proxy with interceptor methods. Returning -`new ForwardingCall(callConstructor(options), outboundInterceptor)` will cause the call to trigger any interceptor -methods in the `outboundInterceptor` (described below). +Most interceptors will define a `caller` object (described below) which is passed to the the `InterceptingCall` +constructor: `return new InterceptingCall(options, caller)` -A short-circuiting interceptor can return a custom `grpc.Call` proxy which skips any downstream interceptors. A simple -example is: +A simple interceptor example: ```javascript -{ - interceptCall: function(callConstructor, options) { - var call = callConstructor(options); - var _originalStartBatch = call.startBatch; - call.startBatch = function(batch, context, cb) { - var cachedResponse = _getCachedResponse(batch); - if (cachedResponse) { - cb(null, { - read: cachedResponse, - metadata: {}, - status: { - code: grpc.status.OK, - metadata: {} - } - }); - } else { - _originalStartBatch(batch, context, cb); - } - }; - return call; - } -} -``` -The `grpc.Call` proxy interface is identical to the `grpc.Call` interface except for the `startBatch` signature. A -`context` argument has been added to carry values needed to serialize/deserialize messages. - -```javascript -/** -* @param {object} client_batch -* @param {object} context -* @param {Function} callback -*/ -startBatch(client_batch, context, callback) - +var interceptor = (new InterceptorBuilder()) + .withInterceptCall(function(options) { + var caller = (new CallerBuilder()) + .withSendMessage(function(message, next) { + logger.log(message); + next(message); + }) + .build(); + return new InterceptingCall(options, caller); + }) + .build(); ``` -The `options` argument to `interceptCall` includes all the options that go into the base `grpc.Call` constructor, plus -an additional `MethodDescriptor` object. The `MethodDescriptor` is a container for properties of the call which are -used internally and may also be useful to consumers: +The `options` argument to the `InterceptingCall` constructor includes all the options accepted by the base `grpc.Call` +constructor, in addition to a `MethodDescriptor` object. The `MethodDescriptor` is a container for properties of the +call which are used internally and may also be useful to consumers: ```javascript /** * @param {string} name The name of the call, i.e. 'myCall' * @param {string} path The full path of the call, i.e. '/MyService/MyCall' - * @param {MethodType} method_type One of four types: MethodType.UNARY, MethodType.CLIENT_STREAMING, - * MethodType.SERVER_STREAMING, MethodType.BIDI_STREAMING + * @param {MethodType} method_type One of four types: + * MethodType.UNARY, + * MethodType.CLIENT_STREAMING, + * MethodType.SERVER_STREAMING, or + * MethodType.BIDI_STREAMING * @param {Function} serialize The function used to serialize a message * @param {Function} deserialize The function used to deserialize a message * @constructor @@ -111,45 +86,71 @@ used internally and may also be useful to consumers: MethodDescriptor(name, path, method_type, serialize, deserialize) ``` -An outbound interceptor is a POJO which implements zero or more of the following methods: +A `caller` object is a POJO implementing zero or more outbound interceptor methods: ```javascript /** * An interceptor method called before an outbound call has started. * @param {Metadata} metadata The call's outbound metadata (request headers). - * @param {function} next A callback which continues the gRPC interceptor chain. - * The first argument to next is required: a Metadata object. - * The second argument to next is optional: an inbound interceptor (described below) + * @param {object} listener The listener which will be intercepting inbound operations + * @param {Function} next A callback which continues the gRPC interceptor chain. + * next takes two arguments: A Metadata object and a listener */ -start(metadata, next) - +start(metadata, listener, next) +``` +```javascript /** * An interceptor method called prior to every outbound message. * @param {object} A protobuf message * @param {function} next A callback which continues the gRPC interceptor chain, called with the message to send. */ sendMessage(message, next) - +``` +```javascript /** * An interceptor method called when the outbound stream closes (after the message is sent). * @param {function} next A callback which continues the gRPC interceptor chain. */ halfClose(next) - +``` +```javascript /** * An interceptor method called when the stream is canceled from the client. * @param {message} string|null A cancel message if provided * @param {function} next A callback which continues the gRPC interceptor chain. */ cancel(message, next) +``` +A `CallerBuilder` will be provided to easily construct an outbound interceptor: + +```javascript +var caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + logger.log(metadata); + next(metadata, listener); + }) + .withSendMessage(function(message, next) { + logger.log(message); + next(message); + }) + .build(); ``` -An `OutboundInterceptorBuilder` will be provided to easily construct an outbound interceptor. +A `listener` object implements zero or more methods for intercepting inbound operations. The listener passed into +a caller's `start` method implements all the inbound interceptor methods and represents the listener chain constructed +by any previous interceptors. Three usage patterns are supported for listeners: +1) Pass the listener along without modification: `next(metadata, listener)`. In this case the interceptor declines +to intercept any inbound operations. +2) Create a new listener with one or more inbound interceptor methods and pass it to `next`. In this case the +interceptor will fire on the inbound operations implemented in the new listener. +3) Store the listener to make direct inbound calls on later. This effectively short-circuits the interceptor stack. +An example of an interceptor using this pattern to provide client-side caching is included below. -An inbound interceptor is an object passed to the `start` method's `next` callback with zero or more of the following -methods: +*Do not modify the listener passed in. Either pass it along unmodified or call methods on it to short-circuit the +interceptor stack.* +The `listener` methods are: ```javascript /** @@ -158,14 +159,16 @@ methods: * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the metadata to respond with. */ onReceiveMetadata(metadata, next) - +``` +```javascript /** * An inbound interceptor method triggered when each message is received. * @param {object} message The protobuf message received. * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the message to respond with. */ onReceiveMessage(message, next) - +``` +```javascript /** * An inbound interceptor method triggered when status is received. * @param {object} status The status received. @@ -174,44 +177,216 @@ onReceiveMessage(message, next) onReceiveStatus(status, next) ``` -An `InboundInterceptorBuilder` will be provided to easily construct an inbound interceptor. +A `ListenerBuilder` will be provided to easily construct a listener: + +```javascript +var listener = (new ListenerBuilder()) + .withOnReceiveMetadata(function(metadata, next) { + logger.log(metadata); + next(metadata); + }) + .withOnReceiveMessage(function(message, next) { + logger.log(message); + next(message); + }) + .build(); +``` To intercept errors, implement the `onReceiveStatus` method and test for `status.code !== grpc.status.OK`. To intercept trailers, examine `status.metadata` in the `onReceiveStatus` method. -This is a full implementation of all interceptor methods using the proposed interceptor builders: +### Examples +#### Simple +A trivial implementation of all interceptor methods using the builders: +```javascript +var interceptor = (new InterceptorBuilder()) + .withInterceptCall(function(options) { + const caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + const newListener = (new ListenerBuilder()) + .withOnReceiveMetadata(function(metadata, next) { + next(metadata); + }) + .withOnReceiveMessage(function(message, next) { + next(message); + }) + .withOnReceiveStatus(function(status, next) { + next(status); + }) + .build(); + next(metadata, newListener); + }) + .withSendMessage(function(message, next) { + next(messasge); + }) + .withHalfClose(function(next) { + next(); + }) + .withCancel(function(message, next) { + next(); + }) + .build(); + return new InterceptingCall(options, caller); + }) + .build(); +``` +(Callers and listeners do not need to implement all methods) + +#### Advanced +These advanced examples are specific to certain types of RPC calls (unary, client-streaming, etc), because interceptors +operate on gRPC batches. Any given interceptor must work for every batch in the RPC calls it applies to. Since different +types of RPC calls construct batches with different sets of operations, interceptors which delay or short-circuit +operations will run into batch boundaries for some RPC call types. The limitations should be intuitive, for example: a +caching interceptor would not make sense for a streaming RPC given that inbound messages are not associated with +outbound messages. + +**Caching** + +An example of a caching interceptor for unary RPCs which stores the provided listener for later use (short-circuiting +the call if there is a cache hit): +```javascript +var interceptor = (new InterceptorBuilder()) + .withInterceptCall(function(options) { + var savedMetadata; + var startNext; + var savedListener; + var savedMessage; + var messageNext; + var caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + savedMetadata = metadata; + savedListener = listener; + startNext = next; + }) + .withSendMessage(function(message, next) { + savedMessage = message; + messageNext = next; + }) + .withHalfClose(function(next) { + var cachedValue = _getCachedResponse(storedMessage.value); + if (cachedValue) { + var cachedMessage = new Message(cachedValue); + storedListener.onReceiveMetadata(new Metadata()); + storedListener.onReceiveMessage(cachedMessage); + storedListener.onReceiveStatus((new StatusBuilder()) + .withCode(grpc.status.OK) + .build()); + } else { + registry.addCall(storedMessage.value + '_miss'); + var newListener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + _store(storedMessage.value, message.value); + next(message); + }) + .build(); + startNext(savedMetadata, newListener); + messageNext(storedMessage); + next(); + } + }) + .build(); + return new InterceptingCall(options, caller); + }) + .build(); +``` + +**Retries** +An example retry interceptor for unary RPCs creates new calls when the status shows a failure: ```javascript -(new InterceptorBuilder()).withInterceptCall(function(callConstructor, options) { - const outboundInterceptor = (new OutboundInterceptorBuilder()) - .withStart(function(metadata, next) { - const inboundInterceptor = (new InboundInterceptorBuilder()) - .withOnReceiveMetadata(function(metadata, next) { - next(metadata); - }) - .withOnReceiveMessage(function(message, next) { - next(message); - }) - .withOnReceiveStatus(function(status, next) { - next(status); - }).build(); - next(metadata, inboundInterceptor); - }) - .withSendMessage(function(message, next) { - next(messasge); - }) - .withHalfClose(function(next) { - next(); - }) - .withCancel(function(message, next) { - next(); - }).build(); - const call = callConstructor(options); - return new ForwardingCall(call, outboundInterceptor); -}).build(); +var maxRetries = 3; +var interceptor = (new InterceptorBuilder()) + .withInterceptCall(function(options) { + var savedMetadata; + var savedSendMessage; + var savedReceiveMessage; + var savedMessageNext; + var methodName = options.method_descriptor.name; + var caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + savedMetadata = metadata; + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + savedReceiveMessage = message; + savedMessageNext = next; + }) + .withOnReceiveStatus(function(status, next) { + var retries = 0; + var retry = function(message, metadata, options) { + retries++; + client[methodName](message, metadata, options, function(err, response) { + if (err) { + if (retries <= maxRetries) { + retry(message, metadata, options); + } else { + savedMessageNext(response); + next(status); + } + return; + } + var ok_status = (new StatusBuilder()) + .withCode(grpc.status.OK) + .build(); + savedMessageNext(response); + next(ok_status); + }); + }; + if (status.code !== grpc.status.OK) { + var new_options = _.clone(options); + new_options.interceptors = []; + retry(savedSendMessage, savedMetadata, new_options); + } else { + savedMessageNext(savedReceiveMessage); + next(status); + } + }) + .build(); + next(metadata, new_listener); + }) + .withSendMessage(function(message, next) { + savedSendMessage = message; + next(message); + }) + .build(); + return new InterceptingCall(options, caller); + }) + .build(); +``` + +**Fallbacks** + +An example of providing fallbacks to failed requests for unary or client-streaming RPCs: +```javascript + +var interceptor = (new InterceptorBuilder()) + .withInterceptCall(function(options) { + var savedMessage; + var savedMessageNext; + var caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + savedMessage = message; + savedMessageNext = next; + }) + .withOnReceiveStatus(function(status, next) { + if (status.code !== grpc.status.OK) { + savedMessageNext(fallback_response); + next((new StatusBuilder()).withCode(grpc.status.OK)); + } else { + savedMessageNext(savedMessage); + next(status); + } + }) + .build(); + next(metadata, new_listener); + }) + .build(); + return new InterceptingCall(options, caller); + }) + .build(); ``` -Outbound and inbound interceptors do not need to implement all methods. ### Use @@ -296,7 +471,7 @@ grpc.opType.RECV_STATUS_ON_CLIENT -> onReceiveStatus Additional, `cancel()` and `cancelWithStatus()` are modified to trigger any `cancel()` interceptors. -#### Interceptor Order +#### Interceptor nesting Interceptors nest in the order provided. Providing an interceptors array like `[interceptorA, interceptorB, interceptorC]` will produce this execution graph: ``` @@ -311,6 +486,19 @@ interceptorA inbound All the appropriate interceptor methods on a given interceptor fire before the next interceptor is evaluated. (i.e. interceptorA's `start` and `sendMessage` would fire before interceptorB's `start` and `sendMessage`) +#### Horizontal execution +The order of execution for interceptor methods is by operation. Given a chain of interceptors and a batch of operations, +we will execute all interceptor methods for the current operation before moving to the next. For example if you +define three interceptors which all implement `start` and `sendMessage` methods, the execution order would be: +``` +interceptorA start +interceptorB start +interceptorC start +interceptorA sendMessage +interceptorB sendMessage +interceptorC sendMessage +``` + #### Serialization and Deserialization To provide interceptors with useful message objects, we will move all serialization and deserialization to a common `startBatch` wrapper. This allows us to intercept the `startBatch` method and trigger outbound interceptor methods From 6debaf77a3748fd76c7e993c0ebb9a1811b3e3a6 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Thu, 9 Mar 2017 14:37:36 -0800 Subject: [PATCH 04/11] Fixes to examples and text --- NODEJS-CLIENT-INTERCEPTORS.md | 49 +++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/NODEJS-CLIENT-INTERCEPTORS.md index 62c23ae4a..5fb1cc32e 100644 --- a/NODEJS-CLIENT-INTERCEPTORS.md +++ b/NODEJS-CLIENT-INTERCEPTORS.md @@ -3,7 +3,7 @@ NodeJS Client Interceptors * Author(s): David Vroom-Duke (dduke@netflix.com), William Thurston (wthurston@netflix.com), David Liu (dliu@netflix.com), Howard Yuan (hyuan@netflix.com), Eran Landau (elandau@netflix.com) * Approver: a11r * Status: Draft -* Implemented in: Java, Go +* Implemented in: NodeJS * Last updated: 2017-03-09 * Discussion at: https://groups.google.com/forum/#!topic/grpc-io/LxT1JjN33Q4 @@ -67,8 +67,11 @@ var interceptor = (new InterceptorBuilder()) ``` The `options` argument to the `InterceptingCall` constructor includes all the options accepted by the base `grpc.Call` -constructor, in addition to a `MethodDescriptor` object. The `MethodDescriptor` is a container for properties of the -call which are used internally and may also be useful to consumers: +constructor. Modifying the options that are passed to the `InterceptingCall` constructor will have the effect of +changing the options passed to the underlying `grpc.Call` constructor. + +Additionally, the `options` argument includes a `MethodDescriptor` object. The `MethodDescriptor` is a container for +properties of the call which are used internally and may also be useful to interceptors: ```javascript /** @@ -86,6 +89,9 @@ call which are used internally and may also be useful to consumers: MethodDescriptor(name, path, method_type, serialize, deserialize) ``` +*Do not modify the `options.method_descriptor` object, it is not used by the underlying gRPC code and will only affect +downstream interceptors* + A `caller` object is a POJO implementing zero or more outbound interceptor methods: ```javascript @@ -192,9 +198,20 @@ var listener = (new ListenerBuilder()) .build(); ``` -To intercept errors, implement the `onReceiveStatus` method and test for `status.code !== grpc.status.OK`. +A `StatusBuilder` will be provided to produce gRPC status objects: + +```javascript +var status = (new StatusBuilder()) + .withCode(grpc.status.OK) + .withDetails('Status message') + .withMetadata(new Metadata()) + .build(); +``` + +**To intercept errors, implement the `onReceiveStatus` method and test for `status.code !== grpc.status.OK`.** + +**To intercept trailers, examine `status.metadata` in the `onReceiveStatus` method.** -To intercept trailers, examine `status.metadata` in the `onReceiveStatus` method. ### Examples #### Simple @@ -246,6 +263,7 @@ outbound messages. An example of a caching interceptor for unary RPCs which stores the provided listener for later use (short-circuiting the call if there is a cache hit): ```javascript +// Unary RPCs only var interceptor = (new InterceptorBuilder()) .withInterceptCall(function(options) { var savedMetadata; @@ -264,24 +282,24 @@ var interceptor = (new InterceptorBuilder()) messageNext = next; }) .withHalfClose(function(next) { - var cachedValue = _getCachedResponse(storedMessage.value); + var cachedValue = _getCachedResponse(savedMessage.value); if (cachedValue) { var cachedMessage = new Message(cachedValue); - storedListener.onReceiveMetadata(new Metadata()); - storedListener.onReceiveMessage(cachedMessage); - storedListener.onReceiveStatus((new StatusBuilder()) + savedListener.onReceiveMetadata(new Metadata()); + savedListener.onReceiveMessage(cachedMessage); + savedListener.onReceiveStatus((new StatusBuilder()) .withCode(grpc.status.OK) .build()); } else { - registry.addCall(storedMessage.value + '_miss'); + registry.addCall(savedMessage.value + '_miss'); var newListener = (new ListenerBuilder()) .withOnReceiveMessage(function(message, next) { - _store(storedMessage.value, message.value); + _store(savedMessage.value, message.value); next(message); }) .build(); startNext(savedMetadata, newListener); - messageNext(storedMessage); + messageNext(savedMessage); next(); } }) @@ -295,6 +313,7 @@ var interceptor = (new InterceptorBuilder()) An example retry interceptor for unary RPCs creates new calls when the status shows a failure: ```javascript +// Unary RPCs only var maxRetries = 3; var interceptor = (new InterceptorBuilder()) .withInterceptCall(function(options) { @@ -358,7 +377,7 @@ var interceptor = (new InterceptorBuilder()) An example of providing fallbacks to failed requests for unary or client-streaming RPCs: ```javascript - +// Unary or client-streaming RPCs only var interceptor = (new InterceptorBuilder()) .withInterceptCall(function(options) { var savedMessage; @@ -373,7 +392,9 @@ var interceptor = (new InterceptorBuilder()) .withOnReceiveStatus(function(status, next) { if (status.code !== grpc.status.OK) { savedMessageNext(fallback_response); - next((new StatusBuilder()).withCode(grpc.status.OK)); + next((new StatusBuilder()) + .withCode(grpc.status.OK) + .build()); } else { savedMessageNext(savedMessage); next(status); From 0f7b239b09d42ae2860d241a48200d33c700c906 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Thu, 9 Mar 2017 16:07:36 -0800 Subject: [PATCH 05/11] Fix credentials type and clarify InterceptorProvider use --- NODEJS-CLIENT-INTERCEPTORS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/NODEJS-CLIENT-INTERCEPTORS.md index 5fb1cc32e..871cc8d04 100644 --- a/NODEJS-CLIENT-INTERCEPTORS.md +++ b/NODEJS-CLIENT-INTERCEPTORS.md @@ -31,7 +31,7 @@ Interceptors must implement `interceptCall`: /** * @param {object} options * @param {MethodDescriptor} options.method_descriptor A container of properties describing the call (see below) - * @param {grpc.ChannelCredentials} [options.credentials] Credentials used to authenticate with the server + * @param {grpc.CallCredentials} [options.credentials] Credentials used to authenticate with the server * @param {number} [options.deadline] The deadline for the call * @param {string} [options.host] The server to call * @param {grpc.Call} [options.parent] The parent call @@ -448,6 +448,8 @@ var constructor_options = { var client = new InterceptingClient('localhost:8080', credentials, constructor_options); ``` +The order of the InterceptorProviders will determine the order of the resulting interceptor stack. + #### At Call Invocation If an array of interceptors is provided at call invocation via `options.interceptors`, the call will ignore all From b95fae7c8fd8f81593e24ec5276bb411f1a88365 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Mon, 13 Mar 2017 10:01:22 -0700 Subject: [PATCH 06/11] Upgrade interceptCall to represent the interceptor --- NODEJS-CLIENT-INTERCEPTORS.md | 392 +++++++++++++++++----------------- 1 file changed, 198 insertions(+), 194 deletions(-) diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/NODEJS-CLIENT-INTERCEPTORS.md index 871cc8d04..53c6403e5 100644 --- a/NODEJS-CLIENT-INTERCEPTORS.md +++ b/NODEJS-CLIENT-INTERCEPTORS.md @@ -24,8 +24,22 @@ None ## Proposal +### A simple example +This example logs all outbound messages: +```javascript +var interceptor = function(options) { + return new InterceptingCall(options, { + sendMessage: function(message, next) { + console.log(message); + next(message); + } + }); +}; +client.myCall(message, { interceptors: [interceptor] }); +``` + ### Interceptor API -Interceptors must implement `interceptCall`: +An interceptor is a function which takes an `options` object and returns and `InterceptingCall` object: ```javascript /** @@ -38,32 +52,30 @@ Interceptors must implement `interceptCall`: * @param {number} [options.propagate_flags] Propagation flags for the underlying grpc.Call * @return {InterceptingCall} */ -interceptCall(options) +var interceptor = function(options) {} ``` -The `interceptCall` method allows developers to modify the call options, store any call-scoped values needed, and define -interceptor methods. An `InterceptorBuilder` will be provided to easily construct an interceptor. +An interceptor function allows developers to modify the call options, store any call-scoped values needed, and define +interception methods. -`interceptCall` must return an `InterceptingCall` object. An `InterceptingCall` is a container for the call options -and an optional `caller` object. Returning `new InterceptingCall(options)` will satisfy the contract (but provide no -interceptor functionality). +The interceptor function must return an `InterceptingCall` object. An `InterceptingCall` is a container for the call +options and an optional `caller` object. Returning `new InterceptingCall(options)` will satisfy the contract (but +provide no interceptor functionality). Most interceptors will define a `caller` object (described below) which is passed to the the `InterceptingCall` constructor: `return new InterceptingCall(options, caller)` -A simple interceptor example: +A simple interceptor example (using the optional CallerBuilder object): ```javascript -var interceptor = (new InterceptorBuilder()) - .withInterceptCall(function(options) { - var caller = (new CallerBuilder()) - .withSendMessage(function(message, next) { - logger.log(message); - next(message); - }) - .build(); - return new InterceptingCall(options, caller); - }) - .build(); +var interceptor = function(options) { + var caller = (new CallerBuilder()) + .withSendMessage(function(message, next) { + logger.log(message); + next(message); + }) + .build(); + return new InterceptingCall(options, caller); +}; ``` The `options` argument to the `InterceptingCall` constructor includes all the options accepted by the base `grpc.Call` @@ -92,11 +104,11 @@ MethodDescriptor(name, path, method_type, serialize, deserialize) *Do not modify the `options.method_descriptor` object, it is not used by the underlying gRPC code and will only affect downstream interceptors* -A `caller` object is a POJO implementing zero or more outbound interceptor methods: +A `caller` object is a POJO implementing zero or more outbound interception methods: ```javascript /** - * An interceptor method called before an outbound call has started. + * An interception method called before an outbound call has started. * @param {Metadata} metadata The call's outbound metadata (request headers). * @param {object} listener The listener which will be intercepting inbound operations * @param {Function} next A callback which continues the gRPC interceptor chain. @@ -106,7 +118,7 @@ start(metadata, listener, next) ``` ```javascript /** - * An interceptor method called prior to every outbound message. + * An interception method called prior to every outbound message. * @param {object} A protobuf message * @param {function} next A callback which continues the gRPC interceptor chain, called with the message to send. */ @@ -114,14 +126,14 @@ sendMessage(message, next) ``` ```javascript /** - * An interceptor method called when the outbound stream closes (after the message is sent). + * An interception method called when the outbound stream closes (after the message is sent). * @param {function} next A callback which continues the gRPC interceptor chain. */ halfClose(next) ``` ```javascript /** - * An interceptor method called when the stream is canceled from the client. + * An interception method called when the stream is canceled from the client. * @param {message} string|null A cancel message if provided * @param {function} next A callback which continues the gRPC interceptor chain. */ @@ -144,11 +156,11 @@ var caller = (new CallerBuilder()) ``` A `listener` object implements zero or more methods for intercepting inbound operations. The listener passed into -a caller's `start` method implements all the inbound interceptor methods and represents the listener chain constructed +a caller's `start` method implements all the inbound interception methods and represents the listener chain constructed by any previous interceptors. Three usage patterns are supported for listeners: 1) Pass the listener along without modification: `next(metadata, listener)`. In this case the interceptor declines to intercept any inbound operations. -2) Create a new listener with one or more inbound interceptor methods and pass it to `next`. In this case the +2) Create a new listener with one or more inbound interception methods and pass it to `next`. In this case the interceptor will fire on the inbound operations implemented in the new listener. 3) Store the listener to make direct inbound calls on later. This effectively short-circuits the interceptor stack. An example of an interceptor using this pattern to provide client-side caching is included below. @@ -160,7 +172,7 @@ The `listener` methods are: ```javascript /** - * An inbound interceptor method triggered when metadata is received. + * An inbound interception method triggered when metadata is received. * @param {Metadata} metadata The metadata received (response headers). * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the metadata to respond with. */ @@ -168,7 +180,7 @@ onReceiveMetadata(metadata, next) ``` ```javascript /** - * An inbound interceptor method triggered when each message is received. + * An inbound interception method triggered when each message is received. * @param {object} message The protobuf message received. * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the message to respond with. */ @@ -176,7 +188,7 @@ onReceiveMessage(message, next) ``` ```javascript /** - * An inbound interceptor method triggered when status is received. + * An inbound interception method triggered when status is received. * @param {object} status The status received. * @param {function} next A callback which continues the gRPC interceptor chain. Pass it the status to respond with. */ @@ -215,38 +227,36 @@ var status = (new StatusBuilder()) ### Examples #### Simple -A trivial implementation of all interceptor methods using the builders: +A trivial implementation of all interception methods using the builders: ```javascript -var interceptor = (new InterceptorBuilder()) - .withInterceptCall(function(options) { - const caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { - const newListener = (new ListenerBuilder()) - .withOnReceiveMetadata(function(metadata, next) { - next(metadata); - }) - .withOnReceiveMessage(function(message, next) { - next(message); - }) - .withOnReceiveStatus(function(status, next) { - next(status); - }) - .build(); - next(metadata, newListener); - }) - .withSendMessage(function(message, next) { - next(messasge); - }) - .withHalfClose(function(next) { - next(); - }) - .withCancel(function(message, next) { - next(); - }) - .build(); - return new InterceptingCall(options, caller); - }) - .build(); +var interceptor = function(options) { + const caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + const newListener = (new ListenerBuilder()) + .withOnReceiveMetadata(function(metadata, next) { + next(metadata); + }) + .withOnReceiveMessage(function(message, next) { + next(message); + }) + .withOnReceiveStatus(function(status, next) { + next(status); + }) + .build(); + next(metadata, newListener); + }) + .withSendMessage(function(message, next) { + next(messasge); + }) + .withHalfClose(function(next) { + next(); + }) + .withCancel(function(message, next) { + next(); + }) + .build(); + return new InterceptingCall(options, caller); +}; ``` (Callers and listeners do not need to implement all methods) @@ -264,49 +274,47 @@ An example of a caching interceptor for unary RPCs which stores the provided lis the call if there is a cache hit): ```javascript // Unary RPCs only -var interceptor = (new InterceptorBuilder()) - .withInterceptCall(function(options) { - var savedMetadata; - var startNext; - var savedListener; - var savedMessage; - var messageNext; - var caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { - savedMetadata = metadata; - savedListener = listener; - startNext = next; - }) - .withSendMessage(function(message, next) { - savedMessage = message; - messageNext = next; - }) - .withHalfClose(function(next) { - var cachedValue = _getCachedResponse(savedMessage.value); - if (cachedValue) { - var cachedMessage = new Message(cachedValue); - savedListener.onReceiveMetadata(new Metadata()); - savedListener.onReceiveMessage(cachedMessage); - savedListener.onReceiveStatus((new StatusBuilder()) - .withCode(grpc.status.OK) - .build()); - } else { - registry.addCall(savedMessage.value + '_miss'); - var newListener = (new ListenerBuilder()) - .withOnReceiveMessage(function(message, next) { - _store(savedMessage.value, message.value); - next(message); - }) - .build(); - startNext(savedMetadata, newListener); - messageNext(savedMessage); - next(); - } - }) - .build(); - return new InterceptingCall(options, caller); - }) - .build(); +var interceptor = function(options) { + var savedMetadata; + var startNext; + var savedListener; + var savedMessage; + var messageNext; + var caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + savedMetadata = metadata; + savedListener = listener; + startNext = next; + }) + .withSendMessage(function(message, next) { + savedMessage = message; + messageNext = next; + }) + .withHalfClose(function(next) { + var cachedValue = _getCachedResponse(savedMessage.value); + if (cachedValue) { + var cachedMessage = new Message(cachedValue); + savedListener.onReceiveMetadata(new Metadata()); + savedListener.onReceiveMessage(cachedMessage); + savedListener.onReceiveStatus((new StatusBuilder()) + .withCode(grpc.status.OK) + .build()); + } else { + registry.addCall(savedMessage.value + '_miss'); + var newListener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + _store(savedMessage.value, message.value); + next(message); + }) + .build(); + startNext(savedMetadata, newListener); + messageNext(savedMessage); + next(); + } + }) + .build(); + return new InterceptingCall(options, caller); +}; ``` **Retries** @@ -315,62 +323,60 @@ An example retry interceptor for unary RPCs creates new calls when the status sh ```javascript // Unary RPCs only var maxRetries = 3; -var interceptor = (new InterceptorBuilder()) - .withInterceptCall(function(options) { - var savedMetadata; - var savedSendMessage; - var savedReceiveMessage; - var savedMessageNext; - var methodName = options.method_descriptor.name; - var caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { - savedMetadata = metadata; - var new_listener = (new ListenerBuilder()) - .withOnReceiveMessage(function(message, next) { - savedReceiveMessage = message; - savedMessageNext = next; - }) - .withOnReceiveStatus(function(status, next) { - var retries = 0; - var retry = function(message, metadata, options) { - retries++; - client[methodName](message, metadata, options, function(err, response) { - if (err) { - if (retries <= maxRetries) { - retry(message, metadata, options); - } else { - savedMessageNext(response); - next(status); - } - return; +var interceptor = function(options) { + var savedMetadata; + var savedSendMessage; + var savedReceiveMessage; + var savedMessageNext; + var methodName = options.method_descriptor.name; + var caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + savedMetadata = metadata; + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + savedReceiveMessage = message; + savedMessageNext = next; + }) + .withOnReceiveStatus(function(status, next) { + var retries = 0; + var retry = function(message, metadata, options) { + retries++; + client[methodName](message, metadata, options, function(err, response) { + if (err) { + if (retries <= maxRetries) { + retry(message, metadata, options); + } else { + savedMessageNext(response); + next(status); } - var ok_status = (new StatusBuilder()) - .withCode(grpc.status.OK) - .build(); - savedMessageNext(response); - next(ok_status); - }); - }; - if (status.code !== grpc.status.OK) { - var new_options = _.clone(options); - new_options.interceptors = []; - retry(savedSendMessage, savedMetadata, new_options); - } else { - savedMessageNext(savedReceiveMessage); - next(status); - } - }) - .build(); - next(metadata, new_listener); - }) - .withSendMessage(function(message, next) { - savedSendMessage = message; - next(message); - }) - .build(); - return new InterceptingCall(options, caller); - }) - .build(); + return; + } + var ok_status = (new StatusBuilder()) + .withCode(grpc.status.OK) + .build(); + savedMessageNext(response); + next(ok_status); + }); + }; + if (status.code !== grpc.status.OK) { + var new_options = _.clone(options); + new_options.interceptors = []; + retry(savedSendMessage, savedMetadata, new_options); + } else { + savedMessageNext(savedReceiveMessage); + next(status); + } + }) + .build(); + next(metadata, new_listener); + }) + .withSendMessage(function(message, next) { + savedSendMessage = message; + next(message); + }) + .build(); + return new InterceptingCall(options, caller); +}; ``` **Fallbacks** @@ -378,35 +384,33 @@ var interceptor = (new InterceptorBuilder()) An example of providing fallbacks to failed requests for unary or client-streaming RPCs: ```javascript // Unary or client-streaming RPCs only -var interceptor = (new InterceptorBuilder()) - .withInterceptCall(function(options) { - var savedMessage; - var savedMessageNext; - var caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { - var new_listener = (new ListenerBuilder()) - .withOnReceiveMessage(function(message, next) { - savedMessage = message; - savedMessageNext = next; - }) - .withOnReceiveStatus(function(status, next) { - if (status.code !== grpc.status.OK) { - savedMessageNext(fallback_response); - next((new StatusBuilder()) - .withCode(grpc.status.OK) - .build()); - } else { - savedMessageNext(savedMessage); - next(status); - } - }) - .build(); - next(metadata, new_listener); - }) - .build(); - return new InterceptingCall(options, caller); - }) - .build(); +var interceptor = function(options) { + var savedMessage; + var savedMessageNext; + var caller = (new CallerBuilder()) + .withStart(function(metadata, listener, next) { + var new_listener = (new ListenerBuilder()) + .withOnReceiveMessage(function(message, next) { + savedMessage = message; + savedMessageNext = next; + }) + .withOnReceiveStatus(function(status, next) { + if (status.code !== grpc.status.OK) { + savedMessageNext(fallback_response); + next((new StatusBuilder()) + .withCode(grpc.status.OK) + .build()); + } else { + savedMessageNext(savedMessage); + next(status); + } + }) + .build(); + next(metadata, new_listener); + }) + .build(); + return new InterceptingCall(options, caller); +}; ``` ### Use @@ -478,14 +482,14 @@ The interceptor module will produce a proxy for the `grpc.Call` object which tri corresponding events. In the invocation of a `grpc.Call`'s `startBatch`, a set of gRPC opTypes are passed. This is the mapping of the -outbound opTypes to the interceptor methods they will trigger: +outbound opTypes to the interception methods they will trigger: ``` grpc.opType.SEND_INITIAL_METADATA -> start grpc.opType.SEND_MESSAGE -> sendMessage grpc.opType.SEND_CLOSE_FROM_CLIENT -> halfClose ``` -On the inbound side, these opTypes will map to these interceptor methods when responses are received from the server: +On the inbound side, these opTypes will map to these interception methods when responses are received from the server: ``` grpc.opType.RECV_INITIAL_METADATA -> onReceiveMetadata grpc.opType.RECV_MESSAGE -> onReceiveMessage @@ -506,12 +510,12 @@ interceptorA outbound -> interceptorB inbound -> interceptorA inbound ``` -All the appropriate interceptor methods on a given interceptor fire before the next interceptor is evaluated. (i.e. +All the appropriate interception methods on a given interceptor fire before the next interceptor is evaluated. (i.e. interceptorA's `start` and `sendMessage` would fire before interceptorB's `start` and `sendMessage`) #### Horizontal execution -The order of execution for interceptor methods is by operation. Given a chain of interceptors and a batch of operations, -we will execute all interceptor methods for the current operation before moving to the next. For example if you +The order of execution for interception methods is by operation. Given a chain of interceptors and a batch of operations, +we will execute all interception methods for the current operation before moving to the next. For example if you define three interceptors which all implement `start` and `sendMessage` methods, the execution order would be: ``` interceptorA start @@ -524,7 +528,7 @@ interceptorC sendMessage #### Serialization and Deserialization To provide interceptors with useful message objects, we will move all serialization and deserialization to a common -`startBatch` wrapper. This allows us to intercept the `startBatch` method and trigger outbound interceptor methods +`startBatch` wrapper. This allows us to intercept the `startBatch` method and trigger outbound interception methods before serialization happens. On the inbound side, we will deserialize the messages before triggering interceptor methods. @@ -557,7 +561,7 @@ The implementation will include: - A `client_interceptors.js` module with the logic for producing a `grpc.Call` proxy. - Modifying the `client.js` module to use the `client_interceptors` module. -- Tests which exercise the client interceptor methods for all four RPC types. +- Tests which exercise the client interception methods for all four RPC types. - Markdown documentation with example interceptors. All steps will be implemented by Netflix engineers. From 522370e2412e6e1df32c620fe4a1797d2dbe17e6 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Mon, 13 Mar 2017 13:17:59 -0700 Subject: [PATCH 07/11] Provider full example without builders --- NODEJS-CLIENT-INTERCEPTORS.md | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/NODEJS-CLIENT-INTERCEPTORS.md index 53c6403e5..b2486c3c3 100644 --- a/NODEJS-CLIENT-INTERCEPTORS.md +++ b/NODEJS-CLIENT-INTERCEPTORS.md @@ -227,34 +227,34 @@ var status = (new StatusBuilder()) ### Examples #### Simple -A trivial implementation of all interception methods using the builders: +A trivial implementation of all interception methods without using the builders: ```javascript var interceptor = function(options) { - const caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { - const newListener = (new ListenerBuilder()) - .withOnReceiveMetadata(function(metadata, next) { + var caller = { + start: function(metadata, listener, next) { + var newListener = { + onReceiveMetadata: function(metadata, next) { next(metadata); - }) - .withOnReceiveMessage(function(message, next) { + }, + onReceiveMessage: function(message, next) { next(message); - }) - .withOnReceiveStatus(function(status, next) { + }, + onReceiveStatus: function(status, next) { next(status); - }) - .build(); + } + }; next(metadata, newListener); - }) - .withSendMessage(function(message, next) { + }, + sendMessage: function(message, next) { next(messasge); - }) - .withHalfClose(function(next) { + }, + halfClose: function(next) { next(); - }) - .withCancel(function(message, next) { + }, + cancel: function(message, next) { next(); - }) - .build(); + } + }; return new InterceptingCall(options, caller); }; ``` From 5217e320057bb59ecdedfa255b19e0adf02d7462 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Wed, 5 Apr 2017 15:29:51 -0700 Subject: [PATCH 08/11] Rename proposal --- ...-CLIENT-INTERCEPTORS.md => L5-NODEJS-CLIENT-INTERCEPTORS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename NODEJS-CLIENT-INTERCEPTORS.md => L5-NODEJS-CLIENT-INTERCEPTORS.md (99%) diff --git a/NODEJS-CLIENT-INTERCEPTORS.md b/L5-NODEJS-CLIENT-INTERCEPTORS.md similarity index 99% rename from NODEJS-CLIENT-INTERCEPTORS.md rename to L5-NODEJS-CLIENT-INTERCEPTORS.md index b2486c3c3..4c1059256 100644 --- a/NODEJS-CLIENT-INTERCEPTORS.md +++ b/L5-NODEJS-CLIENT-INTERCEPTORS.md @@ -88,6 +88,7 @@ properties of the call which are used internally and may also be useful to inter ```javascript /** * @param {string} name The name of the call, i.e. 'myCall' + * @param {string} service_name The name of the service * @param {string} path The full path of the call, i.e. '/MyService/MyCall' * @param {MethodType} method_type One of four types: * MethodType.UNARY, @@ -98,7 +99,7 @@ properties of the call which are used internally and may also be useful to inter * @param {Function} deserialize The function used to deserialize a message * @constructor */ -MethodDescriptor(name, path, method_type, serialize, deserialize) +MethodDescriptor(name, service_name, path, method_type, serialize, deserialize) ``` *Do not modify the `options.method_descriptor` object, it is not used by the underlying gRPC code and will only affect From 78cbb40c0418947233779029eb848c0f2a936b28 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Tue, 2 May 2017 16:44:03 -0700 Subject: [PATCH 09/11] Add nextCall to NodeJS client interceptors proposal Reflects a new API where each interceptor has a parameter which can construct the next interceptor in the chain. Updates terminology and examples. --- L5-NODEJS-CLIENT-INTERCEPTORS.md | 297 +++++++++++++++++-------------- 1 file changed, 159 insertions(+), 138 deletions(-) diff --git a/L5-NODEJS-CLIENT-INTERCEPTORS.md b/L5-NODEJS-CLIENT-INTERCEPTORS.md index 4c1059256..165189af9 100644 --- a/L5-NODEJS-CLIENT-INTERCEPTORS.md +++ b/L5-NODEJS-CLIENT-INTERCEPTORS.md @@ -4,7 +4,7 @@ NodeJS Client Interceptors * Approver: a11r * Status: Draft * Implemented in: NodeJS -* Last updated: 2017-03-09 +* Last updated: 2017-05-02 * Discussion at: https://groups.google.com/forum/#!topic/grpc-io/LxT1JjN33Q4 ## Abstract @@ -27,8 +27,8 @@ None ### A simple example This example logs all outbound messages: ```javascript -var interceptor = function(options) { - return new InterceptingCall(options, { +var interceptor = function(options, nextCall) { + return new InterceptingCall(nextCall(options), { sendMessage: function(message, next) { console.log(message); next(message); @@ -39,7 +39,8 @@ client.myCall(message, { interceptors: [interceptor] }); ``` ### Interceptor API -An interceptor is a function which takes an `options` object and returns and `InterceptingCall` object: +An interceptor is a function which takes an `options` object and a `nextCall` function and returns an +`InterceptingCall` object: ```javascript /** @@ -50,36 +51,27 @@ An interceptor is a function which takes an `options` object and returns and `In * @param {string} [options.host] The server to call * @param {grpc.Call} [options.parent] The parent call * @param {number} [options.propagate_flags] Propagation flags for the underlying grpc.Call + * @param {function} nextCall Constructs the next interceptor in the chain * @return {InterceptingCall} */ -var interceptor = function(options) {} +var interceptor = function(options, nextCall) { + return new InterceptingCall(nextCall(options)); +}; ``` An interceptor function allows developers to modify the call options, store any call-scoped values needed, and define interception methods. -The interceptor function must return an `InterceptingCall` object. An `InterceptingCall` is a container for the call -options and an optional `caller` object. Returning `new InterceptingCall(options)` will satisfy the contract (but -provide no interceptor functionality). - -Most interceptors will define a `caller` object (described below) which is passed to the the `InterceptingCall` -constructor: `return new InterceptingCall(options, caller)` +The interceptor function must return an `InterceptingCall` object. An `InterceptingCall` represents an element in the +interceptor chain. Any intercepted methods are implemented in the `requester` object, an optional parameter to the +constructor. Returning `new InterceptingCall(nextCall(options))` will satisfy the contract (but provide no interceptor +functionality). -A simple interceptor example (using the optional CallerBuilder object): -```javascript -var interceptor = function(options) { - var caller = (new CallerBuilder()) - .withSendMessage(function(message, next) { - logger.log(message); - next(message); - }) - .build(); - return new InterceptingCall(options, caller); -}; -``` +Most interceptors will define a `requester` object (described below) which is passed to the the `InterceptingCall` +constructor: `return new InterceptingCall(nextCall(options), requester)` -The `options` argument to the `InterceptingCall` constructor includes all the options accepted by the base `grpc.Call` -constructor. Modifying the options that are passed to the `InterceptingCall` constructor will have the effect of +The `options` argument to the `nextCall` function includes all the options accepted by the base `grpc.Call` +constructor. Modifying the options that are passed to the `nextCall` function will have the effect of changing the options passed to the underlying `grpc.Call` constructor. Additionally, the `options` argument includes a `MethodDescriptor` object. The `MethodDescriptor` is a container for @@ -102,10 +94,10 @@ properties of the call which are used internally and may also be useful to inter MethodDescriptor(name, service_name, path, method_type, serialize, deserialize) ``` -*Do not modify the `options.method_descriptor` object, it is not used by the underlying gRPC code and will only affect -downstream interceptors* +*Do not modify the `options.method_descriptor` object. The MethodDescriptor passed to the interceptor options is not +consumed by the underlying gRPC code and changes will only affect downstream interceptors* -A `caller` object is a POJO implementing zero or more outbound interception methods: +A `requester` object is a plain Javascript object implementing zero or more outbound interception methods: ```javascript /** @@ -141,10 +133,10 @@ halfClose(next) cancel(message, next) ``` -A `CallerBuilder` will be provided to easily construct an outbound interceptor: +A `RequesterBuilder` will be provided to easily construct an outbound interceptor: ```javascript -var caller = (new CallerBuilder()) +var requester = (new RequesterBuilder()) .withStart(function(metadata, listener, next) { logger.log(metadata); next(metadata, listener); @@ -157,14 +149,15 @@ var caller = (new CallerBuilder()) ``` A `listener` object implements zero or more methods for intercepting inbound operations. The listener passed into -a caller's `start` method implements all the inbound interception methods and represents the listener chain constructed -by any previous interceptors. Three usage patterns are supported for listeners: +a requester's `start` method implements all the inbound interception methods. Inbound operations will be passed through +a listener at each step in the interceptor chain. Three usage patterns are supported for listeners: 1) Pass the listener along without modification: `next(metadata, listener)`. In this case the interceptor declines to intercept any inbound operations. 2) Create a new listener with one or more inbound interception methods and pass it to `next`. In this case the interceptor will fire on the inbound operations implemented in the new listener. 3) Store the listener to make direct inbound calls on later. This effectively short-circuits the interceptor stack. -An example of an interceptor using this pattern to provide client-side caching is included below. +An example of an interceptor using this pattern to provide client-side caching is included below in the Examples +section. *Do not modify the listener passed in. Either pass it along unmodified or call methods on it to short-circuit the interceptor stack.* @@ -230,8 +223,8 @@ var status = (new StatusBuilder()) #### Simple A trivial implementation of all interception methods without using the builders: ```javascript -var interceptor = function(options) { - var caller = { +var interceptor = function(options, nextCall) { + var requester = { start: function(metadata, listener, next) { var newListener = { onReceiveMetadata: function(metadata, next) { @@ -256,18 +249,16 @@ var interceptor = function(options) { next(); } }; - return new InterceptingCall(options, caller); + return new InterceptingCall(nextCall(options), requester); }; ``` -(Callers and listeners do not need to implement all methods) +(Requesters and listeners do not need to implement all methods) -#### Advanced -These advanced examples are specific to certain types of RPC calls (unary, client-streaming, etc), because interceptors -operate on gRPC batches. Any given interceptor must work for every batch in the RPC calls it applies to. Since different -types of RPC calls construct batches with different sets of operations, interceptors which delay or short-circuit -operations will run into batch boundaries for some RPC call types. The limitations should be intuitive, for example: a -caching interceptor would not make sense for a streaming RPC given that inbound messages are not associated with -outbound messages. +#### Advanced Examples +These advanced examples are specific to certain types of RPC calls (unary, client-streaming, etc). Using interceptors +to provide advanced behavior such as delays, retries and caching will not apply to all RPC types. The limitations should +be intuitive, for example: a caching interceptor would not make sense for a bidi streaming RPC given that inbound +messages are not associated with outbound messages. **Caching** @@ -275,46 +266,43 @@ An example of a caching interceptor for unary RPCs which stores the provided lis the call if there is a cache hit): ```javascript // Unary RPCs only -var interceptor = function(options) { +var interceptor = function(options, nextCall) { var savedMetadata; var startNext; var savedListener; var savedMessage; var messageNext; - var caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { + var requester = { + start: function(metadata, listener, next) { savedMetadata = metadata; savedListener = listener; startNext = next; - }) - .withSendMessage(function(message, next) { + }, + sendMessage: function(message, next) { savedMessage = message; messageNext = next; - }) - .withHalfClose(function(next) { + }, + halfClose: function(next) { var cachedValue = _getCachedResponse(savedMessage.value); if (cachedValue) { var cachedMessage = new Message(cachedValue); savedListener.onReceiveMetadata(new Metadata()); savedListener.onReceiveMessage(cachedMessage); - savedListener.onReceiveStatus((new StatusBuilder()) - .withCode(grpc.status.OK) - .build()); + savedListener.onReceiveStatus({code: grpc.status.OK}); } else { - registry.addCall(savedMessage.value + '_miss'); - var newListener = (new ListenerBuilder()) - .withOnReceiveMessage(function(message, next) { + var newListener = { + onReceiveMessage: function(message, next) { _store(savedMessage.value, message.value); next(message); - }) - .build(); + } + }; startNext(savedMetadata, newListener); messageNext(savedMessage); next(); } - }) - .build(); - return new InterceptingCall(options, caller); + } + }; + return new InterceptingCall(nextCall(options), requester); }; ``` @@ -324,59 +312,59 @@ An example retry interceptor for unary RPCs creates new calls when the status sh ```javascript // Unary RPCs only var maxRetries = 3; -var interceptor = function(options) { +var interceptor = function(options, nextCall) { var savedMetadata; var savedSendMessage; var savedReceiveMessage; var savedMessageNext; - var methodName = options.method_descriptor.name; - var caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { + var requester = { + start: function(metadata, listener, next) { savedMetadata = metadata; - var new_listener = (new ListenerBuilder()) - .withOnReceiveMessage(function(message, next) { + var newListener = { + onReceiveMessage: function(message, next) { savedReceiveMessage = message; savedMessageNext = next; - }) - .withOnReceiveStatus(function(status, next) { + }, + onReceiveStatus: function(status, next) { var retries = 0; - var retry = function(message, metadata, options) { + var retry = function(message, metadata) { retries++; - client[methodName](message, metadata, options, function(err, response) { - if (err) { - if (retries <= maxRetries) { - retry(message, metadata, options); + var newCall = nextCall(options); + newCall.start(metadata, { + onReceiveMessage: function(message) { + savedReceiveMessage = message; + }, + onReceiveStatus: function(status) { + if (status.code !== grpc.status.OK) { + if (retries <= maxRetries) { + retry(message, metadata); + } else { + savedMessageNext(savedReceiveMessage); + next(status); + } } else { - savedMessageNext(response); - next(status); + savedMessageNext(savedReceiveMessage); + next({code: grpc.status.OK}); } - return; } - var ok_status = (new StatusBuilder()) - .withCode(grpc.status.OK) - .build(); - savedMessageNext(response); - next(ok_status); - }); + }) }; if (status.code !== grpc.status.OK) { - var new_options = _.clone(options); - new_options.interceptors = []; - retry(savedSendMessage, savedMetadata, new_options); + retry(savedSendMessage, savedMetadata); } else { savedMessageNext(savedReceiveMessage); next(status); } - }) - .build(); - next(metadata, new_listener); - }) - .withSendMessage(function(message, next) { + } + }; + next(metadata, newListener); + }, + sendMessage: function(message, next) { savedSendMessage = message; next(message); - }) - .build(); - return new InterceptingCall(options, caller); + } + }; + return new InterceptingCall(nextCall(options), requester); }; ``` @@ -385,32 +373,31 @@ var interceptor = function(options) { An example of providing fallbacks to failed requests for unary or client-streaming RPCs: ```javascript // Unary or client-streaming RPCs only -var interceptor = function(options) { +var fallbackResponse = new Message('fallback'); +var interceptor = function(options, nextCall) { var savedMessage; var savedMessageNext; - var caller = (new CallerBuilder()) - .withStart(function(metadata, listener, next) { - var new_listener = (new ListenerBuilder()) - .withOnReceiveMessage(function(message, next) { + var requester = { + start: function(metadata, listener, next) { + var new_listener = { + onReceiveMessage: function(message, next) { savedMessage = message; savedMessageNext = next; - }) - .withOnReceiveStatus(function(status, next) { + }, + onReceiveStatus: function(status, next) { if (status.code !== grpc.status.OK) { - savedMessageNext(fallback_response); - next((new StatusBuilder()) - .withCode(grpc.status.OK) - .build()); + savedMessageNext(fallbackResponse); + next({node: grpc.status.OK}); } else { savedMessageNext(savedMessage); next(status); } - }) - .build(); + } + }; next(metadata, new_listener); - }) - .build(); - return new InterceptingCall(options, caller); + } + }; + return new InterceptingCall(nextCall(options), requester); }; ``` @@ -478,19 +465,28 @@ options. ### Internal implementation #### gRPC Operations -A new `client_interceptors` module will be added and the `getCall` method in `client.js` will be modified to use it. -The interceptor module will produce a proxy for the `grpc.Call` object which triggers any interceptors provided on the -corresponding events. +To intercept operations at the call level, two major changes to the gRPC NodeJS client implementation are required: +1) Split out the logic for calling `startBatch` and for handling the results of `startBatch`. Interceptor chains for +each operation need to run on the outbound path and the inbound batch before this logic. + +2) Wrap each call with the logic for assembling interceptor chains and executing them. -In the invocation of a `grpc.Call`'s `startBatch`, a set of gRPC opTypes are passed. This is the mapping of the -outbound opTypes to the interception methods they will trigger: +For 1), we will add a `client_batches` module which will enumerate each distinct 'batch type' used by gRPC clients. The +batching logic currently in the `makeUnaryRequest` and similar functions will be moved to the `client_batches` module, +with methods for wiring a call instance up to the batch logic and the interceptor chains. + +For 2), we will add a `client_interceptors` module with methods for building interceptor chains and triggering +underlying calls. + +The `requester` and `listener` methods correspond to gRPC batch operations. This is the mapping of the +outbound opTypes to the `requester` methods they will trigger: ``` grpc.opType.SEND_INITIAL_METADATA -> start grpc.opType.SEND_MESSAGE -> sendMessage grpc.opType.SEND_CLOSE_FROM_CLIENT -> halfClose ``` -On the inbound side, these opTypes will map to these interception methods when responses are received from the server: +On the inbound side, these opTypes will map to these `listener` methods when responses are received from the server: ``` grpc.opType.RECV_INITIAL_METADATA -> onReceiveMetadata grpc.opType.RECV_MESSAGE -> onReceiveMessage @@ -511,12 +507,10 @@ interceptorA outbound -> interceptorB inbound -> interceptorA inbound ``` -All the appropriate interception methods on a given interceptor fire before the next interceptor is evaluated. (i.e. -interceptorA's `start` and `sendMessage` would fire before interceptorB's `start` and `sendMessage`) #### Horizontal execution The order of execution for interception methods is by operation. Given a chain of interceptors and a batch of operations, -we will execute all interception methods for the current operation before moving to the next. For example if you +we will execute all interception methods for one operation before moving to the next. For example if you define three interceptors which all implement `start` and `sendMessage` methods, the execution order would be: ``` interceptorA start @@ -527,27 +521,53 @@ interceptorB sendMessage interceptorC sendMessage ``` -#### Serialization and Deserialization -To provide interceptors with useful message objects, we will move all serialization and deserialization to a common -`startBatch` wrapper. This allows us to intercept the `startBatch` method and trigger outbound interception methods -before serialization happens. On the inbound side, we will deserialize the messages before triggering interceptor -methods. - -We also box/unbox Metadata objects in the same way so interceptors get useful Metadata. +#### Batch Types +There are 8 distinct batch types in the NodeJS client implementation, and each RPC type uses some combination of those +8 batch types: +- Unary (all operations) +- Metadata only +- Close only +- Sending streaming message +- Receive streaming message +- Receive status +- Send synchronous message +- Receive synchronous message + +This proposal moves the logic for these batch types out of the RPC functions to reduce code duplication and provide +a common mechanism for intercepting each batch type. The execution order for a metadata-only batch with 3 interceptors +will look like this: +##### Outbound +``` +Consumer makes client call +Interceptor A start +Interceptor B start +Interceptor C start +Outbound batch logic: + Convert metadata to internal representation + Call startBatch with metadata operations +``` +##### Inbound +``` +Metadata received from server +Convert internal representation to Metadata object +Interceptor C Receive Metadata +Interceptor B Receive Metadata +Interceptor A Receive Metadata +Inbound batch logic: + Emit resulting metadata +``` -To do this requires passing certain values through the `grpc.Call` proxies via a new `context` parameter to -`startBatch`. +#### Initialization +Interceptors are initialized at each gRPC call invocation. A call which runs multiple batches will reuse the same +interceptors across all batches. A second invocation of the gRPC call will initialize new interceptors. Interceptors +can use this to store call-scoped data in their initialization function. ## Rationale ### Abstraction level -Interceptors are implemented by wrapping the `grpc.Call#startBatch` method. This level of abstraction avoids coupling -interceptors with much of the NodeJS client code since `startBatch` is the common entry point for all gRPC operations. - -- If we intercepted calls at a higher level, the interceptor framework would have to account for the different types -of RPCs (unary, clientStream, serverStream, bidirectional) and could not provide the same interceptor granularity -that other gRPC language interceptors provide. -- We could intercept calls at a lower level (`call.cc`), but no additional interceptor granularity would be provided. +Interceptors are call-scoped so interceptor authors will be able to reason about the initialization of their +interceptors easily. This requires more changes to client.js than intercepting batches individually, but makes it easier +for interceptors to operate on different RPC types without special cases. ### Invocation-specific and client-wide interceptor configuration Clients can be configured with interceptor providers at construction time, or interceptors can be passed to individual @@ -560,8 +580,9 @@ construction are ignored. The implementation will include: -- A `client_interceptors.js` module with the logic for producing a `grpc.Call` proxy. -- Modifying the `client.js` module to use the `client_interceptors` module. +- A `client_interceptors.js` module with the logic for constructing and executing interceptor chains. +- A `client_batches.js` module to define the batch types and provide a common mechanism to intercept them. +- Modifying the `client.js` module to use the new modules. - Tests which exercise the client interception methods for all four RPC types. - Markdown documentation with example interceptors. From 6a01c9a32cc109e8b1d50b780aae3a1ba4b56bc8 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Thu, 1 Jun 2017 11:39:55 -0700 Subject: [PATCH 10/11] Update document header --- L5-NODEJS-CLIENT-INTERCEPTORS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/L5-NODEJS-CLIENT-INTERCEPTORS.md b/L5-NODEJS-CLIENT-INTERCEPTORS.md index 165189af9..a43a3096d 100644 --- a/L5-NODEJS-CLIENT-INTERCEPTORS.md +++ b/L5-NODEJS-CLIENT-INTERCEPTORS.md @@ -1,10 +1,10 @@ NodeJS Client Interceptors ---- * Author(s): David Vroom-Duke (dduke@netflix.com), William Thurston (wthurston@netflix.com), David Liu (dliu@netflix.com), Howard Yuan (hyuan@netflix.com), Eran Landau (elandau@netflix.com) -* Approver: a11r -* Status: Draft +* Approver: murgatroid99 +* Status: Final * Implemented in: NodeJS -* Last updated: 2017-05-02 +* Last updated: 2017-06-01 * Discussion at: https://groups.google.com/forum/#!topic/grpc-io/LxT1JjN33Q4 ## Abstract From 6ad4b09846ae615470e9115c3622a960e60766b6 Mon Sep 17 00:00:00 2001 From: David Vroom Duke Date: Thu, 5 Oct 2017 15:59:21 -0700 Subject: [PATCH 11/11] Add guidance on short-circuiting and errors --- L5-NODEJS-CLIENT-INTERCEPTORS.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/L5-NODEJS-CLIENT-INTERCEPTORS.md b/L5-NODEJS-CLIENT-INTERCEPTORS.md index a43a3096d..7bfdefa33 100644 --- a/L5-NODEJS-CLIENT-INTERCEPTORS.md +++ b/L5-NODEJS-CLIENT-INTERCEPTORS.md @@ -157,7 +157,8 @@ to intercept any inbound operations. interceptor will fire on the inbound operations implemented in the new listener. 3) Store the listener to make direct inbound calls on later. This effectively short-circuits the interceptor stack. An example of an interceptor using this pattern to provide client-side caching is included below in the Examples -section. +section. Short-circuiting a request in this way will skip interceptors which have not yet fired, but will fire the +listeners on any 'earlier' interceptors in the stack. *Do not modify the listener passed in. Either pass it along unmodified or call methods on it to short-circuit the interceptor stack.* @@ -218,6 +219,21 @@ var status = (new StatusBuilder()) **To intercept trailers, examine `status.metadata` in the `onReceiveStatus` method.** +#### Internal Errors and Exceptions + +Exceptions are not handled by the interceptor framework, we expect +interceptor authors to either handle the exceptions their interceptors +can throw or assume the Node process will crash on exceptions. This is in +accordance with the [Joyent guidance](https://www.joyent.com/node-js/production/design/errors) +to only catch exceptions which are known to be operational errors. + +In the case of an error during the execution of an interceptor, +interceptor authors have the following options: +1) Short-circuiting the call and returning a gRPC error to the client's consumer. +2) Throwing an exception which the client's consumer will have to catch +or crash on. +3) Doing nothing and continuing with the call. + ### Examples #### Simple