@namzu/sdk 20.2.0 → 20.4.0
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 +185 -0
- package/dist/manager/run/persistence.d.ts +2 -2
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +14 -5
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/public-runtime.d.ts +2 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +6 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/checkpoint.d.ts +27 -1
- package/dist/runtime/query/checkpoint.d.ts.map +1 -1
- package/dist/runtime/query/checkpoint.js +34 -4
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/context.d.ts +2 -0
- package/dist/runtime/query/context.d.ts.map +1 -1
- package/dist/runtime/query/context.js +1 -0
- package/dist/runtime/query/context.js.map +1 -1
- package/dist/runtime/query/index.d.ts +32 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +5 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/resume-run.d.ts +9 -1
- package/dist/runtime/query/resume-run.d.ts.map +1 -1
- package/dist/runtime/query/resume-run.js +2 -1
- package/dist/runtime/query/resume-run.js.map +1 -1
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +2 -1
- package/dist/store/index.js.map +1 -1
- package/dist/store/run/checkpoint-disk.d.ts +18 -2
- package/dist/store/run/checkpoint-disk.d.ts.map +1 -1
- package/dist/store/run/checkpoint-disk.js +50 -5
- package/dist/store/run/checkpoint-disk.js.map +1 -1
- package/dist/store/run/checkpoint-memory.d.ts +22 -2
- package/dist/store/run/checkpoint-memory.d.ts.map +1 -1
- package/dist/store/run/checkpoint-memory.js +99 -4
- package/dist/store/run/checkpoint-memory.js.map +1 -1
- package/dist/store/run/claim-disk.d.ts +130 -0
- package/dist/store/run/claim-disk.d.ts.map +1 -0
- package/dist/store/run/claim-disk.js +550 -0
- package/dist/store/run/claim-disk.js.map +1 -0
- package/dist/store/run/disk.d.ts +10 -8
- package/dist/store/run/disk.d.ts.map +1 -1
- package/dist/store/run/disk.js.map +1 -1
- package/dist/store/run/listing.d.ts +44 -1
- package/dist/store/run/listing.d.ts.map +1 -1
- package/dist/store/run/listing.js +92 -1
- package/dist/store/run/listing.js.map +1 -1
- package/dist/store/run/memory.d.ts +46 -0
- package/dist/store/run/memory.d.ts.map +1 -0
- package/dist/store/run/memory.js +104 -0
- package/dist/store/run/memory.js.map +1 -0
- package/dist/types/run/checkpoint-store.d.ts +178 -2
- package/dist/types/run/checkpoint-store.d.ts.map +1 -1
- package/dist/types/run/config.d.ts +12 -0
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/index.d.ts +1 -0
- package/dist/types/run/index.d.ts.map +1 -1
- package/dist/types/run/index.js +1 -0
- package/dist/types/run/index.js.map +1 -1
- package/dist/types/run/store.d.ts +103 -0
- package/dist/types/run/store.d.ts.map +1 -0
- package/dist/types/run/store.js +30 -0
- package/dist/types/run/store.js.map +1 -0
- package/package.json +1 -1
- package/src/manager/run/persistence.ts +17 -7
- package/src/public-runtime.ts +6 -0
- package/src/runtime/query/checkpoint.ts +42 -5
- package/src/runtime/query/context.ts +3 -0
- package/src/runtime/query/index.ts +39 -1
- package/src/runtime/query/resume-run.ts +12 -2
- package/src/store/index.ts +5 -0
- package/src/store/run/checkpoint-disk.ts +70 -5
- package/src/store/run/checkpoint-memory.ts +118 -3
- package/src/store/run/claim-disk.ts +593 -0
- package/src/store/run/disk.ts +10 -8
- package/src/store/run/listing.ts +116 -1
- package/src/store/run/memory.ts +121 -0
- package/src/types/run/checkpoint-store.ts +189 -2
- package/src/types/run/config.ts +13 -0
- package/src/types/run/index.ts +1 -0
- package/src/types/run/store.ts +112 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CheckpointId, IterationCheckpoint } from '../../types/hitl/index.js';
|
|
2
|
-
import type { CheckpointListingScope, CheckpointRunScope, CheckpointStore, DurableRunPage, ListDurableRunsOptions } from '../../types/run/checkpoint-store.js';
|
|
2
|
+
import type { CheckpointListingScope, CheckpointRunScope, CheckpointStore, ClaimFence, ClaimRunOptions, DurableRunPage, ListDurableRunsOptions, RunClaim } from '../../types/run/checkpoint-store.js';
|
|
3
3
|
/**
|
|
4
4
|
* Process-local {@link CheckpointStore}, keyed by the full five-layer scope.
|
|
5
5
|
*
|
|
@@ -22,7 +22,27 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
|
|
|
22
22
|
/** Same key → the run's scope, so a listing can rebuild an addressable entry. */
|
|
23
23
|
private readonly scopes;
|
|
24
24
|
private key;
|
|
25
|
-
|
|
25
|
+
/** `tenant/project/session/run` → the run's current holding, if any. */
|
|
26
|
+
private readonly claims;
|
|
27
|
+
/**
|
|
28
|
+
* The highest fence ever issued per run, kept separately from the claim.
|
|
29
|
+
*
|
|
30
|
+
* The claim is removed on release; this is not. That separation is the
|
|
31
|
+
* whole point: the first version deleted the claim and then computed the
|
|
32
|
+
* next fence from it, so releasing rewound the counter to 1 and a worker
|
|
33
|
+
* stalled at fence 1 could write beside a new holder also at fence 1 —
|
|
34
|
+
* and the documented `finally { releaseRun() }` did it on every pass.
|
|
35
|
+
*
|
|
36
|
+
* The disk store gets this property from file names that persist. In
|
|
37
|
+
* memory the equivalent is a high-water mark nothing clears, and the two
|
|
38
|
+
* must agree, because this class is what a host reads when writing a
|
|
39
|
+
* backend of its own.
|
|
40
|
+
*/
|
|
41
|
+
private readonly highWater;
|
|
42
|
+
claimRun(scope: CheckpointRunScope, options: ClaimRunOptions): Promise<RunClaim | null>;
|
|
43
|
+
releaseRun(scope: CheckpointRunScope, fence: ClaimFence): Promise<void>;
|
|
44
|
+
writeCheckpoint(scope: CheckpointRunScope, checkpoint: IterationCheckpoint, fence?: ClaimFence): Promise<void>;
|
|
45
|
+
private writeUnchecked;
|
|
26
46
|
readCheckpoint(scope: CheckpointRunScope, checkpointId: CheckpointId): Promise<IterationCheckpoint | null>;
|
|
27
47
|
listCheckpoints(scope: CheckpointRunScope): Promise<IterationCheckpoint[]>;
|
|
28
48
|
deleteCheckpoint(scope: CheckpointRunScope, checkpointId: CheckpointId): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-memory.d.ts","sourceRoot":"","sources":["../../../src/store/run/checkpoint-memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAClF,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,eAAe,EAEf,cAAc,EACd,sBAAsB,EACtB,MAAM,qCAAqC,CAAA;
|
|
1
|
+
{"version":3,"file":"checkpoint-memory.d.ts","sourceRoot":"","sources":["../../../src/store/run/checkpoint-memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAClF,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,eAAe,EACf,UAAU,EACV,eAAe,EAEf,cAAc,EACd,sBAAsB,EACtB,QAAQ,EACR,MAAM,qCAAqC,CAAA;AAS5C;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAC9D,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4D;IACjF,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwC;IAE/D,OAAO,CAAC,GAAG;IAIX,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IAErD;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgC;IAEpD,QAAQ,CAAC,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAuBvF,UAAU,CAAC,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBvE,eAAe,CACpB,KAAK,EAAE,kBAAkB,EACzB,UAAU,EAAE,mBAAmB,EAC/B,KAAK,CAAC,EAAE,UAAU,GAChB,OAAO,CAAC,IAAI,CAAC;YAgCF,cAAc;IA4BtB,cAAc,CACnB,KAAK,EAAE,kBAAkB,EACzB,YAAY,EAAE,YAAY,GACxB,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IAIhC,eAAe,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAM1E,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAItF,eAAe,CACpB,KAAK,EAAE,sBAAsB,EAC7B,OAAO,CAAC,EAAE,sBAAsB,GAC9B,OAAO,CAAC,cAAc,CAAC;CAoB1B"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertContiguousListingScope, paginateDurableRuns, toDurableRunEntry } from './listing.js';
|
|
1
|
+
import { assertContiguousListingScope, fencedOut, paginateDurableRuns, toClaimSummary, toDurableRunEntry, } from './listing.js';
|
|
2
2
|
/**
|
|
3
3
|
* Process-local {@link CheckpointStore}, keyed by the full five-layer scope.
|
|
4
4
|
*
|
|
@@ -23,7 +23,100 @@ export class InMemoryCheckpointStore {
|
|
|
23
23
|
key(scope) {
|
|
24
24
|
return [scope.tenantId, scope.projectId, scope.sessionId, scope.runId].join('/');
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
/** `tenant/project/session/run` → the run's current holding, if any. */
|
|
27
|
+
claims = new Map();
|
|
28
|
+
/**
|
|
29
|
+
* The highest fence ever issued per run, kept separately from the claim.
|
|
30
|
+
*
|
|
31
|
+
* The claim is removed on release; this is not. That separation is the
|
|
32
|
+
* whole point: the first version deleted the claim and then computed the
|
|
33
|
+
* next fence from it, so releasing rewound the counter to 1 and a worker
|
|
34
|
+
* stalled at fence 1 could write beside a new holder also at fence 1 —
|
|
35
|
+
* and the documented `finally { releaseRun() }` did it on every pass.
|
|
36
|
+
*
|
|
37
|
+
* The disk store gets this property from file names that persist. In
|
|
38
|
+
* memory the equivalent is a high-water mark nothing clears, and the two
|
|
39
|
+
* must agree, because this class is what a host reads when writing a
|
|
40
|
+
* backend of its own.
|
|
41
|
+
*/
|
|
42
|
+
highWater = new Map();
|
|
43
|
+
async claimRun(scope, options) {
|
|
44
|
+
const key = this.key(scope);
|
|
45
|
+
const now = options.now ?? Date.now();
|
|
46
|
+
const held = this.claims.get(key);
|
|
47
|
+
// Held by somebody else and still live. Not an error: two readers on
|
|
48
|
+
// one queue is the ordinary case.
|
|
49
|
+
if (held && now < held.expiresAt && held.holder !== options.holder)
|
|
50
|
+
return null;
|
|
51
|
+
// A reclaim of an expired holding and a renewal by the current holder
|
|
52
|
+
// are the same write. The fence advances either way, so a previous
|
|
53
|
+
// holder that wakes up is fenced out in both cases — a renewal that
|
|
54
|
+
// kept the fence would leave a stalled twin able to write.
|
|
55
|
+
// Counted from the high-water mark, never from the live claim. A
|
|
56
|
+
// released run has no claim, and computing from that absence is what
|
|
57
|
+
// rewound the counter to 1 on every release.
|
|
58
|
+
const fence = (this.highWater.get(key) ?? 0) + 1;
|
|
59
|
+
const claim = { holder: options.holder, fence, expiresAt: now + options.ttlMs };
|
|
60
|
+
this.highWater.set(key, fence);
|
|
61
|
+
this.claims.set(key, claim);
|
|
62
|
+
return claim;
|
|
63
|
+
}
|
|
64
|
+
async releaseRun(scope, fence) {
|
|
65
|
+
const key = this.key(scope);
|
|
66
|
+
const held = this.claims.get(key);
|
|
67
|
+
// A stale fence releases nothing. A worker that stalled past its lease
|
|
68
|
+
// must not be able to hand away a run somebody else now holds.
|
|
69
|
+
//
|
|
70
|
+
// The high-water mark deliberately survives this. Dropping the claim
|
|
71
|
+
// returns the run to the queue; forgetting the number it reached would
|
|
72
|
+
// re-issue a fence a stalled worker still believes it holds.
|
|
73
|
+
if (!held || held.fence !== fence)
|
|
74
|
+
return;
|
|
75
|
+
this.claims.delete(key);
|
|
76
|
+
// And the counter steps past the released holding, because the disk
|
|
77
|
+
// store's release does: it appends a TOMBSTONE at `fence + 1`, which
|
|
78
|
+
// raises the maximum name and therefore the fence every later write is
|
|
79
|
+
// checked against. Without this step a holder that released could still
|
|
80
|
+
// write with the fence it just gave up — refused on disk, accepted here.
|
|
81
|
+
//
|
|
82
|
+
// The consequence is visible in the numbering: a released run's next
|
|
83
|
+
// claim is `fence + 2` in both stores, because the tombstone consumed
|
|
84
|
+
// one. That is parity, not an off-by-one.
|
|
85
|
+
this.highWater.set(key, fence + 1);
|
|
86
|
+
}
|
|
87
|
+
async writeCheckpoint(scope, checkpoint, fence) {
|
|
88
|
+
const key = this.key(scope);
|
|
89
|
+
// An unfenced write is allowed even on a claimed run: a host adopting
|
|
90
|
+
// claims on one worker must not break the workers that have not
|
|
91
|
+
// adopted them. A fenced write is checked, and that check is what
|
|
92
|
+
// makes the lease real.
|
|
93
|
+
if (fence !== undefined) {
|
|
94
|
+
// Against the HIGH-WATER MARK, not the live claim, and the difference
|
|
95
|
+
// is a silent loss rather than a duplicate.
|
|
96
|
+
//
|
|
97
|
+
// `releaseRun` deletes the claim. Reading `claims` here meant that
|
|
98
|
+
// between a release and the next take there was no holding to
|
|
99
|
+
// compare against, so `held` was `undefined` and every fence was
|
|
100
|
+
// accepted however stale. w1 stalls at fence 1; w2 reclaims at 2,
|
|
101
|
+
// finishes the work, releases cleanly; w1 wakes and writes with
|
|
102
|
+
// fence 1 — accepted — and its checkpoint carries a fresh
|
|
103
|
+
// `createdAt`, so it sorts newest and the next resume restores w1's
|
|
104
|
+
// stale history. w2's completed work is gone, with no error
|
|
105
|
+
// anywhere.
|
|
106
|
+
//
|
|
107
|
+
// The disk store refuses that write, because `currentFence` reads
|
|
108
|
+
// file names and the release tombstone raised the maximum. The
|
|
109
|
+
// minting side here already counted from the high-water mark; only
|
|
110
|
+
// the enforcement side was left reading the claim, so the two
|
|
111
|
+
// shipped stores disagreed at the one point that decides whether a
|
|
112
|
+
// lease is real.
|
|
113
|
+
const current = this.highWater.get(key) ?? 0;
|
|
114
|
+
if (fence < current)
|
|
115
|
+
throw fencedOut(scope, fence, current);
|
|
116
|
+
}
|
|
117
|
+
return this.writeUnchecked(scope, checkpoint);
|
|
118
|
+
}
|
|
119
|
+
async writeUnchecked(scope, checkpoint) {
|
|
27
120
|
const key = this.key(scope);
|
|
28
121
|
let run = this.runs.get(key);
|
|
29
122
|
if (!run) {
|
|
@@ -74,8 +167,10 @@ export class InMemoryCheckpointStore {
|
|
|
74
167
|
if (scope.sessionId !== undefined && runScope.sessionId !== scope.sessionId)
|
|
75
168
|
continue;
|
|
76
169
|
const entry = toDurableRunEntry(runScope, [...checkpoints.values()], now);
|
|
77
|
-
if (entry)
|
|
78
|
-
|
|
170
|
+
if (!entry)
|
|
171
|
+
continue;
|
|
172
|
+
const claim = this.claims.get(key);
|
|
173
|
+
entries.push(claim ? { ...entry, claim: toClaimSummary(claim, now) } : entry);
|
|
79
174
|
}
|
|
80
175
|
return paginateDurableRuns(entries, options);
|
|
81
176
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkpoint-memory.js","sourceRoot":"","sources":["../../../src/store/run/checkpoint-memory.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"checkpoint-memory.js","sourceRoot":"","sources":["../../../src/store/run/checkpoint-memory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run claim on a filesystem, correct across PROCESSES.
|
|
3
|
+
*
|
|
4
|
+
* ## The fence is the filename
|
|
5
|
+
*
|
|
6
|
+
* A run's claims live at `{runDir}/claims/{fence}.json`. Taking the run means
|
|
7
|
+
* exclusively creating the next number; the kernel makes exactly one of any
|
|
8
|
+
* number of simultaneous callers the creator, and every other gets `EEXIST`.
|
|
9
|
+
* The current holding is the highest-numbered file.
|
|
10
|
+
*
|
|
11
|
+
* That single decision is the whole mechanism, and it is what the first
|
|
12
|
+
* version of this file got wrong. That version kept ONE mutable `claim.json`
|
|
13
|
+
* and protected it with a lock, which needed a stale-lock breaker, which was
|
|
14
|
+
* `unlink` followed by an exclusive create — two operations. An adversarial
|
|
15
|
+
* pass reproduced the consequence from separate processes: two workers both
|
|
16
|
+
* judge a stale guard breakable, the second unlinks the FIRST one's fresh
|
|
17
|
+
* guard, both end up inside the section believing they hold it, neither has
|
|
18
|
+
* written yet so neither can be the loser of a re-read, and both write the
|
|
19
|
+
* same fence. Twenty-six of three hundred runs went to two workers at an
|
|
20
|
+
* identical fence — and an identical fence fences nobody out, because the
|
|
21
|
+
* comparison is `<`.
|
|
22
|
+
*
|
|
23
|
+
* Numbering the files instead removes every one of those steps. There is no
|
|
24
|
+
* lock to go stale, so no breaker, so no window. Concurrency is decided by
|
|
25
|
+
* one `O_CREAT | O_EXCL`.
|
|
26
|
+
*
|
|
27
|
+
* ## Four properties this layout gives for free
|
|
28
|
+
*
|
|
29
|
+
* **Monotonic across release and deletion.** Fences are file names that stay,
|
|
30
|
+
* so releasing cannot rewind the counter. Deleting `claim.json` used to send
|
|
31
|
+
* the next caller down a fresh-claim path that minted fence 1 again — so a
|
|
32
|
+
* worker stalled at fence 1 could write alongside the new holder, and the
|
|
33
|
+
* documented `finally { releaseRun() }` did it on every pass. Releasing here
|
|
34
|
+
* appends a tombstone rather than removing anything.
|
|
35
|
+
*
|
|
36
|
+
* **Unreadable content cannot wedge the run.** The fence is in the name, so a
|
|
37
|
+
* damaged or half-written body never hides the ordering. A caller reads the
|
|
38
|
+
* highest number and takes the next one; the previous holder, alive or not,
|
|
39
|
+
* is fenced out by arithmetic. Refusing to take an unparseable claim was safe
|
|
40
|
+
* against double-writing and left the run permanently unclaimable, which is
|
|
41
|
+
* the failure a lease exists to prevent.
|
|
42
|
+
*
|
|
43
|
+
* **The write-time fence check needs no parsing at all.** It compares a
|
|
44
|
+
* number against a directory listing, so a corrupt body cannot make the check
|
|
45
|
+
* skip itself.
|
|
46
|
+
*
|
|
47
|
+
* **No `rename` and no `unlink` on the contended path.** Renaming over a path
|
|
48
|
+
* another process merely holds open for reading fails on non-POSIX
|
|
49
|
+
* filesystems — measured at 84% under two concurrent readers — and a listing
|
|
50
|
+
* sweep reads exactly these files. Creating a new name never collides with a
|
|
51
|
+
* reader.
|
|
52
|
+
*
|
|
53
|
+
* ## The name appears already complete, because `link` publishes it
|
|
54
|
+
*
|
|
55
|
+
* An exclusive create decides the winner, but `wx` is open-THEN-write: for an
|
|
56
|
+
* instant the winning name exists and is empty. A reader landing in it parses
|
|
57
|
+
* nothing, reports the holding expired, and a second worker takes the next
|
|
58
|
+
* fence. Their fences differ so the loser's first checkpoint is refused — and
|
|
59
|
+
* both have already restored the run and executed its tools by then, and tool
|
|
60
|
+
* side effects are fenced by nothing.
|
|
61
|
+
*
|
|
62
|
+
* So the body is written to a temporary name first and the fence name is
|
|
63
|
+
* created by `link`ing to it. `link` fails `EEXIST` when the destination
|
|
64
|
+
* exists, so it arbitrates exactly as `wx` did, and the destination it creates
|
|
65
|
+
* is a second name for a file that was already whole. There is no instant at
|
|
66
|
+
* which the fence name exists and its body does not.
|
|
67
|
+
*
|
|
68
|
+
* Measured from separate OS processes on both platform families:
|
|
69
|
+
*
|
|
70
|
+
* - `link` refused an existing destination 20,000/20,000 times. Six writers
|
|
71
|
+
* over 6,000 fences produced no fence with two winners and none with none.
|
|
72
|
+
* - Paired identical fixtures: `wx` showed an empty destination in 15,985 of
|
|
73
|
+
* 16,000 first observations, `link` in 0 of 16,000. All 156,000 frontier
|
|
74
|
+
* observations were `ENOENT` or a complete parseable body — none empty, none
|
|
75
|
+
* torn.
|
|
76
|
+
* - `rename` is disqualified, and not for the reason first assumed. It never
|
|
77
|
+
* reports `EEXIST`; it silently REPLACES, 20,000/20,000. It cannot arbitrate
|
|
78
|
+
* a race at all — two workers publishing one fence would both succeed and
|
|
79
|
+
* the second would erase the first. (Its `EPERM`-under-readers failure is
|
|
80
|
+
* real too, at 93.7% on the non-POSIX family, but the exclusivity failure
|
|
81
|
+
* disqualifies it first.)
|
|
82
|
+
*
|
|
83
|
+
* Cost: three syscalls rather than one, +1.0 ms per acquisition on the
|
|
84
|
+
* non-POSIX family and +0.04 ms on POSIX, for an operation that runs once per
|
|
85
|
+
* run plus renewals.
|
|
86
|
+
*
|
|
87
|
+
* ## What it still does not do
|
|
88
|
+
*
|
|
89
|
+
* It does not detect liveness. Nothing can from here: a stalled holder, a
|
|
90
|
+
* suspended container and a partitioned network are indistinguishable, and
|
|
91
|
+
* they are indistinguishable from the holder's own side too, which is why it
|
|
92
|
+
* keeps writing. The fence is the answer — the write is checked, not the
|
|
93
|
+
* writer.
|
|
94
|
+
*/
|
|
95
|
+
import type { ClaimFence, ClaimRunOptions, RunClaim } from '../../types/run/checkpoint-store.js';
|
|
96
|
+
/**
|
|
97
|
+
* The highest fence ever issued for this run, or 0 when it has never been
|
|
98
|
+
* claimed.
|
|
99
|
+
*
|
|
100
|
+
* Reads names only. This is deliberately the one question the contended path
|
|
101
|
+
* asks, because a name cannot be half-written: a file either exists or does
|
|
102
|
+
* not, where a body can be observed mid-write.
|
|
103
|
+
*/
|
|
104
|
+
export declare function currentFence(runDir: string): Promise<ClaimFence>;
|
|
105
|
+
/**
|
|
106
|
+
* The run's current holding, or `null` when it has never been claimed.
|
|
107
|
+
*
|
|
108
|
+
* Returns `null` for an unreadable body too, and that is safe HERE in a way
|
|
109
|
+
* it was not in the previous design: the fence is known from the name
|
|
110
|
+
* regardless, so an unreadable body means "somebody took this number and its
|
|
111
|
+
* details are unavailable", and the caller's response is to take the NEXT
|
|
112
|
+
* number rather than to give up. Nothing is inferred from the absence.
|
|
113
|
+
*/
|
|
114
|
+
export declare function readClaim(runDir: string): Promise<RunClaim | null>;
|
|
115
|
+
/** Take or extend the run's claim. `null` when somebody else holds it. */
|
|
116
|
+
export declare function acquireClaim(runDir: string, options: ClaimRunOptions): Promise<RunClaim | null>;
|
|
117
|
+
/**
|
|
118
|
+
* Give up a holding early, so the run returns to the queue without waiting
|
|
119
|
+
* out its lease.
|
|
120
|
+
*
|
|
121
|
+
* Appends a tombstone at the next fence rather than deleting anything. The
|
|
122
|
+
* counter must never rewind: a worker stalled at an old fence has to stay
|
|
123
|
+
* fenced out forever, and removing the record would let a later claimer be
|
|
124
|
+
* issued a number that stalled worker already believes it holds.
|
|
125
|
+
*
|
|
126
|
+
* A stale fence releases nothing — a worker that stalled past its lease must
|
|
127
|
+
* not be able to hand away a run somebody else now holds.
|
|
128
|
+
*/
|
|
129
|
+
export declare function releaseClaim(runDir: string, fence: ClaimFence): Promise<void>;
|
|
130
|
+
//# sourceMappingURL=claim-disk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-disk.d.ts","sourceRoot":"","sources":["../../../src/store/run/claim-disk.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6FG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,qCAAqC,CAAA;AAoRhG;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAGtE;AA8DD;;;;;;;;GAQG;AACH,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA6DxE;AAED,0EAA0E;AAC1E,wBAAsB,YAAY,CACjC,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,eAAe,GACtB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAoC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnF"}
|