@camunda8/sdk
Version:
[](https://www.npmjs.com/package/@camunda8/sdk)
545 lines • 27.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.GrpcClient = exports.MiddlewareSignals = void 0;
const events_1 = require("events");
const grpc_js_1 = require("@grpc/grpc-js");
const proto_loader_1 = require("@grpc/proto-loader");
const debug_1 = __importDefault(require("debug"));
const typed_duration_1 = require("typed-duration");
const lib_1 = require("../../lib");
const CamundaSupportLogger_1 = require("../../lib/CamundaSupportLogger");
const GrpcError_1 = require("./GrpcError");
const debug = (0, debug_1.default)('camunda:grpc');
const supportLogger = CamundaSupportLogger_1.CamundaSupportLogger.getInstance();
// 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({ config, connectionTolerance, host, oAuth, options = {}, packageName, protoPath, service, useTLS, customSSL, }) {
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') // 'Service Unavailable'
) {
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');
}
if (callStatus.code === 13 &&
callStatus.details.includes('Protocol error')) {
const { CAMUNDA_SECURE_CONNECTION } = this.config;
const { ZEEBE_INSECURE_CONNECTION } = this.config.zeebeGrpcSettings;
callStatus.details += `. This can be due to a TLS-enablement mismatch between the client and the gateway. The client has TLS ${this.useTLS ? 'enabled' : 'disabled'}. \n\tCheck your configuration: CAMUNDA_SECURE_CONNECTION is set to ${CAMUNDA_SECURE_CONNECTION}. ZEEBE_INSECURE_CONNECTION is set to ${ZEEBE_INSECURE_CONNECTION}.`;
}
}
return nxt(callStatus);
},
};
next(metadata, newListener);
},
};
return new grpc_js_1.InterceptingCall(nextCall(options), requester);
};
debug('Constructing gRPC client...');
supportLogger.log(`Constructing gRPC client`);
supportLogger.log({
config,
connectionTolerance,
host,
options,
packageName,
protoPath,
service,
useTLS,
customSSL,
});
this.config = config;
this.userAgentString = (0, lib_1.createUserAgentString)(config);
this.host = host;
debug('Host:', host);
this.oAuth = oAuth;
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.useTLS = useTLS;
this.on(InternalSignals.Ready, () => this.setReady());
this.on(InternalSignals.Error, () => this.setNotReady());
this.packageDefinition = (0, proto_loader_1.loadSync)(protoPath, {
defaults: options.defaults ?? true,
enums: options.enums ?? String,
keepCase: options.keepCase ?? true,
longs: options.longs ?? String,
oneofs: options.oneofs ?? 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?.rootCerts, customSSL?.privateKey, customSSL?.certChain, customSSL?.verifyOptions)
: grpc_js_1.credentials.createInsecure();
debug('useTLS:', useTLS);
debug('channelCredentials:', channelCredentials);
// 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': this.config.zeebeGrpcSettings.GRPC_INITIAL_RECONNECT_BACKOFF_MS,
/**
* The maximum time between subsequent connection attempts,
* in ms
*/
'grpc.max_reconnect_backoff_ms': this.config.zeebeGrpcSettings.GRPC_MAX_RECONNECT_BACKOFF_MS,
/**
* 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': this.config.zeebeGrpcSettings.GRPC_INITIAL_RECONNECT_BACKOFF_MS,
/**
* 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': this.config.zeebeGrpcSettings.GRPC_KEEPALIVE_TIME_MS,
/**
* 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': this.config.zeebeGrpcSettings.GRPC_KEEPALIVE_TIMEOUT_MS,
'grpc.http2.min_time_between_pings_ms': this.config.zeebeGrpcSettings.GRPC_HTTP2_MIN_TIME_BETWEEN_PINGS_MS,
/**
* Minimum allowed time between a server receiving
* successive ping frames without sending any data
* frame. Int valued, milliseconds. Default: 90000
*/
'grpc.http2.min_ping_interval_without_data_ms': this.config.zeebeGrpcSettings
.GRPC_HTTP2_MIN_PING_INTERVAL_WITHOUT_DATA_MS,
/**
* 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.
* Default is 1.
*/
'grpc.keepalive_permit_without_calls': this.config.zeebeGrpcSettings.GRPC_KEEPALIVE_PERMIT_WITHOUT_CALLS,
/**
* 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': this.config.zeebeGrpcSettings.GRPC_HTTP2_MAX_PINGS_WITHOUT_DATA,
/**
* Default compression algorithm for the channel, applies to sending messages.
*
* Possible values for this option are:
* - `0` - No compression
* - `1` - Compress with DEFLATE algorithm
* - `2` - Compress with GZIP algorithm
* - `3` - Stream compression with GZIP algorithm
*/
'grpc.default_compression_algorithm': 2,
/**
* Default compression level for the channel, applies to receiving messages.
*
* Possible values for this option are:
* - `0` - None
* - `1` - Low level
* - `2` - Medium level
* - `3` - High level
*/
'grpc.default_compression_level': 2,
interceptors: [this.interceptor],
});
this.listNameMethods = [];
// See https://github.com/camunda/camunda-8-js-sdk/issues/150
this.client.waitForReady(new Date(Date.now() + 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...`, host);
supportLogger.log(`Calling ${methodName}Stream:`);
supportLogger.log(data);
if (this.closing) {
return;
}
let stream;
const timeNormalisedRequest = replaceTimeValuesWithMillisecondNumber(data);
debug('TimeNormalisedRequest', timeNormalisedRequest);
try {
const metadata = await this.getAuthToken();
stream = this.client[methodName](timeNormalisedRequest, metadata);
this.setReady();
}
catch (error) {
const e = error;
debug(`${methodName}Stream error: ${e.code}`, e.message);
this.emit(exports.MiddlewareSignals.Log.Error, e.message);
this.emit(exports.MiddlewareSignals.Event.Error);
this.setNotReady();
return { error };
}
let _error;
/**
* 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) => {
_error = error;
clearTimeout(clientSideTimeout);
debug(`${methodName}Stream error emitted by stream`, error);
supportLogger.log(`${methodName}Stream error emitted by stream - ${error.code} - ${error.message} - ${error.details}`);
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();
});
if (stream.errored) {
console.log('error!');
}
if (!stream) {
return {
error: new Error(`No stream returned by call to ${methodName}Stream`),
};
}
// Free the stream resources. When it emits 'end', we remove all listeners and destroy it.
stream.on('end', () => {
stream.destroy();
});
// 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.
// Also, no end event is emitted when there are no jobs, so we have to emit it ourselves.
//
// For long-lived streams (e.g. StreamActivatedJobs) the request has
// no requestTimeout field, so we must not arm a client-side timeout
// — otherwise the stream is destroyed after longPoll + 1 s and never
// reconnected, silently breaking streaming.
const hasRequestTimeout = timeNormalisedRequest.requestTimeout != null &&
timeNormalisedRequest.requestTimeout !== 0 &&
timeNormalisedRequest.requestTimeout !== -1;
const clientsideTimeoutDuration = hasRequestTimeout
? typed_duration_1.Duration.milliseconds.from(timeNormalisedRequest.requestTimeout) +
1000
: 0;
const clientSideTimeout = hasRequestTimeout
? setTimeout(() => {
if (clientsideTimeoutDuration !== 1000 &&
clientsideTimeoutDuration !== 999) {
debug(`Triggered client-side timeout after ${clientsideTimeoutDuration}ms`);
stream.emit('end');
}
}, clientsideTimeoutDuration)
: 0;
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 _error ? { error: _error } : stream;
};
this[`${methodName}Sync`] = (data) => {
debug(`Calling ${methodName}Sync...`, host);
supportLogger.log(`Calling ${methodName}Sync:`);
supportLogger.log(data);
if (this.closing) {
debug(`Aborting ${methodName}Sync due to client closing.`);
return;
}
const timeNormalisedRequest = replaceTimeValuesWithMillisecondNumber(data);
const client = this.client;
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve, reject) => {
try {
const metadata = (await this.getAuthToken()) || {};
debug(methodName, 'timeNormalisedRequest', timeNormalisedRequest);
client[methodName](timeNormalisedRequest, metadata, (err, dat) => {
// This will error on network or business errors
if (err) {
debug(`${methodName}Sync error: ${err.code}`);
debug(err.message);
const isNetworkError = err.code === GrpcError_1.GrpcError.UNAVAILABLE &&
!err.message.includes('partition');
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(); // 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();
}
});
return setTimeout(() => {
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', this.userAgentString);
if (this.oAuth) {
const authProviderHeaders = await this.oAuth.getHeaders('ZEEBE');
// add arbitraty headers to metadata
// See: https://github.com/camunda/camunda-8-js-sdk/issues/448
Object.entries(authProviderHeaders).forEach(([key, value]) => {
metadata.add(key, value);
});
}
supportLogger.log(`gRPC Client - getAuthToken - metadata:`);
supportLogger.log(metadata);
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