UNPKG

@vzeta/zeebe-node-test

Version:

The Node.js client library for the Zeebe Workflow Automation Engine.

490 lines 23.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; 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 _debug = __importStar(require("debug")); const events_1 = require("events"); const typed_duration_1 = require("typed-duration"); const pkg = require("../../package.json"); const transform_API_1 = require("./transform-API"); const debug = _debug.default('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 GrpcError = { OK: 0, CANCELLED: 1, UNKNOWN: 2, INVALID_ARGUMENT: 3, DEADLINE_EXCEEDED: 4, NOT_FOUND: 5, ALREADY_EXISTS: 6, PERMISSION_DENIED: 7, UNAUTHENTICATED: 16, RESOURCE_EXHAUSTED: 8, FAILED_PRECONDITION: 9, ABORTED: 10, OUT_OF_RANGE: 11, UNIMPLEMENTED: 12, INTERNAL: 13, UNAVAILABLE: 14, DATA_LOSS: 15, }; 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; 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); }; 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 = 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 = 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': 90000, /** * 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) => { 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) { 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(`Error`, 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 => { if (this.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) { const isNetworkError = err.code === GrpcError.UNAVAILABLE; if (isNetworkError) { this.setNotReady(); } else { this.setReady(); } return reject(err); } this.emit(exports.MiddlewareSignals.Event.Ready); this.setReady(); resolve(transform_API_1.normaliseAPI1(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