@cap-js-community/event-queue
Version:
An event queue that enables secure transactional processing of asynchronous and periodic events, featuring instant event processing with Redis Pub/Sub and load distribution across all application instances.
200 lines (179 loc) • 6.22 kB
JavaScript
;
const redis = require("redis");
const { getEnvInstance } = require("./env");
const EventQueueError = require("../EventQueueError");
const COMPONENT_NAME = "/eventQueue/shared/redis";
const LOG_AFTER_SEC = 5;
let mainClientPromise;
let subscriberClientPromise;
const subscribedChannels = {};
let lastErrorLog = Date.now();
const createMainClientAndConnect = (options) => {
if (mainClientPromise) {
return mainClientPromise;
}
const errorHandlerCreateClient = (err) => {
mainClientPromise?.then?.(_resilientClientClose);
cds.log(COMPONENT_NAME).error("error from redis main client:", err);
mainClientPromise = null;
setTimeout(() => createMainClientAndConnect(options), LOG_AFTER_SEC * 1000).unref();
};
mainClientPromise = createClientAndConnect(options, errorHandlerCreateClient);
return mainClientPromise;
};
const _createClientBase = (redisOptions = {}) => {
const env = getEnvInstance();
try {
const { credentials, options } = env.redisRequires;
const socket = Object.assign(
{
host: credentials.hostname,
tls: !!credentials.tls,
port: credentials.port,
},
options?.socket,
redisOptions.socket
);
const socketOptions = Object.assign({}, options, redisOptions, {
password: redisOptions.password ?? options.password ?? credentials.password,
socket,
});
delete socketOptions.redisNamespace;
if (credentials.cluster_mode) {
return redis.createCluster({
rootNodes: [socketOptions],
defaults: socketOptions,
});
}
return redis.createClient(socketOptions);
} catch (err) {
throw EventQueueError.redisConnectionFailure(err);
}
};
const createClientAndConnect = async (options, errorHandlerCreateClient, isConnectionCheck) => {
try {
const client = _createClientBase(options);
if (!isConnectionCheck) {
client.on("error", (err) => {
const dateNow = Date.now();
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
cds.log(COMPONENT_NAME).error("error redis client:", err);
lastErrorLog = dateNow;
}
});
client.on("reconnecting", () => {
const dateNow = Date.now();
if (dateNow - lastErrorLog > LOG_AFTER_SEC * 1000) {
cds.log(COMPONENT_NAME).info("redis client trying reconnect...");
lastErrorLog = dateNow;
}
});
}
await client.connect();
return client;
} catch (err) {
errorHandlerCreateClient(err);
}
};
const subscribeRedisChannel = (options, channel, subscribeHandler) => {
subscribedChannels[channel] = subscribeHandler;
const errorHandlerCreateClient = (err) => {
cds.log(COMPONENT_NAME).error(`error from redis client for pub/sub failed for channel ${channel}`, err);
subscriberClientPromise?.then?.(_resilientClientClose);
subscriberClientPromise = null;
setTimeout(() => _subscribeChannels(options, subscribedChannels, subscribeHandler), LOG_AFTER_SEC * 1000).unref();
};
_subscribeChannels(options, { [channel]: subscribeHandler }, errorHandlerCreateClient);
};
const _subscribeChannels = (options, subscribedChannels, errorHandlerCreateClient) => {
subscriberClientPromise = createClientAndConnect(options, errorHandlerCreateClient)
.then((client) => {
for (const channel in subscribedChannels) {
const fn = subscribedChannels[channel];
client._subscribedChannels ??= {};
if (client._subscribedChannels[channel]) {
continue;
}
const prefixedChannelName = [options.redisNamespace, channel].join("_");
cds.log(COMPONENT_NAME).info("subscribe redis client connected channel", { channel: prefixedChannelName });
client
.subscribe(prefixedChannelName, fn)
.then(() => {
client._subscribedChannels ??= {};
client._subscribedChannels[channel] = 1;
})
.catch(() => {
cds.log(COMPONENT_NAME).error("error subscribe to channel - retrying...");
setTimeout(() => _subscribeChannels(options, [channel], fn), LOG_AFTER_SEC * 1000).unref();
});
}
})
.catch((err) => {
cds
.log(COMPONENT_NAME)
.error(
`error from redis client for pub/sub failed during startup - trying to reconnect - ${Object.keys(
subscribedChannels
).join(", ")}`,
err
);
});
};
const publishMessage = async (options, channel, message) => {
const client = await createMainClientAndConnect(options);
return await client.publish([options.redisNamespace, channel].join("_"), message);
};
const closeMainClient = async () => {
await _resilientClientClose(await mainClientPromise);
cds.log(COMPONENT_NAME).info("main redis client closed!");
};
const closeSubscribeClient = async () => {
await _resilientClientClose(await subscriberClientPromise);
cds.log(COMPONENT_NAME).info("subscribe redis client closed!");
};
const _resilientClientClose = async (client) => {
try {
if (client?.quit) {
await client.quit();
}
} catch (err) {
cds.log(COMPONENT_NAME).info("error during redis close - continuing...", err);
}
};
const connectionCheck = async (options) => {
return new Promise((resolve, reject) => {
createClientAndConnect(options, reject, true)
.then((client) => {
if (client) {
_resilientClientClose(client);
resolve();
} else {
reject(new Error());
}
})
.catch(reject);
})
.then(() => true)
.catch((err) => {
cds.log(COMPONENT_NAME).error("Redis connection check failed! Falling back to NO_REDIS mode", err);
return false;
});
};
const isClusterMode = () => {
if (!("__clusterMode" in isClusterMode)) {
const env = getEnvInstance();
const { credentials } = env.redisRequires;
isClusterMode.__clusterMode = credentials.cluster_mode;
}
return isClusterMode.__clusterMode;
};
module.exports = {
createClientAndConnect,
createMainClientAndConnect,
subscribeRedisChannel,
publishMessage,
closeMainClient,
closeSubscribeClient,
connectionCheck,
isClusterMode,
};