1- import type { ContainerOptions , ContainerStartOptions , StopParams , State } from '../types' ;
1+ import type { ContainerStartOptions , Signal , SignalInteger , State } from '../types' ;
22import { parseTimeExpression } from './helpers' ;
33import { DurableObject } from 'cloudflare:workers' ;
44
5- const CONTAINER_STATE_KEY = '__CF_CONTAINER_STATE' ;
6-
75const PING_TIMEOUT_MS = 5000 ;
86
97const DEFAULT_SLEEP_AFTER = '10m' ; // Default sleep after inactivity time
@@ -13,8 +11,6 @@ const INSTANCE_POLL_INTERVAL_MS = 300; // Default interval for polling container
1311// to see if the container is up at all.
1412const FALLBACK_PORT_TO_CHECK = 33 ;
1513
16- export type Signal = 'SIGKILL' | 'SIGINT' | 'SIGTERM' ;
17- export type SignalInteger = number ;
1814const signalToNumbers : Record < Signal , SignalInteger > = {
1915 SIGINT : 2 ,
2016 SIGTERM : 15 ,
@@ -29,20 +25,21 @@ const signalToNumbers: Record<Signal, SignalInteger> = {
2925
3026// ==== Error helpers ====
3127
32- function isErrorOfType ( e : unknown , matchingString : string ) : boolean {
33- const errorString = e instanceof Error ? e . message : String ( e ) ;
34- return errorString . toLowerCase ( ) . includes ( matchingString ) ;
35- }
28+ const MAX_INSTANCES_ERROR = 'Maximum number of running container instances exceeded' ;
3629
3730const NO_CONTAINER_INSTANCE_ERROR =
3831 'there is no container instance that can be provided to this durable object' ;
3932
40- /** we can retry start */
41- const isNoInstanceError = ( error : unknown ) : boolean =>
42- isErrorOfType ( error , NO_CONTAINER_INSTANCE_ERROR ) ;
43-
4433const NOT_LISTENING_ERROR = 'the container is not listening' ;
45- const isNotListeningError = ( error : unknown ) : boolean => isErrorOfType ( error , NOT_LISTENING_ERROR ) ;
34+
35+ function isErrorOfType ( e : unknown , matchingString : string ) : boolean {
36+ const errorString = e instanceof Error ? e . message : String ( e ) ;
37+ return errorString . toLowerCase ( ) . includes ( matchingString ) ;
38+ }
39+
40+ function retryableError ( e : unknown ) : boolean {
41+ return isErrorOfType ( e , NO_CONTAINER_INSTANCE_ERROR ) || isErrorOfType ( e , MAX_INSTANCES_ERROR ) ;
42+ }
4643
4744/**
4845 * Combines the existing user-defined signal with a signal that aborts after the timeout specified by waitInterval.
@@ -67,71 +64,6 @@ function addTimeoutSignals(
6764 return controller . signal ;
6865}
6966
70- // ===============================
71- // CONTAINER STATE WRAPPER
72- // ===============================
73-
74- /**
75- * ContainerState is a wrapper around a DO storage to store and get
76- * the container state.
77- * It's useful to track which kind of events have been handled by the user,
78- * a transition to a new state won't be successful unless the user's hook has been
79- * triggered and waited for.
80- * A user hook might be repeated multiple times if they throw errors.
81- */
82- class ContainerState {
83- status ?: State ;
84- constructor ( private storage : DurableObject [ 'ctx' ] [ 'storage' ] ) { }
85-
86- async setRunning ( ) {
87- await this . setStatusAndupdate ( 'running' ) ;
88- }
89-
90- async setHealthy ( ) {
91- await this . setStatusAndupdate ( 'healthy' ) ;
92- }
93-
94- async setStopping ( ) {
95- await this . setStatusAndupdate ( 'stopping' ) ;
96- }
97-
98- async setStopped ( ) {
99- await this . setStatusAndupdate ( 'stopped' ) ;
100- }
101-
102- async setStoppedWithCode ( exitCode : number ) {
103- this . status = { status : 'stopped_with_code' , lastChange : Date . now ( ) , exitCode } ;
104- await this . update ( ) ;
105- }
106-
107- async getState ( ) : Promise < State > {
108- if ( ! this . status ) {
109- const state = await this . storage . get < State > ( CONTAINER_STATE_KEY ) ;
110- if ( ! state ) {
111- this . status = {
112- status : 'stopped' ,
113- lastChange : Date . now ( ) ,
114- } ;
115- await this . update ( ) ;
116- } else {
117- this . status = state ;
118- }
119- }
120-
121- return this . status ! ;
122- }
123-
124- private async setStatusAndupdate ( status : State [ 'status' ] ) {
125- this . status = { status : status , lastChange : Date . now ( ) } ;
126- await this . update ( ) ;
127- }
128-
129- private async update ( ) {
130- if ( ! this . status ) throw new Error ( 'status should be init' ) ;
131- await this . storage . put < State > ( CONTAINER_STATE_KEY , this . status ) ;
132- }
133- }
134-
13567// ===============================
13668// ===============================
13769// MAIN CONTAINER CLASS
@@ -156,12 +88,12 @@ export class Container<Env = unknown> extends DurableObject<Env> {
15688 envVars : ContainerStartOptions [ 'env' ] = { } ;
15789 entrypoint : ContainerStartOptions [ 'entrypoint' ] ;
15890 enableInternet : ContainerStartOptions [ 'enableInternet' ] = true ;
159-
91+ public container : NonNullable < DurableObject [ 'ctx' ] [ 'container' ] > ;
16092 // =========================
16193 // PUBLIC INTERFACE
16294 // =========================
16395
164- constructor ( ctx : DurableObject [ 'ctx' ] , env : Env , options ?: ContainerOptions ) {
96+ constructor ( ctx : DurableObject [ 'ctx' ] , env : Env ) {
16597 super ( ctx , env ) ;
16698
16799 if ( ctx . container === undefined ) {
@@ -170,51 +102,24 @@ export class Container<Env = unknown> extends DurableObject<Env> {
170102 ) ;
171103 }
172104
173- this . state = new ContainerState ( this . ctx . storage ) ;
174-
175105 this . ctx . blockConcurrencyWhile ( async ( ) => {
176106 await this . ctx . container ?. setInactivityTimeout ( parseTimeExpression ( this . sleepAfter ) * 1000 ) ;
177107 } ) ;
178108
179109 this . container = ctx . container ;
180110
181- // Apply options if provided
182- if ( options ) {
183- if ( options . defaultPort !== undefined ) this . defaultPort = options . defaultPort ;
184- if ( options . sleepAfter !== undefined ) this . sleepAfter = options . sleepAfter ;
185- }
186-
187111 // we are not setting up a global monitor because we cannot guarantee the DO will be alive when the container stops
188112 // if (this.container.running) {
189113 // this.monitor ??= this.setupMonitorCallbacks();
190114 // }
191115 }
192116 /**
193117 * Gets the current state of the container
194- * @returns Promise<State>
195118 */
196- async getState ( ) : Promise < State > {
197- return { ...( await this . state . getState ( ) ) } ;
198- }
199-
200- /**
201- *
202- * Returns a promise that resolves when the container is ready.
203- * By default readiness is defined as being able to successfully fetch 'http://ping' on the specified port.
204- *
205- * This can be overriden by the user and will be called when starting the container
206- *
207- * If called by us, this will receive a port to check, which might be the target port of a fetch if this was called during fetch.
208- * The user could choose to ignore this port, or check other ports as well.
209- * It will also receive an abort signal which is a combination of a user provided signal and a timeout signal.
210- *
211- * If called by the user it won't receive these parameters :/
212- *
213- */
214- public async readinessCheck ( portToCheck ?: number , signal ?: AbortSignal ) : Promise < void > {
215- await this . container
216- . getTcpPort ( portToCheck ?? FALLBACK_PORT_TO_CHECK )
217- . fetch ( 'http://ping' , { signal } ) ;
119+ getState ( ) : State {
120+ return {
121+ status : this . container . running ? ( 'running' as const ) : ( 'stopped' as const ) ,
122+ } ;
218123 }
219124
220125 /**
@@ -292,7 +197,7 @@ export class Container<Env = unknown> extends DurableObject<Env> {
292197 if ( options . signal ?. aborted ) {
293198 throw new Error ( 'Container start aborted by user signal' ) ;
294199 }
295- if ( ! this . container . running && ( attempt === 0 || isNoInstanceError ( lastError ) ) ) {
200+ if ( ! this . container . running && ( attempt === 0 || retryableError ( lastError ) ) ) {
296201 const resolvedEnvVars = options . envVars ?? this . envVars ;
297202 const resolvedEntrypoint = options . entrypoint ?? this . entrypoint ;
298203 this . container . start ( {
@@ -309,20 +214,24 @@ export class Container<Env = unknown> extends DurableObject<Env> {
309214 try {
310215 // by default this pings the container
311216 const timeoutSignal = addTimeoutSignals ( options . signal , options . pingTimeoutMs ) ;
312- await this . readinessCheck ( portToCheck , timeoutSignal ) ;
217+ await this . container
218+ . getTcpPort ( portToCheck )
219+ . fetch ( 'http://ping' , { signal : timeoutSignal } ) ;
220+
221+ // the ping was successful, exit the loop
313222 break ;
314223 } catch ( e ) {
315224 if ( this . container . running ) {
316- // return if the user has specified that we don't need to wait for the container application to be ready
317- if ( isNotListeningError ( e ) && ! options . waitForReady ) {
225+ // exit loop if the user has specified that we don't need to wait for the container application to be ready
226+ if ( isErrorOfType ( e , NOT_LISTENING_ERROR ) && ! options . waitForReady ) {
318227 break ;
319228 }
320229 // otherwise fallthrough to retry the ping...
321230 } else {
322231 // we tried to start the container but it is now not running
323232 await startupMonitor . catch ( async err => {
324233 // if the error is cloudchamberd not providing a container in time, we can retry
325- if ( isNoInstanceError ( err ) ) {
234+ if ( retryableError ( err ) ) {
326235 lastError = err ;
327236 } else {
328237 // for any other reason, we should assume the container crashed and give up
@@ -349,7 +258,7 @@ export class Container<Env = unknown> extends DurableObject<Env> {
349258 }
350259 }
351260
352- // wait a bit before retrying
261+ // Wait a bit before retrying
353262 await Promise . race ( [
354263 new Promise ( res => setTimeout ( res , INSTANCE_POLL_INTERVAL_MS ) ) ,
355264 userSignalPromise ,
@@ -368,10 +277,6 @@ export class Container<Env = unknown> extends DurableObject<Env> {
368277 // this.monitor ??= this.setupMonitorCallbacks();
369278 }
370279
371- // =======================
372- // LIFECYCLE HOOKS
373- // =======================
374-
375280 /**
376281 * Send a signal to the container.
377282 * @param signal - The signal to send to the container (default: 15 for SIGTERM)
@@ -422,17 +327,12 @@ export class Container<Env = unknown> extends DurableObject<Env> {
422327 // throw error;
423328 // }
424329
425- // ============
426- // HTTP
427- // ============
428-
429330 // this should not be overridden by the user
430331 override async fetch ( request : Request ) : Promise < Response > {
431332 const portFromUrl = new URL ( request . url ) . port ;
432333 const targetPort = this . defaultPort ?? ( portFromUrl ? parseInt ( portFromUrl ) : undefined ) ;
433334 if ( targetPort === undefined ) {
434335 throw new Error (
435- // TODO: update this with a docs url.
436336 'No port configured for this container. Set the `defaultPort` in your Container subclass, or specify a port on your request url`.'
437337 ) ;
438338 }
@@ -444,9 +344,6 @@ export class Container<Env = unknown> extends DurableObject<Env> {
444344 return await tcpPort . fetch ( request . url . replace ( 'https:' , 'http:' ) , request ) ;
445345 }
446346
447- public container : NonNullable < DurableObject [ 'ctx' ] [ 'container' ] > ;
448- private state : ContainerState ;
449-
450347 // we are not setting up a global monitor because we cannot guarantee the DO will be alive when the container stops
451348 // private monitor: Promise<unknown> | undefined;
452349
0 commit comments