@cortexkit/opencode-magic-context 0.49.6 → 0.49.8
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/auto-search-worker.js +10 -7
- package/dist/compaction-marker-candidates-worker.js +2 -2
- package/dist/{compaction-marker-candidates-cz72pm1t.js → compaction-marker-candidates-xxz35vwp.js} +3 -3
- package/dist/{conflict-warning-hook-1jq1rxx7.js → conflict-warning-hook-w3crzrar.js} +8 -8
- package/dist/{context-ae3gxdye.js → context-x0ccfv0f.js} +88 -53
- package/dist/{derive-budgets-j4ry8j8n.js → derive-budgets-3nbxsyss.js} +2 -2
- package/dist/embedding-backfill-3h2m5n0q.js +21 -0
- package/dist/embedding-worker-process.js +6 -2
- package/dist/embedding-worker.js +6 -5
- package/dist/features/magic-context/compaction-marker-candidates.d.ts.map +1 -1
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/cron.d.ts +2 -0
- package/dist/features/magic-context/dreamer/cron.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts +6 -3
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts +7 -1
- package/dist/features/magic-context/dreamer/map-memories-prompt.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts +2 -2
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/primer-seed.d.ts +2 -0
- package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/provider-output-failure.d.ts +1 -0
- package/dist/features/magic-context/dreamer/provider-output-failure.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +2 -2
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-worker-client.d.ts.map +1 -1
- package/dist/features/magic-context/message-index-async.d.ts.map +1 -1
- package/dist/features/magic-context/migration-worker-client.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/smart-notes/wake-plane.d.ts.map +1 -1
- package/dist/features/magic-context/storage-clone.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +10 -3
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-session.d.ts.map +1 -1
- package/dist/features/magic-context/storage-replay-document.d.ts +4 -0
- package/dist/features/magic-context/storage-replay-document.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/history-owner-ty29fz0f.js +244 -0
- package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +7 -0
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-worker-client.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/edit-marker.d.ts +2 -0
- package/dist/hooks/magic-context/edit-marker.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/history-boundary-repair.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-persist.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts +1 -0
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/note-nudger.d.ts +5 -3
- package/dist/hooks/magic-context/note-nudger.d.ts.map +1 -1
- package/dist/hooks/magic-context/postprocess-read-cache.d.ts.map +1 -1
- package/dist/hooks/magic-context/raw-ordinal-warmup.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +15 -4
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/replay-committed-drops.d.ts +9 -0
- package/dist/hooks/magic-context/replay-committed-drops.d.ts.map +1 -0
- package/dist/hooks/magic-context/rust-lkg-freeze-registry.d.ts +2 -2
- package/dist/hooks/magic-context/rust-lkg-freeze-registry.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +4 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-refusal-recovery.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-served-raw.d.ts +29 -0
- package/dist/hooks/magic-context/rust-served-raw.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +23 -24
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +2 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +2 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts +5 -1
- package/dist/hooks/magic-context/unresolved-history-boundary.d.ts.map +1 -1
- package/dist/hooks/magic-context/wrapup-orchestrator.d.ts.map +1 -1
- package/dist/{index-g9exahnx.js → index-0jz47061.js} +6 -3
- package/dist/{index-4mjhqadz.js → index-150zz6fh.js} +1 -1
- package/dist/{index-ch08qwgd.js → index-2rw244rh.js} +88 -81
- package/dist/{index-6q5h70h2.js → index-4hb8nt93.js} +3 -3
- package/dist/{index-j55vn694.js → index-7bjgyaj1.js} +4 -4
- package/dist/{index-h2wc01qk.js → index-8kk7yh6m.js} +2 -2
- package/dist/{index-vztbgcdk.js → index-9g1c4bt9.js} +304 -92
- package/dist/{index-7q1m1qjk.js → index-a43xq1pg.js} +5 -5
- package/dist/{index-tqf7yg27.js → index-a4b0gbyg.js} +1 -1
- package/dist/{index-vnjhmkwk.js → index-angx1s0k.js} +4 -3
- package/dist/{index-1xaw2pq8.js → index-b4gcb3rk.js} +8 -8
- package/dist/{index-z1tjp257.js → index-b64n4d0h.js} +1 -1
- package/dist/{index-1bt88epp.js → index-bd9cjab3.js} +1 -1
- package/dist/{index-gbmqa9x9.js → index-bmwfzz28.js} +9 -6
- package/dist/{index-qx53vezw.js → index-gjvjh960.js} +1 -1
- package/dist/{index-zgzdm6dg.js → index-gzg9ekc1.js} +155 -103
- package/dist/{index-vh3e2mvr.js → index-hxrvx6cy.js} +121 -51
- package/dist/{index-w23dh319.js → index-jw4gbfkv.js} +1 -1
- package/dist/index-knvkbfvh.js +25 -0
- package/dist/{index-vkbqdnkj.js → index-kvw6xew3.js} +6 -3
- package/dist/{index-bg5qqene.js → index-mp2kghkv.js} +3 -27
- package/dist/{index-nsjyysj0.js → index-mvvephtj.js} +5 -1
- package/dist/{index-cm4hmxyk.js → index-n78avt2p.js} +1 -1
- package/dist/{index-hr64sth2.js → index-pgy3h49n.js} +4 -4
- package/dist/{index-a796ct3s.js → index-pjgzdezw.js} +13 -9
- package/dist/{index-9m7bn21d.js → index-q28hsbpg.js} +15 -3
- package/dist/{index-ab3bgm2m.js → index-q76br5zn.js} +3 -3
- package/dist/{index-xqyj5c85.js → index-qgp5vnt4.js} +5 -5
- package/dist/{index-9s58tq95.js → index-rg1cezmj.js} +22 -22
- package/dist/{index-f3dnvnw6.js → index-s178h9z8.js} +4 -3
- package/dist/{index-mj1fgfrk.js → index-w1ejb9qr.js} +939 -160
- package/dist/{index-k6xvbj2e.js → index-w28y7d0z.js} +2 -0
- package/dist/{index-0nnd4bry.js → index-wafzvyhg.js} +63 -19
- package/dist/{index-ck6y3td2.js → index-xf6k8szp.js} +4 -4
- package/dist/{index-1j8jcjpa.js → index-xpn05jxq.js} +4 -4
- package/dist/{index-s0gsmthp.js → index-ydjgz36f.js} +7 -7
- package/dist/{index-besxhcsh.js → index-zf9nydbd.js} +50 -39
- package/dist/index.js +53 -52
- package/dist/{logger-ewak5q1s.js → logger-wn328tfg.js} +1 -1
- package/dist/{message-index-bounded-630j53ps.js → message-index-bounded-hxjp56qk.js} +16 -16
- package/dist/{message-index-c84wm2g0.js → message-index-zs882h8e.js} +16 -16
- package/dist/{message-time-backfill-8kj57qe3.js → message-time-backfill-nb5hhnrt.js} +3 -3
- package/dist/migration-worker.js +11 -11
- package/dist/plugin/boot-quiet.d.ts.map +1 -1
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts +2 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{project-embedding-registry-vv59t7ah.js → project-embedding-registry-vy7sv9jt.js} +11 -11
- package/dist/raw-ordinal-warmup-worker.js +17 -15
- package/dist/{read-session-chunk-t3vjd0h0.js → read-session-chunk-t9r7r0sg.js} +13 -11
- package/dist/{read-session-raw-gf7eytnm.js → read-session-raw-gm6xj30v.js} +3 -3
- package/dist/recomp-orchestrator-gr36zvd2.js +60 -0
- package/dist/{safe-notification-target-q0cyrx3x.js → safe-notification-target-jdynm677.js} +2 -2
- package/dist/{search-kth2p6cc.js → search-qjen1pmz.js} +24 -24
- package/dist/{send-session-notification-3j6krd4w.js → send-session-notification-q8ngbap3.js} +6 -6
- package/dist/shared/history-owner.d.ts +12 -0
- package/dist/shared/history-owner.d.ts.map +1 -0
- package/dist/shared/off-thread-migration-clock.d.ts +8 -1
- package/dist/shared/off-thread-migration-clock.d.ts.map +1 -1
- package/dist/shared/processed-image-eligibility.d.ts +22 -0
- package/dist/shared/processed-image-eligibility.d.ts.map +1 -0
- package/dist/shared/recovery-reasoning-prefix.d.ts +9 -0
- package/dist/shared/recovery-reasoning-prefix.d.ts.map +1 -0
- package/dist/shared/redaction.d.ts.map +1 -1
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/sqlite.d.ts +3 -1
- package/dist/shared/sqlite.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts +9 -0
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +2 -2
- package/dist/{sqlite-d7edbk1s.js → sqlite-4pqyynrs.js} +3 -1
- package/dist/{storage-memory-af5zdz98.js → storage-memory-j3v3hc83.js} +2 -2
- package/dist/{storage-tags-dyj7mmbq.js → storage-tags-a513rkj3.js} +5 -5
- package/dist/tools/ctx-expand/search-archive.d.ts +4 -0
- package/dist/tools/ctx-expand/search-archive.d.ts.map +1 -0
- package/dist/tools/ctx-expand/tools.d.ts.map +1 -1
- package/dist/{transform-postprocess-phase-bv3kb5s1.js → transform-postprocess-phase-yv463b7y.js} +30 -30
- package/dist/v2/auto-search-worker.js +10 -7
- package/dist/v2/{compaction-marker-candidates-6gqp5wrc.js → compaction-marker-candidates-a15w3mqh.js} +3 -3
- package/dist/v2/compaction-marker-candidates-worker.js +2 -2
- package/dist/v2/{context-p8f2shp1.js → context-wd82v9z6.js} +1133 -278
- package/dist/v2/{derive-budgets-enckmg7y.js → derive-budgets-0rmpmxjw.js} +3 -3
- package/dist/v2/embedding-backfill-vr7wdtrf.js +20 -0
- package/dist/v2/embedding-worker-process.js +6 -2
- package/dist/v2/embedding-worker.js +6 -5
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/history-owner-2cr9py37.js +243 -0
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/store.d.ts +1 -0
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/{logger-pj02s7sr.js → logger-te6e1j72.js} +1 -1
- package/dist/v2/{message-index-bounded-yzxmf6vp.js → message-index-bounded-madg2kfr.js} +16 -16
- package/dist/v2/{message-index-3ws17x1g.js → message-index-yd94vt9n.js} +16 -16
- package/dist/v2/{message-time-backfill-5nf10c65.js → message-time-backfill-rz1pnpaj.js} +3 -3
- package/dist/v2/migration-worker.js +11 -11
- package/dist/v2/{project-embedding-registry-rsnf2263.js → project-embedding-registry-ja23aygk.js} +11 -11
- package/dist/v2/raw-ordinal-warmup-worker.js +17 -15
- package/dist/v2/{read-session-chunk-vtb0w2kd.js → read-session-chunk-7qs791f3.js} +13 -11
- package/dist/v2/{read-session-raw-gpcddemt.js → read-session-raw-h4azh85b.js} +3 -3
- package/dist/v2/recomp-orchestrator-ee91r9s6.js +59 -0
- package/dist/v2/{safe-notification-target-y3p1hce9.js → safe-notification-target-936374xx.js} +2 -2
- package/dist/v2/search-pn15rr52.js +46 -0
- package/dist/v2/{send-session-notification-5aj2j7vf.js → send-session-notification-6n7chhyp.js} +6 -6
- package/dist/v2/{server-09g9pkvx.js → server-08k4zkfk.js} +50 -39
- package/dist/v2/{server-ras2sxnc.js → server-44ryn9rb.js} +9 -6
- package/dist/v2/{server-158ehmk2.js → server-4easnf78.js} +19 -19
- package/dist/v2/{server-ww9nr8pe.js → server-4gz0xkrc.js} +5 -5
- package/dist/v2/{server-d6g4whc2.js → server-4qr18rfg.js} +3 -3
- package/dist/v2/{server-f9py7s7a.js → server-4rs8sm06.js} +5 -5
- package/dist/v2/{server-yezz0cp2.js → server-63ha7f0f.js} +3 -27
- package/dist/v2/{server-3t0q2cqx.js → server-7dv9wt9r.js} +1 -1
- package/dist/v2/{server-cw6521wn.js → server-7e087rvk.js} +2 -0
- package/dist/v2/{server-hb6xhh9q.js → server-988cqjme.js} +6 -3
- package/dist/v2/{server-keswwkvv.js → server-9a6bwc12.js} +4 -4
- package/dist/v2/{server-t4k4qexw.js → server-9e5r8y60.js} +5 -1
- package/dist/v2/{server-7ekna9zs.js → server-adfte03x.js} +2 -2
- package/dist/v2/{server-9zqewm6g.js → server-ak7w74j9.js} +4 -4
- package/dist/v2/{server-387vkhjq.js → server-bx59ezg6.js} +4 -3
- package/dist/v2/{server-676xw7at.js → server-ckrb5rm8.js} +1 -1
- package/dist/v2/{server-wrem2nmz.js → server-cxsr0ng6.js} +15 -3
- package/dist/v2/{server-t528t8qm.js → server-hn9fb3v0.js} +2 -2
- package/dist/v2/{server-qn23p4h7.js → server-j3dm74ey.js} +9 -9
- package/dist/v2/{server-yd01snxq.js → server-jjsf29aq.js} +4 -4
- package/dist/v2/server-knvkbfvh.js +25 -0
- package/dist/v2/{server-cm4hmxyk.js → server-n78avt2p.js} +1 -1
- package/dist/v2/{server-btgnsv7w.js → server-njnx39q2.js} +6 -3
- package/dist/v2/{server-6a9mxnb2.js → server-npm52h2q.js} +4 -3
- package/dist/v2/{server-vb7pbd9p.js → server-nveratm5.js} +1 -1
- package/dist/v2/{server-7skbfb81.js → server-q360kdb8.js} +121 -51
- package/dist/v2/{server-kzns2hbz.js → server-s6jqh8bj.js} +1 -1
- package/dist/v2/{server-6byqfbqf.js → server-shme41gj.js} +1 -1
- package/dist/v2/{server-0nnd4bry.js → server-wafzvyhg.js} +63 -19
- package/dist/v2/{server-fby0cxbq.js → server-xj4mewze.js} +304 -92
- package/dist/v2/{server-aa9w15cs.js → server-y5g32dhq.js} +88 -81
- package/dist/v2/{server-x2r63c0x.js → server-yak3sxya.js} +7 -7
- package/dist/v2/{server-4tn95znb.js → server-zrwaj29a.js} +2 -2
- package/dist/v2/server.js +9 -8
- package/dist/v2/{sqlite-d7edbk1s.js → sqlite-4pqyynrs.js} +3 -1
- package/dist/v2/{storage-memory-af5zdz98.js → storage-memory-j3v3hc83.js} +2 -2
- package/dist/v2/{storage-tags-d47bpaqa.js → storage-tags-spa92sgj.js} +5 -5
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/v2/{wrapup-orchestrator-cahk37n7.js → wrapup-orchestrator-5wxy1ggv.js} +38 -35
- package/dist/wrapup-orchestrator-gv08c6f9.js +47 -0
- package/package.json +3 -3
- package/src/config/compaction-accessor-guard.test.ts +3 -0
- package/src/features/magic-context/dreamer/cron.ts +8 -0
- package/src/shared/history-owner.test.ts +60 -0
- package/src/shared/history-owner.ts +43 -0
- package/src/shared/host-stall-profiler-node.ts +3 -1
- package/src/shared/host-stall-profiler.ts +3 -1
- package/src/shared/off-thread-migration-clock.test.ts +80 -10
- package/src/shared/off-thread-migration-clock.ts +37 -13
- package/src/shared/processed-image-eligibility.test.ts +68 -0
- package/src/shared/processed-image-eligibility.ts +41 -0
- package/src/shared/processed-image-position-review.json +20 -0
- package/src/shared/processed-image-position-review.test.ts +19 -0
- package/src/shared/recovery-reasoning-prefix.test.ts +34 -0
- package/src/shared/recovery-reasoning-prefix.ts +35 -0
- package/src/shared/redaction.test.ts +16 -0
- package/src/shared/redaction.ts +8 -1
- package/src/shared/root-async-context.probe.mjs +36 -0
- package/src/shared/root-async-context.test.ts +49 -0
- package/src/shared/rpc-server.ts +27 -23
- package/src/shared/sqlite-busy-budget-review.test.ts +291 -0
- package/src/shared/sqlite-foreground-wait.test.ts +253 -1
- package/src/shared/sqlite-writer-diagnostics.test.ts +31 -0
- package/src/shared/sqlite.ts +112 -30
- package/src/shared/tag-transcript.ts +118 -0
- package/src/shared/user-facing-codes.ts +2 -2
- package/dist/embedding-backfill-dtjmyktb.js +0 -21
- package/dist/recomp-orchestrator-1d08g57a.js +0 -60
- package/dist/v2/embedding-backfill-cg4rv90f.js +0 -20
- package/dist/v2/recomp-orchestrator-v64r4s4r.js +0 -59
- package/dist/v2/search-bpa4fxq1.js +0 -46
- package/dist/wrapup-orchestrator-mqp0v195.js +0 -47
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect, test } from "bun:test";
|
|
1
|
+
import { expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -113,6 +113,220 @@ for (const holdMs of [1000, 2500]) {
|
|
|
113
113
|
}, 30_000);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
for (const holdMs of [1000, 2500]) {
|
|
117
|
+
test(`slow uncontended SQL preserves admission for a ${holdMs}ms autocommit lock`, async () => {
|
|
118
|
+
const { db, path, close } = fixture();
|
|
119
|
+
let locker: SqliteWriteLocker | undefined;
|
|
120
|
+
let served = false;
|
|
121
|
+
let refusals = 0;
|
|
122
|
+
const output = {
|
|
123
|
+
messages: [
|
|
124
|
+
{
|
|
125
|
+
info: { id: "u", sessionID: "slow", role: "user" },
|
|
126
|
+
parts: [{ type: "text", text: "raw" }],
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
const handler = createMessagesTransformHandler({
|
|
131
|
+
onStorageBusyRefusal: async () => {
|
|
132
|
+
refusals++;
|
|
133
|
+
},
|
|
134
|
+
magicContext: {
|
|
135
|
+
"experimental.chat.messages.transform": async (_input, result) => {
|
|
136
|
+
// Spend three seconds inside successful SQL, not between writes.
|
|
137
|
+
// Recursive work keeps this independent of CPU speed and UDF APIs.
|
|
138
|
+
const cte =
|
|
139
|
+
"WITH RECURSIVE n(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM n WHERE x<1000000)";
|
|
140
|
+
const slowWrite = db.prepare(
|
|
141
|
+
`${cte} INSERT INTO result SELECT sum(x) FROM n RETURNING value`,
|
|
142
|
+
);
|
|
143
|
+
const slowRead = db.prepare(`${cte} SELECT sum(x) FROM n`);
|
|
144
|
+
const started = performance.now();
|
|
145
|
+
do {
|
|
146
|
+
slowWrite.all();
|
|
147
|
+
slowRead.all();
|
|
148
|
+
} while (performance.now() - started < 3000);
|
|
149
|
+
locker = await startSqliteWriteLocker(path, holdMs);
|
|
150
|
+
db.prepare("INSERT INTO result VALUES ('managed')").run();
|
|
151
|
+
(result.messages[0].parts[0] as { text: string }).text = "managed";
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
try {
|
|
156
|
+
const pass = handler({}, output as Parameters<typeof handler>[1]).then(() => {
|
|
157
|
+
served = true;
|
|
158
|
+
});
|
|
159
|
+
if (holdMs === 1000) {
|
|
160
|
+
await pass;
|
|
161
|
+
expect(served).toBe(true);
|
|
162
|
+
expect(refusals).toBe(0);
|
|
163
|
+
expect(output.messages[0].parts[0].text).toBe("managed");
|
|
164
|
+
} else {
|
|
165
|
+
await expect(pass).rejects.toBeInstanceOf(StorageBusyRefusalError);
|
|
166
|
+
expect(served).toBe(false);
|
|
167
|
+
expect(refusals).toBe(1);
|
|
168
|
+
expect(output.messages[0].parts[0].text).toBe("raw");
|
|
169
|
+
}
|
|
170
|
+
expect(
|
|
171
|
+
db.prepare("SELECT count(*) AS n FROM result WHERE value = 'managed'").get(),
|
|
172
|
+
).toEqual({ n: holdMs === 1000 ? 1 : 0 });
|
|
173
|
+
} finally {
|
|
174
|
+
if (locker) expect(await locker.exited).toBe(0);
|
|
175
|
+
close();
|
|
176
|
+
}
|
|
177
|
+
}, 30_000);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
test("BUSY autocommit probes do not double-apply INSERT OR IGNORE, UPSERT or RETURNING", async () => {
|
|
181
|
+
const { db, path, close } = fixture();
|
|
182
|
+
db.exec(
|
|
183
|
+
"CREATE TABLE counter(id INTEGER PRIMARY KEY, n INTEGER); INSERT INTO counter VALUES (1, 0)",
|
|
184
|
+
);
|
|
185
|
+
const forms = [
|
|
186
|
+
["INSERT OR IGNORE INTO counter VALUES (2, 1)", "run"],
|
|
187
|
+
["INSERT INTO counter VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1", "run"],
|
|
188
|
+
["UPDATE counter SET n = n + 1 WHERE id = 1 RETURNING n", "get"],
|
|
189
|
+
[
|
|
190
|
+
"INSERT INTO counter VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1 RETURNING n",
|
|
191
|
+
"all",
|
|
192
|
+
],
|
|
193
|
+
] as const;
|
|
194
|
+
try {
|
|
195
|
+
for (const [sql, method] of forms) {
|
|
196
|
+
const blocker = new Database(path);
|
|
197
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
198
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
199
|
+
const before = db.prepare("SELECT * FROM counter ORDER BY id").all();
|
|
200
|
+
try {
|
|
201
|
+
expect(() => db.prepare(sql)[method]()).toThrow("locked");
|
|
202
|
+
expect(db.prepare("SELECT * FROM counter ORDER BY id").all()).toEqual(before);
|
|
203
|
+
expect(db.inTransaction).toBe(false);
|
|
204
|
+
} finally {
|
|
205
|
+
blocker.exec("ROLLBACK");
|
|
206
|
+
blocker.close();
|
|
207
|
+
}
|
|
208
|
+
const locker = await startSqliteWriteLocker(path, 100);
|
|
209
|
+
try {
|
|
210
|
+
await withSqliteTransformPass(() => db.prepare(sql)[method]());
|
|
211
|
+
} finally {
|
|
212
|
+
expect(await locker.exited).toBe(0);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
expect(db.prepare("SELECT * FROM counter ORDER BY id").all()).toEqual([
|
|
216
|
+
{ id: 1, n: 3 },
|
|
217
|
+
{ id: 2, n: 1 },
|
|
218
|
+
]);
|
|
219
|
+
} finally {
|
|
220
|
+
close();
|
|
221
|
+
}
|
|
222
|
+
}, 30_000);
|
|
223
|
+
|
|
224
|
+
test("many contended autocommit writes share at most two seconds of lock waiting", async () => {
|
|
225
|
+
const { db, path, close } = fixture();
|
|
226
|
+
const blocker = new Database(path);
|
|
227
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
228
|
+
const timeouts: number[] = [];
|
|
229
|
+
const nativeExec = db.exec.bind(db);
|
|
230
|
+
const exec = spyOn(db, "exec").mockImplementation((sql) => {
|
|
231
|
+
const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
|
|
232
|
+
if (timeout) timeouts.push(Number(timeout[1]));
|
|
233
|
+
return nativeExec(sql);
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
await withSqliteTransformPass(() => {
|
|
237
|
+
// Use real BUSY failures but a controlled accounting clock: each
|
|
238
|
+
// retry costs 400ms regardless of runner scheduling. The deadline
|
|
239
|
+
// keeps the real waits short without hiding lease exhaustion.
|
|
240
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 10 });
|
|
241
|
+
let now = 0;
|
|
242
|
+
const clock = spyOn(performance, "now").mockImplementation(() => (now += 400));
|
|
243
|
+
try {
|
|
244
|
+
for (let i = 0; i < 8; i++) {
|
|
245
|
+
expect(() => db.prepare("INSERT INTO result VALUES ('one')").run()).toThrow(
|
|
246
|
+
SqliteAcquisitionBusyError,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
} finally {
|
|
250
|
+
clock.mockRestore();
|
|
251
|
+
}
|
|
252
|
+
// Five charged retries exhaust 2000ms. All later retries must have
|
|
253
|
+
// zero timeout even though the deadline still offers another 10ms.
|
|
254
|
+
expect(timeouts).toEqual([
|
|
255
|
+
...Array.from({ length: 5 }, () => [0, 10, 5000]).flat(),
|
|
256
|
+
...Array.from({ length: 3 }, () => [0, 0, 5000]).flat(),
|
|
257
|
+
]);
|
|
258
|
+
blocker.exec("ROLLBACK");
|
|
259
|
+
db.prepare("INSERT INTO result VALUES ('uncontended')").run();
|
|
260
|
+
});
|
|
261
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([{ value: "uncontended" }]);
|
|
262
|
+
} finally {
|
|
263
|
+
exec.mockRestore();
|
|
264
|
+
if (blocker.inTransaction) blocker.exec("ROLLBACK");
|
|
265
|
+
blocker.close();
|
|
266
|
+
close();
|
|
267
|
+
}
|
|
268
|
+
}, 30_000);
|
|
269
|
+
|
|
270
|
+
test("an exec script charges its single attempt without a zero-wait probe", () => {
|
|
271
|
+
const { db, path, close } = fixture();
|
|
272
|
+
const blocker = new Database(path);
|
|
273
|
+
const timeouts: number[] = [];
|
|
274
|
+
const nativeExec = db.exec.bind(db);
|
|
275
|
+
const exec = spyOn(db, "exec").mockImplementation((sql) => {
|
|
276
|
+
const timeout = /^PRAGMA busy_timeout=(\d+)$/.exec(sql);
|
|
277
|
+
if (timeout) timeouts.push(Number(timeout[1]));
|
|
278
|
+
return nativeExec(sql);
|
|
279
|
+
});
|
|
280
|
+
try {
|
|
281
|
+
withSqliteTransformPass(() => {
|
|
282
|
+
let now = 0;
|
|
283
|
+
const clock = spyOn(performance, "now").mockImplementation(() => (now += 2500));
|
|
284
|
+
try {
|
|
285
|
+
db.exec(
|
|
286
|
+
"INSERT INTO result VALUES ('prefix'); INSERT INTO result VALUES ('suffix')",
|
|
287
|
+
);
|
|
288
|
+
} finally {
|
|
289
|
+
clock.mockRestore();
|
|
290
|
+
}
|
|
291
|
+
blocker.exec("BEGIN IMMEDIATE");
|
|
292
|
+
expect(() => db.prepare("INSERT INTO result VALUES ('blocked')").run()).toThrow(
|
|
293
|
+
SqliteAcquisitionBusyError,
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
expect(timeouts).toEqual([2000, 5000, 0, 0, 5000]);
|
|
297
|
+
expect(db.prepare("SELECT * FROM result").all()).toEqual([
|
|
298
|
+
{ value: "prefix" },
|
|
299
|
+
{ value: "suffix" },
|
|
300
|
+
]);
|
|
301
|
+
} finally {
|
|
302
|
+
exec.mockRestore();
|
|
303
|
+
if (blocker.inTransaction) blocker.exec("ROLLBACK");
|
|
304
|
+
blocker.close();
|
|
305
|
+
close();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("an explicit transaction keeps its timeout and never probes or retries", () => {
|
|
310
|
+
const { db, path, close } = fixture();
|
|
311
|
+
const other = new Database(path);
|
|
312
|
+
db.exec("PRAGMA busy_timeout=0; BEGIN; SELECT * FROM result");
|
|
313
|
+
other.exec("INSERT INTO result VALUES ('other')");
|
|
314
|
+
const exec = spyOn(db, "exec");
|
|
315
|
+
try {
|
|
316
|
+
withSqliteTransformPass(() => {
|
|
317
|
+
expect(() => db.prepare("INSERT INTO result VALUES ('stale')").run()).toThrow("locked");
|
|
318
|
+
});
|
|
319
|
+
expect(exec).not.toHaveBeenCalled();
|
|
320
|
+
expect(db.inTransaction).toBe(true);
|
|
321
|
+
expect(db.prepare("PRAGMA busy_timeout").get()).toEqual({ timeout: 0 });
|
|
322
|
+
} finally {
|
|
323
|
+
exec.mockRestore();
|
|
324
|
+
if (db.inTransaction) db.exec("ROLLBACK");
|
|
325
|
+
other.close();
|
|
326
|
+
close();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
116
330
|
for (const mode of ["BEGIN", "autocommit"]) {
|
|
117
331
|
test(`foreground ${mode} wait is capped by the pass deadline`, () => {
|
|
118
332
|
const { db, path, close } = fixture();
|
|
@@ -138,3 +352,41 @@ for (const mode of ["BEGIN", "autocommit"]) {
|
|
|
138
352
|
}
|
|
139
353
|
});
|
|
140
354
|
}
|
|
355
|
+
|
|
356
|
+
test("an autocommit exec script never replays a committed prefix after BUSY", () => {
|
|
357
|
+
const main = fixture();
|
|
358
|
+
const auxiliary = fixture();
|
|
359
|
+
const free = fixture();
|
|
360
|
+
try {
|
|
361
|
+
main.db.prepare("ATTACH DATABASE ? AS aux").run(auxiliary.path);
|
|
362
|
+
main.db.prepare("ATTACH DATABASE ? AS free").run(free.path);
|
|
363
|
+
auxiliary.db.exec("BEGIN IMMEDIATE");
|
|
364
|
+
withSqliteTransformPass(() => {
|
|
365
|
+
guardSqliteTransformPass({ assert() {}, remainingMs: () => 100 });
|
|
366
|
+
expect(() =>
|
|
367
|
+
main.db.exec(
|
|
368
|
+
"INSERT INTO main.result VALUES ('prefix'); INSERT INTO aux.result VALUES ('blocked')",
|
|
369
|
+
),
|
|
370
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
371
|
+
expect(() =>
|
|
372
|
+
main.db.exec(
|
|
373
|
+
"CREATE TABLE applied_once(id); INSERT INTO aux.result VALUES ('blocked')",
|
|
374
|
+
),
|
|
375
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
376
|
+
expect(() =>
|
|
377
|
+
main.db.exec(
|
|
378
|
+
"CREATE TABLE free.applied_once(id); INSERT INTO aux.result VALUES ('blocked')",
|
|
379
|
+
),
|
|
380
|
+
).toThrow(SqliteAcquisitionBusyError);
|
|
381
|
+
});
|
|
382
|
+
expect(main.db.prepare("SELECT * FROM main.result").all()).toEqual([{ value: "prefix" }]);
|
|
383
|
+
expect(
|
|
384
|
+
main.db.prepare("SELECT name FROM sqlite_schema WHERE name='applied_once'").all(),
|
|
385
|
+
).toHaveLength(1);
|
|
386
|
+
} finally {
|
|
387
|
+
auxiliary.db.exec("ROLLBACK");
|
|
388
|
+
auxiliary.close();
|
|
389
|
+
main.close();
|
|
390
|
+
free.close();
|
|
391
|
+
}
|
|
392
|
+
});
|
|
@@ -39,6 +39,22 @@ const scenario = `
|
|
|
39
39
|
hold();
|
|
40
40
|
writer.exec("COMMIT");
|
|
41
41
|
|
|
42
|
+
writer.exec("BEGIN IMMEDIATE");
|
|
43
|
+
now += 101;
|
|
44
|
+
writer.exec("ROLLBACK");
|
|
45
|
+
|
|
46
|
+
writer.exec("CREATE TABLE result(value)");
|
|
47
|
+
const statementTimes = [now, now + 101];
|
|
48
|
+
performance.now = () => statementTimes.shift() ?? now;
|
|
49
|
+
writer.prepare("INSERT INTO result VALUES (1)").run();
|
|
50
|
+
now += 101;
|
|
51
|
+
|
|
52
|
+
let clockReads = 0;
|
|
53
|
+
performance.now = () => { clockReads++; return now; };
|
|
54
|
+
writer.exec("BEGIN IMMEDIATE");
|
|
55
|
+
writer.exec("COMMIT");
|
|
56
|
+
if (clockReads !== 2) throw new Error("fast unscoped transaction clock reads=" + clockReads);
|
|
57
|
+
|
|
42
58
|
logger.flushLogger();
|
|
43
59
|
writer.close();
|
|
44
60
|
holder.close();
|
|
@@ -106,6 +122,21 @@ describe("sqlite writer diagnostics", () => {
|
|
|
106
122
|
expect(consoleOutput).not.toContain("sqlite writer");
|
|
107
123
|
});
|
|
108
124
|
|
|
125
|
+
test("unscoped rollback names the host, pid and caller at the 100ms threshold", () => {
|
|
126
|
+
const rolledBack = logLines.find((line) => line.includes("outcome=rolled_back"));
|
|
127
|
+
expect(rolledBack).toContain("lane=background");
|
|
128
|
+
expect(rolledBack).toContain("hold_ms=101");
|
|
129
|
+
expect(rolledBack).toMatch(/pid=\d+ host=opencode/);
|
|
130
|
+
expect(rolledBack).toContain("caller=");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("native autocommit time is labelled as including acquisition, not lock ownership", () => {
|
|
134
|
+
const statement = logLines.find((line) => line.includes("site=autocommit"));
|
|
135
|
+
expect(statement).toContain("elapsed_ms=101");
|
|
136
|
+
expect(statement).toContain("timing=statement_including_acquisition");
|
|
137
|
+
expect(statement).not.toContain("hold_ms=");
|
|
138
|
+
});
|
|
139
|
+
|
|
109
140
|
test("a writer that waited for the lock reports the wait, not its short hold", () => {
|
|
110
141
|
const waited = logLines.find((line) => line.includes("attempts=2"));
|
|
111
142
|
if (!waited) throw new Error(`no retried acquisition logged:\n${logLines.join("\n")}`);
|
package/src/shared/sqlite.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* `run()` → {changes,lastInsertRowid}) is identical and was verified directly.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
42
|
+
import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
|
|
43
43
|
import { statSync } from "node:fs";
|
|
44
44
|
|
|
45
45
|
// Type import only — runtime is loaded dynamically below. @types/better-sqlite3
|
|
@@ -48,6 +48,16 @@ import { statSync } from "node:fs";
|
|
|
48
48
|
// node:sqlite at runtime (both expose prepare/run/get/all/exec/close).
|
|
49
49
|
import type BetterSqlite3 from "better-sqlite3";
|
|
50
50
|
|
|
51
|
+
// Capture the root before creating request-local AsyncLocalStorage scopes.
|
|
52
|
+
// Shared workers and timers must not inherit callbacks that reject stale
|
|
53
|
+
// database writes: those callbacks can retain an obsolete request's inputs.
|
|
54
|
+
const resourceRoot = new AsyncResource("magic-context:root");
|
|
55
|
+
|
|
56
|
+
/** Create host-owned resources without inheriting request-local state. */
|
|
57
|
+
export function inRootAsyncContext<T>(operation: () => T): T {
|
|
58
|
+
return resourceRoot.runInAsyncScope(operation);
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
/**
|
|
52
62
|
* Slow-write reporting is injected rather than imported: this module is also
|
|
53
63
|
* executed directly by Node (the `node:sqlite` smoke in CI, Pi, Desktop), and
|
|
@@ -72,14 +82,21 @@ export function registerSlowWriteReporter(reporter: SlowWriteReporter): void {
|
|
|
72
82
|
*/
|
|
73
83
|
type SqliteDiagnosticSink = (message: string) => void;
|
|
74
84
|
let sqliteDiagnosticSink: SqliteDiagnosticSink | undefined;
|
|
85
|
+
let sqliteDiagnosticHost: () => string = () => "unknown";
|
|
75
86
|
|
|
76
|
-
export function registerSqliteDiagnosticSink(
|
|
87
|
+
export function registerSqliteDiagnosticSink(
|
|
88
|
+
sink: SqliteDiagnosticSink,
|
|
89
|
+
host?: () => string,
|
|
90
|
+
): void {
|
|
77
91
|
sqliteDiagnosticSink = sink;
|
|
92
|
+
if (host) sqliteDiagnosticHost = host;
|
|
78
93
|
}
|
|
79
94
|
|
|
80
95
|
function reportSqliteDiagnostic(message: string): void {
|
|
81
96
|
try {
|
|
82
|
-
sqliteDiagnosticSink?.(
|
|
97
|
+
sqliteDiagnosticSink?.(
|
|
98
|
+
`[magic-context] ${message} pid=${process.pid} host=${sqliteDiagnosticHost()}`,
|
|
99
|
+
);
|
|
83
100
|
} catch {
|
|
84
101
|
// Diagnostics must never change the outcome of the write they describe.
|
|
85
102
|
}
|
|
@@ -393,9 +410,18 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
393
410
|
// A transaction that ended without passing through exec (for
|
|
394
411
|
// example an automatic rollback) must not be timed as this one.
|
|
395
412
|
openWriterTransactions.delete(db);
|
|
396
|
-
if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore())
|
|
413
|
+
if (transformPassScope.getStore()?.active || backgroundWriterScope.getStore()) {
|
|
397
414
|
acquireShort(db, () => nativeExec(sql), sql.trim());
|
|
398
|
-
else
|
|
415
|
+
} else {
|
|
416
|
+
nativeExec(sql);
|
|
417
|
+
openWriterTransactions.set(db, {
|
|
418
|
+
site: sql.trim(),
|
|
419
|
+
lane: "background",
|
|
420
|
+
acquireMs: 0,
|
|
421
|
+
attempts: 1,
|
|
422
|
+
acquiredAt: performance.now(),
|
|
423
|
+
});
|
|
424
|
+
}
|
|
399
425
|
return db;
|
|
400
426
|
}
|
|
401
427
|
if (WRITER_TRANSACTION_END.test(sql) && openWriterTransactions.has(db))
|
|
@@ -404,7 +430,9 @@ function installTransactionRouting(db: BetterSqlite3.Database, readonly: boolean
|
|
|
404
430
|
!/^\s*(?:SELECT|EXPLAIN|PRAGMA|COMMIT|END|ROLLBACK|SAVEPOINT|RELEASE)\b/i.test(sql)
|
|
405
431
|
) {
|
|
406
432
|
try {
|
|
407
|
-
|
|
433
|
+
// exec can commit a script prefix before BUSY. Run it once;
|
|
434
|
+
// only prepared statements are safe to probe and retry.
|
|
435
|
+
return withAutocommitTimeout(db, () => nativeExec(sql), false);
|
|
408
436
|
} finally {
|
|
409
437
|
if (mayChangeTagIdentity(sql)) tagIdentityWriteGeneration += 1;
|
|
410
438
|
}
|
|
@@ -571,7 +599,9 @@ function trackSqliteConnection(
|
|
|
571
599
|
|
|
572
600
|
const TrackedDatabase = new Proxy(DatabaseImpl, {
|
|
573
601
|
construct(target, args) {
|
|
574
|
-
const db =
|
|
602
|
+
const db = inRootAsyncContext(
|
|
603
|
+
() => Reflect.construct(target, args, target) as BetterSqlite3.Database,
|
|
604
|
+
);
|
|
575
605
|
return trackSqliteConnection(db, args[0], args[1]);
|
|
576
606
|
},
|
|
577
607
|
}) as typeof BetterSqlite3;
|
|
@@ -943,7 +973,7 @@ const SHORT_BUSY_TIMEOUT_MS = 25;
|
|
|
943
973
|
const FOREGROUND_IN_PASS_BUSY_TIMEOUT_MS = 2_000;
|
|
944
974
|
const FOREGROUND_ACQUISITION_BUDGET_MS = 16_500;
|
|
945
975
|
/** Waiting this long for the writer lock, or holding it this long, is logged. */
|
|
946
|
-
const SLOW_WRITER_LOG_MS =
|
|
976
|
+
const SLOW_WRITER_LOG_MS = 100;
|
|
947
977
|
const WRITER_TRANSACTION_END = /^\s*(?:COMMIT|END|ROLLBACK)(?:\s+TRANSACTION)?\s*;?\s*$/i;
|
|
948
978
|
|
|
949
979
|
type WriterLane = "foreground" | "background";
|
|
@@ -984,32 +1014,67 @@ function foregroundWaitLease() {
|
|
|
984
1014
|
: undefined;
|
|
985
1015
|
}
|
|
986
1016
|
|
|
987
|
-
/**
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
function withAutocommitTimeout<T>(db: Database, operation: () => T): T {
|
|
1017
|
+
/** Probe a prepared statement's writer lock without charging uncontended work
|
|
1018
|
+
* against the foreground lock-wait budget. An exec script may commit some of
|
|
1019
|
+
* its statements before failing, so it cannot be retried without duplicating
|
|
1020
|
+
* writes; conservatively charge its single attempt's full execution time. */
|
|
1021
|
+
function withAutocommitTimeout<T>(db: Database, operation: () => T, singleStatement = true): T {
|
|
992
1022
|
assertTransformWrite();
|
|
993
1023
|
const lease = foregroundWaitLease();
|
|
994
|
-
if (isInTransaction(db)
|
|
995
|
-
const
|
|
996
|
-
db
|
|
997
|
-
`PRAGMA busy_timeout=${lease ? Math.floor(Math.min(lease.remainingWaitMs, lease.guard?.remainingMs() ?? Infinity)) : SHORT_BUSY_TIMEOUT_MS}`,
|
|
998
|
-
);
|
|
1024
|
+
if (isInTransaction(db)) return operation();
|
|
1025
|
+
const scoped = lease || backgroundWriterScope.getStore();
|
|
1026
|
+
const previous = scoped ? pragmaNumber(db, "busy_timeout") : null;
|
|
999
1027
|
const started = performance.now();
|
|
1028
|
+
let completed = false;
|
|
1029
|
+
const execute = () => {
|
|
1030
|
+
const result = operation();
|
|
1031
|
+
completed = true;
|
|
1032
|
+
return result;
|
|
1033
|
+
};
|
|
1000
1034
|
try {
|
|
1001
|
-
return
|
|
1035
|
+
if (!scoped) return execute();
|
|
1036
|
+
if (singleStatement) {
|
|
1037
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
1038
|
+
try {
|
|
1039
|
+
return execute();
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
if (!isTransientSqliteError(error) || isInTransaction(db)) throw error;
|
|
1042
|
+
}
|
|
1043
|
+
assertTransformWrite();
|
|
1044
|
+
}
|
|
1045
|
+
const timeout = Math.floor(
|
|
1046
|
+
Math.min(
|
|
1047
|
+
lease?.remainingWaitMs ?? SHORT_BUSY_TIMEOUT_MS,
|
|
1048
|
+
lease?.guard?.remainingMs() ?? Infinity,
|
|
1049
|
+
),
|
|
1050
|
+
);
|
|
1051
|
+
db.exec(`PRAGMA busy_timeout=${timeout}`);
|
|
1052
|
+
const waitStarted = performance.now();
|
|
1053
|
+
try {
|
|
1054
|
+
return execute();
|
|
1055
|
+
} finally {
|
|
1056
|
+
// Deduct the prepared statement's contended retry or the exec
|
|
1057
|
+
// script's execution from the remaining foreground wait budget;
|
|
1058
|
+
// the native API cannot separate lock waiting from execution.
|
|
1059
|
+
if (lease)
|
|
1060
|
+
lease.remainingWaitMs = Math.max(
|
|
1061
|
+
0,
|
|
1062
|
+
lease.remainingWaitMs - (performance.now() - waitStarted),
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1002
1065
|
} catch (error) {
|
|
1003
1066
|
if (lease && isTransientSqliteError(error))
|
|
1004
1067
|
throw new SqliteAcquisitionBusyError(error, "autocommit");
|
|
1005
1068
|
throw error;
|
|
1006
1069
|
} finally {
|
|
1007
|
-
|
|
1008
|
-
lease.remainingWaitMs = Math.max(
|
|
1009
|
-
0,
|
|
1010
|
-
lease.remainingWaitMs - (performance.now() - started),
|
|
1011
|
-
);
|
|
1070
|
+
const elapsed = performance.now() - started;
|
|
1012
1071
|
if (previous !== null) db.exec(`PRAGMA busy_timeout=${previous}`);
|
|
1072
|
+
// Native autocommit does not expose the instant its lock was acquired.
|
|
1073
|
+
// Label the upper bound so a waiting statement is not blamed as a holder.
|
|
1074
|
+
if (elapsed >= SLOW_WRITER_LOG_MS)
|
|
1075
|
+
reportSqliteDiagnostic(
|
|
1076
|
+
`sqlite writer site=autocommit lane=${lease ? "foreground" : "background"} elapsed_ms=${Math.round(elapsed)} timing=statement_including_acquisition outcome=${completed ? "completed" : "failed"} caller=${writerCaller()}`,
|
|
1077
|
+
);
|
|
1013
1078
|
}
|
|
1014
1079
|
}
|
|
1015
1080
|
|
|
@@ -1071,20 +1136,18 @@ function acquireShort(db: Database, acquire: () => unknown, site: string): void
|
|
|
1071
1136
|
* `acquireShort`, then report how long the writer waited for the lock
|
|
1072
1137
|
* (`acquire_ms`) and how long it then held it (`hold_ms`) when either crosses
|
|
1073
1138
|
* the threshold. A long hold blocks every other process's writer, so it is
|
|
1074
|
-
* worth seeing even when the lock itself came quickly.
|
|
1075
|
-
*
|
|
1076
|
-
* is not
|
|
1139
|
+
* worth seeing even when the lock itself came quickly. This interval includes
|
|
1140
|
+
* COMMIT: an automatic PASSIVE checkpoint can run after writer-lock release,
|
|
1141
|
+
* so hold_ms is a conservative upper bound, not proof of continuous ownership.
|
|
1077
1142
|
*/
|
|
1078
1143
|
function endWriterTransaction(db: Database, end: () => unknown, sql: string): unknown {
|
|
1079
1144
|
let ended = false;
|
|
1080
|
-
const endStarted = performance.now();
|
|
1081
1145
|
try {
|
|
1082
1146
|
const result = end();
|
|
1083
1147
|
ended = true;
|
|
1084
1148
|
return result;
|
|
1085
1149
|
} finally {
|
|
1086
1150
|
const open = openWriterTransactions.get(db);
|
|
1087
|
-
const endMs = performance.now() - endStarted;
|
|
1088
1151
|
// A COMMIT that failed with the transaction still open has not ended it.
|
|
1089
1152
|
if (open && !isInTransaction(db)) {
|
|
1090
1153
|
openWriterTransactions.delete(db);
|
|
@@ -1096,13 +1159,32 @@ function endWriterTransaction(db: Database, end: () => unknown, sql: string): un
|
|
|
1096
1159
|
? "committed"
|
|
1097
1160
|
: "failed";
|
|
1098
1161
|
reportSqliteDiagnostic(
|
|
1099
|
-
`sqlite writer site=${open.site} lane=${open.lane} acquire_ms=${Math.round(open.acquireMs)} hold_ms=${Math.round(holdMs)}
|
|
1162
|
+
`sqlite writer site=${open.site} lane=${open.lane} acquire_ms=${Math.round(open.acquireMs)} hold_ms=${Math.round(holdMs)} attempts=${open.attempts} outcome=${outcome} timing=through_commit_upper_bound caller=${writerCaller()}`,
|
|
1100
1163
|
);
|
|
1101
1164
|
}
|
|
1102
1165
|
}
|
|
1103
1166
|
}
|
|
1104
1167
|
}
|
|
1105
1168
|
|
|
1169
|
+
// Capture a stack only for slow writes. Synchronous COMMIT retains the writer's
|
|
1170
|
+
// caller, so fast transactions need neither stack allocation nor source mapping.
|
|
1171
|
+
function writerCaller(): string {
|
|
1172
|
+
return (
|
|
1173
|
+
new Error().stack
|
|
1174
|
+
?.split("\n")
|
|
1175
|
+
.find(
|
|
1176
|
+
(line) =>
|
|
1177
|
+
line.includes(" at ") &&
|
|
1178
|
+
!/[/\\]sqlite\.[tj]s[:)]/.test(line) &&
|
|
1179
|
+
!/\b(?:writerCaller|endWriterTransaction|withAutocommitTimeout|value)\b/.test(
|
|
1180
|
+
line,
|
|
1181
|
+
),
|
|
1182
|
+
)
|
|
1183
|
+
?.trim()
|
|
1184
|
+
.replace(/\s+/g, "_") ?? "unknown"
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1106
1188
|
/** Begin an immediate transaction with bounded asynchronous retries on writer contention.
|
|
1107
1189
|
* Callers must recheck any lease or source snapshot after this function returns.
|
|
1108
1190
|
* Resolves with the `performance.now()` time at which BEGIN IMMEDIATE succeeded,
|