UNPKG

@message-queue-toolkit/core

Version:

Useful utilities, interfaces and base classes for message queue handling. Supports AMQP and SQS with a common abstraction on top currently

205 lines 8.05 kB
import { setTimeout } from 'node:timers/promises'; import { isError } from '@lokalise/node-core'; import { NO_TIMEOUT } from "../types/queueOptionsTypes.js"; const DEFAULT_POLLING_INTERVAL_MS = 5000; export class StartupResourcePollingTimeoutError extends Error { resourceName; timeoutMs; constructor(resourceName, timeoutMs) { super(`Timeout waiting for resource "${resourceName}" to become available after ${timeoutMs}ms. ` + 'The resource may not exist or there may be a configuration issue.'); this.name = 'StartupResourcePollingTimeoutError'; this.resourceName = resourceName; this.timeoutMs = timeoutMs; } } function checkTimeoutExceeded(hasTimeout, timeoutMs, startTime, resourceName, attemptCount, logger) { if (!hasTimeout) return { exceeded: false }; const elapsedMs = Date.now() - startTime; if (elapsedMs >= timeoutMs) { logger?.error({ message: `Timeout waiting for resource "${resourceName}" to become available`, resourceName, timeoutMs, attemptCount, elapsedMs, }); return { exceeded: true, error: new StartupResourcePollingTimeoutError(resourceName, timeoutMs), }; } return { exceeded: false }; } /** * Handles timeout condition - either throws or reports and returns new timeout count. * @returns Updated timeout count after handling */ function handleTimeout(params) { const { timeoutResult, throwOnTimeout, resourceName, timeoutMs, attemptCount, timeoutCount, logger, errorReporter, } = params; if (throwOnTimeout) { throw timeoutResult.error; } // Report error and reset timeout counter const newTimeoutCount = timeoutCount + 1; errorReporter?.report({ error: timeoutResult.error, context: { resourceName, timeoutMs, attemptCount, timeoutCount: newTimeoutCount, }, }); logger?.warn({ message: `Timeout waiting for resource "${resourceName}", resetting timeout counter and continuing`, resourceName, timeoutMs, attemptCount, timeoutCount: newTimeoutCount, }); return newTimeoutCount; } function logResourceAvailable(resourceName, attemptCount, startTime, logger) { const elapsedMs = Date.now() - startTime; logger?.info({ message: `Resource "${resourceName}" is now available`, resourceName, attemptCount, elapsedMs, }); } /** * Internal polling loop that waits for a resource to become available. * Separated from main function to support non-blocking mode. */ async function pollForResource(options, pollingIntervalMs, hasTimeout, timeoutMs, throwOnTimeout, initialAttemptCount) { const { checkFn, resourceName, logger, errorReporter } = options; let startTime = Date.now(); let attemptCount = initialAttemptCount; let timeoutCount = 0; while (true) { attemptCount++; const timeoutResult = checkTimeoutExceeded(hasTimeout, timeoutMs, startTime, resourceName, attemptCount, logger); if (timeoutResult.exceeded) { timeoutCount = handleTimeout({ timeoutResult, throwOnTimeout, resourceName, timeoutMs, attemptCount, timeoutCount, logger, errorReporter, }); startTime = Date.now(); } const result = await checkFn().catch((error) => { // Unexpected error during check - log and rethrow logger?.error({ message: `Error checking resource availability for "${resourceName}"`, resourceName, error, attemptCount, }); throw error; }); if (result.isAvailable) { logResourceAvailable(resourceName, attemptCount, startTime, logger); return result.result; } // Resource not available yet, log and wait if (attemptCount === 1 || attemptCount % 12 === 0) { // Log on first attempt and then every minute (assuming 5s interval) const elapsedMs = Date.now() - startTime; logger?.debug({ message: `Resource "${resourceName}" not available yet, will retry`, resourceName, attemptCount, elapsedMs, nextRetryInMs: pollingIntervalMs, }); } // Wait before next attempt await setTimeout(pollingIntervalMs); } } /** * Waits for a resource to become available by polling. * This is used for startup resource polling mode where resources may not exist at startup. * * @param options - Configuration and check function * @returns The result from the check function when resource becomes available, * or undefined if nonBlocking is true and resource was not immediately available * @throws StartupResourcePollingTimeoutError if timeout is reached and throwOnTimeout is true (default) */ export async function waitForResource(options) { const { config, checkFn, resourceName, logger, onResourceAvailable } = options; const pollingIntervalMs = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS; const hasTimeout = config.timeoutMs !== NO_TIMEOUT; const timeoutMs = hasTimeout ? config.timeoutMs : 0; const throwOnTimeout = config.throwOnTimeout !== false; const nonBlocking = config.nonBlocking === true; logger?.info({ message: `Waiting for resource "${resourceName}" to become available`, resourceName, pollingIntervalMs, timeoutMs: hasTimeout ? timeoutMs : 'NO_TIMEOUT', throwOnTimeout, nonBlocking, }); // First check - always done synchronously const initialResult = await checkFn().catch((error) => { logger?.error({ message: `Error checking resource availability for "${resourceName}"`, resourceName, error, attemptCount: 1, }); throw error; }); if (initialResult.isAvailable) { logResourceAvailable(resourceName, 1, Date.now(), logger); return initialResult.result; } // Resource not available on first check if (nonBlocking) { // Start background polling and return immediately logger?.info({ message: `Resource "${resourceName}" not immediately available, starting background polling`, resourceName, pollingIntervalMs, }); // Fire and forget - start polling in background const { onError } = options; setTimeout(pollingIntervalMs).then(() => { pollForResource(options, pollingIntervalMs, hasTimeout, timeoutMs, throwOnTimeout, 1) .then((result) => { onResourceAvailable?.(result); }) .catch((err) => { const error = isError(err) ? err : new Error(String(err)); logger?.error({ message: `Background polling for resource "${resourceName}" failed`, resourceName, error, }); // isFinal: true because pollForResource only throws when it gives up // (timeout with throwOnTimeout: true, or unexpected error) onError?.(error, { isFinal: true }); }); }); return undefined; } // Blocking mode - continue polling and wait for result return pollForResource(options, pollingIntervalMs, hasTimeout, timeoutMs, throwOnTimeout, 1); } /** * Helper to check if startup resource polling is enabled. * Returns true only when config is provided and enabled is explicitly true. */ export function isStartupResourcePollingEnabled(config) { return config?.enabled === true; } //# sourceMappingURL=startupResourcePollingUtils.js.map