barracuda-client-api
Version:
API Client to connect to Barracuda Enterprise Service Bus
292 lines (256 loc) • 12 kB
text/typescript
import { IBarracudaClientConsumer, BarracudaConnectionStatus, IBarracudaClient, ConnectionStateChangeHandler, BarracudaQuery } from "../IBarracudaClient";
import { IBarracudaSubscriptionStatus, BarracudaBridgeReadQueryCommands, BarracudaClient } from "../BarracudaClient";
import { isDebug, logDebug, isInfo, logInfo, isWarn, logWarn, shouldLogErrors, logError } from "../logging/index";
import { BarracudaRecoveryPolicy } from "./BarracudaRecoveryPolicy";
import debounce from "lodash/debounce";
const recoveryDelay: number = 5000;
export interface RecoveryStats {
recoveryCounter: number;
lastRecoveryStatus: RecoveryProgress;
}
export type OnRecoveryCompleteCallBack = (recoveryStats: RecoveryStats, barracudaClient: IBarracudaClient) => void;
function strRecovery(recoveryPolicy: BarracudaRecoveryPolicy): string {
if (typeof (recoveryPolicy) === "number") {
return `${BarracudaRecoveryPolicy[recoveryPolicy]}(${recoveryPolicy})`;
} else {
return recoveryPolicy;
}
}
let reconnectionDetectionCounter: number = 0;
/**
* Return a clone of the supplied BCjs instance with recovery enabled.
* Please note all constructor BarracudaConnectionProps is utilized to clone properties across BCjs instances.
* @param {IBarracudaClientConsumer} originalBC
* @param {BarracudaRecoveryPolicy} recoveryPolicy
* @param {number} overrideRecoveryPeriod, The default is 5000 ms to allow a stable connection to be established.
* @param {OnRecoveryCompleteCallBack} onRecoveryComplete
* @returns {IBarracudaClientConsumer}
*/
export function withRecoveryPolicy(originalBC: IBarracudaClientConsumer,
recoveryPolicy: BarracudaRecoveryPolicy,
overrideRecoveryPeriod: number = recoveryDelay,
onRecoveryComplete?: OnRecoveryCompleteCallBack): IBarracudaClientConsumer {
const originalOnConnectionChange = originalBC.onConnectionStateChange;
if (isInfo(originalBC.loglevel)) {
logInfo(`${originalBC.bcLog(originalBC.lastConnectionProps)} adding recovery policy ${strRecovery(recoveryPolicy)} with ${overrideRecoveryPeriod}ms delay`, {
connectionCounter: originalBC.connectionCounter,
defaultConnectionProps: originalBC.defaultConnectionProps,
});
}
let recoveryReconnectionCounter: number = 0;
let recoverySubscriptions: IBarracudaSubscriptionStatus[] | undefined;
const debouncedStartRecovery = debounce((bc: IBarracudaClientConsumer, onCompl?: OnRecoveryCompleteCallBack) => {
++recoveryReconnectionCounter;
if (isInfo(bc.loglevel)) {
logInfo(`${bc.bcLog(bc.lastConnectionProps)} reconnection detected, scheduling recovery ${strRecovery(recoveryPolicy)} in ${overrideRecoveryPeriod}ms`, {
recoveryReconnectionCounter,
reconnectionDetectionCounter,
connectionCounter: bc.connectionCounter,
});
}
const recoveryProgressPromise: Promise<RecoveryProgress> = recoverSubscriptions(bc as IBarracudaClientConsumer, recoveryPolicy, recoverySubscriptions as IBarracudaSubscriptionStatus[]);
return recoveryProgressPromise.then((r: RecoveryProgress) => {
if (isInfo(bc.loglevel)) {
logInfo(`${bc.bcLog(bc.lastConnectionProps)} Recovery finished with status: ${RecoveryProgress[r]}(${r})`, {
recoveryReconnectionCounter,
reconnectionDetectionCounter,
connectionCounter: bc.connectionCounter,
});
}
switch (r) {
case RecoveryProgress.none:
case RecoveryProgress.inProgress:
return;
case RecoveryProgress.failedConnectedAckVerification:
debouncedStartRecovery(bc, onCompl);
return;
case RecoveryProgress.partialRecoveryFailure:
case RecoveryProgress.completedRecovery:
recoverySubscriptions = undefined;
break;
default:
break;
}
if (onCompl) {
try {
onCompl({
recoveryCounter: recoveryReconnectionCounter,
lastRecoveryStatus: r,
},
bc,
);
} catch (e) {
if (shouldLogErrors(bc.loglevel)) {
logError(`UnhandledException error in onRecoveryComplete. Handler should wrap and handle exceptions instead of bubbling to BCjs level.`, e);
}
}
}
});
}, overrideRecoveryPeriod);
const newOnConnectionStateChange: ConnectionStateChangeHandler = (state, bc) => {
if (originalOnConnectionChange) {
try {
originalOnConnectionChange(state, bc);
} catch (e) {
// ...... original handler should have it's own policy for errors
}
}
if (state === BarracudaConnectionStatus.reconnecting && recoveryPolicy !== BarracudaRecoveryPolicy.None) {
if (!recoverySubscriptions) {
recoverySubscriptions = (bc as IBarracudaClientConsumer).subscriptions;
logInfo("recoverySubscriptions ===>", recoverySubscriptions);
}
++reconnectionDetectionCounter;
debouncedStartRecovery(bc as IBarracudaClientConsumer, onRecoveryComplete);
}
};
const newBc: BarracudaClient = new BarracudaClient({
...originalBC.defaultConnectionProps,
onConnectionStateChange: newOnConnectionStateChange,
});
if (isDebug(newBc.loglevel)) {
logDebug(`${newBc.bcLog(newBc.lastConnectionProps)} new BarraucdaClient with recovery ${recoveryPolicy}`, {defaultConnectionProps: newBc.defaultConnectionProps});
}
return newBc;
}
export enum RecoveryProgress {
none,
inProgress,
partialRecoveryFailure,
completedRecovery,
failedConnectedAckVerification,
}
let recoveryStatus: RecoveryProgress = RecoveryProgress.none;
function assertUnhandled(recoveryPolicy: never): void {
throw new Error(`Unsupported recovery case ${recoveryPolicy}`);
}
function recoverSubscriptions(bc: IBarracudaClientConsumer, recoveryPolicy: BarracudaRecoveryPolicy, baseSubscriptions: IBarracudaSubscriptionStatus[]): Promise<RecoveryProgress> {
if (isInfo(bc.loglevel)) {
logInfo(`${bc.bcLog(bc.lastConnectionProps)} determining which subscriptions to reover ==> ${recoveryPolicy}`, isDebug(bc.loglevel) ? {baseSubscriptions} : undefined);
}
if (!verifyConnectedAck(bc)) {
if (isWarn(bc.loglevel)) {
logWarn(`${bc.bcLog(bc.lastConnectionProps)} failed verifyConnectedAck, skipping recovery.`, {
connectionStatus: bc.connectionState,
recoveryPolicy,
baseSubscriptions,
});
}
return Promise.resolve(RecoveryProgress.failedConnectedAckVerification);
}
switch (recoveryStatus) {
case RecoveryProgress.none:
case RecoveryProgress.partialRecoveryFailure:
case RecoveryProgress.completedRecovery:
break;
case RecoveryProgress.inProgress:
if (isWarn(bc.loglevel)) {
logWarn(`${bc.bcLog(bc.lastConnectionProps)} recovery already in progress. Ignoring.`, {recoveryStatus});
}
return Promise.resolve(RecoveryProgress.inProgress);
}
let recoveryPolicyEnum: BarracudaRecoveryPolicy;
switch (typeof (recoveryPolicy)) {
case "string":
recoveryPolicyEnum = BarracudaRecoveryPolicy[recoveryPolicy as keyof typeof BarracudaRecoveryPolicy] as BarracudaRecoveryPolicy;
break;
case "number":
recoveryPolicyEnum = recoveryPolicy as BarracudaRecoveryPolicy;
break;
}
// const previousSessionSubscriptions: IBarracudaSubscriptionStatus[] = bc.previousSessionSubscriptions;
const previousSessionSubscriptions: IBarracudaSubscriptionStatus[] = baseSubscriptions;
let replaySubscriptions: IBarracudaSubscriptionStatus[] = previousSessionSubscriptions;
switch (recoveryPolicyEnum) {
case BarracudaRecoveryPolicy.None:
logInfo("Skipping recovery, policy set to none");
return Promise.resolve(RecoveryProgress.none);
case BarracudaRecoveryPolicy.ResubscribeAllInterruptedSubscriptions:
replaySubscriptions = previousSessionSubscriptions?.filter(s => !s.unsubscribed && !s.excludeFromRecovery && s.requestCommandType)
?.map(s => ({
...s,
requestCommandType: subscribeOnly(s.requestCommandType as BarracudaBridgeReadQueryCommands),
}
));
break;
case BarracudaRecoveryPolicy.ReplayAllInterruptedSubscriptions:
replaySubscriptions = previousSessionSubscriptions?.filter(s => !s.unsubscribed);
break;
case BarracudaRecoveryPolicy.ReplayAllRequests:
replaySubscriptions = previousSessionSubscriptions;
break;
default:
throw new Error(`Unsupported recovery case ${recoveryPolicy} --> ${recoveryPolicyEnum}`);
}
return executeSubscriptions(replaySubscriptions, bc, recoveryPolicy);
}
function executeSubscriptions(recoverSubcriptions: IBarracudaSubscriptionStatus[], bc: IBarracudaClientConsumer, recoveryPolicy: BarracudaRecoveryPolicy): Promise<RecoveryProgress> {
if (isInfo(bc.loglevel)) {
logInfo(`${bc.bcLog(bc.lastConnectionProps)} executeSubscriptions ==> ${recoveryPolicy}`, {
recoverSubcriptions,
});
}
if (!verifyConnectedAck(bc)) {
if (isWarn(bc.loglevel)) {
logWarn(`${bc.bcLog(bc.lastConnectionProps)} failed verifyConnectedAck, skipping recovery.`, {
connectionStatus: bc.connectionState,
recoveryPolicy,
});
}
return Promise.resolve(RecoveryProgress.failedConnectedAckVerification);
}
if (!recoverSubcriptions || recoverSubcriptions?.length === 0) {
if (isDebug(bc.loglevel)) {
logDebug(`${bc.bcLog(bc.lastConnectionProps)} No subscriptions to recover.`, {
recoverSubcriptions,
previousSessionSubscriptions: bc.previousSessionSubscriptions,
});
}
return Promise.resolve(RecoveryProgress.none);
} else {
const recoveryOperations = recoverSubcriptions?.filter(sub => sub.requestCommandType && sub.requestBarracudaQuery)
?.map(sub => {
return bc.subscribeQuery<unknown>(sub.requestCommandType as BarracudaBridgeReadQueryCommands, sub.requestBarracudaQuery as BarracudaQuery<unknown>);
});
const allRecoveryPromises: Promise<RecoveryProgress.completedRecovery | RecoveryProgress.partialRecoveryFailure> = Promise.all(recoveryOperations)
.then(recoveryOperationResults => {
if (isInfo(bc.loglevel)) {
logInfo(`${bc.bcLog(bc.lastConnectionProps)} Executing recovery ${recoveryPolicy} finished`, {recoveryOperationResults});
}
recoveryStatus = RecoveryProgress.completedRecovery;
return recoveryStatus;
})
.catch(error => {
if (shouldLogErrors(bc.loglevel)) {
logError(`${bc.bcLog(bc.lastConnectionProps)} Error while executing recovery ${recoveryPolicy}`, error);
}
recoveryStatus = RecoveryProgress.partialRecoveryFailure;
return recoveryStatus;
});
return allRecoveryPromises;
}
}
function verifyConnectedAck(bc: IBarracudaClient): boolean {
if (bc.connectionState !== BarracudaConnectionStatus.connectedAck) {
const errMsg: string = `${bc.bcLog(bc.lastConnectionProps)} verification failed: not in ConnectedAck state [${bc.connectionState}]. Canceling recovery..`;
if (shouldLogErrors(bc.loglevel)) {
logError(errMsg);
}
return false;
}
return true;
}
function subscribeOnly(requestCommandType: BarracudaBridgeReadQueryCommands): BarracudaBridgeReadQueryCommands {
switch (requestCommandType) {
case BarracudaBridgeReadQueryCommands.snapshot:
return BarracudaBridgeReadQueryCommands.snapshot;
case BarracudaBridgeReadQueryCommands.snapshotAndSubscribe:
case BarracudaBridgeReadQueryCommands.subscribe:
return BarracudaBridgeReadQueryCommands.subscribe;
case BarracudaBridgeReadQueryCommands.snapshotAndDiffSubscribe:
case BarracudaBridgeReadQueryCommands.diffSubscribe:
return BarracudaBridgeReadQueryCommands.diffSubscribe;
default:
return requestCommandType;
}
}