diff --git a/src/index.ts b/src/index.ts index 60e8b5f..657f47f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,6 @@ export type { ContainerOptions, ContainerEventHandler, ContainerMessage, - ContainerStartConfigOptions, StopParams, - WaitOptions, State, } from './types'; diff --git a/src/lib/container.ts b/src/lib/container.ts index eb038dc..42e0697 100644 --- a/src/lib/container.ts +++ b/src/lib/container.ts @@ -1,13 +1,4 @@ -import type { - ContainerOptions, - ContainerStartOptions, - ContainerStartConfigOptions, - StopParams, - State, - WaitOptions, - CancellationOptions, - StartAndWaitForPortsOptions, -} from '../types'; +import type { ContainerOptions, ContainerStartOptions, StopParams, State } from '../types'; import { parseTimeExpression } from './helpers'; import { DurableObject } from 'cloudflare:workers'; @@ -29,16 +20,6 @@ const PING_TIMEOUT_MS = 5000; const DEFAULT_SLEEP_AFTER = '10m'; // Default sleep after inactivity time const INSTANCE_POLL_INTERVAL_MS = 300; // Default interval for polling container state -// Timeout for getting container instance and launching a VM -// Time to find an instance, attach a DO, call start, but NOT -// the time for the app the actually start -const TIMEOUT_TO_GET_CONTAINER_MS = 8_000; - -// Timeout for getting a container instance and launching -// the actual application and have it listen for specific ports -// One day might be configurable by the end user in Container class attribute -const TIMEOUT_TO_GET_PORTS_MS = 20_000; - // If user has specified no ports and we need to check one // to see if the container is up at all. const FALLBACK_PORT_TO_CHECK = 33; @@ -72,54 +53,21 @@ const isNotListeningError = (error: unknown): boolean => isErrorOfType(error, NO const isContainerExitNonZeroError = (error: unknown): boolean => isErrorOfType(error, UNEXPECTED_EXIT_ERROR); -function getExitCodeFromError(error: unknown): number | null { - if (!(error instanceof Error)) { - return null; - } - - if (isRuntimeSignalledError(error)) { - return +error.message - .toLowerCase() - .slice( - error.message.toLowerCase().indexOf(RUNTIME_SIGNALLED_ERROR) + - RUNTIME_SIGNALLED_ERROR.length + - 1 - ); - } - - if (isContainerExitNonZeroError(error)) { - return +error.message - .toLowerCase() - .slice( - error.message.toLowerCase().indexOf(UNEXPECTED_EXIT_ERROR) + - UNEXPECTED_EXIT_ERROR.length + - 1 - ); - } - - return null; -} - /** * Combines the existing user-defined signal with a signal that aborts after the timeout specified by waitInterval */ function addTimeoutSignal(existingSignal: AbortSignal | undefined, timeoutMs: number): AbortSignal { const controller = new AbortController(); - // Forward existing signal abort - if (existingSignal?.aborted) { - controller.abort(); - return controller.signal; - } - existingSignal?.addEventListener('abort', () => controller.abort()); // Add timeout - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + const timeoutId = setTimeout(() => controller.abort('ping timed out'), timeoutMs); // Clean up timeout if signal is aborted early controller.signal.addEventListener('abort', () => clearTimeout(timeoutId)); + // note the timeout aborting does not clear the existing signal return controller.signal; } @@ -202,10 +150,6 @@ export class Container extends DurableObject { // Default port for the container (undefined means no default port) defaultPort?: number; - // Required ports that should be checked for availability during container startup - // Override this in your subclass to specify ports that must be ready - requiredPorts?: number[]; - // Timeout after which the container will sleep if no activity // The signal sent to the container by default is a SIGTERM. // The container won't get a SIGKILL if this threshold is triggered. @@ -245,11 +189,9 @@ export class Container extends DurableObject { } if (this.container.running) { - this.monitor = this.container.monitor(); - this.setupMonitorCallbacks(); + this.monitor ??= this.setupMonitorCallbacks(); } } - /** * Gets the current state of the container * @returns Promise @@ -258,217 +200,148 @@ export class Container extends DurableObject { return { ...(await this.state.getState()) }; } - // ========================== - // CONTAINER STARTING - // ========================== - /** - * Start the container if it's not running and set up monitoring and lifecycle hooks, - * without waiting for ports to be ready. - * - * It will automatically retry if the container fails to start, using the specified waitOptions - * * - * @example - * await this.start({ - * envVars: { DEBUG: 'true', NODE_ENV: 'development' }, - * entrypoint: ['npm', 'run', 'dev'], - * enableInternet: false - * }); + * Starts container. + * If the container is already started, and waitForReady is false, this will resolve immediately if the container accepts the ping. * - * @param startOptions - Override `envVars`, `entrypoint` and `enableInternet` on a per-instance basis - * @param waitOptions - Optional wait configuration with abort signal for cancellation. Default ~8s timeout. - * @returns A promise that resolves when the container start command has been issued - * @throws Error if no container context is available or if all start attempts fail */ public async start( - startOptions?: ContainerStartConfigOptions, - waitOptions?: WaitOptions + options: { + /** Environment variables to pass to the container */ + envVars?: Record; + /** Custom entrypoint to override container default */ + entrypoint?: string[]; + signal?: AbortSignal; + /** + * Whether to enable internet access for the container + * @default true + */ + enableInternet?: boolean; + /** + * Whether to wait for the application inside the container to be ready + * @default true + */ + waitForReady?: boolean; + /** + * Number of retries to check we have got a container + * and if waitForReady is true, that it's ready + * @default 10 + */ + retries?: number; + /** + * Timeout in milliseconds for each ping attempt + * @default 5000 + */ + pingTimeoutMs?: number; + /** Port to check for readiness, defaults to `defaultPort` or 33 if not set */ + portToCheck?: number; + } = {} ): Promise { - const portToCheck = - waitOptions?.portToCheck ?? - this.defaultPort ?? - (this.requiredPorts ? this.requiredPorts[0] : FALLBACK_PORT_TO_CHECK); - const pollInterval = waitOptions?.waitInterval ?? INSTANCE_POLL_INTERVAL_MS; - await this.startContainerIfNotRunning( - { - signal: waitOptions?.signal, - waitInterval: pollInterval, - retries: waitOptions?.retries ?? Math.ceil(TIMEOUT_TO_GET_CONTAINER_MS / pollInterval), - portToCheck, - }, - startOptions - ); - - this.setupMonitorCallbacks(); - - // TODO: We should consider an onHealthy callback - await this.ctx.blockConcurrencyWhile(async () => { - await this.onStart(); - }); - } - - /** - * Start the container and wait for ports to be available. - * - * For each specified port, it polls until the port is available or `cancellationOptions.portReadyTimeoutMS` is reached. - * - * @param ports - The ports to wait for (if undefined, uses requiredPorts or defaultPort) - * @param cancellationOptions - Options to configure timeouts, polling intereva, and abort signal - * @param startOptions Override configuration on a per-instance basis for env vars, entrypoint command and internet access - * @returns A promise that resolves when the container has been started and the ports are listening - * @throws Error if port checks fail after the specified timeout or if the container fails to start. - */ - public async startAndWaitForPorts(args: StartAndWaitForPortsOptions): Promise; - public async startAndWaitForPorts( - ports?: number | number[], - cancellationOptions?: CancellationOptions, - startOptions?: ContainerStartConfigOptions - ): Promise; - public async startAndWaitForPorts( - portsOrArgs?: number | number[] | StartAndWaitForPortsOptions, - cancellationOptions?: CancellationOptions, - startOptions?: ContainerStartConfigOptions - ): Promise; - public async startAndWaitForPorts( - portsOrArgs?: number | number[] | StartAndWaitForPortsOptions, - cancellationOptions?: CancellationOptions, - startOptions?: ContainerStartConfigOptions - ): Promise { - // Parse arguments to handle different overload signatures - let ports: number | number[] | undefined; - let resolvedCancellationOptions: CancellationOptions | undefined = {}; - let resolvedStartOptions: ContainerStartConfigOptions | undefined = {}; - - if (typeof portsOrArgs === 'object' && portsOrArgs !== null && !Array.isArray(portsOrArgs)) { - // Object-based overload: { startOptions?, ports?, cancellationOptions? } - ports = portsOrArgs.ports; - resolvedCancellationOptions = portsOrArgs.cancellationOptions; - resolvedStartOptions = portsOrArgs.startOptions; - } else { - ports = portsOrArgs; - resolvedCancellationOptions = cancellationOptions; - resolvedStartOptions = startOptions; + // Set defaults for optional properties + options.waitForReady ??= true; + options.retries ??= 10; + options.pingTimeoutMs ??= PING_TIMEOUT_MS; + options.enableInternet ??= true; + + if (this.container.running && options.waitForReady === false) { + // should we still ping? + return; } - // Determine which ports to check - const portsToCheck = await this.getPortsToCheck(ports); - - // Prepare to start the container - resolvedCancellationOptions ??= {}; - const containerGetTimeout = - resolvedCancellationOptions.instanceGetTimeoutMS ?? TIMEOUT_TO_GET_CONTAINER_MS; - const pollInterval = resolvedCancellationOptions.waitInterval ?? INSTANCE_POLL_INTERVAL_MS; - let containerGetRetries = Math.ceil(containerGetTimeout / pollInterval); - - const waitOptions: WaitOptions = { - signal: resolvedCancellationOptions.abort, - retries: containerGetRetries, - waitInterval: pollInterval, - portToCheck: portsToCheck[0], - }; - - // Start the container if it's not running - const triesUsed = await this.startContainerIfNotRunning(waitOptions, resolvedStartOptions); - - // Check each port - - const totalPortReadyTries = Math.ceil( - resolvedCancellationOptions.portReadyTimeoutMS ?? TIMEOUT_TO_GET_PORTS_MS / pollInterval - ); - let triesLeft = totalPortReadyTries - triesUsed; - - for (const port of portsToCheck) { - triesLeft = await this.waitForPort({ - signal: resolvedCancellationOptions.abort, - waitInterval: pollInterval, - retries: triesLeft, - portToCheck: port, + // we will race the ping interval timeout against this + const userSignalPromise = new Promise(res => { + options.signal?.addEventListener('abort', () => { + res(); }); - } + }); - this.setupMonitorCallbacks(); + const portToCheck = options.portToCheck ?? this.defaultPort ?? FALLBACK_PORT_TO_CHECK; - await this.ctx.blockConcurrencyWhile(async () => { - // All ports are ready - await this.state.setHealthy(); - await this.onStart(); - }); - } + let attempt = 0; + let lastError: Error | undefined = undefined; // only if cloudchamberd gives us a no instance error + const initiallyRunning = this.container.running; // checks if we need to trigger onStart + let startupMonitor: Promise | undefined; // we need different callbacks for this particular monitor. - /** - * - * Waits for a specified port to be ready - * - * Returns the number of tries used to get the port, or throws if it couldn't get the port within the specified retry limits. - * - * @param waitOptions - - * - `portToCheck`: The port number to check - * - `abort`: Optional AbortSignal to cancel waiting - * - `retries`: Number of retries before giving up (default: TRIES_TO_GET_PORTS) - * - `waitInterval`: Interval between retries in milliseconds (default: INSTANCE_POLL_INTERVAL_MS) - */ - public async waitForPort(waitOptions: WaitOptions): Promise { - const port = waitOptions.portToCheck; - const tcpPort = this.container.getTcpPort(port); - const abortedSignal = new Promise(res => { - waitOptions.signal?.addEventListener('abort', () => { - res(true); - }); - }); - const pollInterval = waitOptions.waitInterval ?? INSTANCE_POLL_INTERVAL_MS; - let tries = waitOptions.retries ?? Math.ceil(TIMEOUT_TO_GET_PORTS_MS / pollInterval); + while (attempt < options.retries) { + if (options.signal?.aborted) { + throw new Error('Container start aborted by user signal'); + } + if (!this.container.running && (attempt === 0 || isNoInstanceError(lastError))) { + const resolvedEnvVars = options.envVars ?? this.envVars; + const resolvedEntrypoint = options.entrypoint ?? this.entrypoint; + this.container.start({ + ...(resolvedEnvVars && { env: resolvedEnvVars }), + ...(resolvedEntrypoint && { entrypoint: resolvedEntrypoint }), + enableInternet: options.enableInternet ?? this.enableInternet, // defaults to true + }); + lastError = undefined; + } + // TODO: confirm you can register multiple monitor promises + startupMonitor ??= this.container.monitor(); - // Try to connect to the port multiple times - for (let i = 0; i < tries; i++) { + const combinedSignal = addTimeoutSignal(options.signal, options.pingTimeoutMs); try { - const combinedSignal = addTimeoutSignal(waitOptions.signal, PING_TIMEOUT_MS); - await tcpPort.fetch('http://ping', { signal: combinedSignal }); - - // Successfully connected to this port - console.log(`Port ${port} is ready`); + // in the future this could be a user defined healthcheck + await this.container + .getTcpPort(portToCheck) + .fetch('http://ping', { signal: combinedSignal }); break; } catch (e) { - // Check for specific error messages that indicate we should keep retrying - const errorMessage = e instanceof Error ? e.message : String(e); - - console.debug(`Error checking ${port}: ${errorMessage}`); - - // If not running, it means the container crashed - if (!this.container.running) { - try { - await this.onError( - new Error( - `Container crashed while checking for ports, did you start the container and setup the entrypoint correctly?` - ) - ); - } catch {} - - throw e; + if (this.container.running) { + if (!options.waitForReady && !isNotListeningError(e)) { + break; + } + } else { + await startupMonitor.catch(async err => { + // this is the only case where we want to retry, + // as a new instance might become available + if (isNoInstanceError(err) && attempt < options.retries!) { + lastError = err; + } else { + await this.ctx.blockConcurrencyWhile(async () => { + await this.onError(err); + }); + // assume the container crashed and give up + throw err; + } + }); + startupMonitor = undefined; } - // If we're on the last attempt and the port is still not ready, fail - if (i === tries - 1) { - try { - await this.onError( - `Failed to verify port ${port} is available after ${(i + 1) * pollInterval}ms, last error: ${errorMessage}` - ); - } catch {} - throw e; - } + console.debug('The container was not ready:', e instanceof Error ? e.message : String(e)); - // Wait a bit before trying again - await Promise.any([ - new Promise(resolve => setTimeout(resolve, waitOptions.waitInterval)), - abortedSignal, - ]); - if (waitOptions.signal?.aborted) { - throw new Error('Container request aborted.'); + if (attempt === options.retries) { + if (e instanceof Error && e.message.includes('Network connection lost')) { + // We have to abort here, the reasoning is that we might've found + // ourselves in an internal error where the Worker is stuck with a failed connection to the + // container services. + // + // Until we address this issue on the back-end CF side, we will need to abort the + // durable object so it retries to reconnect from scratch. + this.ctx.abort(); + } + // we are out of retries. if we are here the container must have started because otherwise we would have thrown earlier + throw new Error('The container application did not become ready in time.'); } } + + // wait a bit before retrying + await Promise.race([ + new Promise(res => setTimeout(res, INSTANCE_POLL_INTERVAL_MS)), + userSignalPromise, + ]); + attempt++; } - return tries; + + if (initiallyRunning === false) { + await this.ctx.blockConcurrencyWhile(async () => { + await this.onStart(); + }); + } + this.monitor ??= this.setupMonitorCallbacks(); } + // ======================= // LIFECYCLE HOOKS // ======================= @@ -524,105 +397,21 @@ export class Container extends DurableObject { // HTTP // ============ - /** - * Send a request to the container (HTTP or WebSocket) using standard fetch API signature - * - * This method handles HTTP requests to the container. - * - * WebSocket requests done outside the DO won't work until https://github.com/cloudflare/workerd/issues/2319 is addressed. - * Until then, please use `switchPort` + `fetch()`. - * - * Method supports multiple signatures to match standard fetch API: - * - containerFetch(request: Request, port?: number) - * - containerFetch(url: string | URL, init?: RequestInit, port?: number) - * - * Starts the container if not already running, and waits for the target port to be ready. - * - * @returns A Response from the container - */ - public async containerFetch( - requestOrUrl: Request | string | URL, - portOrInit?: number | RequestInit, - portParam?: number - ): Promise { - // Parse the arguments based on their types to handle different method signatures - let { request, port } = this.requestAndPortFromContainerFetchArgs( - requestOrUrl, - portOrInit, - portParam - ); - - const state = await this.state.getState(); - if (!this.container.running || state.status !== 'healthy') { - try { - await this.startAndWaitForPorts(port, { abort: request.signal }); - } catch (e) { - if (isNoInstanceError(e)) { - return new Response( - 'There is no Container instance available at this time.\nThis is likely because you have reached your max concurrent instance count (set in wrangler config) or are you currently provisioning the Container.\nIf you are deploying your Container for the first time, check your dashboard to see provisioning status, this may take a few minutes.', - { status: 503 } - ); - } else { - return new Response( - `Failed to start container: ${e instanceof Error ? e.message : String(e)}`, - { status: 500 } - ); - } - } - } - - const tcpPort = this.container.getTcpPort(port); - - // Create URL for the container request - const containerUrl = request.url.replace('https:', 'http:'); - - try { - const res = await tcpPort.fetch(containerUrl, request); - return res; - } catch (e) { - if (!(e instanceof Error)) { - throw e; - } - - // This error means that the container might've just restarted - if (e.message.includes('Network connection lost.')) { - return new Response('Container suddenly disconnected, try again', { status: 500 }); - } - - console.error(`Error proxying request to container ${this.ctx.id}:`, e); - return new Response( - `Error proxying request to container: ${e instanceof Error ? e.message : String(e)}`, - { status: 500 } - ); - } - } - - /** - * - * Fetch handler on the Container class. - * By default this forwards all requests to the container by calling `containerFetch`. - * Use `switchPort` to specify which port on the container to target, or this will use `defaultPort`. - * @param request The request to handle - */ override async fetch(request: Request): Promise { - if (this.defaultPort === undefined && !request.headers.has('cf-container-target-port')) { + const portFromUrl = new URL(request.url).port; + const targetPort = this.defaultPort ?? (portFromUrl ? parseInt(portFromUrl) : undefined); + if (targetPort === undefined) { throw new Error( - 'No port configured for this container. Set the `defaultPort` in your Container subclass, or specify a port with `container.fetch(switchPort(request, port))`.' + // TODO: update this with a docs url. + 'No port configured for this container. Set the `defaultPort` in your Container subclass, or specify a port on your request url`.' ); } - let portValue = this.defaultPort; + await this.start({ portToCheck: targetPort, waitForReady: true, signal: request.signal }); - if (request.headers.has('cf-container-target-port')) { - const portFromHeaders = parseInt(request.headers.get('cf-container-target-port') ?? ''); - if (isNaN(portFromHeaders)) { - throw new Error('port value from switchPort is not a number'); - } else { - portValue = portFromHeaders; - } - } - // Forward all requests (HTTP and WebSocket) to the container - return await this.containerFetch(request, portValue); + const tcpPort = this.container.getTcpPort(targetPort); + + return await tcpPort.fetch(request.url.replace('https:', 'http:'), request); } // =============================== @@ -639,236 +428,31 @@ export class Container extends DurableObject { private state: ContainerState; private monitor: Promise | undefined; - private monitorSetup = false; - - // ========================== - // GENERAL HELPERS - // ========================== - - private requestAndPortFromContainerFetchArgs( - requestOrUrl: Request | string | URL, - portOrInit?: number | RequestInit, - portParam?: number - ): { request: Request; port: number } { - let request: Request; - let port: number | undefined; - - // Determine if we're using the new signature or the old one - if (requestOrUrl instanceof Request) { - // Request-based: containerFetch(request, port?) - request = requestOrUrl; - port = typeof portOrInit === 'number' ? portOrInit : undefined; - } else { - // URL-based: containerFetch(url, init?, port?) - const url = typeof requestOrUrl === 'string' ? requestOrUrl : requestOrUrl.toString(); - const init = typeof portOrInit === 'number' ? {} : portOrInit || {}; - port = - typeof portOrInit === 'number' - ? portOrInit - : typeof portParam === 'number' - ? portParam - : undefined; - - // Create a Request object - request = new Request(url, init); - } - port ??= this.defaultPort; - // Require a port to be specified, either as a parameter or as a defaultPort property - if (port === undefined) { - throw new Error( - 'No port specified for container fetch. Set defaultPort or specify a port parameter.' - ); - } - - return { request, port }; - } - - /** - * - * The method prioritizes port sources in this order: - * 1. Ports specified directly in the method call - * 2. `requiredPorts` class property (if set) - * 3. `defaultPort` (if neither of the above is specified) - * 4. Falls back to port 33 if none of the above are set - */ - private async getPortsToCheck(overridePorts?: number | number[]) { - let portsToCheck: number[] = []; - - if (overridePorts !== undefined) { - // Use explicitly provided ports (single port or array) - portsToCheck = Array.isArray(overridePorts) ? overridePorts : [overridePorts]; - } else if (this.requiredPorts && this.requiredPorts.length > 0) { - // Use requiredPorts class property if available - portsToCheck = [...this.requiredPorts]; - } else { - // Fall back to defaultPort if available - portsToCheck = [this.defaultPort ?? FALLBACK_PORT_TO_CHECK]; - } - - return portsToCheck; - } - // =========================================== // CONTAINER INTERACTION & MONITORING // =========================================== - /** - * Tries to start a container if it's not already running - * Returns the number of tries used - */ - private async startContainerIfNotRunning( - waitOptions: WaitOptions, - options?: ContainerStartConfigOptions - ): Promise { - // Start the container if it's not running - if (this.container.running) { - if (!this.monitor) { - this.monitor = this.container.monitor(); - } - - return 0; - } - - const abortedSignal = new Promise(res => { - waitOptions.signal?.addEventListener('abort', () => { - res(true); - }); - }); - const pollInterval = waitOptions.waitInterval ?? INSTANCE_POLL_INTERVAL_MS; - const totalTries = waitOptions.retries ?? Math.ceil(TIMEOUT_TO_GET_CONTAINER_MS / pollInterval); - await this.state.setRunning(); - for (let tries = 0; tries < totalTries; tries++) { - // Use provided options or fall back to instance properties - const envVars = options?.envVars ?? this.envVars; - const entrypoint = options?.entrypoint ?? this.entrypoint; - const enableInternet = options?.enableInternet ?? this.enableInternet; - - // Only include properties that are defined - const startConfig: ContainerStartOptions = { - enableInternet, - }; - - if (envVars && Object.keys(envVars).length > 0) startConfig.env = envVars; - if (entrypoint) startConfig.entrypoint = entrypoint; - - const handleError = async () => { - const err = await this.monitor?.catch(err => err as Error); - - if (typeof err === 'number') { - const toThrow = new Error( - `Container exited before we could determine the container health, exit code: ${err}` - ); - - try { - await this.onError(toThrow); - } catch {} - - throw toThrow; - } else if (!isNoInstanceError(err)) { - try { - await this.onError(err); - } catch {} - - throw err; - } - }; - - if (!this.container.running) { - if (tries > 0) { - await handleError(); - } - - this.container.start(startConfig); - this.monitor = this.container.monitor(); - } - - // TODO: Make this the port I'm trying to get! - const port = this.container.getTcpPort(waitOptions.portToCheck); - try { - const combinedSignal = addTimeoutSignal(waitOptions.signal, PING_TIMEOUT_MS); - await port.fetch('http://containerstarthealthcheck', { signal: combinedSignal }); - return tries; - } catch (error) { - if (isNotListeningError(error) && this.container.running) { - return tries; - } - - if (!this.container.running && isNotListeningError(error)) { - await handleError(); - } - - console.debug( - 'Error checking if container is ready:', - error instanceof Error ? error.message : String(error) - ); - - await Promise.any([ - new Promise(res => setTimeout(res, waitOptions.waitInterval)), - abortedSignal, - ]); - - if (waitOptions.signal?.aborted) { - throw new Error( - 'Aborted waiting for container to start as we received a cancellation signal' - ); - } - - // TODO: Make this error specific to this, but then catch it above w something else - if (totalTries === tries + 1) { - if (error instanceof Error && error.message.includes('Network connection lost')) { - // We have to abort here, the reasoning is that we might've found - // ourselves in an internal error where the Worker is stuck with a failed connection to the - // container services. - // - // Until we address this issue on the back-end CF side, we will need to abort the - // durable object so it retries to reconnect from scratch. - this.ctx.abort(); - } - - throw new Error(NO_CONTAINER_INSTANCE_ERROR); - } - - continue; - } - } - - throw new Error(`Container did not start after ${totalTries * pollInterval}ms`); - } - // TODO: we may need to update this to work with setinactivityTimeout - private setupMonitorCallbacks() { - if (this.monitorSetup) { - return; - } - - this.monitorSetup = true; - this.monitor - ?.then(async () => { + /** use this rather than calling monitor directly. */ + private async setupMonitorCallbacks() { + return this.container + .monitor() + .then(async () => { await this.ctx.blockConcurrencyWhile(async () => { await this.onStop({ exitCode: 0, reason: 'exit' }); }); }) .catch(async (error: unknown) => { + await this.ctx.blockConcurrencyWhile(async () => { + await this.onError(error); + }); if (isNoInstanceError(error)) { - // we will inform later + // we will inform later (TODO: why?? when??) return; } - - const exitCode = getExitCodeFromError(error); - if (exitCode !== null) { - await this.state.setStoppedWithCode(exitCode); - this.monitorSetup = false; - this.monitor = undefined; - return; - } - - try { - // TODO: Be able to retrigger onError - await this.onError(error); - } catch {} }) .finally(() => { - this.monitorSetup = false; + this.monitor = undefined; }); } } diff --git a/src/types/index.ts b/src/types/index.ts index 681766f..fd71238 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -48,50 +48,6 @@ export interface ContainerOptions { */ export type ContainerEventHandler = () => void | Promise; -/** - * Options for starting a container with specific configuration - */ -export interface ContainerStartConfigOptions { - /** Environment variables to pass to the container */ - envVars?: Record; - /** Custom entrypoint to override container default */ - entrypoint?: string[]; - /** Whether to enable internet access for the container */ - enableInternet?: boolean; -} - -export interface StartAndWaitForPortsOptions { - startOptions?: ContainerStartConfigOptions; - ports?: number | number[]; - cancellationOptions?: CancellationOptions; -} - -/** cancellationOptions for startAndWaitForPorts() */ -export interface CancellationOptions { - /** abort signal, use to abort startAndWaitForPorts manually. */ - abort?: AbortSignal; - /** max time to get container instance and start it (application inside may not be ready), in milliseconds */ - instanceGetTimeoutMS?: number; - /** max time to wait for application to be listening at all specified ports, in milliseconds. */ - portReadyTimeoutMS?: number; - /** time to wait between polling, in milliseconds */ - waitInterval?: number; -} - -/** - * Options for waitForPort() - */ -export interface WaitOptions { - /** The port number to check for readiness */ - portToCheck: number; - /** Optional AbortSignal, use this to abort waiting for ports */ - signal?: AbortSignal; - /** Number of attempts to wait for port to be ready */ - retries?: number; - /** Time to wait in between polling port for readiness, in milliseconds */ - waitInterval?: number; -} - /** * Params sent to `onStop` method when the container stops */