Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ module.exports = {
transform: {
'^.+\\.ts$': ['ts-jest', { tsconfig: 'tsconfig.json' }]
},
moduleNameMapper: {
'^cloudflare:workers$': '<rootDir>/src/tests/__mocks__/cloudflare-workers.js'
},
setupFilesAfterEnv: ['<rootDir>/src/tests/setup.ts'],
collectCoverage: true,
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 55 additions & 1 deletion src/lib/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ export class Container<Env = unknown> extends DurableObject<Env> {
// The container won't get a SIGKILL if this threshold is triggered.
sleepAfter: string | number = DEFAULT_SLEEP_AFTER;

// Hard timeout after which the container will be forcefully killed
// This timeout is absolute from container start time, regardless of activity
// When this timeout expires, the container is sent a SIGKILL signal
hardTimeout?: string | number;

// Container configuration properties
// Set these properties directly in your container instance
envVars: ContainerStartOptions['env'] = {};
Expand Down Expand Up @@ -261,6 +266,7 @@ export class Container<Env = unknown> extends DurableObject<Env> {
if (options) {
if (options.defaultPort !== undefined) this.defaultPort = options.defaultPort;
if (options.sleepAfter !== undefined) this.sleepAfter = options.sleepAfter;
if (options.hardTimeout !== undefined) this.hardTimeout = options.hardTimeout;
}

// Create schedules table if it doesn't exist
Expand Down Expand Up @@ -577,6 +583,24 @@ export class Container<Env = unknown> extends DurableObject<Env> {
await this.stop();
}

/**
* Lifecycle method called when the container hard timeout expires.
*
* This timeout is absolute from container start time, regardless of activity.
* When this timeout expires, the container will be forcefully killed with SIGKILL.
*
* Override this method in subclasses to handle hard timeout events.
* By default, this method calls `this.destroy()` to forcefully kill the container.
*/
public async onHardTimeoutExpired(): Promise<void> {
if (!this.container.running) {
return;
}

console.log(`Container hard timeout expired after ${this.hardTimeout}. Forcefully killing container.`);
await this.destroy();
}

/**
* Error handler for container errors
* Override this method in subclasses to handle container errors
Expand All @@ -598,6 +622,18 @@ export class Container<Env = unknown> extends DurableObject<Env> {
this.sleepAfterMs = Date.now() + timeoutInMs;
}

/**
* Set up the hard timeout when the container starts
* This is called internally when the container starts
*/
private setupHardTimeout() {
if (this.hardTimeout) {
const hardTimeoutMs = parseTimeExpression(this.hardTimeout) * 1000;
this.containerStartTime = Date.now();
this.hardTimeoutMs = this.containerStartTime + hardTimeoutMs;
}
}

// ==================
// SCHEDULING
// ==================
Expand Down Expand Up @@ -798,6 +834,8 @@ export class Container<Env = unknown> extends DurableObject<Env> {
private monitorSetup = false;

private sleepAfterMs = 0;
private hardTimeoutMs?: number;
private containerStartTime?: number;

// ==========================
// GENERAL HELPERS
Expand Down Expand Up @@ -946,6 +984,9 @@ export class Container<Env = unknown> extends DurableObject<Env> {
await this.scheduleNextAlarm();
this.container.start(startConfig);
this.monitor = this.container.monitor();

// Set up hard timeout when container starts
this.setupHardTimeout();
} else {
await this.scheduleNextAlarm();
}
Expand Down Expand Up @@ -1147,15 +1188,24 @@ export class Container<Env = unknown> extends DurableObject<Env> {
return;
}

// Check hard timeout first (takes priority over activity timeout)
if (this.isHardTimeoutExpired()) {
await this.onHardTimeoutExpired();
return;
}

if (this.isActivityExpired()) {
await this.onActivityExpired();
// renewActivityTimeout makes sure we don't spam calls here
this.renewActivityTimeout();
return;
}

// Math.min(3m or maxTime, sleepTimeout)
// Math.min(3m or maxTime, sleepTimeout, hardTimeout)
minTime = Math.min(minTimeFromSchedules, minTime, this.sleepAfterMs);
if (this.hardTimeoutMs) {
minTime = Math.min(minTime, this.hardTimeoutMs);
}
const timeout = Math.max(0, minTime - Date.now());

// await a sleep for maxTime to keep the DO alive for
Expand Down Expand Up @@ -1292,4 +1342,8 @@ export class Container<Env = unknown> extends DurableObject<Env> {
private isActivityExpired(): boolean {
return this.sleepAfterMs <= Date.now();
}

private isHardTimeoutExpired(): boolean {
return this.hardTimeoutMs !== undefined && this.hardTimeoutMs <= Date.now();
}
}
26 changes: 26 additions & 0 deletions src/tests/__mocks__/cloudflare-workers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Mock for cloudflare:workers module
const DurableObject = class MockDurableObject {
constructor(ctx, env) {
this.ctx = ctx;
this.env = env;
}

fetch() {
return new Response('Mock response');
}

async alarm() {
// Mock alarm implementation
}
};

// Mock ExecutionContext
const ExecutionContext = class MockExecutionContext {
waitUntil() {}
passThroughOnException() {}
};

module.exports = {
DurableObject,
ExecutionContext
};
Loading