@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/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
- };