Skip to content

Commit 32a684e

Browse files
committed
next: more cleanup
Remove unused exports Tidy up error checking a bit Make getState wrap container.running since we do not know the stopping state
1 parent f43d157 commit 32a684e

3 files changed

Lines changed: 32 additions & 195 deletions

File tree

src/index.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,2 @@
11
export { Container } from './lib/container';
2-
export { getRandom, loadBalance, getContainer, switchPort } from './lib/utils';
3-
export type {
4-
ContainerOptions,
5-
ContainerEventHandler,
6-
ContainerMessage,
7-
StopParams,
8-
State,
9-
} from './types';
2+
export type { State } from './types';

src/lib/container.ts

Lines changed: 27 additions & 130 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import type { ContainerOptions, ContainerStartOptions, StopParams, State } from '../types';
1+
import type { ContainerStartOptions, Signal, SignalInteger, State } from '../types';
22
import { parseTimeExpression } from './helpers';
33
import { DurableObject } from 'cloudflare:workers';
44

5-
const CONTAINER_STATE_KEY = '__CF_CONTAINER_STATE';
6-
75
const PING_TIMEOUT_MS = 5000;
86

97
const 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.
1412
const FALLBACK_PORT_TO_CHECK = 33;
1513

16-
export type Signal = 'SIGKILL' | 'SIGINT' | 'SIGTERM';
17-
export type SignalInteger = number;
1814
const 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

3730
const 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-
4433
const 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

src/types/index.ts

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,62 +10,9 @@ export type ContainerStartOptions = NonNullable<
1010
Parameters<NonNullable<DurableObject['ctx']['container']>['start']>[0]
1111
>;
1212

13-
/**
14-
* Message structure for communication with containers
15-
*/
16-
export interface ContainerMessage<T = unknown> {
17-
type: string;
18-
payload?: T;
19-
}
20-
21-
// Container state interface removed
22-
23-
/**
24-
* Options for container configuration
25-
*/
26-
export interface ContainerOptions {
27-
/** Optional ID for the container */
28-
id?: string;
29-
30-
/** Default port number to connect to (defaults to container.defaultPort) */
31-
defaultPort?: number;
32-
33-
/** How long to keep the container alive without activity */
34-
sleepAfter?: string | number;
35-
36-
/** Environment variables to pass to the container */
37-
envVars?: Record<string, string>;
38-
39-
/** Custom entrypoint to override container default */
40-
entrypoint?: string[];
41-
42-
/** Whether to enable internet access for the container */
43-
enableInternet?: boolean;
44-
}
45-
46-
/**
47-
* Function to handle container events
48-
*/
49-
export type ContainerEventHandler = () => void | Promise<void>;
50-
51-
/**
52-
* Params sent to `onStop` method when the container stops
53-
*/
54-
export type StopParams = {
55-
exitCode: number;
56-
reason: 'exit' | 'runtime_signal';
13+
export type State = {
14+
status: 'running' | 'stopped';
5715
};
5816

59-
export type State = {
60-
lastChange: number;
61-
} & (
62-
| {
63-
// 'running' means that the container is trying to start and is transitioning to a healthy status.
64-
// onStop might be triggered if there is an exit code, and it will transition to 'stopped'.
65-
status: 'running' | 'stopping' | 'stopped' | 'healthy';
66-
}
67-
| {
68-
status: 'stopped_with_code';
69-
exitCode?: number;
70-
}
71-
);
17+
export type Signal = 'SIGKILL' | 'SIGINT' | 'SIGTERM';
18+
export type SignalInteger = number;

0 commit comments

Comments
 (0)