zeebe-node
Version:
The Node.js client library for the Zeebe Workflow Automation Engine.
458 lines • 22.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.GrpcClient = exports.MiddlewareSignals = void 0;
const grpc_js_1 = require("@grpc/grpc-js");
const proto_loader_1 = require("@grpc/proto-loader");
const events_1 = require("events");
const typed_duration_1 = require("typed-duration");
const pkg = require("../../package.json");
const GrpcError_1 = require("./GrpcError");
const debug = require('debug')('grpc');
// tslint:disable: object-literal-sort-keys
function replaceTimeValuesWithMillisecondNumber(data) {
if (typeof data !== 'object') {
return data;
}
return Object.entries(data).reduce((acc, [key, value]) => ({
...acc,
[key]: typed_duration_1.Duration.isTypedDuration(value)
? typed_duration_1.Duration.milliseconds.from(value)
: value,
}), {});
}
exports.MiddlewareSignals = {
Log: {
Error: 'MIDDLEWARE_ERROR',
Info: 'MIDDLEWARE_INFO',
Debug: 'MIDDLEWARE_DEBUG',
},
Event: {
Error: 'MIDDLEWARE_EVENT_ERROR',
Ready: 'MIDDLEWARE_EVENT_READY',
GrpcInterceptError: 'MIDDLEWARE_GRPC_INTERCEPT_ERROR',
},
};
const InternalSignals = {
Error: 'INTERNAL_ERROR',
Ready: 'INTERNAL_READY',
};
const GrpcState = {
/**
* The channel is trying to establish a connection and is waiting to make progress on one of the steps involved in name resolution,
* TCP connection establishment or TLS handshake.
*/
CONNECTING: 1,
/**
* This is the state where the channel is not even trying to create a connection because of a lack of new or pending RPCs.
*/
IDLE: 0,
/**
* The channel has successfully established a connection all the way through TLS handshake (or equivalent)
* and all subsequent attempt to communicate have succeeded (or are pending without any known failure ).
*/
READY: 2,
/**
* This channel has started shutting down.
*/
SHUTDOWN: 4,
/**
* There has been some transient failure (such as a TCP 3-way handshake timing out or a socket error).
*/
TRANSIENT_FAILURE: 3,
};
const connectivityState = [
'IDLE',
'CONNECTING',
'READY',
'TRANSIENT_FAILURE',
'SHUTDOWN',
];
class GrpcClient extends events_1.EventEmitter {
constructor({ basicAuth, connectionTolerance, host, oAuth, options = {}, packageName, protoPath, service, useTLS, customSSL, }) {
var _a, _b, _c, _d, _e, _f;
super();
this.channelClosed = false;
this.connected = false;
this.closing = false;
this.channelState = 0;
this.gRPCRetryCount = 0;
// https://github.com/grpc/proposal/blob/master/L5-node-client-interceptors.md#proposal
this.interceptor = (options, nextCall) => {
const requester = {
start: (metadata, _, next) => {
const newListener = {
onReceiveStatus: (callStatus, nxt) => {
const isError = callStatus.code !== grpc_js_1.status.OK;
if (isError) {
if (callStatus.code === 1 &&
callStatus.details.includes('503') // ||
// callStatus.code === 13
) {
return this.emit(exports.MiddlewareSignals.Event.GrpcInterceptError, { callStatus, options });
}
if (callStatus.code === 1 && this.closing) {
return this.emit(exports.MiddlewareSignals.Log.Debug, 'Closing, and error received from server');
}
}
return nxt(callStatus);
},
};
next(metadata, newListener);
},
};
return new grpc_js_1.InterceptingCall(nextCall(options), requester);
};
debug(`Constructing gRPC client...`);
this.host = host;
this.oAuth = oAuth;
this.basicAuth = basicAuth;
this.longPoll = options.longPoll;
this.connectionTolerance = typed_duration_1.Duration.milliseconds.from(connectionTolerance);
this.emit(exports.MiddlewareSignals.Log.Debug, `Connection Tolerance: ${typed_duration_1.Duration.milliseconds.from(connectionTolerance)}ms`);
this.on(InternalSignals.Ready, () => this.setReady());
this.on(InternalSignals.Error, () => this.setNotReady());
this.packageDefinition = (0, proto_loader_1.loadSync)(protoPath, {
defaults: (_a = options.defaults) !== null && _a !== void 0 ? _a : true,
enums: (_b = options.enums) !== null && _b !== void 0 ? _b : String,
keepCase: (_c = options.keepCase) !== null && _c !== void 0 ? _c : true,
longs: (_d = options.longs) !== null && _d !== void 0 ? _d : String,
oneofs: (_e = options.oneofs) !== null && _e !== void 0 ? _e : true,
});
const proto = (0, grpc_js_1.loadPackageDefinition)(this.packageDefinition)[packageName];
const listMethods = this.packageDefinition[`${packageName}.${service}`];
const channelCredentials = useTLS
? grpc_js_1.credentials.createSsl(customSSL === null || customSSL === void 0 ? void 0 : customSSL.rootCerts, customSSL === null || customSSL === void 0 ? void 0 : customSSL.privateKey, customSSL === null || customSSL === void 0 ? void 0 : customSSL.certChain, customSSL === null || customSSL === void 0 ? void 0 : customSSL.verifyOptions)
: grpc_js_1.credentials.createInsecure();
// Options documented here: https://github.com/grpc/grpc/blob/master/include/grpc/impl/codegen/grpc_types.h
this.client = new proto[service](host, channelCredentials, {
/**
* If set to zero, disables retry behavior.
* Otherwise, transparent retries are enabled for all RPCs,
* and configurable retries are enabled when they are configured
* via the service config. For details, see:
* https://github.com/grpc/proposal/blob/master/A6-client-retries.md
*/
'grpc.enable_retries': 1,
/**
* The time between the first and second connection attempts,
* in ms
*/
'grpc.initial_reconnect_backoff_ms': 1000,
/**
* The maximum time between subsequent connection attempts,
* in ms
*/
'grpc.max_reconnect_backoff_ms': 10000,
/**
* The minimum time between subsequent connection attempts,
* in ms. Default is 1000ms, but this can cause an SSL Handshake failure.
* This causes an intermittent failure in the Worker-LongPoll test when run
* against Camunda Cloud.
* Raised to 5000ms.
* See: https://github.com/grpc/grpc/issues/8382#issuecomment-259482949
*/
'grpc.min_reconnect_backoff_ms': 5000,
/**
* After a duration of this time the client/server
* pings its peer to see if the transport is still alive.
* Int valued, milliseconds.
*/
'grpc.keepalive_time_ms': (_f = process.env.GRPC_KEEPALIVE_TIME_MS) !== null && _f !== void 0 ? _f : 360000,
/**
* After waiting for a duration of this time,
* if the keepalive ping sender does
* not receive the ping ack, it will close the
* transport. Int valued, milliseconds.
*/
'grpc.keepalive_timeout_ms': 120000,
'grpc.http2.min_time_between_pings_ms': 90000,
/**
* Minimum allowed time between a server receiving
* successive ping frames without sending any data
* frame. Int valued, milliseconds
*/
'grpc.http2.min_ping_interval_without_data_ms': 90000,
/**
* This channel argument if set to 1
* (0 : false; 1 : true), allows keepalive pings
* to be sent even if there are no calls in flight.
*/
'grpc.keepalive_permit_without_calls': 1,
/**
* This channel argument controls the maximum number
* of pings that can be sent when there is no other
* data (data frame or header frame) to be sent.
* GRPC Core will not continue sending pings if we
* run over the limit. Setting it to 0 allows sending
* pings without sending data.
*/
'grpc.http2.max_pings_without_data': 0,
interceptors: [this.interceptor],
});
this.listNameMethods = [];
this.client.waitForReady(10000, error => error
? this.emit(exports.MiddlewareSignals.Event.Error, error)
: this.emit(exports.MiddlewareSignals.Event.Ready));
for (const key in listMethods) {
if (listMethods[key]) {
const methodName = listMethods[key].originalName;
this.listNameMethods.push(methodName);
this[`${methodName}Stream`] = async (data) => {
debug(`Calling ${methodName}Stream...`);
if (this.closing) {
// tslint:disable-next-line: no-console
console.log('Short-circuited on channel closed'); // @DEBUG
return;
}
let stream;
const timeNormalisedRequest = replaceTimeValuesWithMillisecondNumber(data);
try {
const metadata = await this.getAuthToken();
stream = this.client[methodName](timeNormalisedRequest, metadata);
this.setReady();
}
catch (error) {
debug(`${methodName}Stream error: ${error.code}`, error.message);
this.emit(exports.MiddlewareSignals.Log.Error, error.message);
this.emit(exports.MiddlewareSignals.Event.Error);
this.setNotReady();
return { error };
}
if (!stream) {
return {
error: new Error(`No stream returned by call to ${methodName}Stream`),
};
}
// This deals with the case where during a broker restart the call returns a stream
// but that stream is not a legit Gateway activation. In that case, the Gateway will
// never time out or close the stream. So we have to manage that case.
const clientsideTimeoutDuration = typed_duration_1.Duration.milliseconds.from(this.longPoll) + 1000;
const clientSideTimeout = setTimeout(() => {
debug(`Triggered client-side timeout after ${clientsideTimeoutDuration}ms`);
stream.emit('end');
}, clientsideTimeoutDuration);
/**
* Once this gets attached here, it is attached to *all* calls
* This is an issue if you do a sync call like cancelWorkflowSync
* The error will not propagate, and the channel will be closed.
* So we use a separate GRPCClient for the client, which never does
* streaming calls, and each worker, which only does streaming calls
*/
stream.on('error', (error) => {
clearTimeout(clientSideTimeout);
debug(`${methodName}Stream error emitted by stream`, error);
this.emit(exports.MiddlewareSignals.Event.Error);
if (error.message.includes('14 UNAVAILABLE')) {
this.emit(exports.MiddlewareSignals.Log.Error, `Grpc Stream Error: ${error.message} - ${host}`);
}
else {
this.emit(exports.MiddlewareSignals.Log.Error, `Grpc Stream Error: ${error.message}`);
}
// Do not handle stream errors the same way
// this.handleGrpcError(stream)(error)
this.setNotReady();
});
stream.on('data', () => (this.gRPCRetryCount = 0));
stream.on('metadata', md => this.emit(exports.MiddlewareSignals.Log.Debug, JSON.stringify(md)));
stream.on('status', s => this.emit(exports.MiddlewareSignals.Log.Debug, `gRPC Status event: ${JSON.stringify(s)}`));
stream.on('end', () => clearTimeout(clientSideTimeout));
return stream;
};
this[`${methodName}Sync`] = data => {
debug(`Calling ${methodName}Sync...`);
if (this.closing) {
debug(`Aborting ${methodName}Sync due to client closing.`);
return;
}
const timeNormalisedRequest = replaceTimeValuesWithMillisecondNumber(data);
const client = this.client;
return new Promise(async (resolve, reject) => {
try {
const metadata = (await this.getAuthToken()) || {};
client[methodName](timeNormalisedRequest, metadata, (err, dat) => {
// This will error on network or business errors
if (err) {
debug(`${methodName}Sync error: ${err.code}`);
const isNetworkError = err.code === GrpcError_1.GrpcError.UNAVAILABLE;
if (isNetworkError) {
this.setNotReady();
}
else {
this.setReady();
}
return reject(err);
}
this.emit(exports.MiddlewareSignals.Event.Ready);
this.setReady();
debug(`${methodName}Sync completed`);
resolve(dat);
});
}
catch (e) {
reject(e);
}
});
};
}
}
}
runService(fnName, data, fnAnswer) {
this.client[fnName](data, fnAnswer);
}
listMethods() {
return this.listNameMethods;
}
close(timeout = 5000) {
const STATE_SHUTDOWN = 4;
const isClosed = state => state === STATE_SHUTDOWN;
this.closing = true;
let alreadyClosed = false;
return new Promise((resolve, reject) => {
const gRPC = this.client;
gRPC.getChannel().close();
gRPC.close();
try {
this.channelState = gRPC
.getChannel()
.getConnectivityState(false);
}
catch (e) {
const msg = e.toString();
alreadyClosed =
isClosed(this.channelState) ||
msg.includes('Cannot call getConnectivityState on a closed Channel'); // C-based library
}
const closed = isClosed(this.channelState);
if (closed || alreadyClosed) {
this.channelClosed = true;
this.emit(exports.MiddlewareSignals.Log.Info, `Grpc channel closed`);
return resolve(null); // setTimeout(() => resolve(), 2000)
}
this.emit(exports.MiddlewareSignals.Log.Info, `Grpc Channel State: ${connectivityState[this.channelState]}`);
const deadline = new Date().setSeconds(new Date().getSeconds() + 300);
gRPC.getChannel().watchConnectivityState(this.channelState, deadline, async () => {
try {
this.channelState = gRPC
.getChannel()
.getConnectivityState(false);
this.emit(exports.MiddlewareSignals.Log.Info, `Grpc Channel State: ${connectivityState[this.channelState]}`);
alreadyClosed = isClosed(this.channelState);
}
catch (e) {
const msg = e.toString();
alreadyClosed =
msg.includes('Cannot call getConnectivityState on a closed Channel') || isClosed(this.channelState);
this.emit(exports.MiddlewareSignals.Log.Info, `Closed: ${alreadyClosed}`);
}
if (alreadyClosed) {
return resolve(null);
}
});
return setTimeout(() => {
// tslint:disable-next-line: no-console
console.log(`Channel timeout after ${timeout}`); // @DEBUG
return isClosed(this.channelState)
? null
: reject(new Error(`Didn't close in time: ${this.channelState}`));
}, timeout);
});
}
async getAuthToken() {
const metadata = new grpc_js_1.Metadata({ waitForReady: false });
metadata.add('user-agent', `zeebe-client-nodejs/${pkg.version}`);
if (this.oAuth) {
const token = await this.oAuth.getToken();
metadata.add('Authorization', `Bearer ${token}`);
}
if (this.basicAuth) {
const token = Buffer.from(`${this.basicAuth.username}:${this.basicAuth.password}`).toString('base64');
metadata.add('Authorization', `Basic ${token}`);
}
return metadata;
}
waitForGrpcChannelReconnect() {
this.emit(exports.MiddlewareSignals.Log.Debug, 'Start watching Grpc channel...');
return new Promise(resolve => {
const tryToConnect = true;
const gRPC = this.client;
if (this.channelClosed) {
return;
}
const currentChannelState = gRPC
.getChannel()
.getConnectivityState(tryToConnect);
this.emit(exports.MiddlewareSignals.Log.Error, `Grpc Channel State: ${connectivityState[currentChannelState]}`);
const delay = currentChannelState === GrpcState.TRANSIENT_FAILURE ? 5 : 30;
const deadline = new Date().setSeconds(new Date().getSeconds() + delay);
if (currentChannelState === GrpcState.IDLE ||
currentChannelState === GrpcState.READY) {
this.gRPCRetryCount = 0;
return resolve(currentChannelState);
}
gRPC.getChannel().watchConnectivityState(currentChannelState, deadline, async (error) => {
if (this.channelClosed) {
return;
}
this.gRPCRetryCount++;
if (error) {
this.emit(exports.MiddlewareSignals.Log.Error, error);
}
const newState = gRPC
.getChannel()
.getConnectivityState(tryToConnect);
this.emit(exports.MiddlewareSignals.Log.Error, `Grpc Channel State: ${connectivityState[newState]}`);
this.emit(exports.MiddlewareSignals.Log.Error, `Grpc Retry count: ${this.gRPCRetryCount}`);
if (newState === GrpcState.READY ||
newState === GrpcState.IDLE) {
return resolve(newState);
}
else {
this.emit(exports.MiddlewareSignals.Log.Error, `Grpc Retry count: ${this.gRPCRetryCount}`);
return resolve(await this.waitForGrpcChannelReconnect());
}
});
});
}
setReady() {
// debounce rapid connect / disconnect
if (this.readyTimer) {
this.emit(exports.MiddlewareSignals.Log.Debug, `Reset Grpc channel ready timer.`);
clearTimeout(this.readyTimer);
}
this.emit(exports.MiddlewareSignals.Log.Debug, `Set Grpc channel ready timer for ${this.connectionTolerance}ms`);
this.readyTimer = setTimeout(() => {
if (this.failTimer) {
clearTimeout(this.failTimer);
this.failTimer = undefined;
}
this.readyTimer = undefined;
this.connected = true;
this.emit(exports.MiddlewareSignals.Log.Debug, `Set Grpc channel state ready after ${this.connectionTolerance}ms`);
this.emit(exports.MiddlewareSignals.Event.Ready);
}, this.connectionTolerance);
}
setNotReady() {
if (this.readyTimer) {
this.emit(exports.MiddlewareSignals.Log.Debug, `Cancelled channel ready timer`);
clearTimeout(this.readyTimer);
this.readyTimer = undefined;
}
this.connected = false;
if (!this.failTimer) {
this.emit(exports.MiddlewareSignals.Log.Debug, `Set Grpc channel failure timer for ${this.connectionTolerance}ms`);
this.failTimer = setTimeout(() => {
if (this.readyTimer) {
this.failTimer = undefined;
this.emit(exports.MiddlewareSignals.Log.Debug, `Grpc channel ready timer is running, not failing channel...`);
return;
}
this.emit(exports.MiddlewareSignals.Log.Debug, `Set Grpc Channel state to failed after ${this.connectionTolerance}ms`);
this.failTimer = undefined;
this.connected = false;
this.emit(exports.MiddlewareSignals.Event.Error);
}, this.connectionTolerance);
}
}
}
exports.GrpcClient = GrpcClient;
//# sourceMappingURL=GrpcClient.js.map
;