Native: Add initial metadata options

This commit is contained in:
murgatroid99 2019-03-20 14:59:16 -07:00
parent 9bbe7057b5
commit fe090a089a
6 changed files with 180 additions and 35 deletions

View File

@ -81,8 +81,13 @@ Local<Value> nanErrorWithCode(const char *msg, grpc_call_error code) {
return scope.Escape(err);
}
bool CreateMetadataArray(Local<Object> metadata, grpc_metadata_array *array) {
bool CreateMetadataArray(Local<Object> metadata_obj, grpc_metadata_array *array) {
HandleScope scope;
Local<Value> metadata_value = (Nan::Get(metadata_obj, Nan::New("metadata").ToLocalChecked())).ToLocalChecked();
if (!metadata_value->IsObject()) {
return false;
}
Local<Object> metadata = Nan::To<Object>(metadata_value).ToLocalChecked();
Local<Array> keys = Nan::GetOwnPropertyNames(metadata).ToLocalChecked();
for (unsigned int i = 0; i < keys->Length(); i++) {
Local<String> current_key =
@ -159,7 +164,10 @@ Local<Value> ParseMetadata(const grpc_metadata_array *metadata_array) {
Nan::Set(array, array->Length(), CopyStringFromSlice(elem->value));
}
}
return scope.Escape(metadata_object);
Local<Object> result = Nan::New<Object>();
Nan::Set(result, Nan::New("metadata").ToLocalChecked(), metadata_object);
Nan::Set(result, Nan::New("flags").ToLocalChecked(), Nan::New<v8::Uint32>(0));
return scope.Escape(result);
}
Local<Value> 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<Object> metadata_object = maybe_metadata.ToLocalChecked();
MaybeLocal<Value> maybe_flag_value =
Nan::Get(metadata_object, Nan::New("flags").ToLocalChecked());
if (!maybe_flag_value.IsEmpty()) {
Local<Value> flag_value = maybe_flag_value.ToLocalChecked();
if (flag_value->IsUint32()) {
Maybe<uint32_t> maybe_flag = Nan::To<uint32_t>(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<Value> flag_value = maybe_flag_value.ToLocalChecked();
if (flag_value->IsUint32()) {
Maybe<uint32_t> maybe_flag = Nan::To<uint32_t>(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);

View File

@ -489,10 +489,29 @@ declare module "grpc" {
type sendUnaryData<ResponseType> =
(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;

View File

@ -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.<String, Array.<String|Buffer>>} 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.<String, Array.<String|Buffer>>} 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.<String, Array.<String|Buffer>>} 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;
};

View File

@ -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() {

View File

@ -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
};

View File

@ -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]);