@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/dist/index.mjs
DELETED
|
@@ -1,967 +0,0 @@
|
|
|
1
|
-
// src/lib/helpers.ts
|
|
2
|
-
function generateId(length = 9) {
|
|
3
|
-
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
4
|
-
const bytes = new Uint8Array(length);
|
|
5
|
-
crypto.getRandomValues(bytes);
|
|
6
|
-
let result = "";
|
|
7
|
-
for (let i = 0; i < length; i++) {
|
|
8
|
-
result += alphabet[bytes[i] % alphabet.length];
|
|
9
|
-
}
|
|
10
|
-
return result;
|
|
11
|
-
}
|
|
12
|
-
function parseTimeExpression(timeExpression) {
|
|
13
|
-
if (typeof timeExpression === "number") {
|
|
14
|
-
return timeExpression;
|
|
15
|
-
}
|
|
16
|
-
if (typeof timeExpression === "string") {
|
|
17
|
-
const match = timeExpression.match(/^(\d+)([smh])$/);
|
|
18
|
-
if (!match) {
|
|
19
|
-
throw new Error(`invalid time expression ${timeExpression}`);
|
|
20
|
-
}
|
|
21
|
-
const value = parseInt(match[1]);
|
|
22
|
-
const unit = match[2];
|
|
23
|
-
switch (unit) {
|
|
24
|
-
case "s":
|
|
25
|
-
return value;
|
|
26
|
-
case "m":
|
|
27
|
-
return value * 60;
|
|
28
|
-
case "h":
|
|
29
|
-
return value * 60 * 60;
|
|
30
|
-
default:
|
|
31
|
-
throw new Error(`unknown time unit ${unit}`);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
throw new Error(`invalid type for a time expression: ${typeof timeExpression}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// src/lib/container.ts
|
|
38
|
-
import { DurableObject } from "cloudflare:workers";
|
|
39
|
-
var NO_CONTAINER_INSTANCE_ERROR = "there is no container instance that can be provided to this durable object";
|
|
40
|
-
var RUNTIME_SIGNALLED_ERROR = "runtime signalled the container to exit:";
|
|
41
|
-
var UNEXPECTED_EDIT_ERROR = "container exited with unexpected exit code:";
|
|
42
|
-
var NOT_LISTENING_ERROR = "the container is not listening";
|
|
43
|
-
var CONTAINER_STATE_KEY = "__CF_CONTAINER_STATE";
|
|
44
|
-
var MAX_ALAEM_RETRIES = 3;
|
|
45
|
-
var DEFAULT_SLEEP_AFTER = "10m";
|
|
46
|
-
var INSTANCE_POLL_INTERVAL_MS = 300;
|
|
47
|
-
var TIMEOUT_TO_GET_CONTAINER_SECONDS = 8;
|
|
48
|
-
var TIMEOUT_TO_GET_PORTS = 20;
|
|
49
|
-
var TRIES_TO_GET_CONTAINER = Math.ceil(TIMEOUT_TO_GET_CONTAINER_SECONDS * 1e3 / INSTANCE_POLL_INTERVAL_MS);
|
|
50
|
-
var TRIES_TO_GET_PORTS = Math.ceil(TIMEOUT_TO_GET_PORTS * 1e3 / INSTANCE_POLL_INTERVAL_MS);
|
|
51
|
-
var FALLBACK_PORT_TO_CHECK = 33;
|
|
52
|
-
var TEMPORARY_HARDCODED_ATTEMPT_MAX = 6;
|
|
53
|
-
function isErrorOfType(e, matchingString) {
|
|
54
|
-
const errorString = e instanceof Error ? e.message : String(e);
|
|
55
|
-
return errorString.includes(matchingString);
|
|
56
|
-
}
|
|
57
|
-
var isNoInstanceError = (error) => isErrorOfType(error, NO_CONTAINER_INSTANCE_ERROR);
|
|
58
|
-
var isRuntimeSignalledError = (error) => isErrorOfType(error, RUNTIME_SIGNALLED_ERROR);
|
|
59
|
-
var isNotListeningError = (error) => isErrorOfType(error, NOT_LISTENING_ERROR);
|
|
60
|
-
var isContainerExitNonZeroError = (error) => isErrorOfType(error, UNEXPECTED_EDIT_ERROR);
|
|
61
|
-
function getExitCodeFromError(error) {
|
|
62
|
-
if (!(error instanceof Error)) {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
if (isRuntimeSignalledError(error)) {
|
|
66
|
-
return +error.message.slice(
|
|
67
|
-
error.message.indexOf(RUNTIME_SIGNALLED_ERROR) + RUNTIME_SIGNALLED_ERROR.length + 1
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
if (isContainerExitNonZeroError(error)) {
|
|
71
|
-
return +error.message.slice(
|
|
72
|
-
error.message.indexOf(UNEXPECTED_EDIT_ERROR) + UNEXPECTED_EDIT_ERROR.length + 1
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
function attachOnClosedHook(stream, onClosed) {
|
|
78
|
-
let destructor = () => {
|
|
79
|
-
onClosed();
|
|
80
|
-
destructor = null;
|
|
81
|
-
};
|
|
82
|
-
const transformStream = new TransformStream({
|
|
83
|
-
transform(chunk, controller) {
|
|
84
|
-
controller.enqueue(chunk);
|
|
85
|
-
},
|
|
86
|
-
flush() {
|
|
87
|
-
if (destructor) {
|
|
88
|
-
destructor();
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
cancel() {
|
|
92
|
-
if (destructor) {
|
|
93
|
-
destructor();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
return stream.pipeThrough(transformStream);
|
|
98
|
-
}
|
|
99
|
-
var ContainerState = class {
|
|
100
|
-
constructor(storage) {
|
|
101
|
-
this.storage = storage;
|
|
102
|
-
}
|
|
103
|
-
status;
|
|
104
|
-
async setRunning() {
|
|
105
|
-
await this.setStatusAndupdate("running");
|
|
106
|
-
}
|
|
107
|
-
async setHealthy() {
|
|
108
|
-
await this.setStatusAndupdate("healthy");
|
|
109
|
-
}
|
|
110
|
-
async setStopping() {
|
|
111
|
-
await this.setStatusAndupdate("stopping");
|
|
112
|
-
}
|
|
113
|
-
async setStopped() {
|
|
114
|
-
await this.setStatusAndupdate("stopped");
|
|
115
|
-
}
|
|
116
|
-
async setStoppedWithCode(exitCode) {
|
|
117
|
-
this.status = { status: "stopped_with_code", lastChange: Date.now(), exitCode };
|
|
118
|
-
await this.update();
|
|
119
|
-
}
|
|
120
|
-
async getState() {
|
|
121
|
-
if (!this.status) {
|
|
122
|
-
const state = await this.storage.get(CONTAINER_STATE_KEY);
|
|
123
|
-
if (!state) {
|
|
124
|
-
this.status = {
|
|
125
|
-
status: "stopped",
|
|
126
|
-
lastChange: Date.now()
|
|
127
|
-
};
|
|
128
|
-
await this.update();
|
|
129
|
-
} else {
|
|
130
|
-
this.status = state;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return this.status;
|
|
134
|
-
}
|
|
135
|
-
async setStatusAndupdate(status) {
|
|
136
|
-
this.status = { status, lastChange: Date.now() };
|
|
137
|
-
await this.update();
|
|
138
|
-
}
|
|
139
|
-
async update() {
|
|
140
|
-
if (!this.status) throw new Error("status should be init");
|
|
141
|
-
await this.storage.put(CONTAINER_STATE_KEY, this.status);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
var Container = class extends DurableObject {
|
|
145
|
-
// =========================
|
|
146
|
-
// Public Attributes
|
|
147
|
-
// =========================
|
|
148
|
-
// Default port for the container (undefined means no default port)
|
|
149
|
-
defaultPort;
|
|
150
|
-
// Required ports that should be checked for availability during container startup
|
|
151
|
-
// Override this in your subclass to specify ports that must be ready
|
|
152
|
-
requiredPorts;
|
|
153
|
-
// Timeout after which the container will sleep if no activity
|
|
154
|
-
// The signal sent to the container by default is a SIGTERM.
|
|
155
|
-
// The container won't get a SIGKILL if this threshold is triggered.
|
|
156
|
-
sleepAfter = DEFAULT_SLEEP_AFTER;
|
|
157
|
-
// Container configuration properties
|
|
158
|
-
// Set these properties directly in your container instance
|
|
159
|
-
envVars = {};
|
|
160
|
-
entrypoint;
|
|
161
|
-
enableInternet = true;
|
|
162
|
-
// =========================
|
|
163
|
-
// PUBLIC INTERFACE
|
|
164
|
-
// =========================
|
|
165
|
-
constructor(ctx, env, options) {
|
|
166
|
-
super(ctx, env);
|
|
167
|
-
this.state = new ContainerState(this.ctx.storage);
|
|
168
|
-
this.ctx.blockConcurrencyWhile(async () => {
|
|
169
|
-
this.renewActivityTimeout();
|
|
170
|
-
await this.scheduleNextAlarm();
|
|
171
|
-
});
|
|
172
|
-
if (ctx.container === void 0) {
|
|
173
|
-
throw new Error(
|
|
174
|
-
"Container is not enabled for this durable object class. Have you correctly setup your wrangler.toml?"
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
this.container = ctx.container;
|
|
178
|
-
if (options) {
|
|
179
|
-
if (options.defaultPort !== void 0) this.defaultPort = options.defaultPort;
|
|
180
|
-
if (options.sleepAfter !== void 0) this.sleepAfter = options.sleepAfter;
|
|
181
|
-
}
|
|
182
|
-
this.sql`
|
|
183
|
-
CREATE TABLE IF NOT EXISTS container_schedules (
|
|
184
|
-
id TEXT PRIMARY KEY NOT NULL DEFAULT (randomblob(9)),
|
|
185
|
-
callback TEXT NOT NULL,
|
|
186
|
-
payload TEXT,
|
|
187
|
-
type TEXT NOT NULL CHECK(type IN ('scheduled', 'delayed')),
|
|
188
|
-
time INTEGER NOT NULL,
|
|
189
|
-
delayInSeconds INTEGER,
|
|
190
|
-
created_at INTEGER DEFAULT (unixepoch())
|
|
191
|
-
)
|
|
192
|
-
`;
|
|
193
|
-
}
|
|
194
|
-
// ==========================
|
|
195
|
-
// CONTAINER STARTING
|
|
196
|
-
// ==========================
|
|
197
|
-
/**
|
|
198
|
-
* Start the container if it's not running and set up monitoring
|
|
199
|
-
*
|
|
200
|
-
* This method handles the core container startup process without waiting for ports to be ready.
|
|
201
|
-
* It will automatically retry if the container fails to start, up to maxTries attempts.
|
|
202
|
-
*
|
|
203
|
-
* It's useful when you need to:
|
|
204
|
-
* - Start a container without blocking until a port is available
|
|
205
|
-
* - Initialize a container that doesn't expose ports
|
|
206
|
-
* - Perform custom port availability checks separately
|
|
207
|
-
*
|
|
208
|
-
* The method applies the container configuration from your instance properties by default, but allows
|
|
209
|
-
* overriding these values for this specific startup:
|
|
210
|
-
* - Environment variables (defaults to this.envVars)
|
|
211
|
-
* - Custom entrypoint commands (defaults to this.entrypoint)
|
|
212
|
-
* - Internet access settings (defaults to this.enableInternet)
|
|
213
|
-
*
|
|
214
|
-
* It also sets up monitoring to track container lifecycle events and automatically
|
|
215
|
-
* calls the onStop handler when the container terminates.
|
|
216
|
-
*
|
|
217
|
-
* @example
|
|
218
|
-
* // Basic usage in a custom Container implementation
|
|
219
|
-
* async customInitialize() {
|
|
220
|
-
* // Start the container without waiting for a port
|
|
221
|
-
* await this.start();
|
|
222
|
-
*
|
|
223
|
-
* // Perform additional initialization steps
|
|
224
|
-
* // that don't require port access
|
|
225
|
-
* }
|
|
226
|
-
*
|
|
227
|
-
* @example
|
|
228
|
-
* // Start with custom configuration
|
|
229
|
-
* await this.start({
|
|
230
|
-
* envVars: { DEBUG: 'true', NODE_ENV: 'development' },
|
|
231
|
-
* entrypoint: ['npm', 'run', 'dev'],
|
|
232
|
-
* enableInternet: false
|
|
233
|
-
* });
|
|
234
|
-
*
|
|
235
|
-
* @param options - Optional configuration to override instance defaults
|
|
236
|
-
* @param waitOptions - Optional wait configuration with abort signal for cancellation
|
|
237
|
-
* @returns A promise that resolves when the container start command has been issued
|
|
238
|
-
* @throws Error if no container context is available or if all start attempts fail
|
|
239
|
-
*/
|
|
240
|
-
async start(options, waitOptions) {
|
|
241
|
-
const portToCheck = this.defaultPort ?? (this.requiredPorts ? this.requiredPorts[0] : FALLBACK_PORT_TO_CHECK);
|
|
242
|
-
await this.startContainerIfNotRunning(
|
|
243
|
-
{
|
|
244
|
-
abort: waitOptions?.signal,
|
|
245
|
-
waitInterval: INSTANCE_POLL_INTERVAL_MS,
|
|
246
|
-
retries: TRIES_TO_GET_CONTAINER,
|
|
247
|
-
portToCheck
|
|
248
|
-
},
|
|
249
|
-
options
|
|
250
|
-
);
|
|
251
|
-
this.setupMonitor();
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Start the container and wait for ports to be available
|
|
255
|
-
* Based on containers-starter-go implementation
|
|
256
|
-
*
|
|
257
|
-
* This method builds on start() by adding port availability verification:
|
|
258
|
-
* 1. Calls start() to ensure the container is running
|
|
259
|
-
* 2. If no ports are specified and requiredPorts is not set, it uses defaultPort (if set)
|
|
260
|
-
* 3. If no ports can be determined, it calls onStart and renewActivityTimeout immediately
|
|
261
|
-
* 4. For each specified port, it polls until the port is available or maxTries is reached
|
|
262
|
-
* 5. When all ports are available, it triggers onStart and renewActivityTimeout
|
|
263
|
-
*
|
|
264
|
-
* The method prioritizes port sources in this order:
|
|
265
|
-
* 1. Ports specified directly in the method call
|
|
266
|
-
* 2. requiredPorts class property (if set)
|
|
267
|
-
* 3. defaultPort (if neither of the above is specified)
|
|
268
|
-
*
|
|
269
|
-
* @param ports - The ports to wait for (if undefined, uses requiredPorts or defaultPort)
|
|
270
|
-
* @param maxTries - Maximum number of attempts to connect to each port before failing
|
|
271
|
-
* @throws Error if port checks fail after maxTries attempts
|
|
272
|
-
*/
|
|
273
|
-
async startAndWaitForPorts(ports, cancellationOptions) {
|
|
274
|
-
let portsToCheck = [];
|
|
275
|
-
if (ports !== void 0) {
|
|
276
|
-
portsToCheck = Array.isArray(ports) ? ports : [ports];
|
|
277
|
-
} else if (this.requiredPorts && this.requiredPorts.length > 0) {
|
|
278
|
-
portsToCheck = [...this.requiredPorts];
|
|
279
|
-
} else if (this.defaultPort !== void 0) {
|
|
280
|
-
portsToCheck = [this.defaultPort];
|
|
281
|
-
}
|
|
282
|
-
const state = await this.state.getState();
|
|
283
|
-
cancellationOptions ??= {};
|
|
284
|
-
let containerGetRetries = cancellationOptions.instanceGetTimeoutMS ? Math.ceil(cancellationOptions.instanceGetTimeoutMS / INSTANCE_POLL_INTERVAL_MS) : TRIES_TO_GET_CONTAINER;
|
|
285
|
-
cancellationOptions ??= {};
|
|
286
|
-
let totalPortReadyTries = cancellationOptions.portReadyTimeoutMS ? Math.ceil(cancellationOptions.portReadyTimeoutMS / INSTANCE_POLL_INTERVAL_MS) : TRIES_TO_GET_PORTS;
|
|
287
|
-
const options = {
|
|
288
|
-
abort: cancellationOptions.abort,
|
|
289
|
-
retries: containerGetRetries,
|
|
290
|
-
waitInterval: cancellationOptions.waitInterval ?? INSTANCE_POLL_INTERVAL_MS,
|
|
291
|
-
portToCheck: portsToCheck[0] ?? FALLBACK_PORT_TO_CHECK
|
|
292
|
-
};
|
|
293
|
-
if (state.status === "healthy" && this.container.running) {
|
|
294
|
-
if (this.container.running && !this.monitor) {
|
|
295
|
-
await this.startContainerIfNotRunning(options);
|
|
296
|
-
this.setupMonitor();
|
|
297
|
-
}
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
await this.syncPendingStoppedEvents();
|
|
301
|
-
const abortedSignal = new Promise((res) => {
|
|
302
|
-
options.abort?.addEventListener("abort", () => {
|
|
303
|
-
res(true);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
let errorFromBCW = await this.blockConcurrencyThrowable(async () => {
|
|
307
|
-
let triesUsed = await this.startContainerIfNotRunning(options);
|
|
308
|
-
let triesLeft = totalPortReadyTries - triesUsed;
|
|
309
|
-
for (const port of portsToCheck) {
|
|
310
|
-
const tcpPort = this.container.getTcpPort(port);
|
|
311
|
-
let portReady = false;
|
|
312
|
-
for (let i = 0; i < triesLeft && !portReady; i++) {
|
|
313
|
-
try {
|
|
314
|
-
await tcpPort.fetch("http://ping", { signal: options.abort });
|
|
315
|
-
portReady = true;
|
|
316
|
-
console.log(`Port ${port} is ready`);
|
|
317
|
-
} catch (e) {
|
|
318
|
-
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
319
|
-
console.warn(`Error checking ${port}: ${errorMessage}`);
|
|
320
|
-
if (!this.container.running) {
|
|
321
|
-
try {
|
|
322
|
-
await this.onError(
|
|
323
|
-
new Error(
|
|
324
|
-
`Container crashed while checking for ports, did you setup the entrypoint correctly?`
|
|
325
|
-
)
|
|
326
|
-
);
|
|
327
|
-
} catch {
|
|
328
|
-
}
|
|
329
|
-
throw e;
|
|
330
|
-
}
|
|
331
|
-
if (i === triesLeft - 1) {
|
|
332
|
-
try {
|
|
333
|
-
this.onError(
|
|
334
|
-
`Failed to verify port ${port} is available after ${options.retries} attempts, last error: ${errorMessage}`
|
|
335
|
-
);
|
|
336
|
-
} catch {
|
|
337
|
-
}
|
|
338
|
-
throw e;
|
|
339
|
-
}
|
|
340
|
-
await Promise.any([
|
|
341
|
-
new Promise((resolve) => setTimeout(resolve, options.waitInterval)),
|
|
342
|
-
abortedSignal
|
|
343
|
-
]);
|
|
344
|
-
if (options.abort?.aborted) {
|
|
345
|
-
throw new Error("Container request timed out.");
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
if (errorFromBCW) {
|
|
352
|
-
throw errorFromBCW;
|
|
353
|
-
}
|
|
354
|
-
this.setupMonitor();
|
|
355
|
-
await this.ctx.blockConcurrencyWhile(async () => {
|
|
356
|
-
await this.onStart();
|
|
357
|
-
await this.state.setHealthy();
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
// =======================
|
|
361
|
-
// LIFECYCLE HOOKS
|
|
362
|
-
// =======================
|
|
363
|
-
/**
|
|
364
|
-
* Shuts down the container.
|
|
365
|
-
* @param signal - The signal to send to the container (default: 15 for SIGTERM)
|
|
366
|
-
*/
|
|
367
|
-
async stop(signal = 15) {
|
|
368
|
-
this.container.signal(signal);
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
|
-
* Destroys the container. It will trigger onError instead of onStop.
|
|
372
|
-
*/
|
|
373
|
-
async destroy() {
|
|
374
|
-
await this.container.destroy();
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Lifecycle method called when container starts successfully
|
|
378
|
-
* Override this method in subclasses to handle container start events
|
|
379
|
-
*/
|
|
380
|
-
onStart() {
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* Lifecycle method called when container shuts down
|
|
384
|
-
* Override this method in subclasses to handle Container stopped events
|
|
385
|
-
* @param params - Object containing exitCode and reason for the stop
|
|
386
|
-
*/
|
|
387
|
-
onStop(_) {
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Error handler for container errors
|
|
391
|
-
* Override this method in subclasses to handle container errors
|
|
392
|
-
* @param error - The error that occurred
|
|
393
|
-
* @returns Can return any value or throw the error
|
|
394
|
-
*/
|
|
395
|
-
onError(error) {
|
|
396
|
-
console.error("Container error:", error);
|
|
397
|
-
throw error;
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* Renew the container's activity timeout
|
|
401
|
-
*
|
|
402
|
-
* Call this method whenever there is activity on the container
|
|
403
|
-
*/
|
|
404
|
-
renewActivityTimeout() {
|
|
405
|
-
const timeoutInMs = parseTimeExpression(this.sleepAfter) * 1e3;
|
|
406
|
-
this.sleepAfterMs = Date.now() + timeoutInMs;
|
|
407
|
-
}
|
|
408
|
-
// ==================
|
|
409
|
-
// SCHEDULING
|
|
410
|
-
// ==================
|
|
411
|
-
/**
|
|
412
|
-
* Schedule a task to be executed in the future
|
|
413
|
-
* @template T Type of the payload data
|
|
414
|
-
* @param when When to execute the task (Date object or number of seconds delay)
|
|
415
|
-
* @param callback Name of the method to call
|
|
416
|
-
* @param payload Data to pass to the callback
|
|
417
|
-
* @returns Schedule object representing the scheduled task
|
|
418
|
-
*/
|
|
419
|
-
async schedule(when, callback, payload) {
|
|
420
|
-
const id = generateId(9);
|
|
421
|
-
if (typeof callback !== "string") {
|
|
422
|
-
throw new Error("Callback must be a string (method name)");
|
|
423
|
-
}
|
|
424
|
-
if (typeof this[callback] !== "function") {
|
|
425
|
-
throw new Error(`this.${callback} is not a function`);
|
|
426
|
-
}
|
|
427
|
-
if (when instanceof Date) {
|
|
428
|
-
const timestamp = Math.floor(when.getTime() / 1e3);
|
|
429
|
-
this.sql`
|
|
430
|
-
INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, time)
|
|
431
|
-
VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'scheduled', ${timestamp})
|
|
432
|
-
`;
|
|
433
|
-
await this.scheduleNextAlarm();
|
|
434
|
-
return {
|
|
435
|
-
taskId: id,
|
|
436
|
-
callback,
|
|
437
|
-
payload,
|
|
438
|
-
time: timestamp,
|
|
439
|
-
type: "scheduled"
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
if (typeof when === "number") {
|
|
443
|
-
const time = Math.floor(Date.now() / 1e3 + when);
|
|
444
|
-
this.sql`
|
|
445
|
-
INSERT OR REPLACE INTO container_schedules (id, callback, payload, type, delayInSeconds, time)
|
|
446
|
-
VALUES (${id}, ${callback}, ${JSON.stringify(payload)}, 'delayed', ${when}, ${time})
|
|
447
|
-
`;
|
|
448
|
-
await this.scheduleNextAlarm();
|
|
449
|
-
return {
|
|
450
|
-
taskId: id,
|
|
451
|
-
callback,
|
|
452
|
-
payload,
|
|
453
|
-
delayInSeconds: when,
|
|
454
|
-
time,
|
|
455
|
-
type: "delayed"
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
throw new Error("Invalid schedule type. 'when' must be a Date or number of seconds");
|
|
459
|
-
}
|
|
460
|
-
// ============
|
|
461
|
-
// HTTP
|
|
462
|
-
// ============
|
|
463
|
-
/**
|
|
464
|
-
* Send a request to the container (HTTP or WebSocket) using standard fetch API signature
|
|
465
|
-
* Based on containers-starter-go implementation
|
|
466
|
-
*
|
|
467
|
-
* This method handles both HTTP and WebSocket requests to the container.
|
|
468
|
-
* For WebSocket requests, it sets up bidirectional message forwarding with proper
|
|
469
|
-
* activity timeout renewal.
|
|
470
|
-
*
|
|
471
|
-
* Method supports multiple signatures to match standard fetch API:
|
|
472
|
-
* - containerFetch(request: Request, port?: number)
|
|
473
|
-
* - containerFetch(url: string | URL, init?: RequestInit, port?: number)
|
|
474
|
-
*
|
|
475
|
-
* @param requestOrUrl The request object or URL string/object to send to the container
|
|
476
|
-
* @param portOrInit Port number or fetch RequestInit options
|
|
477
|
-
* @param portParam Optional port number when using URL+init signature
|
|
478
|
-
* @returns A Response from the container, or WebSocket connection
|
|
479
|
-
*/
|
|
480
|
-
async containerFetch(requestOrUrl, portOrInit, portParam) {
|
|
481
|
-
let { request, port } = this.requestAndPortFromContainerFetchArgs(
|
|
482
|
-
requestOrUrl,
|
|
483
|
-
portOrInit,
|
|
484
|
-
portParam
|
|
485
|
-
);
|
|
486
|
-
const state = await this.state.getState();
|
|
487
|
-
if (!this.container.running || state.status !== "healthy") {
|
|
488
|
-
try {
|
|
489
|
-
await this.startAndWaitForPorts(port, { abort: request.signal });
|
|
490
|
-
} catch (e) {
|
|
491
|
-
if (isNoInstanceError(e)) {
|
|
492
|
-
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 });
|
|
493
|
-
} else {
|
|
494
|
-
return new Response(
|
|
495
|
-
`Failed to start container: ${e instanceof Error ? e.message : String(e)}`,
|
|
496
|
-
{ status: 500 }
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
const tcpPort = this.container.getTcpPort(port);
|
|
502
|
-
const containerUrl = request.url.replace("https:", "http:");
|
|
503
|
-
try {
|
|
504
|
-
this.renewActivityTimeout();
|
|
505
|
-
if (request.body != null) {
|
|
506
|
-
this.openStreamCount++;
|
|
507
|
-
const destructor = () => {
|
|
508
|
-
this.openStreamCount--;
|
|
509
|
-
this.renewActivityTimeout();
|
|
510
|
-
};
|
|
511
|
-
const readable = attachOnClosedHook(request.body, destructor);
|
|
512
|
-
request = new Request(request, { body: readable });
|
|
513
|
-
}
|
|
514
|
-
const res = await tcpPort.fetch(containerUrl, request);
|
|
515
|
-
if (res.webSocket) {
|
|
516
|
-
this.openStreamCount++;
|
|
517
|
-
res.webSocket.addEventListener("close", async () => {
|
|
518
|
-
this.openStreamCount--;
|
|
519
|
-
this.renewActivityTimeout();
|
|
520
|
-
});
|
|
521
|
-
} else if (res.body != null) {
|
|
522
|
-
this.openStreamCount++;
|
|
523
|
-
const destructor = () => {
|
|
524
|
-
this.openStreamCount--;
|
|
525
|
-
this.renewActivityTimeout();
|
|
526
|
-
};
|
|
527
|
-
const readable = attachOnClosedHook(res.body, destructor);
|
|
528
|
-
return new Response(readable, res);
|
|
529
|
-
}
|
|
530
|
-
return res;
|
|
531
|
-
} catch (e) {
|
|
532
|
-
console.error(`Error proxying request to container ${this.ctx.id}:`, e);
|
|
533
|
-
return new Response(
|
|
534
|
-
`Error proxying request to container: ${e instanceof Error ? e.message : String(e)}`,
|
|
535
|
-
{ status: 500 }
|
|
536
|
-
);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Handle fetch requests to the Container
|
|
541
|
-
* Default implementation forwards all HTTP and WebSocket requests to the container
|
|
542
|
-
* Override this in your subclass to specify a port or implement custom request handling
|
|
543
|
-
*
|
|
544
|
-
* @param request The request to handle
|
|
545
|
-
*/
|
|
546
|
-
async fetch(request) {
|
|
547
|
-
if (this.defaultPort === void 0) {
|
|
548
|
-
return new Response(
|
|
549
|
-
"No default port configured for this container. Override the fetch method or set defaultPort in your Container subclass.",
|
|
550
|
-
{ status: 500 }
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
return await this.containerFetch(request, this.defaultPort);
|
|
554
|
-
}
|
|
555
|
-
// ===============================
|
|
556
|
-
// ===============================
|
|
557
|
-
// PRIVATE METHODS & ATTRS
|
|
558
|
-
// ===============================
|
|
559
|
-
// ===============================
|
|
560
|
-
// ==========================
|
|
561
|
-
// PRIVATE ATTRIBUTES
|
|
562
|
-
// ==========================
|
|
563
|
-
container;
|
|
564
|
-
state;
|
|
565
|
-
monitor;
|
|
566
|
-
monitorSetup = false;
|
|
567
|
-
// openStreamCount keeps track of the number of open streams to the container
|
|
568
|
-
openStreamCount = 0;
|
|
569
|
-
sleepAfterMs = 0;
|
|
570
|
-
alarmSleepPromise;
|
|
571
|
-
alarmSleepResolve = (_) => {
|
|
572
|
-
};
|
|
573
|
-
// ==========================
|
|
574
|
-
// GENERAL HELPERS
|
|
575
|
-
// ==========================
|
|
576
|
-
// This wraps blockConcurrencyWhile so you can throw in it,
|
|
577
|
-
// then check for a string return value that you can throw from the parent
|
|
578
|
-
// Note that the DO will continue to run, unlike normal errors in blockConcurrencyWhile
|
|
579
|
-
async blockConcurrencyThrowable(blockingFunction) {
|
|
580
|
-
return this.ctx.blockConcurrencyWhile(async () => {
|
|
581
|
-
try {
|
|
582
|
-
return await blockingFunction();
|
|
583
|
-
} catch (e) {
|
|
584
|
-
return `${e instanceof Error ? e.message : String(e)}`;
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Try-catch wrapper for async operations
|
|
590
|
-
*/
|
|
591
|
-
async tryCatch(fn) {
|
|
592
|
-
try {
|
|
593
|
-
return await fn();
|
|
594
|
-
} catch (e) {
|
|
595
|
-
this.onError(e);
|
|
596
|
-
throw e;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Execute SQL queries against the Container's database
|
|
601
|
-
*/
|
|
602
|
-
sql(strings, ...values) {
|
|
603
|
-
let query = "";
|
|
604
|
-
try {
|
|
605
|
-
query = strings.reduce((acc, str, i) => acc + str + (i < values.length ? "?" : ""), "");
|
|
606
|
-
return [...this.ctx.storage.sql.exec(query, ...values)];
|
|
607
|
-
} catch (e) {
|
|
608
|
-
console.error(`Failed to execute SQL query: ${query}`, e);
|
|
609
|
-
throw this.onError(e);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
requestAndPortFromContainerFetchArgs(requestOrUrl, portOrInit, portParam) {
|
|
613
|
-
let request;
|
|
614
|
-
let port;
|
|
615
|
-
if (requestOrUrl instanceof Request) {
|
|
616
|
-
request = requestOrUrl;
|
|
617
|
-
port = typeof portOrInit === "number" ? portOrInit : void 0;
|
|
618
|
-
} else {
|
|
619
|
-
const url = typeof requestOrUrl === "string" ? requestOrUrl : requestOrUrl.toString();
|
|
620
|
-
const init = typeof portOrInit === "number" ? {} : portOrInit || {};
|
|
621
|
-
port = typeof portOrInit === "number" ? portOrInit : typeof portParam === "number" ? portParam : void 0;
|
|
622
|
-
request = new Request(url, init);
|
|
623
|
-
}
|
|
624
|
-
if (port === void 0 && this.defaultPort === void 0) {
|
|
625
|
-
throw new Error(
|
|
626
|
-
"No port specified for container fetch. Set defaultPort or specify a port parameter."
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
port = port ?? this.defaultPort;
|
|
630
|
-
return { request, port };
|
|
631
|
-
}
|
|
632
|
-
// ===========================================
|
|
633
|
-
// CONTAINER INTERACTION & MONITORING
|
|
634
|
-
// ===========================================
|
|
635
|
-
// Tries to start a container if it's not running
|
|
636
|
-
// Reutns the number of tries used
|
|
637
|
-
async startContainerIfNotRunning(waitOptions, options) {
|
|
638
|
-
if (this.container.running) {
|
|
639
|
-
if (!this.monitor) {
|
|
640
|
-
this.monitor = this.container.monitor();
|
|
641
|
-
}
|
|
642
|
-
return 0;
|
|
643
|
-
}
|
|
644
|
-
const abortedSignal = new Promise((res) => {
|
|
645
|
-
waitOptions.abort?.addEventListener("abort", () => {
|
|
646
|
-
res(true);
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
await this.state.setRunning();
|
|
650
|
-
for (let tries = 0; tries < waitOptions.retries; tries++) {
|
|
651
|
-
const envVars = options?.envVars ?? this.envVars;
|
|
652
|
-
const entrypoint = options?.entrypoint ?? this.entrypoint;
|
|
653
|
-
const enableInternet = options?.enableInternet ?? this.enableInternet;
|
|
654
|
-
const startConfig = {
|
|
655
|
-
enableInternet
|
|
656
|
-
};
|
|
657
|
-
if (envVars && Object.keys(envVars).length > 0) startConfig.env = envVars;
|
|
658
|
-
if (entrypoint) startConfig.entrypoint = entrypoint;
|
|
659
|
-
this.renewActivityTimeout();
|
|
660
|
-
const handleError = async () => {
|
|
661
|
-
const err = await this.monitor?.catch((err2) => err2);
|
|
662
|
-
if (typeof err === "number") {
|
|
663
|
-
const toThrow = new Error(
|
|
664
|
-
`Error starting container, early exit code 0 before we could check for healthiness, did it crash early?`
|
|
665
|
-
);
|
|
666
|
-
try {
|
|
667
|
-
await this.onError(toThrow);
|
|
668
|
-
} catch {
|
|
669
|
-
}
|
|
670
|
-
throw toThrow;
|
|
671
|
-
} else if (!isNoInstanceError(err)) {
|
|
672
|
-
try {
|
|
673
|
-
await this.onError(err);
|
|
674
|
-
} catch {
|
|
675
|
-
}
|
|
676
|
-
throw err;
|
|
677
|
-
}
|
|
678
|
-
};
|
|
679
|
-
if (!this.container.running) {
|
|
680
|
-
if (tries > 0) {
|
|
681
|
-
await handleError();
|
|
682
|
-
}
|
|
683
|
-
this.container.start(startConfig);
|
|
684
|
-
this.monitor = this.container.monitor();
|
|
685
|
-
}
|
|
686
|
-
this.renewActivityTimeout();
|
|
687
|
-
await this.scheduleNextAlarm();
|
|
688
|
-
const port = this.container.getTcpPort(waitOptions.portToCheck);
|
|
689
|
-
try {
|
|
690
|
-
await port.fetch("http://containerstarthealthcheck", { signal: waitOptions.abort });
|
|
691
|
-
return tries;
|
|
692
|
-
} catch (error) {
|
|
693
|
-
if (isNotListeningError(error) && this.container.running) {
|
|
694
|
-
return tries;
|
|
695
|
-
}
|
|
696
|
-
if (!this.container.running && isNotListeningError(error)) {
|
|
697
|
-
try {
|
|
698
|
-
await this.onError(new Error(`container crashed when checking if it was ready`));
|
|
699
|
-
} catch {
|
|
700
|
-
}
|
|
701
|
-
throw error;
|
|
702
|
-
}
|
|
703
|
-
console.warn(
|
|
704
|
-
"Error checking if container is ready:",
|
|
705
|
-
error instanceof Error ? error.message : String(error)
|
|
706
|
-
);
|
|
707
|
-
await Promise.any([
|
|
708
|
-
new Promise((res) => setTimeout(res, waitOptions.waitInterval)),
|
|
709
|
-
abortedSignal
|
|
710
|
-
]);
|
|
711
|
-
if (waitOptions.abort?.aborted) {
|
|
712
|
-
throw new Error(
|
|
713
|
-
"Aborted waiting for container to start as we received a cancellation signal"
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
if (TEMPORARY_HARDCODED_ATTEMPT_MAX === tries) {
|
|
717
|
-
throw new Error(NO_CONTAINER_INSTANCE_ERROR);
|
|
718
|
-
}
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
throw new Error(`Container did not start after ${waitOptions.retries} attempts`);
|
|
723
|
-
}
|
|
724
|
-
setupMonitor() {
|
|
725
|
-
if (this.monitorSetup) {
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
this.monitorSetup = true;
|
|
729
|
-
this.monitor?.then(async () => {
|
|
730
|
-
const state = await this.state.getState();
|
|
731
|
-
await this.ctx.blockConcurrencyWhile(async () => {
|
|
732
|
-
const newState = await this.state.getState();
|
|
733
|
-
if (newState.status !== state.status) {
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
await this.state.setStoppedWithCode(0);
|
|
737
|
-
await this.onStop({ exitCode: 0, reason: "exit" });
|
|
738
|
-
await this.state.setStopped();
|
|
739
|
-
});
|
|
740
|
-
}).catch(async (error) => {
|
|
741
|
-
if (isNoInstanceError(error)) {
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
const exitCode = getExitCodeFromError(error);
|
|
745
|
-
if (exitCode !== null) {
|
|
746
|
-
const state = await this.state.getState();
|
|
747
|
-
this.ctx.blockConcurrencyWhile(async () => {
|
|
748
|
-
const newState = await this.state.getState();
|
|
749
|
-
if (newState.status !== state.status) {
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
await this.state.setStoppedWithCode(exitCode);
|
|
753
|
-
await this.onStop({
|
|
754
|
-
exitCode,
|
|
755
|
-
reason: isRuntimeSignalledError(error) ? "runtime_signal" : "exit"
|
|
756
|
-
});
|
|
757
|
-
await this.state.setStopped();
|
|
758
|
-
});
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
try {
|
|
762
|
-
await this.onError(error);
|
|
763
|
-
} catch {
|
|
764
|
-
}
|
|
765
|
-
}).finally(() => {
|
|
766
|
-
this.monitorSetup = false;
|
|
767
|
-
this.alarmSleepResolve("monitor finally");
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
// ============================
|
|
771
|
-
// ALARMS AND SCHEDULES
|
|
772
|
-
// ============================
|
|
773
|
-
/**
|
|
774
|
-
* Method called when an alarm fires
|
|
775
|
-
* Executes any scheduled tasks that are due
|
|
776
|
-
*/
|
|
777
|
-
async alarm(alarmProps) {
|
|
778
|
-
if (alarmProps.isRetry && alarmProps.retryCount > MAX_ALAEM_RETRIES) {
|
|
779
|
-
const scheduleCount = Number(this.sql`SELECT COUNT(*) as count FROM container_schedules`[0]?.count) || 0;
|
|
780
|
-
const hasScheduledTasks = scheduleCount > 0;
|
|
781
|
-
if (hasScheduledTasks || this.container.running) {
|
|
782
|
-
await this.scheduleNextAlarm();
|
|
783
|
-
}
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
await this.tryCatch(async () => {
|
|
787
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
788
|
-
const result = this.sql`
|
|
789
|
-
SELECT * FROM container_schedules;
|
|
790
|
-
`;
|
|
791
|
-
let maxTime = 0;
|
|
792
|
-
for (const row of result) {
|
|
793
|
-
if (row.time > now) {
|
|
794
|
-
maxTime = Math.max(maxTime, row.time * 1e3);
|
|
795
|
-
continue;
|
|
796
|
-
}
|
|
797
|
-
const callback = this[row.callback];
|
|
798
|
-
if (!callback || typeof callback !== "function") {
|
|
799
|
-
console.error(`Callback ${row.callback} not found or is not a function`);
|
|
800
|
-
continue;
|
|
801
|
-
}
|
|
802
|
-
const schedule = this.getSchedule(row.id);
|
|
803
|
-
try {
|
|
804
|
-
const payload = row.payload ? JSON.parse(row.payload) : void 0;
|
|
805
|
-
await callback.call(this, payload, await schedule);
|
|
806
|
-
} catch (e) {
|
|
807
|
-
console.error(`Error executing scheduled callback "${row.callback}":`, e);
|
|
808
|
-
}
|
|
809
|
-
this.sql`DELETE FROM container_schedules WHERE id = ${row.id}`;
|
|
810
|
-
}
|
|
811
|
-
await this.syncPendingStoppedEvents();
|
|
812
|
-
if (!this.container.running) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
const scheduleCount = Number(this.sql`SELECT COUNT(*) as count FROM container_schedules`[0]?.count) || 0;
|
|
816
|
-
const hasScheduledTasks = scheduleCount > 0;
|
|
817
|
-
if (hasScheduledTasks) {
|
|
818
|
-
await this.scheduleNextAlarm();
|
|
819
|
-
}
|
|
820
|
-
if (this.isActivityExpired()) {
|
|
821
|
-
await this.stopDueToInactivity();
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
let resolve = (_) => {
|
|
825
|
-
};
|
|
826
|
-
this.alarmSleepPromise = new Promise((res) => {
|
|
827
|
-
this.alarmSleepResolve = (val) => {
|
|
828
|
-
res(val);
|
|
829
|
-
};
|
|
830
|
-
resolve = res;
|
|
831
|
-
});
|
|
832
|
-
maxTime = maxTime === 0 ? Date.now() + 60 * 3 * 1e3 : maxTime;
|
|
833
|
-
maxTime = Math.min(maxTime, this.sleepAfterMs);
|
|
834
|
-
const timeout = Math.max(0, maxTime - Date.now());
|
|
835
|
-
const timeoutRef = setTimeout(() => {
|
|
836
|
-
resolve("setTimeout");
|
|
837
|
-
}, timeout);
|
|
838
|
-
await this.alarmSleepPromise;
|
|
839
|
-
clearTimeout(timeoutRef);
|
|
840
|
-
});
|
|
841
|
-
}
|
|
842
|
-
// synchronises container state with the container source of truth to process events
|
|
843
|
-
async syncPendingStoppedEvents() {
|
|
844
|
-
const state = await this.state.getState();
|
|
845
|
-
if (!this.container.running && state.status === "healthy") {
|
|
846
|
-
await new Promise(
|
|
847
|
-
(res) => (
|
|
848
|
-
// setTimeout to process monitor() just in case
|
|
849
|
-
setTimeout(async () => {
|
|
850
|
-
await this.ctx.blockConcurrencyWhile(async () => {
|
|
851
|
-
const newState = await this.state.getState();
|
|
852
|
-
if (newState.status !== state.status) {
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
await this.onStop({ exitCode: 0, reason: "exit" });
|
|
856
|
-
await this.state.setStopped();
|
|
857
|
-
});
|
|
858
|
-
res(true);
|
|
859
|
-
})
|
|
860
|
-
)
|
|
861
|
-
);
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
if (!this.container.running && state.status === "stopped_with_code") {
|
|
865
|
-
await new Promise(
|
|
866
|
-
(res) => (
|
|
867
|
-
// setTimeout to process monitor() just in case
|
|
868
|
-
setTimeout(async () => {
|
|
869
|
-
await this.ctx.blockConcurrencyWhile(async () => {
|
|
870
|
-
const newState = await this.state.getState();
|
|
871
|
-
if (newState.status !== state.status) {
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
await this.onStop({ exitCode: state.exitCode ?? 0, reason: "exit" });
|
|
875
|
-
await this.state.setStopped();
|
|
876
|
-
res(true);
|
|
877
|
-
});
|
|
878
|
-
})
|
|
879
|
-
)
|
|
880
|
-
);
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Schedule the next alarm based on upcoming tasks
|
|
886
|
-
* @private
|
|
887
|
-
*/
|
|
888
|
-
async scheduleNextAlarm(ms = 1e3) {
|
|
889
|
-
const existingAlarm = await this.ctx.storage.getAlarm();
|
|
890
|
-
const nextTime = ms + Date.now();
|
|
891
|
-
if (existingAlarm === null || existingAlarm > nextTime || existingAlarm < Date.now()) {
|
|
892
|
-
await this.ctx.storage.setAlarm(nextTime);
|
|
893
|
-
await this.ctx.storage.sync();
|
|
894
|
-
this.alarmSleepResolve("scheduling next alarm");
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* Get a scheduled task by ID
|
|
899
|
-
* @template T Type of the payload data
|
|
900
|
-
* @param id ID of the scheduled task
|
|
901
|
-
* @returns The Schedule object or undefined if not found
|
|
902
|
-
*/
|
|
903
|
-
async getSchedule(id) {
|
|
904
|
-
const result = this.sql`
|
|
905
|
-
SELECT * FROM container_schedules WHERE id = ${id} LIMIT 1
|
|
906
|
-
`;
|
|
907
|
-
if (!result || result.length === 0) {
|
|
908
|
-
return void 0;
|
|
909
|
-
}
|
|
910
|
-
const schedule = result[0];
|
|
911
|
-
let payload;
|
|
912
|
-
try {
|
|
913
|
-
payload = JSON.parse(schedule.payload);
|
|
914
|
-
} catch (e) {
|
|
915
|
-
console.error(`Error parsing payload for schedule ${id}:`, e);
|
|
916
|
-
payload = void 0;
|
|
917
|
-
}
|
|
918
|
-
if (schedule.type === "delayed") {
|
|
919
|
-
return {
|
|
920
|
-
taskId: schedule.id,
|
|
921
|
-
callback: schedule.callback,
|
|
922
|
-
payload,
|
|
923
|
-
type: "delayed",
|
|
924
|
-
time: schedule.time,
|
|
925
|
-
delayInSeconds: schedule.delayInSeconds
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
|
-
return {
|
|
929
|
-
taskId: schedule.id,
|
|
930
|
-
callback: schedule.callback,
|
|
931
|
-
payload,
|
|
932
|
-
type: "scheduled",
|
|
933
|
-
time: schedule.time
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
isActivityExpired() {
|
|
937
|
-
return this.sleepAfterMs <= Date.now();
|
|
938
|
-
}
|
|
939
|
-
/**
|
|
940
|
-
* Method called by scheduled task to stop the container due to inactivity
|
|
941
|
-
*/
|
|
942
|
-
async stopDueToInactivity() {
|
|
943
|
-
const alreadyStopped = !this.container.running;
|
|
944
|
-
const hasOpenStream = this.openStreamCount > 0;
|
|
945
|
-
if (alreadyStopped || hasOpenStream) {
|
|
946
|
-
return;
|
|
947
|
-
}
|
|
948
|
-
await this.stop();
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
|
-
|
|
952
|
-
// src/lib/utils.ts
|
|
953
|
-
async function loadBalance(binding, instances = 3) {
|
|
954
|
-
const id = Math.floor(Math.random() * instances).toString();
|
|
955
|
-
const objectId = binding.idFromName(`instance-${id}`);
|
|
956
|
-
return binding.get(objectId);
|
|
957
|
-
}
|
|
958
|
-
var singletonContainerId = "cf-singleton-container";
|
|
959
|
-
function getContainer(binding, name) {
|
|
960
|
-
const objectId = binding.idFromName(name ?? singletonContainerId);
|
|
961
|
-
return binding.get(objectId);
|
|
962
|
-
}
|
|
963
|
-
export {
|
|
964
|
-
Container,
|
|
965
|
-
getContainer,
|
|
966
|
-
loadBalance
|
|
967
|
-
};
|