bunderstack 0.23.1 → 0.23.3
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/CHANGELOG.md +12 -0
- package/dist/jobs/define.d.ts +15 -6
- package/dist/jobs/define.d.ts.map +1 -1
- package/dist/jobs/define.js +15 -3
- package/dist/jobs/define.js.map +1 -1
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/index.js.map +1 -1
- package/dist/jobs/worker.d.ts.map +1 -1
- package/dist/jobs/worker.js +149 -18
- package/dist/jobs/worker.js.map +1 -1
- package/package.json +1 -1
- package/skills/migrating-to-bunderstack/SKILL.md +554 -81
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `bunderstack` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.23.2] - 2026-08-31
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Renewable job leases.** Active handlers renew their fenced lease while
|
|
10
|
+
running, preventing a healthy long-running job from being reclaimed.
|
|
11
|
+
- **Bounded execution.** Jobs may use `leaseDuration`, `maxRuntime`, and
|
|
12
|
+
`ctx.signal`; `timeout` remains a deprecated alias for the lease duration.
|
|
13
|
+
- **Safe observability.** Queue lifecycle events are structured JSON without
|
|
14
|
+
payloads or exception stacks; agent-chat forwards queue cancellation to its
|
|
15
|
+
provider request.
|
|
16
|
+
|
|
5
17
|
## [0.23.1] - 2026-08-31
|
|
6
18
|
|
|
7
19
|
### Changed
|
package/dist/jobs/define.d.ts
CHANGED
|
@@ -4,7 +4,18 @@ import type { EmailFacade } from '../email.js';
|
|
|
4
4
|
import type { StorageFacade } from '../runtime.js';
|
|
5
5
|
import { type CatchUp } from './slots.js';
|
|
6
6
|
export declare const DEFAULT_RETRIES = 3;
|
|
7
|
+
export declare const DEFAULT_LEASE_DURATION_MS = 60000;
|
|
8
|
+
/** @deprecated Use DEFAULT_LEASE_DURATION_MS. */
|
|
7
9
|
export declare const DEFAULT_TIMEOUT_MS = 60000;
|
|
10
|
+
export type BackgroundTiming = {
|
|
11
|
+
/** Lease duration in ms; an expired lease sends the job back to pending. */
|
|
12
|
+
leaseDuration?: number;
|
|
13
|
+
/** Optional cooperative execution deadline in ms. */
|
|
14
|
+
maxRuntime?: number;
|
|
15
|
+
/** @deprecated Use leaseDuration. */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
};
|
|
18
|
+
export declare function leaseDurationFor<T extends BackgroundTiming>(def: T): number;
|
|
8
19
|
export type EnqueueOptions = {
|
|
9
20
|
/** Collapse duplicate enqueues while the queue row is non-terminal. */
|
|
10
21
|
dedupeKey?: string;
|
|
@@ -40,9 +51,11 @@ export type JobContext<TSchema extends Record<string, unknown> = Record<string,
|
|
|
40
51
|
storage: StorageFacade;
|
|
41
52
|
jobs: JobsRuntimeFacade;
|
|
42
53
|
realtime: RealtimeFacade<TSchema>;
|
|
54
|
+
/** Aborted when execution reaches its deadline or the job loses its lease. */
|
|
55
|
+
signal: AbortSignal;
|
|
43
56
|
};
|
|
44
57
|
export type BunderstackJobContext<TSchema extends Record<string, unknown> = Record<string, unknown>, TEnvResult = Record<string, unknown>> = JobContext<TSchema, TEnvResult>;
|
|
45
|
-
export type QueueJobDefinition<TInput, TSchema extends Record<string, unknown> = Record<string, unknown>, TEnvResult = Record<string, unknown>> = {
|
|
58
|
+
export type QueueJobDefinition<TInput, TSchema extends Record<string, unknown> = Record<string, unknown>, TEnvResult = Record<string, unknown>> = BackgroundTiming & {
|
|
46
59
|
kind: 'job';
|
|
47
60
|
/** Standard Schema payload; parsed at enqueue AND before the handler runs. */
|
|
48
61
|
input?: StandardSchemaV1<unknown, TInput>;
|
|
@@ -55,8 +68,6 @@ export type QueueJobDefinition<TInput, TSchema extends Record<string, unknown> =
|
|
|
55
68
|
};
|
|
56
69
|
/** Max simultaneous `running` rows of this type, enforced per worker. */
|
|
57
70
|
concurrency?: number;
|
|
58
|
-
/** Lease duration in ms; an expired lease sends the job back to pending. */
|
|
59
|
-
timeout?: number;
|
|
60
71
|
handler: (input: TInput, ctx: JobContext<TSchema, TEnvResult>) => Promise<void> | void;
|
|
61
72
|
/** Fires once, after the final attempt fails. Errors here are logged, never retried. */
|
|
62
73
|
onFailed?: (input: TInput, error: Error, ctx: JobContext<TSchema, TEnvResult>) => Promise<void> | void;
|
|
@@ -64,7 +75,7 @@ export type QueueJobDefinition<TInput, TSchema extends Record<string, unknown> =
|
|
|
64
75
|
export type CronInvocation = {
|
|
65
76
|
scheduledFor: Date;
|
|
66
77
|
};
|
|
67
|
-
export type CronDefinition<TSchema extends Record<string, unknown> = Record<string, unknown>, TEnvResult = Record<string, unknown>, TSchedule extends string = string> = {
|
|
78
|
+
export type CronDefinition<TSchema extends Record<string, unknown> = Record<string, unknown>, TEnvResult = Record<string, unknown>, TSchedule extends string = string> = BackgroundTiming & {
|
|
68
79
|
kind: 'cron';
|
|
69
80
|
schedule: TSchedule;
|
|
70
81
|
/** Attempts after the first failure. Default 3 (so 4 total attempts). */
|
|
@@ -74,8 +85,6 @@ export type CronDefinition<TSchema extends Record<string, unknown> = Record<stri
|
|
|
74
85
|
baseMs?: number;
|
|
75
86
|
factor?: number;
|
|
76
87
|
};
|
|
77
|
-
/** Lease duration in ms; an expired lease sends the slot back to pending. */
|
|
78
|
-
timeout?: number;
|
|
79
88
|
/** How missed slots are handled on wake. Default 'latest'. */
|
|
80
89
|
catchUp?: CatchUp;
|
|
81
90
|
/** How far back catch-up looks, in ms. Default 1 hour. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/jobs/define.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAE7D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,OAAO,CAAA;AAClC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAG/C,OAAO,EAAe,KAAK,OAAO,EAAE,MAAM,SAAS,CAAA;AAEnD,eAAO,MAAM,eAAe,IAAI,CAAA;AAChC,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/jobs/define.ts"],"names":[],"mappings":"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"}
|
package/dist/jobs/define.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { parseCron } from './cron.js';
|
|
2
2
|
import { CRON_PREFIX } from './slots.js';
|
|
3
3
|
export const DEFAULT_RETRIES = 3;
|
|
4
|
-
export const
|
|
4
|
+
export const DEFAULT_LEASE_DURATION_MS = 60_000;
|
|
5
|
+
/** @deprecated Use DEFAULT_LEASE_DURATION_MS. */
|
|
6
|
+
export const DEFAULT_TIMEOUT_MS = DEFAULT_LEASE_DURATION_MS;
|
|
7
|
+
export function leaseDurationFor(def) {
|
|
8
|
+
return def.leaseDuration ?? def.timeout ?? DEFAULT_LEASE_DURATION_MS;
|
|
9
|
+
}
|
|
5
10
|
/** Throws when a definition is unusable. Safe to call more than once. */
|
|
6
11
|
export function validateBackgroundDefs(defs) {
|
|
7
12
|
for (const [name, def] of Object.entries(defs)) {
|
|
@@ -15,8 +20,15 @@ export function validateBackgroundDefs(defs) {
|
|
|
15
20
|
(def.retries < 0 || !Number.isInteger(def.retries))) {
|
|
16
21
|
throw new Error(`[bunderstack] background task "${name}": retries must be a non-negative integer`);
|
|
17
22
|
}
|
|
18
|
-
if (def.timeout !== undefined && def.
|
|
19
|
-
throw new Error(`[bunderstack] background task "${name}": timeout
|
|
23
|
+
if (def.timeout !== undefined && def.leaseDuration !== undefined) {
|
|
24
|
+
throw new Error(`[bunderstack] background task "${name}": timeout and leaseDuration cannot both be declared`);
|
|
25
|
+
}
|
|
26
|
+
for (const field of ['timeout', 'leaseDuration', 'maxRuntime']) {
|
|
27
|
+
const value = def[field];
|
|
28
|
+
if (value !== undefined &&
|
|
29
|
+
(!Number.isFinite(value) || !Number.isInteger(value) || value <= 0)) {
|
|
30
|
+
throw new Error(`[bunderstack] background task "${name}": ${field} must be a positive finite integer`);
|
|
31
|
+
}
|
|
20
32
|
}
|
|
21
33
|
if (def.kind === 'cron') {
|
|
22
34
|
parseCron(def.schedule);
|
package/dist/jobs/define.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define.js","sourceRoot":"","sources":["../../src/jobs/define.ts"],"names":[],"mappings":"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","sourcesContent":["// src/jobs/define.ts — job definition types and the typed builder.\n// `createJobsBuilder` exists purely to carry\n// TSchema/TEnvResult typing into inline callbacks and extracted files.\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\n\nimport type { DbFor } from '../db'\nimport type { EmailFacade } from '../email'\nimport type { StorageFacade } from '../runtime'\n\nimport { parseCron } from './cron'\nimport { CRON_PREFIX, type CatchUp } from './slots'\n\nexport const DEFAULT_RETRIES = 3\nexport const DEFAULT_TIMEOUT_MS = 60_000\n\nexport type EnqueueOptions = {\n /** Collapse duplicate enqueues while the queue row is non-terminal. */\n dedupeKey?: string\n /** Milliseconds from now until the job becomes claimable. */\n delay?: number\n /** Absolute time the job becomes claimable; wins over `delay`. */\n runAt?: Date | number\n}\n\nexport type TickResult = {\n /** Rows moved from pending to running this tick. */\n claimed: number\n /** Handlers that completed successfully. */\n ran: number\n /** Handlers that threw, whether or not they will be retried. */\n failed: number\n}\n\n/**\n * The untyped runtime facade. Job handlers and API context expose this shape;\n * `app.jobs` narrows `enqueue` to the declared job names/payloads.\n */\nexport type JobsRuntimeFacade = {\n enqueue(\n name: string,\n input?: unknown,\n opts?: EnqueueOptions,\n ): Promise<{ id: string }>\n /** Run one poll cycle deterministically (tests). `now` defaults to Date.now(). */\n tick(now?: number): Promise<TickResult>\n}\n\nimport type { RealtimeFacade } from '../realtime/facade'\n\nexport type JobContext<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = {\n db: DbFor<TSchema>\n env: TEnvResult\n email: EmailFacade\n storage: StorageFacade\n jobs: JobsRuntimeFacade\n realtime: RealtimeFacade<TSchema>\n}\n\nexport type BunderstackJobContext<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = JobContext<TSchema, TEnvResult>\n\nexport type QueueJobDefinition<\n TInput,\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = {\n kind: 'job'\n /** Standard Schema payload; parsed at enqueue AND before the handler runs. */\n input?: StandardSchemaV1<unknown, TInput>\n /** Attempts after the first failure. Default 3 (so 4 total attempts). */\n retries?: number\n /** Delay before retry N (1-based). Default exponential: 1s, 2s, 4s, … */\n backoff?: ((attempt: number) => number) | { baseMs?: number; factor?: number }\n /** Max simultaneous `running` rows of this type, enforced per worker. */\n concurrency?: number\n /** Lease duration in ms; an expired lease sends the job back to pending. */\n timeout?: number\n handler: (\n input: TInput,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n /** Fires once, after the final attempt fails. Errors here are logged, never retried. */\n onFailed?: (\n input: TInput,\n error: Error,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n}\n\nexport type CronInvocation = { scheduledFor: Date }\n\nexport type CronDefinition<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n TSchedule extends string = string,\n> = {\n kind: 'cron'\n schedule: TSchedule\n /** Attempts after the first failure. Default 3 (so 4 total attempts). */\n retries?: number\n /** Delay before retry N (1-based). Default exponential: 1s, 2s, 4s, … */\n backoff?: ((attempt: number) => number) | { baseMs?: number; factor?: number }\n /** Lease duration in ms; an expired lease sends the slot back to pending. */\n timeout?: number\n /** How missed slots are handled on wake. Default 'latest'. */\n catchUp?: CatchUp\n /** How far back catch-up looks, in ms. Default 1 hour. */\n catchUpWindow?: number\n handler: (\n invocation: CronInvocation,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n /** Fires once, after the final attempt fails. Errors here are logged, never retried. */\n onFailed?: (\n invocation: CronInvocation,\n error: Error,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n}\n\nexport type BackgroundDefinition =\n | QueueJobDefinition<any, any, any>\n | CronDefinition<any, any>\nexport type BackgroundDefs = Record<string, BackgroundDefinition>\n\n/** @deprecated Use QueueJobDefinition. */\nexport type JobDefinition<\n TInput,\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = QueueJobDefinition<TInput, TSchema, TEnvResult>\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyJobDefinition = QueueJobDefinition<any, any, any>\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyBackgroundDefinition =\n | QueueJobDefinition<any, any, any>\n | CronDefinition<any, any, any>\nexport type JobsDefs = BackgroundDefs\n\nexport type QueueJobKeys<TDefs extends BackgroundDefs> = {\n [K in keyof TDefs & string]: TDefs[K] extends QueueJobDefinition<\n any,\n any,\n any\n >\n ? K\n : never\n}[keyof TDefs & string]\n\n/** Throws when a definition is unusable. Safe to call more than once. */\nexport function validateBackgroundDefs(defs: BackgroundDefs): void {\n for (const [name, def] of Object.entries(defs)) {\n if (typeof def.handler !== 'function') {\n throw new Error(`[bunderstack] background task \"${name}\" has no handler`)\n }\n if (def.kind === 'job' && name.startsWith(CRON_PREFIX)) {\n throw new Error(\n `[bunderstack] job \"${name}\": the \"${CRON_PREFIX}\" prefix is reserved for cron tasks`,\n )\n }\n if (\n def.retries !== undefined &&\n (def.retries < 0 || !Number.isInteger(def.retries))\n ) {\n throw new Error(\n `[bunderstack] background task \"${name}\": retries must be a non-negative integer`,\n )\n }\n if (def.timeout !== undefined && def.timeout <= 0) {\n throw new Error(\n `[bunderstack] background task \"${name}\": timeout must be positive`,\n )\n }\n if (def.kind === 'cron') {\n parseCron(def.schedule)\n if ((def as { concurrency?: number }).concurrency !== undefined) {\n throw new Error(\n `[bunderstack] cron \"${name}\": concurrency is not supported for cron tasks — slots are already unique`,\n )\n }\n if (def.catchUpWindow !== undefined && def.catchUpWindow <= 0) {\n throw new Error(\n `[bunderstack] cron \"${name}\": catchUpWindow must be positive`,\n )\n }\n continue\n }\n if (\n def.concurrency !== undefined &&\n (def.concurrency < 1 || !Number.isInteger(def.concurrency))\n ) {\n throw new Error(\n `[bunderstack] job \"${name}\": concurrency must be a positive integer`,\n )\n }\n }\n}\n\n/** @deprecated Use validateBackgroundDefs. */\nexport const validateJobsDefs = validateBackgroundDefs\n\n/**\n * Delay in ms before retry `attempt` (1-based = the attempt that just failed).\n * Jittered by ±20% so a shared outage does not retry every job in lockstep.\n * A caller-supplied backoff function is returned verbatim — the caller owns it.\n */\nexport function backoffMs(\n def: AnyBackgroundDefinition,\n attempt: number,\n): number {\n const b = def.backoff\n if (typeof b === 'function') return b(attempt)\n const baseMs = b?.baseMs ?? 1000\n const factor = b?.factor ?? 2\n const flat = baseMs * factor ** (attempt - 1)\n return Math.round(flat * (0.8 + Math.random() * 0.4))\n}\n\n/**\n * Build the `j` instance bunderstack hands to the config's `jobs` builder\n * callback (and exports for multi-file job setups).\n */\nexport function createJobsBuilder<\n TSchema extends Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n>() {\n return {\n /** Identity with inference: pins TInput from the schema output. */\n job<TInput = undefined>(\n def: Omit<QueueJobDefinition<TInput, TSchema, TEnvResult>, 'kind'>,\n ): QueueJobDefinition<TInput, TSchema, TEnvResult> {\n return { kind: 'job', ...def }\n },\n cron<const TSchedule extends string>(\n def: Omit<CronDefinition<TSchema, TEnvResult, TSchedule>, 'kind'>,\n ): CronDefinition<TSchema, TEnvResult, TSchedule> {\n parseCron(def.schedule)\n return { kind: 'cron', ...def }\n },\n /** Identity with validation: returns the defs map, typed. */\n define<TDefs extends BackgroundDefs>(defs: TDefs): TDefs {\n validateBackgroundDefs(defs)\n return defs\n },\n }\n}\n\n/** Type of the `j` instance — for builder callbacks declared in separate files. */\nexport type BunderstackJobsBuilder<\n TSchema extends Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = ReturnType<typeof createJobsBuilder<TSchema, TEnvResult>>\n\n// Infers TInput from the JobDefinition's own type argument rather than\n// pattern-matching the (optional, so union-with-undefined) `input` property —\n// A required-property pattern fails structurally because `input` is optional,\n// so infer from the definition's own type argument instead.\ntype JobInputOf<TDef> =\n TDef extends QueueJobDefinition<infer TInput, any, any> ? TInput : undefined\n\n/**\n * `app.jobs`: `enqueue` narrowed to declared names + payloads. `Omit`s the\n * runtime facade's loose `enqueue` first — intersecting two same-named\n * methods instead would make TS treat them as overloaded, so the loose\n * `(name: string, ...)` signature would still accept any name.\n */\nexport type JobsFacade<TDefs extends JobsDefs> = Omit<\n JobsRuntimeFacade,\n 'enqueue'\n> & {\n enqueue<K extends QueueJobKeys<TDefs>>(\n name: K,\n ...rest: JobInputOf<TDefs[K]> extends undefined\n ? [input?: undefined, opts?: EnqueueOptions]\n : [input: JobInputOf<TDefs[K]>, opts?: EnqueueOptions]\n ): Promise<{ id: string }>\n}\n"]}
|
|
1
|
+
{"version":3,"file":"define.js","sourceRoot":"","sources":["../../src/jobs/define.ts"],"names":[],"mappings":"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","sourcesContent":["// src/jobs/define.ts — job definition types and the typed builder.\n// `createJobsBuilder` exists purely to carry\n// TSchema/TEnvResult typing into inline callbacks and extracted files.\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\n\nimport type { DbFor } from '../db'\nimport type { EmailFacade } from '../email'\nimport type { StorageFacade } from '../runtime'\n\nimport { parseCron } from './cron'\nimport { CRON_PREFIX, type CatchUp } from './slots'\n\nexport const DEFAULT_RETRIES = 3\nexport const DEFAULT_LEASE_DURATION_MS = 60_000\n/** @deprecated Use DEFAULT_LEASE_DURATION_MS. */\nexport const DEFAULT_TIMEOUT_MS = DEFAULT_LEASE_DURATION_MS\n\nexport type BackgroundTiming = {\n /** Lease duration in ms; an expired lease sends the job back to pending. */\n leaseDuration?: number\n /** Optional cooperative execution deadline in ms. */\n maxRuntime?: number\n /** @deprecated Use leaseDuration. */\n timeout?: number\n}\n\nexport function leaseDurationFor<T extends BackgroundTiming>(def: T): number {\n return def.leaseDuration ?? def.timeout ?? DEFAULT_LEASE_DURATION_MS\n}\n\nexport type EnqueueOptions = {\n /** Collapse duplicate enqueues while the queue row is non-terminal. */\n dedupeKey?: string\n /** Milliseconds from now until the job becomes claimable. */\n delay?: number\n /** Absolute time the job becomes claimable; wins over `delay`. */\n runAt?: Date | number\n}\n\nexport type TickResult = {\n /** Rows moved from pending to running this tick. */\n claimed: number\n /** Handlers that completed successfully. */\n ran: number\n /** Handlers that threw, whether or not they will be retried. */\n failed: number\n}\n\n/**\n * The untyped runtime facade. Job handlers and API context expose this shape;\n * `app.jobs` narrows `enqueue` to the declared job names/payloads.\n */\nexport type JobsRuntimeFacade = {\n enqueue(\n name: string,\n input?: unknown,\n opts?: EnqueueOptions,\n ): Promise<{ id: string }>\n /** Run one poll cycle deterministically (tests). `now` defaults to Date.now(). */\n tick(now?: number): Promise<TickResult>\n}\n\nimport type { RealtimeFacade } from '../realtime/facade'\n\nexport type JobContext<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = {\n db: DbFor<TSchema>\n env: TEnvResult\n email: EmailFacade\n storage: StorageFacade\n jobs: JobsRuntimeFacade\n realtime: RealtimeFacade<TSchema>\n /** Aborted when execution reaches its deadline or the job loses its lease. */\n signal: AbortSignal\n}\n\nexport type BunderstackJobContext<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = JobContext<TSchema, TEnvResult>\n\nexport type QueueJobDefinition<\n TInput,\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = BackgroundTiming & {\n kind: 'job'\n /** Standard Schema payload; parsed at enqueue AND before the handler runs. */\n input?: StandardSchemaV1<unknown, TInput>\n /** Attempts after the first failure. Default 3 (so 4 total attempts). */\n retries?: number\n /** Delay before retry N (1-based). Default exponential: 1s, 2s, 4s, … */\n backoff?: ((attempt: number) => number) | { baseMs?: number; factor?: number }\n /** Max simultaneous `running` rows of this type, enforced per worker. */\n concurrency?: number\n handler: (\n input: TInput,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n /** Fires once, after the final attempt fails. Errors here are logged, never retried. */\n onFailed?: (\n input: TInput,\n error: Error,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n}\n\nexport type CronInvocation = { scheduledFor: Date }\n\nexport type CronDefinition<\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n TSchedule extends string = string,\n> = BackgroundTiming & {\n kind: 'cron'\n schedule: TSchedule\n /** Attempts after the first failure. Default 3 (so 4 total attempts). */\n retries?: number\n /** Delay before retry N (1-based). Default exponential: 1s, 2s, 4s, … */\n backoff?: ((attempt: number) => number) | { baseMs?: number; factor?: number }\n /** How missed slots are handled on wake. Default 'latest'. */\n catchUp?: CatchUp\n /** How far back catch-up looks, in ms. Default 1 hour. */\n catchUpWindow?: number\n handler: (\n invocation: CronInvocation,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n /** Fires once, after the final attempt fails. Errors here are logged, never retried. */\n onFailed?: (\n invocation: CronInvocation,\n error: Error,\n ctx: JobContext<TSchema, TEnvResult>,\n ) => Promise<void> | void\n}\n\nexport type BackgroundDefinition =\n | QueueJobDefinition<any, any, any>\n | CronDefinition<any, any>\nexport type BackgroundDefs = Record<string, BackgroundDefinition>\n\n/** @deprecated Use QueueJobDefinition. */\nexport type JobDefinition<\n TInput,\n TSchema extends Record<string, unknown> = Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = QueueJobDefinition<TInput, TSchema, TEnvResult>\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyJobDefinition = QueueJobDefinition<any, any, any>\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyBackgroundDefinition =\n | QueueJobDefinition<any, any, any>\n | CronDefinition<any, any, any>\nexport type JobsDefs = BackgroundDefs\n\nexport type QueueJobKeys<TDefs extends BackgroundDefs> = {\n [K in keyof TDefs & string]: TDefs[K] extends QueueJobDefinition<\n any,\n any,\n any\n >\n ? K\n : never\n}[keyof TDefs & string]\n\n/** Throws when a definition is unusable. Safe to call more than once. */\nexport function validateBackgroundDefs(defs: BackgroundDefs): void {\n for (const [name, def] of Object.entries(defs)) {\n if (typeof def.handler !== 'function') {\n throw new Error(`[bunderstack] background task \"${name}\" has no handler`)\n }\n if (def.kind === 'job' && name.startsWith(CRON_PREFIX)) {\n throw new Error(\n `[bunderstack] job \"${name}\": the \"${CRON_PREFIX}\" prefix is reserved for cron tasks`,\n )\n }\n if (\n def.retries !== undefined &&\n (def.retries < 0 || !Number.isInteger(def.retries))\n ) {\n throw new Error(\n `[bunderstack] background task \"${name}\": retries must be a non-negative integer`,\n )\n }\n if (def.timeout !== undefined && def.leaseDuration !== undefined) {\n throw new Error(\n `[bunderstack] background task \"${name}\": timeout and leaseDuration cannot both be declared`,\n )\n }\n for (const field of ['timeout', 'leaseDuration', 'maxRuntime'] as const) {\n const value = def[field]\n if (\n value !== undefined &&\n (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0)\n ) {\n throw new Error(\n `[bunderstack] background task \"${name}\": ${field} must be a positive finite integer`,\n )\n }\n }\n if (def.kind === 'cron') {\n parseCron(def.schedule)\n if ((def as { concurrency?: number }).concurrency !== undefined) {\n throw new Error(\n `[bunderstack] cron \"${name}\": concurrency is not supported for cron tasks — slots are already unique`,\n )\n }\n if (def.catchUpWindow !== undefined && def.catchUpWindow <= 0) {\n throw new Error(\n `[bunderstack] cron \"${name}\": catchUpWindow must be positive`,\n )\n }\n continue\n }\n if (\n def.concurrency !== undefined &&\n (def.concurrency < 1 || !Number.isInteger(def.concurrency))\n ) {\n throw new Error(\n `[bunderstack] job \"${name}\": concurrency must be a positive integer`,\n )\n }\n }\n}\n\n/** @deprecated Use validateBackgroundDefs. */\nexport const validateJobsDefs = validateBackgroundDefs\n\n/**\n * Delay in ms before retry `attempt` (1-based = the attempt that just failed).\n * Jittered by ±20% so a shared outage does not retry every job in lockstep.\n * A caller-supplied backoff function is returned verbatim — the caller owns it.\n */\nexport function backoffMs(\n def: AnyBackgroundDefinition,\n attempt: number,\n): number {\n const b = def.backoff\n if (typeof b === 'function') return b(attempt)\n const baseMs = b?.baseMs ?? 1000\n const factor = b?.factor ?? 2\n const flat = baseMs * factor ** (attempt - 1)\n return Math.round(flat * (0.8 + Math.random() * 0.4))\n}\n\n/**\n * Build the `j` instance bunderstack hands to the config's `jobs` builder\n * callback (and exports for multi-file job setups).\n */\nexport function createJobsBuilder<\n TSchema extends Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n>() {\n return {\n /** Identity with inference: pins TInput from the schema output. */\n job<TInput = undefined>(\n def: Omit<QueueJobDefinition<TInput, TSchema, TEnvResult>, 'kind'>,\n ): QueueJobDefinition<TInput, TSchema, TEnvResult> {\n return { kind: 'job', ...def }\n },\n cron<const TSchedule extends string>(\n def: Omit<CronDefinition<TSchema, TEnvResult, TSchedule>, 'kind'>,\n ): CronDefinition<TSchema, TEnvResult, TSchedule> {\n parseCron(def.schedule)\n return { kind: 'cron', ...def }\n },\n /** Identity with validation: returns the defs map, typed. */\n define<TDefs extends BackgroundDefs>(defs: TDefs): TDefs {\n validateBackgroundDefs(defs)\n return defs\n },\n }\n}\n\n/** Type of the `j` instance — for builder callbacks declared in separate files. */\nexport type BunderstackJobsBuilder<\n TSchema extends Record<string, unknown>,\n TEnvResult = Record<string, unknown>,\n> = ReturnType<typeof createJobsBuilder<TSchema, TEnvResult>>\n\n// Infers TInput from the JobDefinition's own type argument rather than\n// pattern-matching the (optional, so union-with-undefined) `input` property —\n// A required-property pattern fails structurally because `input` is optional,\n// so infer from the definition's own type argument instead.\ntype JobInputOf<TDef> =\n TDef extends QueueJobDefinition<infer TInput, any, any> ? TInput : undefined\n\n/**\n * `app.jobs`: `enqueue` narrowed to declared names + payloads. `Omit`s the\n * runtime facade's loose `enqueue` first — intersecting two same-named\n * methods instead would make TS treat them as overloaded, so the loose\n * `(name: string, ...)` signature would still accept any name.\n */\nexport type JobsFacade<TDefs extends JobsDefs> = Omit<\n JobsRuntimeFacade,\n 'enqueue'\n> & {\n enqueue<K extends QueueJobKeys<TDefs>>(\n name: K,\n ...rest: JobInputOf<TDefs[K]> extends undefined\n ? [input?: undefined, opts?: EnqueueOptions]\n : [input: JobInputOf<TDefs[K]>, opts?: EnqueueOptions]\n ): Promise<{ id: string }>\n}\n"]}
|
package/dist/jobs/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { createJobsBuilder, validateBackgroundDefs, validateJobsDefs, DEFAULT_RETRIES, DEFAULT_TIMEOUT_MS, } from './define.js';
|
|
2
|
-
export type { AnyJobDefinition, BackgroundDefinition, BackgroundDefs, BunderstackJobContext, BunderstackJobsBuilder, EnqueueOptions, JobContext, JobDefinition, QueueJobDefinition, CronDefinition, CronInvocation, QueueJobKeys, JobsDefs, JobsFacade, JobsRuntimeFacade, } from './define.js';
|
|
1
|
+
export { createJobsBuilder, validateBackgroundDefs, validateJobsDefs, DEFAULT_RETRIES, DEFAULT_LEASE_DURATION_MS, DEFAULT_TIMEOUT_MS, leaseDurationFor, } from './define.js';
|
|
2
|
+
export type { AnyJobDefinition, BackgroundTiming, BackgroundDefinition, BackgroundDefs, BunderstackJobContext, BunderstackJobsBuilder, EnqueueOptions, JobContext, JobDefinition, QueueJobDefinition, CronDefinition, CronInvocation, QueueJobKeys, JobsDefs, JobsFacade, JobsRuntimeFacade, } from './define.js';
|
|
3
3
|
export { enqueueJob } from './queue.js';
|
|
4
4
|
export { createJobRunner } from './worker.js';
|
|
5
5
|
export { startJobWorker } from './runtime.js';
|
package/dist/jobs/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jobs/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/jobs/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,yBAAyB,EACzB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,UAAU,CAAA;AACjB,YAAY,EACV,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACrB,sBAAsB,EACtB,cAAc,EACd,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,cAAc,EACd,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,iBAAiB,GAClB,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,GACb,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACnE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA"}
|
package/dist/jobs/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/jobs/index.ts — module surface consumed by bunderstack.
|
|
2
|
-
export { createJobsBuilder, validateBackgroundDefs, validateJobsDefs, DEFAULT_RETRIES, DEFAULT_TIMEOUT_MS, } from './define.js';
|
|
2
|
+
export { createJobsBuilder, validateBackgroundDefs, validateJobsDefs, DEFAULT_RETRIES, DEFAULT_LEASE_DURATION_MS, DEFAULT_TIMEOUT_MS, leaseDurationFor, } from './define.js';
|
|
3
3
|
export { enqueueJob } from './queue.js';
|
|
4
4
|
export { createJobRunner } from './worker.js';
|
|
5
5
|
export { startJobWorker } from './runtime.js';
|
package/dist/jobs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/jobs/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/jobs/index.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,OAAO,EACL,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,yBAAyB,EACzB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,UAAU,CAAA;AAmBjB,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAO1C,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA","sourcesContent":["// src/jobs/index.ts — module surface consumed by bunderstack.\nexport {\n createJobsBuilder,\n validateBackgroundDefs,\n validateJobsDefs,\n DEFAULT_RETRIES,\n DEFAULT_LEASE_DURATION_MS,\n DEFAULT_TIMEOUT_MS,\n leaseDurationFor,\n} from './define'\nexport type {\n AnyJobDefinition,\n BackgroundTiming,\n BackgroundDefinition,\n BackgroundDefs,\n BunderstackJobContext,\n BunderstackJobsBuilder,\n EnqueueOptions,\n JobContext,\n JobDefinition,\n QueueJobDefinition,\n CronDefinition,\n CronInvocation,\n QueueJobKeys,\n JobsDefs,\n JobsFacade,\n JobsRuntimeFacade,\n} from './define'\nexport { enqueueJob } from './queue'\nexport { createJobRunner } from './worker'\nexport { startJobWorker } from './runtime'\nexport type {\n StartWorkerOptions,\n RunWorkerOptions,\n WorkerCycleResult,\n WorkerHandle,\n} from './runtime'\nexport { parseCron, cronMatches } from './cron'\nexport { slotsDue, floorSlot, CRON_PREFIX, SLOT_MS } from './slots'\nexport type { CatchUp } from './slots'\nexport type { TickResult } from './define'\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/jobs/worker.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EAEV,QAAQ,EACR,iBAAiB,EACjB,UAAU,EACX,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../src/jobs/worker.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AACvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,KAAK,EAEV,QAAQ,EACR,iBAAiB,EACjB,UAAU,EACX,MAAM,UAAU,CAAA;AAqDjB,KAAK,UAAU,GAAG;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAA;AAkD1C,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,EAAE,EAAE,KAAK,CAAA;IACT,IAAI,EAAE,QAAQ,CAAA;IACd,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC5B,MAAM,CAAC,EAAE,iBAAiB,CAAA;CAC3B;eA0fmB,MAAM,GAAgB,OAAO,CAAC,UAAU,CAAC;iBAnBlC,MAAM,KAAgB,OAAO,CAAC,UAAU,CAAC;iBAa1C,OAAO,CAAC,IAAI,CAAC;iBAYhB,MAAM;;;;;;;;;;;;oBAiEb,SAAS,GACT,SAAS,GACT,WAAW,GACX,QAAQ;;;;;;;qBAQH,iBAAiB;EAIrC"}
|
package/dist/jobs/worker.js
CHANGED
|
@@ -6,12 +6,34 @@ import { jobsTableFor } from '../internal-tables.js';
|
|
|
6
6
|
import { consoleLogger } from '../logging.js';
|
|
7
7
|
import { validateStandardSchema } from '../standard-schema.js';
|
|
8
8
|
import { parseCron } from './cron.js';
|
|
9
|
-
import { backoffMs, DEFAULT_RETRIES,
|
|
9
|
+
import { backoffMs, DEFAULT_RETRIES, leaseDurationFor } from './define.js';
|
|
10
10
|
import { enqueueJob } from './queue.js';
|
|
11
11
|
import { CRON_PREFIX, floorSlot, slotsDue, SLOT_MS } from './slots.js';
|
|
12
12
|
const CLAIM_BATCH = 10;
|
|
13
13
|
const SUCCEEDED_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
14
14
|
const REAP_INTERVAL_MS = 60 * 60_000;
|
|
15
|
+
class JobExecutionTimeoutError extends Error {
|
|
16
|
+
constructor(maxRuntime) {
|
|
17
|
+
super(`execution timed out after ${maxRuntime}ms`);
|
|
18
|
+
this.name = 'JobExecutionTimeoutError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
class JobLeaseLostError extends Error {
|
|
22
|
+
constructor() {
|
|
23
|
+
super('job lease ownership was lost');
|
|
24
|
+
this.name = 'JobLeaseLostError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function jobEvent(event, row, fields = {}) {
|
|
28
|
+
return {
|
|
29
|
+
source: 'bunderstack.jobs',
|
|
30
|
+
event,
|
|
31
|
+
jobId: row.id,
|
|
32
|
+
jobType: row.type,
|
|
33
|
+
attempt: Number(row.attempts),
|
|
34
|
+
...fields,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
15
37
|
function capacityFor(def) {
|
|
16
38
|
return def.kind === 'job' && def.concurrency !== undefined
|
|
17
39
|
? def.concurrency
|
|
@@ -44,6 +66,54 @@ export function createJobRunner(deps) {
|
|
|
44
66
|
const ctx = { ...deps.ctx };
|
|
45
67
|
let lastReapAt = 0;
|
|
46
68
|
const active = new Map();
|
|
69
|
+
function ownershipPredicate(id, owner) {
|
|
70
|
+
return and(eq(t.id, id), eq(t.status, 'running'), eq(t.attempts, owner.attempt), eq(t.lockedUntil, owner.lockedUntil));
|
|
71
|
+
}
|
|
72
|
+
function startLeaseHeartbeat(args) {
|
|
73
|
+
const owner = args.owner;
|
|
74
|
+
const intervalMs = Math.max(10, Math.floor(leaseDurationFor(args.def) / 3));
|
|
75
|
+
let stopped = false;
|
|
76
|
+
let resolveLost;
|
|
77
|
+
const lost = new Promise((resolve) => {
|
|
78
|
+
resolveLost = resolve;
|
|
79
|
+
});
|
|
80
|
+
let chain = Promise.resolve();
|
|
81
|
+
const lose = () => {
|
|
82
|
+
if (stopped)
|
|
83
|
+
return;
|
|
84
|
+
stopped = true;
|
|
85
|
+
clearInterval(timer);
|
|
86
|
+
resolveLost();
|
|
87
|
+
args.onLost();
|
|
88
|
+
};
|
|
89
|
+
const renew = () => {
|
|
90
|
+
chain = chain
|
|
91
|
+
.then(async () => {
|
|
92
|
+
if (stopped)
|
|
93
|
+
return;
|
|
94
|
+
const nextLockedUntil = Date.now() + leaseDurationFor(args.def);
|
|
95
|
+
const updated = await db
|
|
96
|
+
.update(t)
|
|
97
|
+
.set({ lockedUntil: nextLockedUntil })
|
|
98
|
+
.where(ownershipPredicate(args.row.id, owner))
|
|
99
|
+
.returning({ id: t.id });
|
|
100
|
+
if (!updated[0])
|
|
101
|
+
return lose();
|
|
102
|
+
owner.lockedUntil = nextLockedUntil;
|
|
103
|
+
})
|
|
104
|
+
.catch(() => lose());
|
|
105
|
+
};
|
|
106
|
+
const timer = setInterval(renew, intervalMs);
|
|
107
|
+
return {
|
|
108
|
+
owner,
|
|
109
|
+
lost,
|
|
110
|
+
async stop() {
|
|
111
|
+
stopped = true;
|
|
112
|
+
clearInterval(timer);
|
|
113
|
+
await chain;
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
47
117
|
/** Cron rows carry no payload — their handler input is the slot itself. */
|
|
48
118
|
function resolveInput(def, row) {
|
|
49
119
|
if (def.kind === 'cron') {
|
|
@@ -110,10 +180,15 @@ export function createJobRunner(deps) {
|
|
|
110
180
|
attempts: t.attempts,
|
|
111
181
|
lastError: t.lastError,
|
|
112
182
|
runAt: t.runAt,
|
|
183
|
+
lockedUntil: t.lockedUntil,
|
|
113
184
|
})
|
|
114
185
|
.from(t)
|
|
115
186
|
.where(and(eq(t.status, 'running'), isNotNull(t.lockedUntil), lt(t.lockedUntil, now)));
|
|
116
187
|
for (const row of expired) {
|
|
188
|
+
const owner = {
|
|
189
|
+
attempt: Number(row.attempts),
|
|
190
|
+
lockedUntil: Number(row.lockedUntil),
|
|
191
|
+
};
|
|
117
192
|
const def = definitionFor(defs, row.type);
|
|
118
193
|
const error = new Error('lease expired (worker crashed or timed out)');
|
|
119
194
|
if (!def) {
|
|
@@ -126,11 +201,11 @@ export function createJobRunner(deps) {
|
|
|
126
201
|
lastError: `unknown job type "${row.type}"`,
|
|
127
202
|
dedupeKey: null,
|
|
128
203
|
})
|
|
129
|
-
.where(
|
|
204
|
+
.where(ownershipPredicate(row.id, owner));
|
|
130
205
|
continue;
|
|
131
206
|
}
|
|
132
207
|
if (Number(row.attempts) >= maxAttempts(def)) {
|
|
133
|
-
await db
|
|
208
|
+
const updated = await db
|
|
134
209
|
.update(t)
|
|
135
210
|
.set({
|
|
136
211
|
status: 'failed',
|
|
@@ -139,8 +214,10 @@ export function createJobRunner(deps) {
|
|
|
139
214
|
lastError: error.message,
|
|
140
215
|
...terminalPatch(),
|
|
141
216
|
})
|
|
142
|
-
.where(
|
|
143
|
-
|
|
217
|
+
.where(ownershipPredicate(row.id, owner))
|
|
218
|
+
.returning({ id: t.id });
|
|
219
|
+
if (updated[0])
|
|
220
|
+
await fireOnFailed(def, resolveInput(def, row), error);
|
|
144
221
|
}
|
|
145
222
|
else {
|
|
146
223
|
await db
|
|
@@ -151,7 +228,7 @@ export function createJobRunner(deps) {
|
|
|
151
228
|
runAt: now + backoffMs(def, Number(row.attempts)),
|
|
152
229
|
lastError: error.message,
|
|
153
230
|
})
|
|
154
|
-
.where(
|
|
231
|
+
.where(ownershipPredicate(row.id, owner));
|
|
155
232
|
}
|
|
156
233
|
}
|
|
157
234
|
}
|
|
@@ -198,13 +275,18 @@ export function createJobRunner(deps) {
|
|
|
198
275
|
let available = capacityFor(def) - runningRows.length;
|
|
199
276
|
if (available <= 0)
|
|
200
277
|
return [];
|
|
201
|
-
const leaseUntil = now + (def
|
|
278
|
+
const leaseUntil = now + leaseDurationFor(def);
|
|
202
279
|
const work = [];
|
|
203
280
|
while (available > 0) {
|
|
204
281
|
const limit = Math.min(CLAIM_BATCH, available);
|
|
205
282
|
const rows = await claim(type, limit, now, leaseUntil);
|
|
206
|
-
for (const row of rows)
|
|
207
|
-
work.push({
|
|
283
|
+
for (const row of rows) {
|
|
284
|
+
work.push({
|
|
285
|
+
row,
|
|
286
|
+
def,
|
|
287
|
+
owner: { attempt: Number(row.attempts), lockedUntil: leaseUntil },
|
|
288
|
+
});
|
|
289
|
+
}
|
|
208
290
|
available -= rows.length;
|
|
209
291
|
if (rows.length < limit)
|
|
210
292
|
break;
|
|
@@ -214,7 +296,8 @@ export function createJobRunner(deps) {
|
|
|
214
296
|
// `now` is the tick's injected clock: retry runAt math uses it so tests can
|
|
215
297
|
// drive backoff deterministically. finishedAt uses the real clock (a handler
|
|
216
298
|
// may run long past the tick's start).
|
|
217
|
-
async function runJob(row, def, now,
|
|
299
|
+
async function runJob(row, def, now, owner) {
|
|
300
|
+
const startedAt = Date.now();
|
|
218
301
|
let input;
|
|
219
302
|
try {
|
|
220
303
|
input = resolveInput(def, row);
|
|
@@ -231,15 +314,47 @@ export function createJobRunner(deps) {
|
|
|
231
314
|
lastError: e.message,
|
|
232
315
|
...terminalPatch(),
|
|
233
316
|
})
|
|
234
|
-
.where(
|
|
317
|
+
.where(ownershipPredicate(row.id, owner))
|
|
235
318
|
.returning({ id: t.id });
|
|
236
319
|
if (!updated[0])
|
|
237
320
|
return 'lost';
|
|
238
321
|
await fireOnFailed(def, undefined, e);
|
|
239
322
|
return 'failed';
|
|
240
323
|
}
|
|
324
|
+
const controller = new AbortController();
|
|
325
|
+
const handlerCtx = { ...ctx, signal: controller.signal };
|
|
326
|
+
logger.info(JSON.stringify(jobEvent('job.claimed', row)));
|
|
327
|
+
let lost = false;
|
|
328
|
+
const heartbeat = startLeaseHeartbeat({
|
|
329
|
+
row,
|
|
330
|
+
def,
|
|
331
|
+
owner,
|
|
332
|
+
onLost: () => {
|
|
333
|
+
lost = true;
|
|
334
|
+
controller.abort(new JobLeaseLostError());
|
|
335
|
+
logger.warn(JSON.stringify(jobEvent('job.lease_lost', row, {
|
|
336
|
+
durationMs: Date.now() - startedAt,
|
|
337
|
+
})));
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
const deadline = def.maxRuntime === undefined
|
|
341
|
+
? undefined
|
|
342
|
+
: setTimeout(() => {
|
|
343
|
+
controller.abort(new JobExecutionTimeoutError(def.maxRuntime));
|
|
344
|
+
logger.warn(JSON.stringify(jobEvent('job.execution_timed_out', row, {
|
|
345
|
+
durationMs: Date.now() - startedAt,
|
|
346
|
+
})));
|
|
347
|
+
}, def.maxRuntime);
|
|
241
348
|
try {
|
|
242
|
-
await def.handler(input,
|
|
349
|
+
await def.handler(input, handlerCtx);
|
|
350
|
+
if (controller.signal.reason instanceof JobExecutionTimeoutError) {
|
|
351
|
+
throw controller.signal.reason;
|
|
352
|
+
}
|
|
353
|
+
if (deadline)
|
|
354
|
+
clearTimeout(deadline);
|
|
355
|
+
await heartbeat.stop();
|
|
356
|
+
if (lost)
|
|
357
|
+
return 'lost';
|
|
243
358
|
const updated = await db
|
|
244
359
|
.update(t)
|
|
245
360
|
.set({
|
|
@@ -248,14 +363,22 @@ export function createJobRunner(deps) {
|
|
|
248
363
|
lockedUntil: null,
|
|
249
364
|
...terminalPatch(),
|
|
250
365
|
})
|
|
251
|
-
.where(
|
|
366
|
+
.where(ownershipPredicate(row.id, owner))
|
|
252
367
|
.returning({ id: t.id });
|
|
253
368
|
if (!updated[0])
|
|
254
369
|
return 'lost';
|
|
370
|
+
logger.info(JSON.stringify(jobEvent('job.completed', row, { durationMs: Date.now() - startedAt })));
|
|
255
371
|
return 'ran';
|
|
256
372
|
}
|
|
257
373
|
catch (err) {
|
|
258
|
-
|
|
374
|
+
if (deadline)
|
|
375
|
+
clearTimeout(deadline);
|
|
376
|
+
const e = controller.signal.reason instanceof JobExecutionTimeoutError
|
|
377
|
+
? controller.signal.reason
|
|
378
|
+
: toError(err);
|
|
379
|
+
await heartbeat.stop();
|
|
380
|
+
if (lost)
|
|
381
|
+
return 'lost';
|
|
259
382
|
if (Number(row.attempts) < maxAttempts(def)) {
|
|
260
383
|
const updated = await db
|
|
261
384
|
.update(t)
|
|
@@ -265,10 +388,14 @@ export function createJobRunner(deps) {
|
|
|
265
388
|
runAt: now + backoffMs(def, Number(row.attempts)),
|
|
266
389
|
lastError: e.message,
|
|
267
390
|
})
|
|
268
|
-
.where(
|
|
391
|
+
.where(ownershipPredicate(row.id, owner))
|
|
269
392
|
.returning({ id: t.id });
|
|
270
393
|
if (!updated[0])
|
|
271
394
|
return 'lost';
|
|
395
|
+
logger.warn(JSON.stringify(jobEvent('job.retrying', row, {
|
|
396
|
+
durationMs: Date.now() - startedAt,
|
|
397
|
+
error: e.message,
|
|
398
|
+
})));
|
|
272
399
|
}
|
|
273
400
|
else {
|
|
274
401
|
const updated = await db
|
|
@@ -280,10 +407,14 @@ export function createJobRunner(deps) {
|
|
|
280
407
|
lastError: e.message,
|
|
281
408
|
...terminalPatch(),
|
|
282
409
|
})
|
|
283
|
-
.where(
|
|
410
|
+
.where(ownershipPredicate(row.id, owner))
|
|
284
411
|
.returning({ id: t.id });
|
|
285
412
|
if (!updated[0])
|
|
286
413
|
return 'lost';
|
|
414
|
+
logger.error(JSON.stringify(jobEvent('job.failed', row, {
|
|
415
|
+
durationMs: Date.now() - startedAt,
|
|
416
|
+
error: e.message,
|
|
417
|
+
})));
|
|
287
418
|
await fireOnFailed(def, input, e);
|
|
288
419
|
}
|
|
289
420
|
return 'failed';
|
|
@@ -301,7 +432,7 @@ export function createJobRunner(deps) {
|
|
|
301
432
|
// Claim the whole tick snapshot before starting any handler. Work enqueued
|
|
302
433
|
// by a handler therefore belongs to the next tick, regardless of the
|
|
303
434
|
// declaration order of its target job type.
|
|
304
|
-
const outcomes = await Promise.all(claimedWork.map(({ row, def,
|
|
435
|
+
const outcomes = await Promise.all(claimedWork.map(({ row, def, owner }) => runJob(row, def, now, owner)));
|
|
305
436
|
let ran = 0;
|
|
306
437
|
let failed = 0;
|
|
307
438
|
for (const outcome of outcomes) {
|
|
@@ -327,7 +458,7 @@ export function createJobRunner(deps) {
|
|
|
327
458
|
active.set(type, tasks);
|
|
328
459
|
}
|
|
329
460
|
let task;
|
|
330
|
-
task = runJob(work.row, work.def, claimedAt, work.
|
|
461
|
+
task = runJob(work.row, work.def, claimedAt, work.owner)
|
|
331
462
|
.then(() => undefined)
|
|
332
463
|
.catch((error) => {
|
|
333
464
|
logger.error('[bunderstack] background job execution failed:', error);
|