@smthrs/engine-store 1.0.0-rc.6 → 1.0.0-rc.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/README.md +13 -11
- package/dist/cjs/ArtifactSync.d.ts.map +1 -1
- package/dist/cjs/ArtifactSync.js +5 -0
- package/dist/cjs/ArtifactSync.js.map +2 -2
- package/dist/cjs/DisasterRecovery.d.ts +9 -26
- package/dist/cjs/DisasterRecovery.d.ts.map +1 -1
- package/dist/cjs/DisasterRecovery.js +27 -18
- package/dist/cjs/DisasterRecovery.js.map +2 -2
- package/dist/cjs/StepBoundary.d.ts +8 -6
- package/dist/cjs/StepBoundary.d.ts.map +1 -1
- package/dist/cjs/StepBoundary.js +10 -7
- package/dist/cjs/StepBoundary.js.map +2 -2
- package/dist/cjs/index.d.ts +12 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +2 -2
- package/dist/cjs/internal/ActionPersistence.d.ts.map +1 -1
- package/dist/cjs/internal/ActionPersistence.js +27 -2
- package/dist/cjs/internal/ActionPersistence.js.map +3 -3
- package/dist/esm/ArtifactSync.d.ts.map +1 -1
- package/dist/esm/ArtifactSync.js +6 -1
- package/dist/esm/ArtifactSync.js.map +1 -1
- package/dist/esm/DisasterRecovery.d.ts +9 -26
- package/dist/esm/DisasterRecovery.d.ts.map +1 -1
- package/dist/esm/DisasterRecovery.js +46 -27
- package/dist/esm/DisasterRecovery.js.map +1 -1
- package/dist/esm/StepBoundary.d.ts +8 -6
- package/dist/esm/StepBoundary.d.ts.map +1 -1
- package/dist/esm/StepBoundary.js +17 -8
- package/dist/esm/StepBoundary.js.map +1 -1
- package/dist/esm/index.d.ts +12 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +12 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/internal/ActionPersistence.d.ts.map +1 -1
- package/dist/esm/internal/ActionPersistence.js +59 -3
- package/dist/esm/internal/ActionPersistence.js.map +1 -1
- package/docs/README.md +3 -1
- package/docs/api.md +23 -17
- package/docs/concepts/attempts-and-replay.md +15 -2
- package/docs/guides/back-up-and-restore.md +9 -3
- package/docs/guides/reclaim-runs-from-a-dead-host.md +16 -0
- package/docs/installation.md +5 -1
- package/docs/quickstart.md +9 -1
- package/docs/troubleshooting.md +3 -0
- package/package.json +18 -18
- package/src/ArtifactSync.ts +7 -0
- package/src/DisasterRecovery.ts +90 -58
- package/src/StepBoundary.ts +29 -14
- package/src/index.ts +14 -0
- package/src/internal/ActionPersistence.ts +64 -3
|
@@ -1229,7 +1229,21 @@ export const make = (deps: Dependencies) => {
|
|
|
1229
1229
|
// permits and interleave the very span the permit serializes. The step
|
|
1230
1230
|
// key, not the narrowed cache address, is what serializes here, because
|
|
1231
1231
|
// the attempt rows two scopes of one key share are what they race over.
|
|
1232
|
-
|
|
1232
|
+
//
|
|
1233
|
+
// A cache-eligible dispatch also holds the permit for its cache address,
|
|
1234
|
+
// taken second so every dispatch acquires the two in one order (issue
|
|
1235
|
+
// #3898). Runs share that address, so a second run asking for a result
|
|
1236
|
+
// this process is already computing waits for it and is answered by its
|
|
1237
|
+
// cache row instead of running the body beside it. That includes a run
|
|
1238
|
+
// the stale-running sweep reclaimed from a dead owner, whose in-flight
|
|
1239
|
+
// attempt would otherwise re-execute next to the run that superseded
|
|
1240
|
+
// it. Other processes are not excluded: two processes may still execute
|
|
1241
|
+
// one key at once, and the second recording resolves as before.
|
|
1242
|
+
const exclusive = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
1243
|
+
admission.withPermit(`run:${deps.runId}|${stepKeyDigest}`)(
|
|
1244
|
+
cacheDeclaration._tag === "Eligible" ? admission.withPermit(`cache:${keyDigest}`)(effect) : effect
|
|
1245
|
+
)
|
|
1246
|
+
return yield* exclusive(
|
|
1233
1247
|
Effect.gen(function*() {
|
|
1234
1248
|
// Lifecycle announcements take a per-attempt producer identity
|
|
1235
1249
|
// (issue #91): adoption — or a replay after a crash in the
|
|
@@ -1476,6 +1490,26 @@ export const make = (deps: Dependencies) => {
|
|
|
1476
1490
|
// A run's admitted attempt is replay authority. Shared cache rows
|
|
1477
1491
|
// are replaceable and cannot overwrite its result or failure.
|
|
1478
1492
|
const existing = yield* attempts.get(attemptId)
|
|
1493
|
+
/**
|
|
1494
|
+
* AN ORPHANED ATTEMPT ASKS THE CACHE FIRST (issue #3898). A `running`
|
|
1495
|
+
* row read under the permit is crash evidence (see adoption below):
|
|
1496
|
+
* the attempt never settled, so it holds no result for the cache to
|
|
1497
|
+
* overwrite. A reclaimed run reaching it is in the same position as
|
|
1498
|
+
* a fresh dispatch, and must be answered by a verified row another
|
|
1499
|
+
* run recorded under this key rather than by re-executing the body.
|
|
1500
|
+
* A crossing that already succeeded is the one exception: the row
|
|
1501
|
+
* carries this run's own result, which adoption settles from.
|
|
1502
|
+
*
|
|
1503
|
+
* Only cache-eligible declarations get here, which are sealed with a
|
|
1504
|
+
* hard boundary: an irreversible or compensable attempt, or a sealed
|
|
1505
|
+
* one without a boundary, adopts and re-executes exactly as before.
|
|
1506
|
+
* An unkeyed sealed step's key folds in its run, so no other run can
|
|
1507
|
+
* have recorded it, and its adoption re-executes as before too.
|
|
1508
|
+
*/
|
|
1509
|
+
const orphaned = Option.isSome(existing) && existing.value.state === "running" &&
|
|
1510
|
+
decodeMeta(existing.value.meta)?.effectCrossing !== "succeeded"
|
|
1511
|
+
? existing.value
|
|
1512
|
+
: undefined
|
|
1479
1513
|
if (cacheDeclaration._tag === "Eligible") {
|
|
1480
1514
|
const observed = yield* cache.get(keyDigest).pipe(
|
|
1481
1515
|
Effect.catch((error) =>
|
|
@@ -1513,7 +1547,7 @@ export const make = (deps: Dependencies) => {
|
|
|
1513
1547
|
}
|
|
1514
1548
|
))
|
|
1515
1549
|
}
|
|
1516
|
-
if (candidate._tag === "CandidateEvidence" && Option.isNone(existing)) {
|
|
1550
|
+
if (candidate._tag === "CandidateEvidence" && (Option.isNone(existing) || orphaned !== undefined)) {
|
|
1517
1551
|
const boundary = yield* StepBoundary.StepBoundary
|
|
1518
1552
|
// Skyframe's dirty check, not "the declaration changed" (issue
|
|
1519
1553
|
// #90): the read-set digests folded into the step key are caller
|
|
@@ -1560,6 +1594,32 @@ export const make = (deps: Dependencies) => {
|
|
|
1560
1594
|
recordedRunId: cached.value.recordedRunId,
|
|
1561
1595
|
recordedEventSeq: cached.value.recordedEventSeq
|
|
1562
1596
|
}))
|
|
1597
|
+
if (orphaned !== undefined) {
|
|
1598
|
+
// Close the orphaned attempt with the result that answered
|
|
1599
|
+
// it, so the run carries no attempt that started and never
|
|
1600
|
+
// finished, and a later re-drive replays this row. The
|
|
1601
|
+
// meta records no boundary evidence: this attempt measured
|
|
1602
|
+
// none of its own, so the row is never published back.
|
|
1603
|
+
const finishedAtMs = yield* Clock.currentTimeMillis.pipe(Effect.map(Math.floor))
|
|
1604
|
+
const settled = yield* settleAttempt({
|
|
1605
|
+
...attemptId,
|
|
1606
|
+
state: "succeeded",
|
|
1607
|
+
finishedAtMs,
|
|
1608
|
+
outcome: attemptPayload(cached.value.result),
|
|
1609
|
+
meta: attemptPayload(
|
|
1610
|
+
{
|
|
1611
|
+
...decodeMeta(orphaned.meta),
|
|
1612
|
+
...declarationMeta,
|
|
1613
|
+
admittedBy: deps.owner
|
|
1614
|
+
} satisfies AttemptMeta
|
|
1615
|
+
)
|
|
1616
|
+
}, [
|
|
1617
|
+
JournalRecords.attemptFinished(attemptSource("finished"), { ...attemptId, state: "succeeded" })
|
|
1618
|
+
])
|
|
1619
|
+
if (!settled) {
|
|
1620
|
+
return yield* Effect.interrupt
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1563
1623
|
// Counted after the provenance emit: `verified_hit` means
|
|
1564
1624
|
// the cached result was served, and a journal failure on
|
|
1565
1625
|
// the emit fails the dispatch before any result is
|
|
@@ -1953,7 +2013,8 @@ export const make = (deps: Dependencies) => {
|
|
|
1953
2013
|
* is crash evidence, not a live admission (issue #71): the incarnation
|
|
1954
2014
|
* that admitted the attempt died before finishing (SIGKILL, OOM), and
|
|
1955
2015
|
* the #53 stale-running sweep re-drove the run to a new owner — or the
|
|
1956
|
-
* same owner after restart. The attempt never completed, so
|
|
2016
|
+
* same owner after restart. The attempt never completed, so unless a
|
|
2017
|
+
* verified cache row answered it above (issue #3898), it must
|
|
1957
2018
|
* re-execute under its original number rather than fall through to
|
|
1958
2019
|
* `attempts.put` (whose `Conflict` on the differing `startedAtMs`
|
|
1959
2020
|
* surfaced as `AttemptAdmissionRejected`, permanently failing a
|