@cloudflare/containers 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -73
- package/dist/lib/container.d.ts +18 -4
- package/dist/lib/container.js +97 -103
- package/dist/lib/container.js.map +1 -1
- package/package.json +1 -1
- package/dist/index.d.mts +0 -287
- package/dist/index.mjs +0 -967
package/README.md
CHANGED
|
@@ -63,11 +63,10 @@ The main class that extends a container-enbled Durable Object to provide additio
|
|
|
63
63
|
- `defaultPort?`: Optional default port to use when communicating with the container. If not set, you must specify port in containerFetch calls
|
|
64
64
|
- `requiredPorts?`: Array of ports that should be checked for availability during container startup. Used by startAndWaitForPorts when no specific ports are provided.
|
|
65
65
|
- `sleepAfter`: How long to keep the container alive without activity (format: number for seconds, or string like "5m", "30s", "1h")
|
|
66
|
-
- `manualStart`: If true, container won't start automatically on DO start (default: false). Set as a class property or via constructor options.
|
|
67
66
|
- `env`: Environment variables to pass to the container (Record<string, string>)
|
|
68
67
|
- `entrypoint?`: Custom entrypoint to override container default (string[])
|
|
69
68
|
- `enableInternet`: Whether to enable internet access for the container (boolean, default: true)
|
|
70
|
-
- Lifecycle methods: `onStart`, `onStop`, `onError`
|
|
69
|
+
- Lifecycle methods: `onStart`, `onStop`, `onError`, `onActivityExpired`
|
|
71
70
|
|
|
72
71
|
#### Constructor Options
|
|
73
72
|
|
|
@@ -75,8 +74,6 @@ The main class that extends a container-enbled Durable Object to provide additio
|
|
|
75
74
|
constructor(ctx: any, env: Env, options?: {
|
|
76
75
|
defaultPort?: number; // Override default port
|
|
77
76
|
sleepAfter?: string | number; // Override sleep timeout
|
|
78
|
-
manualStart?: boolean; // Disable automatic container start (preferred way)
|
|
79
|
-
explicitContainerStart?: boolean; // Legacy option, use manualStart instead
|
|
80
77
|
env?: Record<string, string>; // Environment variables to pass to the container
|
|
81
78
|
entrypoint?: string[]; // Custom entrypoint to override container default
|
|
82
79
|
enableInternet?: boolean; // Whether to enable internet access for the container
|
|
@@ -86,23 +83,31 @@ constructor(ctx: any, env: Env, options?: {
|
|
|
86
83
|
#### Methods
|
|
87
84
|
|
|
88
85
|
##### Lifecycle Methods
|
|
86
|
+
All lifecycle methods can be implemented as async if needed.
|
|
89
87
|
|
|
90
88
|
- `onStart()`: Called when container starts successfully - override to add custom behavior
|
|
91
89
|
- `onStop()`: Called when container shuts down - override to add custom behavior
|
|
92
90
|
- `onError(error)`: Called when container encounters an error - override to add custom behavior
|
|
91
|
+
- `onActivityExpired()`: Called when the activity is expired - override to add custom behavior, like communicating with the container to see if it should be shutdown.
|
|
92
|
+
|
|
93
|
+
By default, it calls `ctx.container.destroy()`.
|
|
94
|
+
If you don't stop the container here, the activity tracker will be renewed, and this lifecycle hook will be called again when the timer re-expires.
|
|
93
95
|
|
|
94
96
|
##### Container Methods
|
|
95
97
|
|
|
96
98
|
- `fetch(request)`: Default handler to forward HTTP requests to the container. Can be overridden.
|
|
97
|
-
- `containerFetch(...)`: Sends an HTTP
|
|
99
|
+
- `containerFetch(...)`: Sends an HTTP request to the container. Supports both standard fetch API signatures:
|
|
98
100
|
- `containerFetch(request, port?)`: Traditional signature with Request object
|
|
99
101
|
- `containerFetch(url, init?, port?)`: Standard fetch-like signature with URL string/object and RequestInit options
|
|
100
|
-
Either port parameter or defaultPort must be specified.
|
|
102
|
+
Either port parameter or defaultPort must be specified.
|
|
103
|
+
When you call any of the fetch functions, the activity will be automatically renewed, and if the container will be started if not already running.
|
|
101
104
|
- `start()`: Starts the container if it's not running and sets up monitoring, without waiting for any ports to be ready.
|
|
102
105
|
- `startAndWaitForPorts(ports?, maxTries?)`: Starts the container using `start()` and then waits for specified ports to be ready. If no ports are specified, uses `requiredPorts` or `defaultPort`. If no ports can be determined, just starts the container without port checks.
|
|
103
|
-
- `stop(
|
|
106
|
+
- `stop(signal = SIGTERM)`: Sends the specified signal to the container.
|
|
107
|
+
- `destroy()`: Forcefully destroys the container.
|
|
104
108
|
- `renewActivityTimeout()`: Manually renews the container activity timeout (extends container lifetime)
|
|
105
109
|
- `stopDueToInactivity()`: Called automatically when the container times out due to inactivity
|
|
110
|
+
- `alarm()`: Default alarm handler. It's in charge of renewing the container activity and keeping the durable object alive. You can override `alarm()`, but because its functionality is currently vital to managing the container lifecycle, we recommend calling `schedule` to schedule tasks instead.
|
|
106
111
|
|
|
107
112
|
### Utility Functions
|
|
108
113
|
|
|
@@ -131,6 +136,8 @@ export class MyContainer extends Container {
|
|
|
131
136
|
// Lifecycle method called when container shuts down
|
|
132
137
|
override onStop(): void {
|
|
133
138
|
console.log('Container stopped!');
|
|
139
|
+
// you can also call startAndWaitForPorts() again
|
|
140
|
+
// this.startAndWaitForPorts();
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
// Lifecycle method called on errors
|
|
@@ -139,6 +146,14 @@ export class MyContainer extends Container {
|
|
|
139
146
|
throw error;
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
// Lifecycle method when the container class considers the activity to be expired
|
|
150
|
+
override onActivityExpired() {
|
|
151
|
+
console.log(
|
|
152
|
+
'Container activity expired'
|
|
153
|
+
);
|
|
154
|
+
await this.destroy();
|
|
155
|
+
}
|
|
156
|
+
|
|
142
157
|
// Custom method that will extend the container's lifetime
|
|
143
158
|
async performBackgroundTask(): Promise<void> {
|
|
144
159
|
// Do some work...
|
|
@@ -205,72 +220,6 @@ export class ConfiguredContainer extends Container {
|
|
|
205
220
|
}
|
|
206
221
|
```
|
|
207
222
|
|
|
208
|
-
### Manual Container Start Example
|
|
209
|
-
|
|
210
|
-
For more control over container lifecycle, you can use the `explicitContainerStart` option to disable automatic container startup:
|
|
211
|
-
|
|
212
|
-
```typescript
|
|
213
|
-
import { Container } from '@cloudflare/containers';
|
|
214
|
-
|
|
215
|
-
export class ManualStartContainer extends Container {
|
|
216
|
-
// Configure default port for the container
|
|
217
|
-
defaultPort = 8080;
|
|
218
|
-
|
|
219
|
-
// Specify multiple required ports that must be ready before the container is considered started
|
|
220
|
-
// if this is not specified, by default, you will wait only defaultPort
|
|
221
|
-
requiredPorts = [8080, 9090, 3000];
|
|
222
|
-
|
|
223
|
-
// Disable automatic container startup
|
|
224
|
-
manualStart = true;
|
|
225
|
-
|
|
226
|
-
constructor(ctx: any, env: any) {
|
|
227
|
-
// You can also set explicitContainerStart via constructor options
|
|
228
|
-
// super(ctx, env, {
|
|
229
|
-
// explicitContainerStart: true
|
|
230
|
-
// });
|
|
231
|
-
super(ctx, env);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Handle incoming requests - start the container on demand
|
|
236
|
-
*/
|
|
237
|
-
async fetch(request: Request): Promise<Response> {
|
|
238
|
-
const url = new URL(request.url);
|
|
239
|
-
|
|
240
|
-
// Start the container if it's not already running
|
|
241
|
-
if (!this.ctx.container.running) {
|
|
242
|
-
try {
|
|
243
|
-
// Handle different startup paths
|
|
244
|
-
if (url.pathname === '/start') {
|
|
245
|
-
// Just start the container without waiting for any ports
|
|
246
|
-
await this.start();
|
|
247
|
-
return new Response('Container started but ports not yet verified!');
|
|
248
|
-
}
|
|
249
|
-
else if (url.pathname === '/start-api') {
|
|
250
|
-
// Only wait for the API port (3000)
|
|
251
|
-
await this.startAndWaitForPorts(3000);
|
|
252
|
-
return new Response('API port is ready!');
|
|
253
|
-
}
|
|
254
|
-
else if (url.pathname === '/start-all') {
|
|
255
|
-
// Wait for all required ports (uses requiredPorts property)
|
|
256
|
-
await this.startAndWaitForPorts();
|
|
257
|
-
return new Response('All container ports are ready!');
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
// For other paths, just wait for the default port
|
|
261
|
-
await this.startAndWaitForPorts(this.defaultPort);
|
|
262
|
-
}
|
|
263
|
-
} catch (error) {
|
|
264
|
-
return new Response(`Failed to start container: ${error}`, { status: 500 });
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// For all other requests, forward to the container
|
|
269
|
-
return await this.containerFetch(request);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
223
|
### Multiple Ports and Custom Routing
|
|
275
224
|
|
|
276
225
|
You can create a container that doesn't use a default port and instead routes traffic to different ports based on request path or other factors:
|
package/dist/lib/container.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ContainerOptions, ContainerStartOptions, ContainerStartConfigOptions, Schedule, StopParams } from '../types';
|
|
2
2
|
import { DurableObject } from 'cloudflare:workers';
|
|
3
|
+
export type Signal = 'SIGKILL' | 'SIGINT' | 'SIGTERM';
|
|
4
|
+
export type SignalInteger = number;
|
|
3
5
|
export declare class Container<Env = unknown> extends DurableObject<Env> {
|
|
4
6
|
defaultPort?: number;
|
|
5
7
|
requiredPorts?: number[];
|
|
@@ -84,7 +86,7 @@ export declare class Container<Env = unknown> extends DurableObject<Env> {
|
|
|
84
86
|
* Shuts down the container.
|
|
85
87
|
* @param signal - The signal to send to the container (default: 15 for SIGTERM)
|
|
86
88
|
*/
|
|
87
|
-
stop(signal?:
|
|
89
|
+
stop(signal?: Signal | SignalInteger): Promise<void>;
|
|
88
90
|
/**
|
|
89
91
|
* Destroys the container. It will trigger onError instead of onStop.
|
|
90
92
|
*/
|
|
@@ -100,6 +102,15 @@ export declare class Container<Env = unknown> extends DurableObject<Env> {
|
|
|
100
102
|
* @param params - Object containing exitCode and reason for the stop
|
|
101
103
|
*/
|
|
102
104
|
onStop(_: StopParams): void | Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Lifecycle method called when the container is running, and the activity timeout
|
|
107
|
+
* expiration has been reached.
|
|
108
|
+
*
|
|
109
|
+
* If you want to shutdown the container, you should call this.destroy() here
|
|
110
|
+
*
|
|
111
|
+
* By default, this method calls `this.destroy()`
|
|
112
|
+
*/
|
|
113
|
+
onActivityExpired(): Promise<void>;
|
|
103
114
|
/**
|
|
104
115
|
* Error handler for container errors
|
|
105
116
|
* Override this method in subclasses to handle container errors
|
|
@@ -153,7 +164,6 @@ export declare class Container<Env = unknown> extends DurableObject<Env> {
|
|
|
153
164
|
private state;
|
|
154
165
|
private monitor;
|
|
155
166
|
private monitorSetup;
|
|
156
|
-
private openStreamCount;
|
|
157
167
|
private sleepAfterMs;
|
|
158
168
|
private blockConcurrencyThrowable;
|
|
159
169
|
/**
|
|
@@ -163,6 +173,7 @@ export declare class Container<Env = unknown> extends DurableObject<Env> {
|
|
|
163
173
|
private requestAndPortFromContainerFetchArgs;
|
|
164
174
|
private startContainerIfNotRunning;
|
|
165
175
|
private setupMonitorCallbacks;
|
|
176
|
+
deleteSchedules(name: string): void;
|
|
166
177
|
/**
|
|
167
178
|
* Method called when an alarm fires
|
|
168
179
|
* Executes any scheduled tasks that are due
|
|
@@ -171,13 +182,16 @@ export declare class Container<Env = unknown> extends DurableObject<Env> {
|
|
|
171
182
|
isRetry: boolean;
|
|
172
183
|
retryCount: number;
|
|
173
184
|
}): Promise<void>;
|
|
185
|
+
timeout?: ReturnType<typeof setTimeout>;
|
|
186
|
+
resolve?: () => void;
|
|
174
187
|
private syncPendingStoppedEvents;
|
|
175
188
|
private callOnStop;
|
|
176
189
|
/**
|
|
177
190
|
* Schedule the next alarm based on upcoming tasks
|
|
178
|
-
* @private
|
|
179
191
|
*/
|
|
180
|
-
|
|
192
|
+
scheduleNextAlarm(ms?: number): Promise<void>;
|
|
193
|
+
listSchedules<T = string>(name: string): Promise<Schedule<T>[]>;
|
|
194
|
+
private toSchedule;
|
|
181
195
|
/**
|
|
182
196
|
* Get a scheduled task by ID
|
|
183
197
|
* @template T Type of the payload data
|
package/dist/lib/container.js
CHANGED
|
@@ -7,14 +7,14 @@ import { DurableObject } from 'cloudflare:workers';
|
|
|
7
7
|
// ====================
|
|
8
8
|
const NO_CONTAINER_INSTANCE_ERROR = 'there is no container instance that can be provided to this durable object';
|
|
9
9
|
const RUNTIME_SIGNALLED_ERROR = 'runtime signalled the container to exit:';
|
|
10
|
-
const
|
|
10
|
+
const UNEXPECTED_EXIT_ERROR = 'container exited with unexpected exit code:';
|
|
11
11
|
const NOT_LISTENING_ERROR = 'the container is not listening';
|
|
12
12
|
const CONTAINER_STATE_KEY = '__CF_CONTAINER_STATE';
|
|
13
13
|
// maxRetries before scheduling next alarm is purposely set to 3,
|
|
14
14
|
// as according to DO docs at https://developers.cloudflare.com/durable-objects/api/alarms/
|
|
15
15
|
// the maximum amount for alarm retries is 6.
|
|
16
|
-
const
|
|
17
|
-
const PING_TIMEOUT_MS =
|
|
16
|
+
const MAX_ALARM_RETRIES = 3;
|
|
17
|
+
const PING_TIMEOUT_MS = 5000;
|
|
18
18
|
const DEFAULT_SLEEP_AFTER = '10m'; // Default sleep after inactivity time
|
|
19
19
|
const INSTANCE_POLL_INTERVAL_MS = 300; // Default interval for polling container state
|
|
20
20
|
// Timeout for getting container instance and launching a VM
|
|
@@ -34,6 +34,11 @@ const FALLBACK_PORT_TO_CHECK = 33;
|
|
|
34
34
|
// Since the timing isn't working, hard coding a max attempts seems
|
|
35
35
|
// to be the only viable solution for now
|
|
36
36
|
const TEMPORARY_HARDCODED_ATTEMPT_MAX = 6;
|
|
37
|
+
const signalToNumbers = {
|
|
38
|
+
SIGINT: 2,
|
|
39
|
+
SIGTERM: 15,
|
|
40
|
+
SIGKILL: 9,
|
|
41
|
+
};
|
|
37
42
|
// =====================
|
|
38
43
|
// =====================
|
|
39
44
|
// HELPER FUNCTIONS
|
|
@@ -47,7 +52,7 @@ function isErrorOfType(e, matchingString) {
|
|
|
47
52
|
const isNoInstanceError = (error) => isErrorOfType(error, NO_CONTAINER_INSTANCE_ERROR);
|
|
48
53
|
const isRuntimeSignalledError = (error) => isErrorOfType(error, RUNTIME_SIGNALLED_ERROR);
|
|
49
54
|
const isNotListeningError = (error) => isErrorOfType(error, NOT_LISTENING_ERROR);
|
|
50
|
-
const isContainerExitNonZeroError = (error) => isErrorOfType(error,
|
|
55
|
+
const isContainerExitNonZeroError = (error) => isErrorOfType(error, UNEXPECTED_EXIT_ERROR);
|
|
51
56
|
function getExitCodeFromError(error) {
|
|
52
57
|
if (!(error instanceof Error)) {
|
|
53
58
|
return null;
|
|
@@ -62,8 +67,8 @@ function getExitCodeFromError(error) {
|
|
|
62
67
|
if (isContainerExitNonZeroError(error)) {
|
|
63
68
|
return +error.message
|
|
64
69
|
.toLowerCase()
|
|
65
|
-
.slice(error.message.toLowerCase().indexOf(
|
|
66
|
-
|
|
70
|
+
.slice(error.message.toLowerCase().indexOf(UNEXPECTED_EXIT_ERROR) +
|
|
71
|
+
UNEXPECTED_EXIT_ERROR.length +
|
|
67
72
|
1);
|
|
68
73
|
}
|
|
69
74
|
return null;
|
|
@@ -85,31 +90,6 @@ function addTimeoutSignal(existingSignal, timeoutMs) {
|
|
|
85
90
|
controller.signal.addEventListener('abort', () => clearTimeout(timeoutId));
|
|
86
91
|
return controller.signal;
|
|
87
92
|
}
|
|
88
|
-
// ==== Stream helpers ====
|
|
89
|
-
function attachOnClosedHook(stream, onClosed) {
|
|
90
|
-
let destructor = () => {
|
|
91
|
-
onClosed();
|
|
92
|
-
destructor = null;
|
|
93
|
-
};
|
|
94
|
-
// we pass the readableStream through a transform stream to detect if the
|
|
95
|
-
// body has been closed.
|
|
96
|
-
const transformStream = new TransformStream({
|
|
97
|
-
transform(chunk, controller) {
|
|
98
|
-
controller.enqueue(chunk);
|
|
99
|
-
},
|
|
100
|
-
flush() {
|
|
101
|
-
if (destructor) {
|
|
102
|
-
destructor();
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
cancel() {
|
|
106
|
-
if (destructor) {
|
|
107
|
-
destructor();
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
return stream.pipeThrough(transformStream);
|
|
112
|
-
}
|
|
113
93
|
// ===============================
|
|
114
94
|
// CONTAINER STATE WRAPPER
|
|
115
95
|
// ===============================
|
|
@@ -385,7 +365,7 @@ export class Container extends DurableObject {
|
|
|
385
365
|
if (i === triesLeft - 1) {
|
|
386
366
|
try {
|
|
387
367
|
// TODO: Remove attempts, the end user doesn't care about this
|
|
388
|
-
this.onError(`Failed to verify port ${port} is available after ${options.retries} attempts, last error: ${errorMessage}`);
|
|
368
|
+
await this.onError(`Failed to verify port ${port} is available after ${options.retries} attempts, last error: ${errorMessage}`);
|
|
389
369
|
}
|
|
390
370
|
catch { }
|
|
391
371
|
throw e;
|
|
@@ -419,8 +399,8 @@ export class Container extends DurableObject {
|
|
|
419
399
|
* Shuts down the container.
|
|
420
400
|
* @param signal - The signal to send to the container (default: 15 for SIGTERM)
|
|
421
401
|
*/
|
|
422
|
-
async stop(signal =
|
|
423
|
-
this.container.signal(signal);
|
|
402
|
+
async stop(signal = 'SIGTERM') {
|
|
403
|
+
this.container.signal(typeof signal === 'string' ? signalToNumbers[signal] : signal);
|
|
424
404
|
}
|
|
425
405
|
/**
|
|
426
406
|
* Destroys the container. It will trigger onError instead of onStop.
|
|
@@ -443,6 +423,17 @@ export class Container extends DurableObject {
|
|
|
443
423
|
onStop(_) {
|
|
444
424
|
// Default implementation does nothing
|
|
445
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Lifecycle method called when the container is running, and the activity timeout
|
|
428
|
+
* expiration has been reached.
|
|
429
|
+
*
|
|
430
|
+
* If you want to shutdown the container, you should call this.destroy() here
|
|
431
|
+
*
|
|
432
|
+
* By default, this method calls `this.destroy()`
|
|
433
|
+
*/
|
|
434
|
+
async onActivityExpired() {
|
|
435
|
+
await this.stop();
|
|
436
|
+
}
|
|
446
437
|
/**
|
|
447
438
|
* Error handler for container errors
|
|
448
439
|
* Override this method in subclasses to handle container errors
|
|
@@ -562,33 +553,7 @@ export class Container extends DurableObject {
|
|
|
562
553
|
try {
|
|
563
554
|
// Renew the activity timeout whenever a request is proxied
|
|
564
555
|
this.renewActivityTimeout();
|
|
565
|
-
if (request.body != null) {
|
|
566
|
-
this.openStreamCount++;
|
|
567
|
-
const destructor = () => {
|
|
568
|
-
this.openStreamCount--;
|
|
569
|
-
this.renewActivityTimeout();
|
|
570
|
-
};
|
|
571
|
-
const readable = attachOnClosedHook(request.body, destructor);
|
|
572
|
-
request = new Request(request, { body: readable });
|
|
573
|
-
}
|
|
574
556
|
const res = await tcpPort.fetch(containerUrl, request);
|
|
575
|
-
if (res.webSocket) {
|
|
576
|
-
this.openStreamCount++;
|
|
577
|
-
// TODO: This does not seem to work :(
|
|
578
|
-
res.webSocket.addEventListener('close', async () => {
|
|
579
|
-
this.openStreamCount--;
|
|
580
|
-
this.renewActivityTimeout();
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
else if (res.body != null) {
|
|
584
|
-
this.openStreamCount++;
|
|
585
|
-
const destructor = () => {
|
|
586
|
-
this.openStreamCount--;
|
|
587
|
-
this.renewActivityTimeout();
|
|
588
|
-
};
|
|
589
|
-
const readable = attachOnClosedHook(res.body, destructor);
|
|
590
|
-
return new Response(readable, res);
|
|
591
|
-
}
|
|
592
557
|
return res;
|
|
593
558
|
}
|
|
594
559
|
catch (e) {
|
|
@@ -631,8 +596,6 @@ export class Container extends DurableObject {
|
|
|
631
596
|
state;
|
|
632
597
|
monitor;
|
|
633
598
|
monitorSetup = false;
|
|
634
|
-
// openStreamCount keeps track of the number of open streams to the container
|
|
635
|
-
openStreamCount = 0;
|
|
636
599
|
sleepAfterMs = 0;
|
|
637
600
|
// ==========================
|
|
638
601
|
// GENERAL HELPERS
|
|
@@ -655,16 +618,10 @@ export class Container extends DurableObject {
|
|
|
655
618
|
*/
|
|
656
619
|
sql(strings, ...values) {
|
|
657
620
|
let query = '';
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
return [...this.ctx.storage.sql.exec(query, ...values)];
|
|
663
|
-
}
|
|
664
|
-
catch (e) {
|
|
665
|
-
console.error(`Failed to execute SQL query: ${query}`, e);
|
|
666
|
-
throw this.onError(e);
|
|
667
|
-
}
|
|
621
|
+
// Construct the SQL query with placeholders
|
|
622
|
+
query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? '?' : ''), '');
|
|
623
|
+
// Execute the SQL query with the provided values
|
|
624
|
+
return [...this.ctx.storage.sql.exec(query, ...values)];
|
|
668
625
|
}
|
|
669
626
|
requestAndPortFromContainerFetchArgs(requestOrUrl, portOrInit, portParam) {
|
|
670
627
|
let request;
|
|
@@ -821,14 +778,26 @@ export class Container extends DurableObject {
|
|
|
821
778
|
await this.state.setStoppedWithCode(exitCode);
|
|
822
779
|
this.monitorSetup = false;
|
|
823
780
|
this.monitor = undefined;
|
|
781
|
+
return;
|
|
824
782
|
}
|
|
825
783
|
try {
|
|
826
784
|
// TODO: Be able to retrigger onError
|
|
827
785
|
await this.onError(error);
|
|
828
786
|
}
|
|
829
787
|
catch { }
|
|
788
|
+
})
|
|
789
|
+
.finally(() => {
|
|
790
|
+
this.monitorSetup = false;
|
|
791
|
+
if (this.timeout) {
|
|
792
|
+
if (this.resolve)
|
|
793
|
+
this.resolve();
|
|
794
|
+
clearTimeout(this.timeout);
|
|
795
|
+
}
|
|
830
796
|
});
|
|
831
797
|
}
|
|
798
|
+
deleteSchedules(name) {
|
|
799
|
+
this.sql `DELETE FROM container_schedules WHERE callback = ${name}`;
|
|
800
|
+
}
|
|
832
801
|
// ============================
|
|
833
802
|
// ALARMS AND SCHEDULES
|
|
834
803
|
// ============================
|
|
@@ -837,7 +806,7 @@ export class Container extends DurableObject {
|
|
|
837
806
|
* Executes any scheduled tasks that are due
|
|
838
807
|
*/
|
|
839
808
|
async alarm(alarmProps) {
|
|
840
|
-
if (alarmProps.isRetry && alarmProps.retryCount >
|
|
809
|
+
if (alarmProps.isRetry && alarmProps.retryCount > MAX_ALARM_RETRIES) {
|
|
841
810
|
const scheduleCount = Number(this.sql `SELECT COUNT(*) as count FROM container_schedules`[0]?.count) || 0;
|
|
842
811
|
const hasScheduledTasks = scheduleCount > 0;
|
|
843
812
|
if (hasScheduledTasks || this.container.running) {
|
|
@@ -849,18 +818,19 @@ export class Container extends DurableObject {
|
|
|
849
818
|
// The only way for this DO to stop having alarms is:
|
|
850
819
|
// 1. The container is not running anymore.
|
|
851
820
|
// 2. Activity expired and it exits.
|
|
852
|
-
|
|
821
|
+
const prevAlarm = Date.now();
|
|
822
|
+
await this.ctx.storage.setAlarm(prevAlarm);
|
|
853
823
|
await this.ctx.storage.sync();
|
|
854
824
|
const now = Math.floor(Date.now() / 1000);
|
|
855
825
|
// Get all schedules that should be executed now
|
|
856
826
|
const result = this.sql `
|
|
857
827
|
SELECT * FROM container_schedules;
|
|
858
828
|
`;
|
|
859
|
-
let
|
|
829
|
+
let minTime = Date.now() + 3 * 60 * 1000;
|
|
860
830
|
// Process each due schedule
|
|
861
831
|
for (const row of result) {
|
|
862
832
|
if (row.time > now) {
|
|
863
|
-
|
|
833
|
+
minTime = Math.min(minTime, row.time * 1000);
|
|
864
834
|
continue;
|
|
865
835
|
}
|
|
866
836
|
const callback = this[row.callback];
|
|
@@ -891,25 +861,36 @@ export class Container extends DurableObject {
|
|
|
891
861
|
}
|
|
892
862
|
if (this.isActivityExpired()) {
|
|
893
863
|
await this.stopDueToInactivity();
|
|
894
|
-
|
|
864
|
+
// renewActivityTimeout makes sure we don't spam calls here
|
|
865
|
+
this.renewActivityTimeout();
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
const alarmNow = await this.ctx.storage.getAlarm();
|
|
869
|
+
if (alarmNow !== prevAlarm) {
|
|
870
|
+
await this.ctx.storage.setAlarm(Date.now());
|
|
895
871
|
return;
|
|
896
872
|
}
|
|
897
873
|
// Math.min(3m or maxTime, sleepTimeout)
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const timeout = Math.max(0, maxTime - Date.now());
|
|
901
|
-
await this.ctx.storage.setAlarm(timeout + Date.now());
|
|
902
|
-
await this.ctx.storage.sync();
|
|
874
|
+
minTime = Math.min(minTime, this.sleepAfterMs);
|
|
875
|
+
const timeout = Math.max(0, minTime - Date.now());
|
|
903
876
|
// await a sleep for maxTime to keep the DO alive for
|
|
904
877
|
// at least this long
|
|
905
878
|
await new Promise(resolve => {
|
|
906
|
-
|
|
879
|
+
this.resolve = resolve;
|
|
880
|
+
if (!this.container.running) {
|
|
881
|
+
resolve();
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
this.timeout = setTimeout(() => {
|
|
907
885
|
resolve();
|
|
908
886
|
}, timeout);
|
|
909
887
|
});
|
|
888
|
+
await this.ctx.storage.setAlarm(Date.now());
|
|
910
889
|
// we exit and we have another alarm,
|
|
911
890
|
// the next alarm is the one that decides if it should stop the loop.
|
|
912
891
|
}
|
|
892
|
+
timeout;
|
|
893
|
+
resolve;
|
|
913
894
|
// synchronises container state with the container source of truth to process events
|
|
914
895
|
async syncPendingStoppedEvents() {
|
|
915
896
|
const state = await this.state.getState();
|
|
@@ -940,37 +921,34 @@ export class Container extends DurableObject {
|
|
|
940
921
|
}
|
|
941
922
|
/**
|
|
942
923
|
* Schedule the next alarm based on upcoming tasks
|
|
943
|
-
* @private
|
|
944
924
|
*/
|
|
945
925
|
async scheduleNextAlarm(ms = 1000) {
|
|
946
|
-
const existingAlarm = await this.ctx.storage.getAlarm();
|
|
947
926
|
const nextTime = ms + Date.now();
|
|
948
927
|
// if not already set
|
|
949
|
-
if (
|
|
950
|
-
|
|
951
|
-
|
|
928
|
+
if (this.timeout) {
|
|
929
|
+
if (this.resolve)
|
|
930
|
+
this.resolve();
|
|
931
|
+
clearTimeout(this.timeout);
|
|
952
932
|
}
|
|
933
|
+
await this.ctx.storage.setAlarm(nextTime);
|
|
934
|
+
await this.ctx.storage.sync();
|
|
953
935
|
}
|
|
954
|
-
|
|
955
|
-
* Get a scheduled task by ID
|
|
956
|
-
* @template T Type of the payload data
|
|
957
|
-
* @param id ID of the scheduled task
|
|
958
|
-
* @returns The Schedule object or undefined if not found
|
|
959
|
-
*/
|
|
960
|
-
async getSchedule(id) {
|
|
936
|
+
async listSchedules(name) {
|
|
961
937
|
const result = this.sql `
|
|
962
|
-
SELECT * FROM container_schedules WHERE
|
|
938
|
+
SELECT * FROM container_schedules WHERE callback = ${name} LIMIT 1
|
|
963
939
|
`;
|
|
964
940
|
if (!result || result.length === 0) {
|
|
965
|
-
return
|
|
941
|
+
return [];
|
|
966
942
|
}
|
|
967
|
-
|
|
943
|
+
return result.map((this.toSchedule));
|
|
944
|
+
}
|
|
945
|
+
toSchedule(schedule) {
|
|
968
946
|
let payload;
|
|
969
947
|
try {
|
|
970
948
|
payload = JSON.parse(schedule.payload);
|
|
971
949
|
}
|
|
972
950
|
catch (e) {
|
|
973
|
-
console.error(`Error parsing payload for schedule ${id}:`, e);
|
|
951
|
+
console.error(`Error parsing payload for schedule ${schedule.id}:`, e);
|
|
974
952
|
payload = undefined;
|
|
975
953
|
}
|
|
976
954
|
if (schedule.type === 'delayed') {
|
|
@@ -991,6 +969,22 @@ export class Container extends DurableObject {
|
|
|
991
969
|
time: schedule.time,
|
|
992
970
|
};
|
|
993
971
|
}
|
|
972
|
+
/**
|
|
973
|
+
* Get a scheduled task by ID
|
|
974
|
+
* @template T Type of the payload data
|
|
975
|
+
* @param id ID of the scheduled task
|
|
976
|
+
* @returns The Schedule object or undefined if not found
|
|
977
|
+
*/
|
|
978
|
+
async getSchedule(id) {
|
|
979
|
+
const result = this.sql `
|
|
980
|
+
SELECT * FROM container_schedules WHERE id = ${id} LIMIT 1
|
|
981
|
+
`;
|
|
982
|
+
if (!result || result.length === 0) {
|
|
983
|
+
return undefined;
|
|
984
|
+
}
|
|
985
|
+
const schedule = result[0];
|
|
986
|
+
return this.toSchedule(schedule);
|
|
987
|
+
}
|
|
994
988
|
isActivityExpired() {
|
|
995
989
|
return this.sleepAfterMs <= Date.now();
|
|
996
990
|
}
|
|
@@ -999,11 +993,11 @@ export class Container extends DurableObject {
|
|
|
999
993
|
*/
|
|
1000
994
|
async stopDueToInactivity() {
|
|
1001
995
|
const alreadyStopped = !this.container.running;
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
return;
|
|
996
|
+
if (alreadyStopped) {
|
|
997
|
+
return false;
|
|
1005
998
|
}
|
|
1006
|
-
await this.
|
|
999
|
+
await this.onActivityExpired();
|
|
1000
|
+
return true;
|
|
1007
1001
|
}
|
|
1008
1002
|
}
|
|
1009
1003
|
//# sourceMappingURL=container.js.map
|