@namzu/sdk 20.3.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 +142 -0
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -0
- 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/index.d.ts +21 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +4 -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 +1 -1
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -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/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/types/run/checkpoint-store.d.ts +178 -2
- package/dist/types/run/checkpoint-store.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/public-runtime.ts +5 -0
- package/src/runtime/query/checkpoint.ts +42 -5
- package/src/runtime/query/index.ts +26 -1
- package/src/runtime/query/resume-run.ts +12 -2
- package/src/store/index.ts +4 -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/listing.ts +116 -1
- package/src/types/run/checkpoint-store.ts +189 -2
|
@@ -7,14 +7,24 @@ import type {
|
|
|
7
7
|
CheckpointListingScope,
|
|
8
8
|
CheckpointRunScope,
|
|
9
9
|
CheckpointStore,
|
|
10
|
+
ClaimFence,
|
|
11
|
+
ClaimRunOptions,
|
|
10
12
|
DurableRunEntry,
|
|
11
13
|
DurableRunPage,
|
|
12
14
|
ListDurableRunsOptions,
|
|
15
|
+
RunClaim,
|
|
13
16
|
} from '../../types/run/checkpoint-store.js'
|
|
14
17
|
import type { RunStoreConfig } from '../../types/run/index.js'
|
|
15
18
|
import type { ProjectId } from '../../types/session/ids.js'
|
|
19
|
+
import { acquireClaim, currentFence, readClaim, releaseClaim } from './claim-disk.js'
|
|
16
20
|
import { RunDiskStore, readCheckpointsIn } from './disk.js'
|
|
17
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
assertContiguousListingScope,
|
|
23
|
+
fencedOut,
|
|
24
|
+
paginateDurableRuns,
|
|
25
|
+
toClaimSummary,
|
|
26
|
+
toDurableRunEntry,
|
|
27
|
+
} from './listing.js'
|
|
18
28
|
|
|
19
29
|
/**
|
|
20
30
|
* The attribution a disk store's own layout does not record.
|
|
@@ -84,11 +94,47 @@ export class DiskCheckpointStore implements CheckpointStore {
|
|
|
84
94
|
return promise
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
async writeCheckpoint(
|
|
97
|
+
async writeCheckpoint(
|
|
98
|
+
scope: CheckpointRunScope,
|
|
99
|
+
checkpoint: IterationCheckpoint,
|
|
100
|
+
fence?: ClaimFence,
|
|
101
|
+
): Promise<void> {
|
|
88
102
|
const store = await this.bind(scope)
|
|
103
|
+
if (fence !== undefined) {
|
|
104
|
+
// Read at the moment of the write, not at the start of the run.
|
|
105
|
+
// A holder that stalled past its lease believes it still holds,
|
|
106
|
+
// and this is the only point at which it can be told otherwise.
|
|
107
|
+
// Names only, no parsing. The fence is the file name, so a corrupt
|
|
108
|
+
// body cannot make this check skip itself — which the previous shape
|
|
109
|
+
// did, at the one site whose entire job is refusing.
|
|
110
|
+
const current = await currentFence(this.runDir(scope))
|
|
111
|
+
if (fence < current) throw fencedOut(scope, fence, current)
|
|
112
|
+
}
|
|
89
113
|
await store.writeCheckpoint(checkpoint)
|
|
90
114
|
}
|
|
91
115
|
|
|
116
|
+
async claimRun(scope: CheckpointRunScope, options: ClaimRunOptions): Promise<RunClaim | null> {
|
|
117
|
+
return acquireClaim(this.runDir(scope), options)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async releaseRun(scope: CheckpointRunScope, fence: ClaimFence): Promise<void> {
|
|
121
|
+
await releaseClaim(this.runDir(scope), fence)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The run's directory, resolved the same way `RunDiskStore.initRun` does.
|
|
126
|
+
*
|
|
127
|
+
* Duplicated rather than shared because the claim path must be derivable
|
|
128
|
+
* WITHOUT binding a store — binding creates the directory, and a claim
|
|
129
|
+
* read is a read. Kept beside the layout comment on `listDurableRuns` so
|
|
130
|
+
* the two stay together if the layout ever moves.
|
|
131
|
+
*/
|
|
132
|
+
private runDir(scope: CheckpointRunScope): string {
|
|
133
|
+
return scope.parentRunId
|
|
134
|
+
? join(this.config.baseDir, scope.parentRunId, 'children', scope.runId)
|
|
135
|
+
: join(this.config.baseDir, scope.runId)
|
|
136
|
+
}
|
|
137
|
+
|
|
92
138
|
async readCheckpoint(
|
|
93
139
|
scope: CheckpointRunScope,
|
|
94
140
|
checkpointId: CheckpointId,
|
|
@@ -163,21 +209,40 @@ export class DiskCheckpointStore implements CheckpointStore {
|
|
|
163
209
|
const runDir = join(this.config.baseDir, runId)
|
|
164
210
|
|
|
165
211
|
const own = toDurableRunEntry({ ...attribution, runId }, await readCheckpointsIn(runDir), now)
|
|
166
|
-
if (own) entries.push(own)
|
|
212
|
+
if (own) entries.push(await this.withClaim(own, runDir, now))
|
|
167
213
|
|
|
168
214
|
for (const childId of await this.readRunDirs(join(runDir, 'children'))) {
|
|
215
|
+
const childDir = join(runDir, 'children', childId)
|
|
169
216
|
const child = toDurableRunEntry(
|
|
170
217
|
{ ...attribution, runId: childId, parentRunId: runId },
|
|
171
|
-
await readCheckpointsIn(
|
|
218
|
+
await readCheckpointsIn(childDir),
|
|
172
219
|
now,
|
|
173
220
|
)
|
|
174
|
-
if (child) entries.push(child)
|
|
221
|
+
if (child) entries.push(await this.withClaim(child, childDir, now))
|
|
175
222
|
}
|
|
176
223
|
}
|
|
177
224
|
|
|
178
225
|
return paginateDurableRuns(entries, options)
|
|
179
226
|
}
|
|
180
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Attach the run's claim to its listing row, judged against the page's
|
|
230
|
+
* own clock so one page cannot disagree with itself about availability.
|
|
231
|
+
*/
|
|
232
|
+
private async withClaim(
|
|
233
|
+
entry: DurableRunEntry,
|
|
234
|
+
runDir: string,
|
|
235
|
+
now: number,
|
|
236
|
+
): Promise<DurableRunEntry> {
|
|
237
|
+
const claim = await readClaim(runDir)
|
|
238
|
+
// A holding whose body could not be read still appears on the row,
|
|
239
|
+
// carrying an expiry of 0 so it reads as available. Dropping the field
|
|
240
|
+
// entirely — which is what happened before — put the run under
|
|
241
|
+
// `claimed: false` by looking unclaimed rather than by being
|
|
242
|
+
// reclaimable, so a queue reader was told a wedged run was free work.
|
|
243
|
+
return claim ? { ...entry, claim: toClaimSummary(claim, now) } : entry
|
|
244
|
+
}
|
|
245
|
+
|
|
181
246
|
/** Directory names under `dir`, or none when `dir` does not exist. */
|
|
182
247
|
private async readRunDirs(dir: string): Promise<RunId[]> {
|
|
183
248
|
try {
|
|
@@ -3,11 +3,20 @@ import type {
|
|
|
3
3
|
CheckpointListingScope,
|
|
4
4
|
CheckpointRunScope,
|
|
5
5
|
CheckpointStore,
|
|
6
|
+
ClaimFence,
|
|
7
|
+
ClaimRunOptions,
|
|
6
8
|
DurableRunEntry,
|
|
7
9
|
DurableRunPage,
|
|
8
10
|
ListDurableRunsOptions,
|
|
11
|
+
RunClaim,
|
|
9
12
|
} from '../../types/run/checkpoint-store.js'
|
|
10
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
assertContiguousListingScope,
|
|
15
|
+
fencedOut,
|
|
16
|
+
paginateDurableRuns,
|
|
17
|
+
toClaimSummary,
|
|
18
|
+
toDurableRunEntry,
|
|
19
|
+
} from './listing.js'
|
|
11
20
|
|
|
12
21
|
/**
|
|
13
22
|
* Process-local {@link CheckpointStore}, keyed by the full five-layer scope.
|
|
@@ -35,7 +44,111 @@ export class InMemoryCheckpointStore implements CheckpointStore {
|
|
|
35
44
|
return [scope.tenantId, scope.projectId, scope.sessionId, scope.runId].join('/')
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
|
|
47
|
+
/** `tenant/project/session/run` → the run's current holding, if any. */
|
|
48
|
+
private readonly claims = new Map<string, RunClaim>()
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The highest fence ever issued per run, kept separately from the claim.
|
|
52
|
+
*
|
|
53
|
+
* The claim is removed on release; this is not. That separation is the
|
|
54
|
+
* whole point: the first version deleted the claim and then computed the
|
|
55
|
+
* next fence from it, so releasing rewound the counter to 1 and a worker
|
|
56
|
+
* stalled at fence 1 could write beside a new holder also at fence 1 —
|
|
57
|
+
* and the documented `finally { releaseRun() }` did it on every pass.
|
|
58
|
+
*
|
|
59
|
+
* The disk store gets this property from file names that persist. In
|
|
60
|
+
* memory the equivalent is a high-water mark nothing clears, and the two
|
|
61
|
+
* must agree, because this class is what a host reads when writing a
|
|
62
|
+
* backend of its own.
|
|
63
|
+
*/
|
|
64
|
+
private readonly highWater = new Map<string, ClaimFence>()
|
|
65
|
+
|
|
66
|
+
async claimRun(scope: CheckpointRunScope, options: ClaimRunOptions): Promise<RunClaim | null> {
|
|
67
|
+
const key = this.key(scope)
|
|
68
|
+
const now = options.now ?? Date.now()
|
|
69
|
+
const held = this.claims.get(key)
|
|
70
|
+
|
|
71
|
+
// Held by somebody else and still live. Not an error: two readers on
|
|
72
|
+
// one queue is the ordinary case.
|
|
73
|
+
if (held && now < held.expiresAt && held.holder !== options.holder) return null
|
|
74
|
+
|
|
75
|
+
// A reclaim of an expired holding and a renewal by the current holder
|
|
76
|
+
// are the same write. The fence advances either way, so a previous
|
|
77
|
+
// holder that wakes up is fenced out in both cases — a renewal that
|
|
78
|
+
// kept the fence would leave a stalled twin able to write.
|
|
79
|
+
// Counted from the high-water mark, never from the live claim. A
|
|
80
|
+
// released run has no claim, and computing from that absence is what
|
|
81
|
+
// rewound the counter to 1 on every release.
|
|
82
|
+
const fence = (this.highWater.get(key) ?? 0) + 1
|
|
83
|
+
const claim: RunClaim = { holder: options.holder, fence, expiresAt: now + options.ttlMs }
|
|
84
|
+
this.highWater.set(key, fence)
|
|
85
|
+
this.claims.set(key, claim)
|
|
86
|
+
return claim
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async releaseRun(scope: CheckpointRunScope, fence: ClaimFence): Promise<void> {
|
|
90
|
+
const key = this.key(scope)
|
|
91
|
+
const held = this.claims.get(key)
|
|
92
|
+
// A stale fence releases nothing. A worker that stalled past its lease
|
|
93
|
+
// must not be able to hand away a run somebody else now holds.
|
|
94
|
+
//
|
|
95
|
+
// The high-water mark deliberately survives this. Dropping the claim
|
|
96
|
+
// returns the run to the queue; forgetting the number it reached would
|
|
97
|
+
// re-issue a fence a stalled worker still believes it holds.
|
|
98
|
+
if (!held || held.fence !== fence) return
|
|
99
|
+
this.claims.delete(key)
|
|
100
|
+
// And the counter steps past the released holding, because the disk
|
|
101
|
+
// store's release does: it appends a TOMBSTONE at `fence + 1`, which
|
|
102
|
+
// raises the maximum name and therefore the fence every later write is
|
|
103
|
+
// checked against. Without this step a holder that released could still
|
|
104
|
+
// write with the fence it just gave up — refused on disk, accepted here.
|
|
105
|
+
//
|
|
106
|
+
// The consequence is visible in the numbering: a released run's next
|
|
107
|
+
// claim is `fence + 2` in both stores, because the tombstone consumed
|
|
108
|
+
// one. That is parity, not an off-by-one.
|
|
109
|
+
this.highWater.set(key, fence + 1)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async writeCheckpoint(
|
|
113
|
+
scope: CheckpointRunScope,
|
|
114
|
+
checkpoint: IterationCheckpoint,
|
|
115
|
+
fence?: ClaimFence,
|
|
116
|
+
): Promise<void> {
|
|
117
|
+
const key = this.key(scope)
|
|
118
|
+
// An unfenced write is allowed even on a claimed run: a host adopting
|
|
119
|
+
// claims on one worker must not break the workers that have not
|
|
120
|
+
// adopted them. A fenced write is checked, and that check is what
|
|
121
|
+
// makes the lease real.
|
|
122
|
+
if (fence !== undefined) {
|
|
123
|
+
// Against the HIGH-WATER MARK, not the live claim, and the difference
|
|
124
|
+
// is a silent loss rather than a duplicate.
|
|
125
|
+
//
|
|
126
|
+
// `releaseRun` deletes the claim. Reading `claims` here meant that
|
|
127
|
+
// between a release and the next take there was no holding to
|
|
128
|
+
// compare against, so `held` was `undefined` and every fence was
|
|
129
|
+
// accepted however stale. w1 stalls at fence 1; w2 reclaims at 2,
|
|
130
|
+
// finishes the work, releases cleanly; w1 wakes and writes with
|
|
131
|
+
// fence 1 — accepted — and its checkpoint carries a fresh
|
|
132
|
+
// `createdAt`, so it sorts newest and the next resume restores w1's
|
|
133
|
+
// stale history. w2's completed work is gone, with no error
|
|
134
|
+
// anywhere.
|
|
135
|
+
//
|
|
136
|
+
// The disk store refuses that write, because `currentFence` reads
|
|
137
|
+
// file names and the release tombstone raised the maximum. The
|
|
138
|
+
// minting side here already counted from the high-water mark; only
|
|
139
|
+
// the enforcement side was left reading the claim, so the two
|
|
140
|
+
// shipped stores disagreed at the one point that decides whether a
|
|
141
|
+
// lease is real.
|
|
142
|
+
const current = this.highWater.get(key) ?? 0
|
|
143
|
+
if (fence < current) throw fencedOut(scope, fence, current)
|
|
144
|
+
}
|
|
145
|
+
return this.writeUnchecked(scope, checkpoint)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private async writeUnchecked(
|
|
149
|
+
scope: CheckpointRunScope,
|
|
150
|
+
checkpoint: IterationCheckpoint,
|
|
151
|
+
): Promise<void> {
|
|
39
152
|
const key = this.key(scope)
|
|
40
153
|
let run = this.runs.get(key)
|
|
41
154
|
if (!run) {
|
|
@@ -93,7 +206,9 @@ export class InMemoryCheckpointStore implements CheckpointStore {
|
|
|
93
206
|
if (scope.sessionId !== undefined && runScope.sessionId !== scope.sessionId) continue
|
|
94
207
|
|
|
95
208
|
const entry = toDurableRunEntry(runScope, [...checkpoints.values()], now)
|
|
96
|
-
if (entry)
|
|
209
|
+
if (!entry) continue
|
|
210
|
+
const claim = this.claims.get(key)
|
|
211
|
+
entries.push(claim ? { ...entry, claim: toClaimSummary(claim, now) } : entry)
|
|
97
212
|
}
|
|
98
213
|
|
|
99
214
|
return paginateDurableRuns(entries, options)
|