@absurd-sqlite/bun-worker 0.2.2-alpha.1 → 0.2.2-alpha.2
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/bun.lock +3 -3
- package/dist/index.js +60 -25
- package/package.json +3 -3
- package/test/basic.test.ts +52 -0
package/bun.lock
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"name": "@absurd-sqlite/bun-worker",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@absurd-sqlite/sdk": "next",
|
|
9
|
-
"absurd-sdk": "
|
|
9
|
+
"absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz",
|
|
10
10
|
"cac": "^6.7.14",
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
"packages": {
|
|
19
|
-
"@absurd-sqlite/sdk": ["@absurd-sqlite/sdk@0.2.1-alpha.
|
|
19
|
+
"@absurd-sqlite/sdk": ["@absurd-sqlite/sdk@0.2.1-alpha.2", "", { "dependencies": { "absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz" }, "peerDependencies": { "better-sqlite3": "^12.5.0" } }, "sha512-+8qBOtV/aoHX7BbzxIkzGpuG/o8RQ73Njyxt8Og04NvevceSeUtQ1IlF1Syil5v8EVMsAa2sshuBU0LzZKW/5A=="],
|
|
20
20
|
|
|
21
21
|
"@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="],
|
|
22
22
|
|
|
23
|
-
"absurd-sdk": ["absurd-sdk@0.0.7",
|
|
23
|
+
"absurd-sdk": ["absurd-sdk@https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz", { "peerDependencies": { "pg": "^8.0.0" } }],
|
|
24
24
|
|
|
25
25
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
|
26
26
|
|
package/dist/index.js
CHANGED
|
@@ -4772,6 +4772,47 @@ class TimeoutError extends Error {
|
|
|
4772
4772
|
}
|
|
4773
4773
|
}
|
|
4774
4774
|
|
|
4775
|
+
class LeaseTimerManager {
|
|
4776
|
+
log;
|
|
4777
|
+
taskLabel;
|
|
4778
|
+
fatalOnLeaseTimeout;
|
|
4779
|
+
warnTimer = null;
|
|
4780
|
+
fatalTimer = null;
|
|
4781
|
+
constructor(log, taskLabel, fatalOnLeaseTimeout) {
|
|
4782
|
+
this.log = log;
|
|
4783
|
+
this.taskLabel = taskLabel;
|
|
4784
|
+
this.fatalOnLeaseTimeout = fatalOnLeaseTimeout;
|
|
4785
|
+
}
|
|
4786
|
+
update(leaseSeconds) {
|
|
4787
|
+
this.clear();
|
|
4788
|
+
if (leaseSeconds <= 0) {
|
|
4789
|
+
return;
|
|
4790
|
+
}
|
|
4791
|
+
this.warnTimer = setTimeout(() => {
|
|
4792
|
+
this.log.warn(`task ${this.taskLabel} exceeded claim timeout of ${leaseSeconds}s`);
|
|
4793
|
+
}, leaseSeconds * 1000);
|
|
4794
|
+
if (this.fatalOnLeaseTimeout) {
|
|
4795
|
+
this.fatalTimer = setTimeout(() => {
|
|
4796
|
+
this.log.error(`task ${this.taskLabel} exceeded claim timeout of ${leaseSeconds}s by more than 100%; terminating process`);
|
|
4797
|
+
process.exit(1);
|
|
4798
|
+
}, leaseSeconds * 1000 * 2);
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
stop() {
|
|
4802
|
+
this.clear();
|
|
4803
|
+
}
|
|
4804
|
+
clear() {
|
|
4805
|
+
if (this.warnTimer) {
|
|
4806
|
+
clearTimeout(this.warnTimer);
|
|
4807
|
+
this.warnTimer = null;
|
|
4808
|
+
}
|
|
4809
|
+
if (this.fatalTimer) {
|
|
4810
|
+
clearTimeout(this.fatalTimer);
|
|
4811
|
+
this.fatalTimer = null;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
|
|
4775
4816
|
class TaskContext {
|
|
4776
4817
|
log;
|
|
4777
4818
|
taskID;
|
|
@@ -4780,8 +4821,9 @@ class TaskContext {
|
|
|
4780
4821
|
task;
|
|
4781
4822
|
checkpointCache;
|
|
4782
4823
|
claimTimeout;
|
|
4824
|
+
leaseTimer;
|
|
4783
4825
|
stepNameCounter = new Map;
|
|
4784
|
-
constructor(log, taskID, con, queueName, task, checkpointCache, claimTimeout) {
|
|
4826
|
+
constructor(log, taskID, con, queueName, task, checkpointCache, claimTimeout, leaseTimer) {
|
|
4785
4827
|
this.log = log;
|
|
4786
4828
|
this.taskID = taskID;
|
|
4787
4829
|
this.con = con;
|
|
@@ -4789,19 +4831,21 @@ class TaskContext {
|
|
|
4789
4831
|
this.task = task;
|
|
4790
4832
|
this.checkpointCache = checkpointCache;
|
|
4791
4833
|
this.claimTimeout = claimTimeout;
|
|
4834
|
+
this.leaseTimer = leaseTimer;
|
|
4792
4835
|
}
|
|
4793
4836
|
get headers() {
|
|
4794
4837
|
return this.task.headers ?? {};
|
|
4795
4838
|
}
|
|
4796
4839
|
static async create(args) {
|
|
4797
|
-
const { log, taskID, con, queueName, task, claimTimeout } = args;
|
|
4840
|
+
const { log, taskID, con, queueName, task, claimTimeout, leaseTimer } = args;
|
|
4798
4841
|
const result = await con.query(`SELECT checkpoint_name, state, status, owner_run_id, updated_at
|
|
4799
4842
|
FROM absurd.get_task_checkpoint_states($1, $2, $3)`, [queueName, task.task_id, task.run_id]);
|
|
4800
4843
|
const cache = new Map;
|
|
4801
4844
|
for (const row of result.rows) {
|
|
4802
4845
|
cache.set(row.checkpoint_name, row.state);
|
|
4803
4846
|
}
|
|
4804
|
-
|
|
4847
|
+
const ctx = new TaskContext(log, taskID, con, queueName, task, cache, claimTimeout, leaseTimer);
|
|
4848
|
+
return ctx;
|
|
4805
4849
|
}
|
|
4806
4850
|
async queryWithCancelCheck(sql, params) {
|
|
4807
4851
|
try {
|
|
@@ -4868,6 +4912,7 @@ class TaskContext {
|
|
|
4868
4912
|
this.claimTimeout
|
|
4869
4913
|
]);
|
|
4870
4914
|
this.checkpointCache.set(checkpointName, value);
|
|
4915
|
+
this.recordLeaseExtension(this.claimTimeout);
|
|
4871
4916
|
}
|
|
4872
4917
|
async scheduleRun(wakeAt) {
|
|
4873
4918
|
await this.con.query(`SELECT absurd.schedule_run($1, $2, $3)`, [
|
|
@@ -4913,11 +4958,16 @@ class TaskContext {
|
|
|
4913
4958
|
throw new SuspendTask;
|
|
4914
4959
|
}
|
|
4915
4960
|
async heartbeat(seconds) {
|
|
4961
|
+
const leaseSeconds = seconds ?? this.claimTimeout;
|
|
4916
4962
|
await this.queryWithCancelCheck(`SELECT absurd.extend_claim($1, $2, $3)`, [
|
|
4917
4963
|
this.queueName,
|
|
4918
4964
|
this.task.run_id,
|
|
4919
|
-
|
|
4965
|
+
leaseSeconds
|
|
4920
4966
|
]);
|
|
4967
|
+
this.recordLeaseExtension(leaseSeconds);
|
|
4968
|
+
}
|
|
4969
|
+
recordLeaseExtension(leaseSeconds) {
|
|
4970
|
+
this.leaseTimer.update(leaseSeconds);
|
|
4921
4971
|
}
|
|
4922
4972
|
async emitEvent(eventName, payload) {
|
|
4923
4973
|
if (!eventName) {
|
|
@@ -5171,30 +5221,20 @@ class Absurd {
|
|
|
5171
5221
|
}
|
|
5172
5222
|
}
|
|
5173
5223
|
async executeTask(task, claimTimeout, options) {
|
|
5174
|
-
let warnTimer;
|
|
5175
|
-
let fatalTimer;
|
|
5176
5224
|
const registration = this.registry.get(task.task_name);
|
|
5225
|
+
const taskLabel = `${task.task_name} (${task.task_id})`;
|
|
5226
|
+
const leaseTimer = new LeaseTimerManager(this.log, taskLabel, options?.fatalOnLeaseTimeout ?? false);
|
|
5177
5227
|
const ctx = await TaskContext.create({
|
|
5178
5228
|
log: this.log,
|
|
5179
5229
|
taskID: task.task_id,
|
|
5180
5230
|
con: this.con,
|
|
5181
5231
|
queueName: registration?.queue ?? "unknown",
|
|
5182
5232
|
task,
|
|
5183
|
-
claimTimeout
|
|
5233
|
+
claimTimeout,
|
|
5234
|
+
leaseTimer
|
|
5184
5235
|
});
|
|
5236
|
+
leaseTimer.update(claimTimeout);
|
|
5185
5237
|
try {
|
|
5186
|
-
if (claimTimeout > 0) {
|
|
5187
|
-
const taskLabel = `${task.task_name} (${task.task_id})`;
|
|
5188
|
-
warnTimer = setTimeout(() => {
|
|
5189
|
-
this.log.warn(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s`);
|
|
5190
|
-
}, claimTimeout * 1000);
|
|
5191
|
-
if (options?.fatalOnLeaseTimeout) {
|
|
5192
|
-
fatalTimer = setTimeout(() => {
|
|
5193
|
-
this.log.error(`task ${taskLabel} exceeded claim timeout of ${claimTimeout}s by more than 100%; terminating process`);
|
|
5194
|
-
process.exit(1);
|
|
5195
|
-
}, claimTimeout * 1000 * 2);
|
|
5196
|
-
}
|
|
5197
|
-
}
|
|
5198
5238
|
if (!registration) {
|
|
5199
5239
|
throw new Error("Unknown task");
|
|
5200
5240
|
} else if (registration.queue !== this.queueName) {
|
|
@@ -5216,12 +5256,7 @@ class Absurd {
|
|
|
5216
5256
|
this.log.error("[absurd] task execution failed:", err);
|
|
5217
5257
|
await failTaskRun(this.con, this.queueName, task.run_id, err);
|
|
5218
5258
|
} finally {
|
|
5219
|
-
|
|
5220
|
-
clearTimeout(warnTimer);
|
|
5221
|
-
}
|
|
5222
|
-
if (fatalTimer) {
|
|
5223
|
-
clearTimeout(fatalTimer);
|
|
5224
|
-
}
|
|
5259
|
+
leaseTimer.stop();
|
|
5225
5260
|
}
|
|
5226
5261
|
}
|
|
5227
5262
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absurd-sqlite/bun-worker",
|
|
3
|
-
"version": "0.2.2-alpha.
|
|
3
|
+
"version": "0.2.2-alpha.2",
|
|
4
4
|
"description": "Bun worker utilities for Absurd-SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"homepage": "https://github.com/b4fun/absurd-sqlite#readme",
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@absurd-sqlite/sdk": "next",
|
|
42
|
-
"absurd-sdk": "
|
|
42
|
+
"absurd-sdk": "https://github.com/bcho/absurd/releases/download/sdks%2Ftypescript%2Fv0.0.7/typescript-sdk-v0.0.7.tgz",
|
|
43
43
|
"cac": "^6.7.14"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"engines": {
|
|
50
50
|
"bun": ">=1.1.0"
|
|
51
51
|
}
|
|
52
|
-
}
|
|
52
|
+
}
|
package/test/basic.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
expect,
|
|
5
5
|
beforeAll,
|
|
6
6
|
afterEach,
|
|
7
|
+
jest,
|
|
7
8
|
} from "bun:test";
|
|
8
9
|
import assert from "node:assert/strict";
|
|
9
10
|
import type { Absurd } from "absurd-sdk";
|
|
@@ -501,5 +502,56 @@ describe("Basic SDK Operations", () => {
|
|
|
501
502
|
);
|
|
502
503
|
});
|
|
503
504
|
});
|
|
505
|
+
|
|
506
|
+
test("heartbeat keeps task alive past original claim timeout", async () => {
|
|
507
|
+
const claimTimeout = 1;
|
|
508
|
+
const extension = 10;
|
|
509
|
+
const longWorkMs = claimTimeout * 2000 + 100;
|
|
510
|
+
let heartbeatFired = false;
|
|
511
|
+
|
|
512
|
+
absurd.registerTask(
|
|
513
|
+
{ name: "heartbeat-long-task" },
|
|
514
|
+
async (_params, taskCtx) => {
|
|
515
|
+
await taskCtx.heartbeat(extension);
|
|
516
|
+
heartbeatFired = true;
|
|
517
|
+
await ctx.sleep(longWorkMs);
|
|
518
|
+
return { ok: true };
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const { taskID } = await absurd.spawn("heartbeat-long-task", {});
|
|
523
|
+
|
|
524
|
+
const exitSpy = jest
|
|
525
|
+
.spyOn(process, "exit")
|
|
526
|
+
.mockImplementation(() => undefined as never);
|
|
527
|
+
|
|
528
|
+
const worker = await absurd.startWorker({
|
|
529
|
+
claimTimeout,
|
|
530
|
+
concurrency: 1,
|
|
531
|
+
pollInterval: 0.01,
|
|
532
|
+
fatalOnLeaseTimeout: true,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
await waitFor(() => expect(heartbeatFired).toBe(true), {
|
|
537
|
+
timeout: 500,
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
await waitFor(async () => {
|
|
541
|
+
const task = await ctx.getTask(taskID);
|
|
542
|
+
expect(task?.state).toBe("completed");
|
|
543
|
+
}, {
|
|
544
|
+
timeout: longWorkMs + 2000,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const runs = await ctx.getRuns(taskID);
|
|
548
|
+
expect(runs).toHaveLength(1);
|
|
549
|
+
expect(runs[0]?.state).toBe("completed");
|
|
550
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
551
|
+
} finally {
|
|
552
|
+
await worker.close();
|
|
553
|
+
exitSpy.mockRestore();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
504
556
|
});
|
|
505
557
|
});
|