couchbase
Version:
The official Couchbase Node.js Client Library.
157 lines (156 loc) • 6.33 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WaitUntilReadyExecutor = void 0;
const diagnosticsexecutor_1 = require("./diagnosticsexecutor");
const diagnosticstypes_1 = require("./diagnosticstypes");
const errors_1 = require("./errors");
const generaltypes_1 = require("./generaltypes");
const utilities_1 = require("./utilities");
const ALL_SERVICE_TYPES = Object.values(generaltypes_1.ServiceType);
const BACKOFF_BASE_MS = 50;
const BACKOFF_CAP_MS = 500;
const BACKOFF_MAX_EXP = 4;
/**
* @internal
*/
class WaitUntilReadyExecutor {
/**
* @internal
*/
constructor(cluster, bucketName) {
this._cluster = cluster;
this._bucketName = bucketName;
this._diagExec = new diagnosticsexecutor_1.DiagnoticsExecutor(cluster);
this._pingExec = new diagnosticsexecutor_1.PingExecutor(cluster);
}
/**
* Checks whether the endpoints for a single service satisfy the desired cluster state.
*
* @internal
*/
_meetsDesiredState(endpoints, desiredState) {
if (endpoints.length === 0) {
return false;
}
const connected = endpoints.filter((ep) => ep.state === diagnosticstypes_1.EndpointState.Connected).length;
if (desiredState === diagnosticstypes_1.ClusterState.Online) {
return connected === endpoints.length;
}
if (desiredState === diagnosticstypes_1.ClusterState.Degraded) {
return connected > 0;
}
return false;
}
/**
* Logs any ping endpoints that reported an error, at warn level.
*
* @internal
*/
_logPingErrors(pingResult) {
for (const [serviceType, endpoints] of Object.entries(pingResult.services)) {
for (const ep of endpoints) {
if (ep.state === diagnosticstypes_1.PingState.Error && ep.error) {
this._cluster.logger.warn(`waitUntilReady: ping error on service ${serviceType}, remote ${ep.remote}: ${ep.error}`);
}
}
}
}
/**
* Builds the set of services to wait on, either from the user provided list or via discovery ping.
*
* @internal
*/
async _resolveWaitSet(serviceTypes, deadline) {
let waitSet;
if (serviceTypes) {
waitSet = [...serviceTypes];
if (this._bucketName === undefined) {
const filtered = waitSet.filter((s) => s === generaltypes_1.ServiceType.KeyValue || s === generaltypes_1.ServiceType.Views);
if (filtered.length > 0) {
this._cluster.logger.warn(`waitUntilReady: ignoring [${filtered.join(', ')}] at cluster scope (no bucket context)`);
waitSet = waitSet.filter((s) => s !== generaltypes_1.ServiceType.KeyValue && s !== generaltypes_1.ServiceType.Views);
}
}
}
else {
const discoveryResult = await this._pingExec.ping({
bucket: this._bucketName,
timeout: deadline.left(),
});
this._logPingErrors(discoveryResult);
waitSet = [];
for (const [serviceType, endpoints] of Object.entries(discoveryResult.services)) {
if (endpoints.length > 0) {
waitSet.push(serviceType);
}
}
}
return waitSet;
}
/**
* @internal
*/
async waitUntilReady(timeout, options) {
var _a, _b;
if (typeof timeout !== 'number' || !isFinite(timeout) || timeout <= 0) {
throw new errors_1.InvalidArgumentError(new Error('timeout must be a positive finite number'));
}
const desiredState = (_a = options.desiredState) !== null && _a !== void 0 ? _a : diagnosticstypes_1.ClusterState.Online;
if (desiredState === diagnosticstypes_1.ClusterState.Offline) {
throw new errors_1.InvalidArgumentError(new Error('ClusterState.Offline is not a valid desired state'));
}
if (options.serviceTypes) {
for (const st of options.serviceTypes) {
if (!ALL_SERVICE_TYPES.includes(st)) {
throw new errors_1.InvalidArgumentError(new Error(`Invalid service type: ${st}`));
}
}
if (options.serviceTypes.length === 0) {
return;
}
}
const deadline = new utilities_1.CompoundTimeout(timeout);
const waitSet = await this._resolveWaitSet(options.serviceTypes, deadline);
if (waitSet.length === 0) {
return;
}
let iterations = 0;
let needPing = [...waitSet];
while (true) {
const diagResult = await this._diagExec.diagnostics({});
needPing = [];
for (const svc of waitSet) {
const endpoints = diagResult.services[svc] || [];
if (!this._meetsDesiredState(endpoints, desiredState)) {
needPing.push(svc);
}
}
if (needPing.length === 0) {
return;
}
if (deadline.expired()) {
throw new errors_1.UnambiguousTimeoutError(new Error('Timed out prior to reaching desired state. ' +
'Services not ready: ' + needPing.join(', ')));
}
try {
const pingResult = await this._pingExec.ping({
serviceTypes: needPing,
bucket: this._bucketName,
timeout: deadline.left(),
});
this._logPingErrors(pingResult);
}
catch {
// One failed ping isn't fatal, try again or eventually time out after deadline is reached.
}
const backoffMs = Math.min(BACKOFF_BASE_MS * Math.pow(2, Math.min(iterations, BACKOFF_MAX_EXP)), BACKOFF_CAP_MS);
const timeLeft = (_b = deadline.left()) !== null && _b !== void 0 ? _b : 0;
const sleepMs = Math.min(backoffMs, timeLeft);
if (sleepMs > 0) {
await new Promise((resolve) => setTimeout(resolve, sleepMs));
}
iterations++;
}
}
}
exports.WaitUntilReadyExecutor = WaitUntilReadyExecutor;