diff --git a/packages/grpc-native-core/ext/call.cc b/packages/grpc-native-core/ext/call.cc index 8922bc66..25370759 100644 --- a/packages/grpc-native-core/ext/call.cc +++ b/packages/grpc-native-core/ext/call.cc @@ -81,8 +81,13 @@ Local nanErrorWithCode(const char *msg, grpc_call_error code) { return scope.Escape(err); } -bool CreateMetadataArray(Local metadata, grpc_metadata_array *array) { +bool CreateMetadataArray(Local metadata_obj, grpc_metadata_array *array) { HandleScope scope; + Local metadata_value = (Nan::Get(metadata_obj, Nan::New("metadata").ToLocalChecked())).ToLocalChecked(); + if (!metadata_value->IsObject()) { + return false; + } + Local metadata = Nan::To(metadata_value).ToLocalChecked(); Local keys = Nan::GetOwnPropertyNames(metadata).ToLocalChecked(); for (unsigned int i = 0; i < keys->Length(); i++) { Local current_key = @@ -159,7 +164,10 @@ Local ParseMetadata(const grpc_metadata_array *metadata_array) { Nan::Set(array, array->Length(), CopyStringFromSlice(elem->value)); } } - return scope.Escape(metadata_object); + Local result = Nan::New(); + Nan::Set(result, Nan::New("metadata").ToLocalChecked(), metadata_object); + Nan::Set(result, Nan::New("flags").ToLocalChecked(), Nan::New(0)); + return scope.Escape(result); } Local Op::GetOpType() const { @@ -185,7 +193,17 @@ class SendMetadataOp : public Op { if (maybe_metadata.IsEmpty()) { return false; } - if (!CreateMetadataArray(maybe_metadata.ToLocalChecked(), &send_metadata)) { + Local metadata_object = maybe_metadata.ToLocalChecked(); + MaybeLocal maybe_flag_value = + Nan::Get(metadata_object, Nan::New("flags").ToLocalChecked()); + if (!maybe_flag_value.IsEmpty()) { + Local flag_value = maybe_flag_value.ToLocalChecked(); + if (flag_value->IsUint32()) { + Maybe maybe_flag = Nan::To(flag_value); + out->flags |= maybe_flag.FromMaybe(0) & GRPC_INITIAL_METADATA_USED_MASK; + } + } + if (!CreateMetadataArray(metadata_object, &send_metadata)) { return false; } out->data.send_initial_metadata.count = send_metadata.count; @@ -225,7 +243,7 @@ class SendMessageOp : public Op { Local flag_value = maybe_flag_value.ToLocalChecked(); if (flag_value->IsUint32()) { Maybe maybe_flag = Nan::To(flag_value); - out->flags = maybe_flag.FromMaybe(0) & GRPC_WRITE_USED_MASK; + out->flags |= maybe_flag.FromMaybe(0) & GRPC_WRITE_USED_MASK; } } send_message = BufferToByteBuffer(value); diff --git a/packages/grpc-native-core/index.d.ts b/packages/grpc-native-core/index.d.ts index 808051d0..17790654 100644 --- a/packages/grpc-native-core/index.d.ts +++ b/packages/grpc-native-core/index.d.ts @@ -489,10 +489,29 @@ declare module "grpc" { type sendUnaryData = (error: ServiceError | null, value: ResponseType | null, trailer?: Metadata, flags?: number) => void; + interface MetadataOptions { + /* Signal that the request is idempotent. Defaults to false */ + idempotentRequest?: boolean; + /* Signal that the call should not return UNAVAILABLE before it has + * started. Defaults to true. */ + waitForReady?: boolean; + /* Signal that the call is cacheable. GRPC is free to use GET verb. + * Defaults to false */ + cacheableRequest?: boolean; + /* Signal that the initial metadata should be corked. Defaults to false. */ + corked?: boolean; + } + /** * A class for storing metadata. Keys are normalized to lowercase ASCII. */ export class Metadata { + /** + * @param options Boolean options for the beginning of the call. + * These options only have any effect when passed at the beginning of + * a client request. + */ + constructor(options?: MetadataOptions); /** * Sets the given value for the given key by replacing any other values * associated with that key. Normalizes the key. @@ -536,6 +555,14 @@ declare module "grpc" { * @return The newly cloned object. */ clone(): Metadata; + + /** + * Set options on the metadata object + * @param options Boolean options for the beginning of the call. + * These options only have any effect when passed at the beginning of + * a client request. + */ + setOptions(options: MetadataOptions); } export type MetadataValue = string | Buffer; diff --git a/packages/grpc-native-core/src/metadata.js b/packages/grpc-native-core/src/metadata.js index 31c6329e..ee442953 100644 --- a/packages/grpc-native-core/src/metadata.js +++ b/packages/grpc-native-core/src/metadata.js @@ -22,18 +22,36 @@ var clone = require('lodash.clone'); var grpc = require('./grpc_extension'); +const IDEMPOTENT_REQUEST_FLAG = 0x10; +const WAIT_FOR_READY_FLAG = 0x20; +const CACHEABLE_REQUEST_FLAG = 0x40; +const WAIT_FOR_READY_EXPLICITLY_SET_FLAG = 0x80; +const CORKED_FLAG = 0x100; + /** * Class for storing metadata. Keys are normalized to lowercase ASCII. * @memberof grpc * @constructor + * @param {Object=} options Boolean options for the beginning of the call. + * These options only have any effect when passed at the beginning of + * a client request. + * @param {boolean=} [options.idempotentRequest=false] Signal that the request + * is idempotent + * @param {boolean=} [options.waitForReady=true] Signal that the call should + * not return UNAVAILABLE before it has started. + * @param {boolean=} [options.cacheableRequest=false] Signal that the call is + * cacheable. GRPC is free to use GET verb. + * @param {boolean=} [options.corked=false] Signal that the initial metadata + * should be corked. * @example * var metadata = new metadata_module.Metadata(); * metadata.set('key1', 'value1'); * metadata.add('key1', 'value2'); * metadata.get('key1') // returns ['value1', 'value2'] */ -function Metadata() { +function Metadata(options) { this._internal_repr = {}; + this.setOptions(options); } function normalizeKey(key) { @@ -141,34 +159,82 @@ Metadata.prototype.clone = function() { const value = this._internal_repr[key]; copy._internal_repr[key] = clone(value); }); + copy.flags = this.flags; return copy; }; +/** + * Set options on the metadata object + * @param {Object} options Boolean options for the beginning of the call. + * These options only have any effect when passed at the beginning of + * a client request. + * @param {boolean=} [options.idempotentRequest=false] Signal that the request + * is idempotent + * @param {boolean=} [options.waitForReady=true] Signal that the call should + * not return UNAVAILABLE before it has started. + * @param {boolean=} [options.cacheableRequest=false] Signal that the call is + * cacheable. GRPC is free to use GET verb. + * @param {boolean=} [options.corked=false] Signal that the initial metadata + * should be corked. + */ +Metadata.prototype.setOptions = function(options) { + let flags = 0; + if (options) { + if (options.idempotentRequest) { + flags |= IDEMPOTENT_REQUEST_FLAG; + } + if (options.hasOwnProperty('waitForReady')) { + flags |= WAIT_FOR_READY_EXPLICITLY_SET_FLAG; + if (options.waitForReady) { + flags |= WAIT_FOR_READY_FLAG; + } + } + if (options.cacheableRequest) { + flags |= CACHEABLE_REQUEST_FLAG; + } + if (options.corked) { + flags |= CORKED_FLAG; + } + } + this.flags = flags; +} + +/** + * Metadata representation as passed to and the native addon + * @typedef {object} grpc~CoreMetadata + * @param {Object.>} metadata The metadata + * @param {number} flags Metadata flags + */ + /** * Gets the metadata in the format used by interal code. Intended for internal * use only. API stability is not guaranteed. * @private - * @return {Object.>} The metadata + * @return {grpc~CoreMetadata} The metadata */ Metadata.prototype._getCoreRepresentation = function() { - return this._internal_repr; + return { + metadata: this._internal_repr, + flags: this.flags + }; }; /** * Creates a Metadata object from a metadata map in the internal format. * Intended for internal use only. API stability is not guaranteed. * @private - * @param {Object.>} The metadata + * @param {grpc~CoreMetadata} metadata The metadata object from core * @return {Metadata} The new Metadata object */ Metadata._fromCoreRepresentation = function(metadata) { var newMetadata = new Metadata(); if (metadata) { - Object.keys(metadata).forEach(key => { - const value = metadata[key]; + Object.keys(metadata.metadata).forEach(key => { + const value = metadata.metadata[key]; newMetadata._internal_repr[key] = clone(value); }); } + newMetadata.flags = metadata.flags; return newMetadata; }; diff --git a/packages/grpc-native-core/test/call_test.js b/packages/grpc-native-core/test/call_test.js index bba4cce0..42fd3d01 100644 --- a/packages/grpc-native-core/test/call_test.js +++ b/packages/grpc-native-core/test/call_test.js @@ -128,8 +128,9 @@ describe('call', function() { var call = channel.createCall('method', getDeadline(1)); assert.doesNotThrow(function() { var batch = {}; - batch[grpc.opType.SEND_INITIAL_METADATA] = {'key1': ['value1'], - 'key2': ['value2']}; + batch[grpc.opType.SEND_INITIAL_METADATA] = { + metadata: {'key1': ['value1'], 'key2': ['value2']} + }; call.startBatch(batch, function(err, resp) { assert.ifError(err); assert.deepEqual(resp, {'send_metadata': true}); @@ -142,8 +143,10 @@ describe('call', function() { assert.doesNotThrow(function() { var batch = {}; batch[grpc.opType.SEND_INITIAL_METADATA] = { - 'key1-bin': [Buffer.from('value1')], - 'key2-bin': [Buffer.from('value2')] + metadata: { + 'key1-bin': [Buffer.from('value1')], + 'key2-bin': [Buffer.from('value2')] + } }; call.startBatch(batch, function(err, resp) { assert.ifError(err); @@ -174,6 +177,11 @@ describe('call', function() { batch[grpc.opType.SEND_INITIAL_METADATA] = 5; call.startBatch(batch, function(){}); }, TypeError); + assert.throws(function() { + var batch = {}; + batch[grpc.opType.SEND_INITIAL_METADATA] = {}; + call.startBatch(batch, function(){}); + }, TypeError); }); }); describe('startBatch with message', function() { diff --git a/packages/grpc-native-core/test/end_to_end_test.js b/packages/grpc-native-core/test/end_to_end_test.js index da6fac6c..ae165b14 100644 --- a/packages/grpc-native-core/test/end_to_end_test.js +++ b/packages/grpc-native-core/test/end_to_end_test.js @@ -65,7 +65,7 @@ describe('end-to-end', function() { 'dummy_method', Infinity); var client_batch = {}; - client_batch[grpc.opType.SEND_INITIAL_METADATA] = {}; + client_batch[grpc.opType.SEND_INITIAL_METADATA] = {metadata: {}}; client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; client_batch[grpc.opType.RECV_INITIAL_METADATA] = true; client_batch[grpc.opType.RECV_STATUS_ON_CLIENT] = true; @@ -74,11 +74,17 @@ describe('end-to-end', function() { assert.deepEqual(response, { send_metadata: true, client_close: true, - metadata: {}, + metadata: { + metadata: {}, + flags: 0 + }, status: { code: constants.status.OK, details: status_text, - metadata: {} + metadata: { + metadata: {}, + flags: 0 + } } }); done(); @@ -90,9 +96,9 @@ describe('end-to-end', function() { var server_call = new_call.call; assert.notEqual(server_call, null); var server_batch = {}; - server_batch[grpc.opType.SEND_INITIAL_METADATA] = {}; + server_batch[grpc.opType.SEND_INITIAL_METADATA] = {metadata: {}}; server_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = { - metadata: {}, + metadata: {metadata: {}}, code: constants.status.OK, details: status_text }; @@ -116,7 +122,9 @@ describe('end-to-end', function() { Infinity); var client_batch = {}; client_batch[grpc.opType.SEND_INITIAL_METADATA] = { - client_key: ['client_value'] + metadata: { + client_key: ['client_value'] + } }; client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; client_batch[grpc.opType.RECV_INITIAL_METADATA] = true; @@ -126,10 +134,13 @@ describe('end-to-end', function() { assert.deepEqual(response,{ send_metadata: true, client_close: true, - metadata: {server_key: ['server_value']}, + metadata: {metadata: { + server_key: ['server_value']}, + flags: 0 + }, status: {code: constants.status.OK, details: status_text, - metadata: {}} + metadata: {metadata: {}, flags: 0}} }); done(); }); @@ -137,16 +148,18 @@ describe('end-to-end', function() { server.requestCall(function(err, call_details) { var new_call = call_details.new_call; assert.notEqual(new_call, null); - assert.strictEqual(new_call.metadata.client_key[0], + assert.strictEqual(new_call.metadata.metadata.client_key[0], 'client_value'); var server_call = new_call.call; assert.notEqual(server_call, null); var server_batch = {}; server_batch[grpc.opType.SEND_INITIAL_METADATA] = { - server_key: ['server_value'] + metadata: { + server_key: ['server_value'] + } }; server_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = { - metadata: {}, + metadata: {metadata: {}}, code: constants.status.OK, details: status_text }; @@ -171,7 +184,7 @@ describe('end-to-end', function() { 'dummy_method', Infinity); var client_batch = {}; - client_batch[grpc.opType.SEND_INITIAL_METADATA] = {}; + client_batch[grpc.opType.SEND_INITIAL_METADATA] = {metadata: {}}; client_batch[grpc.opType.SEND_MESSAGE] = Buffer.from(req_text); client_batch[grpc.opType.SEND_CLOSE_FROM_CLIENT] = true; client_batch[grpc.opType.RECV_INITIAL_METADATA] = true; @@ -181,12 +194,12 @@ describe('end-to-end', function() { assert.ifError(err); assert(response.send_metadata); assert(response.client_close); - assert.deepEqual(response.metadata, {}); + assert.deepEqual(response.metadata, {metadata: {}, flags: 0}); assert(response.send_message); assert.strictEqual(response.read.toString(), reply_text); assert.deepEqual(response.status, {code: constants.status.OK, details: status_text, - metadata: {}}); + metadata: {metadata: {}, flags: 0}}); done(); }); @@ -196,7 +209,7 @@ describe('end-to-end', function() { var server_call = new_call.call; assert.notEqual(server_call, null); var server_batch = {}; - server_batch[grpc.opType.SEND_INITIAL_METADATA] = {}; + server_batch[grpc.opType.SEND_INITIAL_METADATA] = {metadata: {}}; server_batch[grpc.opType.RECV_MESSAGE] = true; server_call.startBatch(server_batch, function(err, response) { assert.ifError(err); @@ -205,7 +218,7 @@ describe('end-to-end', function() { var response_batch = {}; response_batch[grpc.opType.SEND_MESSAGE] = Buffer.from(reply_text); response_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = { - metadata: {}, + metadata: {metadata: {}}, code: constants.status.OK, details: status_text }; @@ -226,7 +239,7 @@ describe('end-to-end', function() { 'dummy_method', Infinity); var client_batch = {}; - client_batch[grpc.opType.SEND_INITIAL_METADATA] = {}; + client_batch[grpc.opType.SEND_INITIAL_METADATA] = {metadata: {}}; client_batch[grpc.opType.SEND_MESSAGE] = Buffer.from(requests[0]); client_batch[grpc.opType.RECV_INITIAL_METADATA] = true; call.startBatch(client_batch, function(err, response) { @@ -234,7 +247,7 @@ describe('end-to-end', function() { assert.deepEqual(response, { send_metadata: true, send_message: true, - metadata: {} + metadata: {metadata: {}, flags: 0} }); var req2_batch = {}; req2_batch[grpc.opType.SEND_MESSAGE] = Buffer.from(requests[1]); @@ -248,7 +261,7 @@ describe('end-to-end', function() { status: { code: constants.status.OK, details: status_text, - metadata: {} + metadata: {metadata: {}, flags: 0} } }); done(); @@ -261,7 +274,7 @@ describe('end-to-end', function() { var server_call = new_call.call; assert.notEqual(server_call, null); var server_batch = {}; - server_batch[grpc.opType.SEND_INITIAL_METADATA] = {}; + server_batch[grpc.opType.SEND_INITIAL_METADATA] = {metadata: {}}; server_batch[grpc.opType.RECV_MESSAGE] = true; server_call.startBatch(server_batch, function(err, response) { assert.ifError(err); @@ -275,7 +288,7 @@ describe('end-to-end', function() { var end_batch = {}; end_batch[grpc.opType.RECV_CLOSE_ON_SERVER] = true; end_batch[grpc.opType.SEND_STATUS_FROM_SERVER] = { - metadata: {}, + metadata: {metadata: {}}, code: constants.status.OK, details: status_text }; diff --git a/packages/grpc-native-core/test/surface_test.js b/packages/grpc-native-core/test/surface_test.js index bc745957..75183d5e 100644 --- a/packages/grpc-native-core/test/surface_test.js +++ b/packages/grpc-native-core/test/surface_test.js @@ -934,6 +934,19 @@ describe('Other conditions', function() { call.write({}); call.end(); }); + it('client should drop a call if not connected with waitForReady off', function(done) { + /* We have to wait for the client to reach the first connection timeout + * and go to TRANSIENT_FAILURE to confirm that the waitForReady option + * makes it end the call instead of continuing to try. A DNS resolution + * failure makes that transition very fast. */ + const disconnectedClient = new Client('nothing.invalid:50051', grpc.credentials.createInsecure()); + const metadata = new grpc.Metadata({waitForReady: false}); + disconnectedClient.unary({}, metadata, (error, value) =>{ + assert(error); + assert.strictEqual(error.code, grpc.status.UNAVAILABLE); + done(); + }); + }); describe('Server recieving bad input', function() { var misbehavingClient; var badArg = Buffer.from([0xFF]);