@effect-agent/storage-cloudflare 0.1.0-beta.113 → 0.1.0-beta.115
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/DoMessageDeliveryStore.mjs +1 -1
- package/dist/{DoStorageVersion-Db6fjP-I.d.mts → DoStorageVersion-DJwZzqel.d.mts} +2 -2
- package/dist/DoStorageVersion.d.mts +1 -1
- package/dist/DoStorageVersion.mjs +2 -2
- package/dist/DoSubmissionLedger.mjs +63 -3
- package/dist/DoSubmissionLedger.mjs.map +1 -1
- package/dist/DoThreadStore.mjs +1 -1
- package/dist/PortProtocol.d.mts +781 -200
- package/dist/PortProtocol.mjs +15 -2
- package/dist/PortProtocol.mjs.map +1 -1
- package/dist/PortRouting.d.mts +2 -2
- package/dist/PortRouting.mjs +23 -1
- package/dist/PortRouting.mjs.map +1 -1
- package/dist/{do-journal-BeHu8_Bh.mjs → do-journal-D65Iuln6.mjs} +97 -35
- package/dist/do-journal-D65Iuln6.mjs.map +1 -0
- package/dist/index.d.mts +1 -1
- package/dist/{migrations-DZKTNU88.mjs → migrations-Br6QqMIf.mjs} +19 -4
- package/dist/migrations-Br6QqMIf.mjs.map +1 -0
- package/package.json +1 -1
- package/src/DoSubmissionLedger.ts +136 -2
- package/src/PortProtocol.ts +27 -0
- package/src/PortRouting.ts +66 -0
- package/src/internal/do-journal.ts +143 -33
- package/src/internal/migrations.ts +18 -1
- package/dist/do-journal-BeHu8_Bh.mjs.map +0 -1
- package/dist/migrations-DZKTNU88.mjs.map +0 -1
|
@@ -16,6 +16,9 @@ import {
|
|
|
16
16
|
} from "effect-agent/records";
|
|
17
17
|
import {
|
|
18
18
|
AbortCommand,
|
|
19
|
+
WorkerStopCommand,
|
|
20
|
+
WorkerLedgerState,
|
|
21
|
+
workerTerminalFromRecord,
|
|
19
22
|
AbortIntent,
|
|
20
23
|
AbortIntentRequest,
|
|
21
24
|
AdmissionAdmitted,
|
|
@@ -1204,6 +1207,14 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1204
1207
|
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
1205
1208
|
}
|
|
1206
1209
|
|
|
1210
|
+
const stopped =
|
|
1211
|
+
yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
|
|
1212
|
+
Effect.mapError(sqlFailure(operation)),
|
|
1213
|
+
);
|
|
1214
|
+
|
|
1215
|
+
if (stopped.length > 0)
|
|
1216
|
+
return yield* AdmissionPolicyError.make({ reason: "refused", code: "worker-stopped" });
|
|
1217
|
+
|
|
1207
1218
|
// The first accepted input fixes ordinary/worker lane identity atomically with admission.
|
|
1208
1219
|
// Canonical origin materialization can lag admission; a log scan cannot fence that race.
|
|
1209
1220
|
const firstRows = yield* sql<Record<string, unknown>>`
|
|
@@ -1947,7 +1958,7 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
1947
1958
|
);
|
|
1948
1959
|
}
|
|
1949
1960
|
|
|
1950
|
-
return { reservation: reservation.value, settlementFailure };
|
|
1961
|
+
return { reservation: reservation.value, reservationRecord, settlementFailure };
|
|
1951
1962
|
});
|
|
1952
1963
|
|
|
1953
1964
|
const replayFinalization = Effect.fn("DoSubmissionLedger.replayFinalization")(function* (
|
|
@@ -2045,13 +2056,49 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2045
2056
|
yield* readReservation(operation, validated.submissionId),
|
|
2046
2057
|
);
|
|
2047
2058
|
|
|
2048
|
-
const { reservation, settlementFailure } = state;
|
|
2059
|
+
const { reservation, reservationRecord, settlementFailure } = state;
|
|
2049
2060
|
const submission = yield* requireSubmission(operation, validated.submissionId);
|
|
2050
2061
|
|
|
2051
2062
|
if (submission.state === "settled")
|
|
2052
2063
|
return yield* replayFinalization(validated, submission, state);
|
|
2053
2064
|
const now = yield* currentInstant;
|
|
2054
2065
|
|
|
2066
|
+
const terminal =
|
|
2067
|
+
submission.worker_admission_json === null
|
|
2068
|
+
? undefined
|
|
2069
|
+
: workerTerminalFromRecord(
|
|
2070
|
+
yield* decodeSubmissionSnapshot(operation, submission),
|
|
2071
|
+
reservationRecord,
|
|
2072
|
+
);
|
|
2073
|
+
|
|
2074
|
+
if (terminal !== undefined) {
|
|
2075
|
+
// Admission and finalization serialize here. An accepted correction that this Run
|
|
2076
|
+
// has not applied vetoes its completion, including admission after RunCompleted.
|
|
2077
|
+
const pending =
|
|
2078
|
+
terminal === "completed"
|
|
2079
|
+
? yield* sql`SELECT submission_id FROM effect_agent_submissions
|
|
2080
|
+
WHERE thread_id = ${submission.thread_id} AND queue_sequence > ${submission.queue_sequence}
|
|
2081
|
+
AND queue_sequence = (SELECT MAX(queue_sequence) FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id})
|
|
2082
|
+
AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
|
|
2083
|
+
OR input_applied_record_id IS NULL) LIMIT 1`.pipe(
|
|
2084
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2085
|
+
)
|
|
2086
|
+
: [];
|
|
2087
|
+
|
|
2088
|
+
if (pending.length === 0) {
|
|
2089
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id, terminal)
|
|
2090
|
+
VALUES (${submission.thread_id}, ${terminal})`.pipe(
|
|
2091
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2092
|
+
);
|
|
2093
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
|
|
2094
|
+
SELECT submission_id, ${submission.principal}, ${`Worker assignment ${terminal}`}, ${now.iso}
|
|
2095
|
+
FROM effect_agent_submissions WHERE thread_id = ${submission.thread_id} AND state <> 'settled'
|
|
2096
|
+
AND submission_id <> ${submission.submission_id}
|
|
2097
|
+
AND (joined_host_submission_id IS NULL OR joined_host_submission_id <> ${submission.submission_id}
|
|
2098
|
+
OR input_applied_record_id IS NULL)`.pipe(Effect.mapError(sqlFailure(operation)));
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2055
2102
|
yield* sql`
|
|
2056
2103
|
UPDATE effect_agent_submissions
|
|
2057
2104
|
SET state = 'settled', settled_outcome = ${reservation.outcome}
|
|
@@ -2083,6 +2130,84 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2083
2130
|
return settlement;
|
|
2084
2131
|
});
|
|
2085
2132
|
|
|
2133
|
+
const inspectWorker = Effect.fn("DoSubmissionLedger.inspectWorker")(function* (
|
|
2134
|
+
threadId: SubmissionSnapshot["threadId"],
|
|
2135
|
+
) {
|
|
2136
|
+
const operation = "inspect worker";
|
|
2137
|
+
|
|
2138
|
+
return yield* sql
|
|
2139
|
+
.withTransaction(
|
|
2140
|
+
Effect.gen(function* () {
|
|
2141
|
+
const read = Effect.fnUntraced(function* (active: boolean) {
|
|
2142
|
+
const rows =
|
|
2143
|
+
yield* sql`SELECT ${sql.literal(SUBMISSION_COLUMNS)} FROM effect_agent_submissions
|
|
2144
|
+
WHERE thread_id = ${threadId} ${active ? sql`AND state <> 'settled'` : sql``}
|
|
2145
|
+
ORDER BY queue_sequence ${active ? sql`ASC` : sql`DESC`} LIMIT 1`.pipe(
|
|
2146
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2147
|
+
);
|
|
2148
|
+
|
|
2149
|
+
const decoded = yield* decodeSubmissionRows(operation, threadId, rows);
|
|
2150
|
+
|
|
2151
|
+
return decoded[0] === undefined
|
|
2152
|
+
? null
|
|
2153
|
+
: yield* decodeSubmissionSnapshot(operation, decoded[0]);
|
|
2154
|
+
});
|
|
2155
|
+
|
|
2156
|
+
const latest = yield* read(false);
|
|
2157
|
+
const active = yield* read(true);
|
|
2158
|
+
|
|
2159
|
+
const stops =
|
|
2160
|
+
yield* sql`SELECT terminal FROM effect_agent_worker_stops WHERE thread_id = ${threadId}`.pipe(
|
|
2161
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2162
|
+
);
|
|
2163
|
+
|
|
2164
|
+
return yield* Schema.decodeUnknownEffect(Schema.toType(WorkerLedgerState))({
|
|
2165
|
+
latest,
|
|
2166
|
+
active,
|
|
2167
|
+
stopped: stops.length > 0,
|
|
2168
|
+
...(stops[0] === undefined || stops[0].terminal === null
|
|
2169
|
+
? {}
|
|
2170
|
+
: { terminal: stops[0].terminal }),
|
|
2171
|
+
}).pipe(Effect.mapError(internalFailure(operation)));
|
|
2172
|
+
}),
|
|
2173
|
+
)
|
|
2174
|
+
.pipe(Effect.catchTag("SqlError", (cause) => sqlFailure(operation)(cause)));
|
|
2175
|
+
});
|
|
2176
|
+
|
|
2177
|
+
const stopWorker = Effect.fn("DoSubmissionLedger.stopWorker")(function* (
|
|
2178
|
+
request: WorkerStopCommand,
|
|
2179
|
+
) {
|
|
2180
|
+
const operation = "ledger stop worker";
|
|
2181
|
+
|
|
2182
|
+
const validated = yield* Schema.decodeEffect(WorkerStopCommand)(request).pipe(
|
|
2183
|
+
Effect.mapError(internalFailure(operation)),
|
|
2184
|
+
);
|
|
2185
|
+
|
|
2186
|
+
return yield* inWriteTransaction(
|
|
2187
|
+
operation,
|
|
2188
|
+
Effect.gen(function* () {
|
|
2189
|
+
const now = yield* currentInstant;
|
|
2190
|
+
|
|
2191
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_worker_stops (thread_id) VALUES (${validated.threadId})`.pipe(
|
|
2192
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2193
|
+
);
|
|
2194
|
+
yield* sql`INSERT OR IGNORE INTO effect_agent_abort_intents (submission_id, author, reason, requested_at)
|
|
2195
|
+
SELECT submission_id, ${validated.author}, 'Worker owner stopped the worker', ${now.iso}
|
|
2196
|
+
FROM effect_agent_submissions WHERE thread_id = ${validated.threadId} AND state <> 'settled'`.pipe(
|
|
2197
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2198
|
+
);
|
|
2199
|
+
|
|
2200
|
+
const rows = yield* sql`SELECT o.submission_id FROM effect_agent_submission_ownership o
|
|
2201
|
+
JOIN effect_agent_submissions s ON s.submission_id = o.submission_id
|
|
2202
|
+
WHERE s.thread_id = ${validated.threadId} AND s.state <> 'settled'`.pipe(
|
|
2203
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2204
|
+
);
|
|
2205
|
+
|
|
2206
|
+
return rows.length;
|
|
2207
|
+
}),
|
|
2208
|
+
);
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2086
2211
|
const requestAbort: SubmissionLedger["Service"]["requestAbort"] = Effect.fn(
|
|
2087
2212
|
"DoSubmissionLedger.requestAbort",
|
|
2088
2213
|
)(function* (request: AbortCommand) {
|
|
@@ -2208,6 +2333,13 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
2208
2333
|
// The host Attempt already owns the lane; no epoch bump happens here (plan §2.5).
|
|
2209
2334
|
yield* requireOwnership(operation, host, validated.ownershipToken);
|
|
2210
2335
|
|
|
2336
|
+
const stopped =
|
|
2337
|
+
yield* sql`SELECT thread_id FROM effect_agent_worker_stops WHERE thread_id = ${validated.threadId}`.pipe(
|
|
2338
|
+
Effect.mapError(sqlFailure(operation)),
|
|
2339
|
+
);
|
|
2340
|
+
|
|
2341
|
+
if (stopped.length > 0) return [];
|
|
2342
|
+
|
|
2211
2343
|
const laterRows = yield* sql<Record<string, unknown>>`
|
|
2212
2344
|
SELECT ${sql.literal(SUBMISSION_COLUMNS)}
|
|
2213
2345
|
FROM effect_agent_submissions
|
|
@@ -3610,6 +3742,8 @@ const makeServices = Effect.fn("DoSubmissionLedger.makeServices")(function* () {
|
|
|
3610
3742
|
reserveSettlement,
|
|
3611
3743
|
finalizeSettlement,
|
|
3612
3744
|
requestAbort,
|
|
3745
|
+
stopWorker,
|
|
3746
|
+
inspectWorker,
|
|
3613
3747
|
claimJoining,
|
|
3614
3748
|
markJoined,
|
|
3615
3749
|
revertJoining,
|
package/src/PortProtocol.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
+
import { ThreadId } from "effect-agent/identifiers";
|
|
2
3
|
import {
|
|
3
4
|
MessageDeliveryError,
|
|
4
5
|
MessageDeliveryPageRequest,
|
|
@@ -7,6 +8,8 @@ import {
|
|
|
7
8
|
import { CanonicalRecordEnvelope } from "effect-agent/records";
|
|
8
9
|
import {
|
|
9
10
|
AbortCommand,
|
|
11
|
+
WorkerStopCommand,
|
|
12
|
+
WorkerLedgerState,
|
|
10
13
|
AbortIntent,
|
|
11
14
|
AdmissionConflict,
|
|
12
15
|
AdmissionPolicyError,
|
|
@@ -123,6 +126,26 @@ export class LedgerResolveAdmissionCall extends Schema.TaggedClass<LedgerResolve
|
|
|
123
126
|
request: SubmissionLookupByKey,
|
|
124
127
|
}) {}
|
|
125
128
|
|
|
129
|
+
export class LedgerInspectWorkerCall extends Schema.TaggedClass<LedgerInspectWorkerCall>()(
|
|
130
|
+
"LedgerInspectWorker",
|
|
131
|
+
{ request: Schema.Struct({ threadId: ThreadId }) },
|
|
132
|
+
) {}
|
|
133
|
+
|
|
134
|
+
export class LedgerInspectWorkerResult extends Schema.TaggedClass<LedgerInspectWorkerResult>()(
|
|
135
|
+
"LedgerInspectWorkerResult",
|
|
136
|
+
{ state: WorkerLedgerState },
|
|
137
|
+
) {}
|
|
138
|
+
|
|
139
|
+
export class LedgerStopWorkerCall extends Schema.TaggedClass<LedgerStopWorkerCall>()(
|
|
140
|
+
"LedgerStopWorker",
|
|
141
|
+
{ request: WorkerStopCommand },
|
|
142
|
+
) {}
|
|
143
|
+
|
|
144
|
+
export class LedgerStopWorkerResult extends Schema.TaggedClass<LedgerStopWorkerResult>()(
|
|
145
|
+
"LedgerStopWorkerResult",
|
|
146
|
+
{ owned: Schema.Natural },
|
|
147
|
+
) {}
|
|
148
|
+
|
|
126
149
|
/** Routed `SubmissionLedger.requestAbort` — abort propagation across Objects. */
|
|
127
150
|
export class LedgerRequestAbortCall extends Schema.TaggedClass<LedgerRequestAbortCall>(
|
|
128
151
|
"@effect-agent/storage-cloudflare/LedgerRequestAbortCall",
|
|
@@ -190,6 +213,8 @@ export const PortRequest = Schema.Union([
|
|
|
190
213
|
LedgerLookupCall,
|
|
191
214
|
LedgerResolveAdmissionCall,
|
|
192
215
|
LedgerRequestAbortCall,
|
|
216
|
+
LedgerStopWorkerCall,
|
|
217
|
+
LedgerInspectWorkerCall,
|
|
193
218
|
LedgerRecordChildSettledCall,
|
|
194
219
|
StoreMaterializeCall,
|
|
195
220
|
StoreAppendCall,
|
|
@@ -290,6 +315,8 @@ export const PortResult = Schema.Union([
|
|
|
290
315
|
LedgerLookupResult,
|
|
291
316
|
LedgerResolveAdmissionResult,
|
|
292
317
|
LedgerRequestAbortResult,
|
|
318
|
+
LedgerStopWorkerResult,
|
|
319
|
+
LedgerInspectWorkerResult,
|
|
293
320
|
LedgerRecordChildSettledResult,
|
|
294
321
|
StoreMaterializeResult,
|
|
295
322
|
StoreAppendResult,
|
package/src/PortRouting.ts
CHANGED
|
@@ -40,6 +40,10 @@ import {
|
|
|
40
40
|
LedgerRecordChildSettledCall,
|
|
41
41
|
LedgerRecordChildSettledResult,
|
|
42
42
|
LedgerRequestAbortCall,
|
|
43
|
+
LedgerStopWorkerCall,
|
|
44
|
+
LedgerInspectWorkerCall,
|
|
45
|
+
LedgerInspectWorkerResult,
|
|
46
|
+
LedgerStopWorkerResult,
|
|
43
47
|
LedgerRequestAbortResult,
|
|
44
48
|
LedgerResolveAdmissionCall,
|
|
45
49
|
LedgerResolveAdmissionResult,
|
|
@@ -534,6 +538,39 @@ const makeRoutedLedgerServices = Effect.fn("DoPortRouting.makeRoutedLedgerServic
|
|
|
534
538
|
? local.resolveAdmission(request)
|
|
535
539
|
: resolveForeignAdmission(request.threadId, request),
|
|
536
540
|
|
|
541
|
+
inspectWorker: (threadId) =>
|
|
542
|
+
options.ownsThread(threadId)
|
|
543
|
+
? local.inspectWorker === undefined
|
|
544
|
+
? Effect.fail(
|
|
545
|
+
LedgerError.make({
|
|
546
|
+
operation: "inspectWorker",
|
|
547
|
+
message: "Worker inspection unavailable",
|
|
548
|
+
}),
|
|
549
|
+
)
|
|
550
|
+
: local.inspectWorker(threadId)
|
|
551
|
+
: foreignLedgerCall(
|
|
552
|
+
"inspect worker",
|
|
553
|
+
threadId,
|
|
554
|
+
LedgerInspectWorkerCall.make({ request: { threadId } }),
|
|
555
|
+
LedgerInspectWorkerResult,
|
|
556
|
+
NoAdditionalPortFailure,
|
|
557
|
+
).pipe(Effect.map((reply) => reply.state)),
|
|
558
|
+
|
|
559
|
+
stopWorker: (request) =>
|
|
560
|
+
options.ownsThread(request.threadId)
|
|
561
|
+
? local.stopWorker === undefined
|
|
562
|
+
? Effect.fail(
|
|
563
|
+
LedgerError.make({ operation: "stopWorker", message: "Worker stop unavailable" }),
|
|
564
|
+
)
|
|
565
|
+
: local.stopWorker(request)
|
|
566
|
+
: foreignLedgerCall(
|
|
567
|
+
"ledger stop worker",
|
|
568
|
+
request.threadId,
|
|
569
|
+
LedgerStopWorkerCall.make({ request }),
|
|
570
|
+
LedgerStopWorkerResult,
|
|
571
|
+
NoAdditionalPortFailure,
|
|
572
|
+
).pipe(Effect.map((reply) => reply.owned)),
|
|
573
|
+
|
|
537
574
|
requestAbort: (request) =>
|
|
538
575
|
submissionTarget("ledger request abort", request.submissionId).pipe(
|
|
539
576
|
Effect.flatMap((target) =>
|
|
@@ -1065,6 +1102,35 @@ export const executePortRequest = Effect.fn("DoPortRouting.executePortRequest")(
|
|
|
1065
1102
|
.pipe(Effect.map((resolution) => LedgerResolveAdmissionResult.make({ resolution }))),
|
|
1066
1103
|
);
|
|
1067
1104
|
}
|
|
1105
|
+
case "LedgerInspectWorker": {
|
|
1106
|
+
const ledger = yield* SubmissionLedger;
|
|
1107
|
+
|
|
1108
|
+
return yield* capture(
|
|
1109
|
+
ledger.inspectWorker === undefined
|
|
1110
|
+
? Effect.fail(
|
|
1111
|
+
LedgerError.make({
|
|
1112
|
+
operation: "inspectWorker",
|
|
1113
|
+
message: "Worker inspection unavailable",
|
|
1114
|
+
}),
|
|
1115
|
+
)
|
|
1116
|
+
: ledger
|
|
1117
|
+
.inspectWorker(request.request.threadId)
|
|
1118
|
+
.pipe(Effect.map((state) => LedgerInspectWorkerResult.make({ state }))),
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
case "LedgerStopWorker": {
|
|
1122
|
+
const ledger = yield* SubmissionLedger;
|
|
1123
|
+
|
|
1124
|
+
return yield* capture(
|
|
1125
|
+
ledger.stopWorker === undefined
|
|
1126
|
+
? Effect.fail(
|
|
1127
|
+
LedgerError.make({ operation: "stopWorker", message: "Worker stop unavailable" }),
|
|
1128
|
+
)
|
|
1129
|
+
: ledger
|
|
1130
|
+
.stopWorker(request.request)
|
|
1131
|
+
.pipe(Effect.map((owned) => LedgerStopWorkerResult.make({ owned }))),
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1068
1134
|
case "LedgerRequestAbort": {
|
|
1069
1135
|
const ledger = yield* SubmissionLedger;
|
|
1070
1136
|
|
|
@@ -32,7 +32,12 @@ import {
|
|
|
32
32
|
type DoStorageFailpointLocation,
|
|
33
33
|
} from "../DoStorageError.ts";
|
|
34
34
|
import { createMessageDeliveryTables } from "./message-delivery-schema.ts";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
CurrentDoStorageVersion,
|
|
37
|
+
createNonterminalIndex,
|
|
38
|
+
createWorkerStops,
|
|
39
|
+
doMigrations,
|
|
40
|
+
} from "./migrations.ts";
|
|
36
41
|
import { createRecoveryCheckpointTable } from "./recovery-checkpoint-schema.ts";
|
|
37
42
|
|
|
38
43
|
/**
|
|
@@ -376,7 +381,7 @@ const predecessorColumns = {
|
|
|
376
381
|
} as const;
|
|
377
382
|
|
|
378
383
|
const checkPredecessorLayout = Effect.fn("DoJournal.checkPredecessorLayout")(function* (
|
|
379
|
-
version: 3 | 4 | 5,
|
|
384
|
+
version: 3 | 4 | 5 | 7,
|
|
380
385
|
) {
|
|
381
386
|
const sql = yield* SqlClient.SqlClient;
|
|
382
387
|
|
|
@@ -402,7 +407,15 @@ const checkPredecessorLayout = Effect.fn("DoJournal.checkPredecessorLayout")(fun
|
|
|
402
407
|
|
|
403
408
|
const expectedColumns = {
|
|
404
409
|
...messageColumns,
|
|
405
|
-
...(version ===
|
|
410
|
+
...(version === 7
|
|
411
|
+
? {
|
|
412
|
+
effect_agent_canonical_records: [
|
|
413
|
+
...predecessorColumns.effect_agent_canonical_records,
|
|
414
|
+
"outstanding",
|
|
415
|
+
],
|
|
416
|
+
}
|
|
417
|
+
: {}),
|
|
418
|
+
...(version >= 5
|
|
406
419
|
? {
|
|
407
420
|
effect_agent_recovery_checkpoints: [
|
|
408
421
|
"thread_id",
|
|
@@ -462,6 +475,34 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
462
475
|
failpoint: DoJournalFailpoint = noFailpoint,
|
|
463
476
|
maxStoredValueBytes: number,
|
|
464
477
|
) {
|
|
478
|
+
const verifyWorkerPredecessor = Effect.fnUntraced(function* (workerContract: boolean) {
|
|
479
|
+
const requiredRows = yield* sql<Record<string, unknown>>`
|
|
480
|
+
SELECT name
|
|
481
|
+
FROM sqlite_master
|
|
482
|
+
WHERE (type = 'table'
|
|
483
|
+
AND name IN ${sql.in([...REQUIRED_TABLES, "effect_agent_message_deliveries", "effect_agent_recovery_checkpoints"])}
|
|
484
|
+
) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
|
|
485
|
+
OR (${workerContract ? 1 : 0} = 1 AND name IN ('effect_agent_worker_stops', 'effect_agent_worker_starts', 'effect_agent_worker_pending', 'effect_agent_worker_execution'))
|
|
486
|
+
ORDER BY name
|
|
487
|
+
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
488
|
+
|
|
489
|
+
const required = yield* decodeRows(
|
|
490
|
+
Schema.Array(DoNameRow),
|
|
491
|
+
"sqlite_master",
|
|
492
|
+
"required_tables",
|
|
493
|
+
requiredRows,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
if (required.length !== REQUIRED_TABLES.length + 9 + (workerContract ? 4 : 0)) {
|
|
497
|
+
return yield* DoStorageCompatibilityError.make({
|
|
498
|
+
actualVersion: CurrentDoStorageVersion,
|
|
499
|
+
supportedVersion: CurrentDoStorageVersion,
|
|
500
|
+
message:
|
|
501
|
+
"The Durable Object claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection.",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
465
506
|
const metaTableRows = yield* sql<Record<string, unknown>>`
|
|
466
507
|
SELECT name
|
|
467
508
|
FROM sqlite_master
|
|
@@ -643,7 +684,8 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
643
684
|
}),
|
|
644
685
|
),
|
|
645
686
|
);
|
|
646
|
-
yield*
|
|
687
|
+
yield* createWorkerStops;
|
|
688
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
647
689
|
yield* failpoint("upgrade:after-version");
|
|
648
690
|
}),
|
|
649
691
|
)
|
|
@@ -679,11 +721,11 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
679
721
|
value: string;
|
|
680
722
|
}>`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
681
723
|
|
|
682
|
-
if (current[0]?.value === "
|
|
724
|
+
if (current[0]?.value === "9") return;
|
|
683
725
|
if (current[0]?.value !== "6")
|
|
684
726
|
return yield* DoStorageCompatibilityError.make({
|
|
685
727
|
actualVersion: -1,
|
|
686
|
-
supportedVersion:
|
|
728
|
+
supportedVersion: 9,
|
|
687
729
|
message: "Storage version changed during native index upgrade",
|
|
688
730
|
});
|
|
689
731
|
yield* checkPredecessorLayout(5);
|
|
@@ -694,7 +736,7 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
694
736
|
if (requiredIndex.length !== 1)
|
|
695
737
|
return yield* DoStorageCompatibilityError.make({
|
|
696
738
|
actualVersion: 6,
|
|
697
|
-
supportedVersion:
|
|
739
|
+
supportedVersion: 9,
|
|
698
740
|
message: "Predecessor storage is missing its required nonterminal index",
|
|
699
741
|
});
|
|
700
742
|
yield* failpoint("upgrade:before-mutation");
|
|
@@ -711,7 +753,8 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
711
753
|
);
|
|
712
754
|
yield* failpoint("upgrade:after-mutation");
|
|
713
755
|
yield* failpoint("upgrade:before-version");
|
|
714
|
-
yield*
|
|
756
|
+
yield* createWorkerStops;
|
|
757
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
715
758
|
yield* failpoint("upgrade:after-version");
|
|
716
759
|
}),
|
|
717
760
|
)
|
|
@@ -724,6 +767,96 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
724
767
|
}),
|
|
725
768
|
),
|
|
726
769
|
);
|
|
770
|
+
} else if (version.value === "7") {
|
|
771
|
+
yield* sql
|
|
772
|
+
.withTransaction(
|
|
773
|
+
Effect.gen(function* () {
|
|
774
|
+
const current = yield* sql<{
|
|
775
|
+
value: string;
|
|
776
|
+
}>`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
777
|
+
|
|
778
|
+
if (current[0]?.value === "9") return;
|
|
779
|
+
if (current[0]?.value !== "7")
|
|
780
|
+
return yield* DoStorageCompatibilityError.make({
|
|
781
|
+
actualVersion: -1,
|
|
782
|
+
supportedVersion: 9,
|
|
783
|
+
message: "Storage version changed during worker stop upgrade",
|
|
784
|
+
});
|
|
785
|
+
yield* checkPredecessorLayout(7);
|
|
786
|
+
yield* verifyWorkerPredecessor(false);
|
|
787
|
+
yield* failpoint("upgrade:before-mutation");
|
|
788
|
+
yield* createWorkerStops;
|
|
789
|
+
yield* failpoint("upgrade:after-mutation");
|
|
790
|
+
yield* failpoint("upgrade:before-version");
|
|
791
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
792
|
+
yield* failpoint("upgrade:after-version");
|
|
793
|
+
}),
|
|
794
|
+
)
|
|
795
|
+
.pipe(
|
|
796
|
+
Effect.mapError((cause) =>
|
|
797
|
+
DoStorageError.make({
|
|
798
|
+
operation: "upgrade worker stop",
|
|
799
|
+
message: "Worker stop upgrade failed",
|
|
800
|
+
cause,
|
|
801
|
+
}),
|
|
802
|
+
),
|
|
803
|
+
);
|
|
804
|
+
} else if (version.value === "8") {
|
|
805
|
+
yield* sql
|
|
806
|
+
.withTransaction(
|
|
807
|
+
Effect.gen(function* () {
|
|
808
|
+
const current = yield* sql<{
|
|
809
|
+
value: string;
|
|
810
|
+
}>`SELECT value FROM effect_agent_meta WHERE key = 'storage_version'`;
|
|
811
|
+
|
|
812
|
+
if (current[0]?.value === "9") return;
|
|
813
|
+
if (current[0]?.value !== "8")
|
|
814
|
+
return yield* DoStorageCompatibilityError.make({
|
|
815
|
+
actualVersion: -1,
|
|
816
|
+
supportedVersion: 9,
|
|
817
|
+
message: "Storage version changed during assignment seal upgrade",
|
|
818
|
+
});
|
|
819
|
+
yield* checkPredecessorLayout(7);
|
|
820
|
+
yield* verifyWorkerPredecessor(true);
|
|
821
|
+
const columns = yield* sql`PRAGMA table_info(effect_agent_worker_stops)`;
|
|
822
|
+
|
|
823
|
+
yield* Schema.decodeUnknownEffect(
|
|
824
|
+
Schema.Tuple([
|
|
825
|
+
Schema.Struct({
|
|
826
|
+
cid: Schema.Literal(0),
|
|
827
|
+
name: Schema.Literal("thread_id"),
|
|
828
|
+
type: Schema.Literal("TEXT"),
|
|
829
|
+
notnull: Schema.Literal(1),
|
|
830
|
+
dflt_value: Schema.Null,
|
|
831
|
+
pk: Schema.Literal(1),
|
|
832
|
+
}),
|
|
833
|
+
]),
|
|
834
|
+
)(columns).pipe(
|
|
835
|
+
Effect.mapError(() =>
|
|
836
|
+
DoStorageCompatibilityError.make({
|
|
837
|
+
actualVersion: 8,
|
|
838
|
+
supportedVersion: 9,
|
|
839
|
+
message: "Unsupported worker seal layout; no upgrade was committed",
|
|
840
|
+
}),
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
yield* failpoint("upgrade:before-mutation");
|
|
844
|
+
yield* sql`ALTER TABLE effect_agent_worker_stops ADD COLUMN terminal TEXT`;
|
|
845
|
+
yield* failpoint("upgrade:after-mutation");
|
|
846
|
+
yield* failpoint("upgrade:before-version");
|
|
847
|
+
yield* sql`UPDATE effect_agent_meta SET value='9' WHERE key='storage_version'`;
|
|
848
|
+
yield* failpoint("upgrade:after-version");
|
|
849
|
+
}),
|
|
850
|
+
)
|
|
851
|
+
.pipe(
|
|
852
|
+
Effect.mapError((cause) =>
|
|
853
|
+
DoStorageError.make({
|
|
854
|
+
operation: "upgrade assignment seals",
|
|
855
|
+
message: "Assignment seal upgrade failed",
|
|
856
|
+
cause,
|
|
857
|
+
}),
|
|
858
|
+
),
|
|
859
|
+
);
|
|
727
860
|
} else if (version.value !== String(CurrentDoStorageVersion)) {
|
|
728
861
|
const actualVersion = Number.parseInt(version.value, 10);
|
|
729
862
|
|
|
@@ -733,35 +866,12 @@ const ensureCurrentStorage = Effect.fn("DoJournal.ensureCurrentStorage")(functio
|
|
|
733
866
|
message:
|
|
734
867
|
`The Durable Object uses unsupported storage version ${version.value}; ` +
|
|
735
868
|
`this build supports exactly version ${CurrentDoStorageVersion}. ` +
|
|
736
|
-
"Only supported v2, v3, v4, v5 and
|
|
869
|
+
"Only supported v2, v3, v4, v5, v6, v7 and v8 can be upgraded automatically. Keep the original store and use a compatible library version.",
|
|
737
870
|
});
|
|
738
871
|
}
|
|
739
872
|
}
|
|
740
873
|
|
|
741
|
-
|
|
742
|
-
SELECT name
|
|
743
|
-
FROM sqlite_master
|
|
744
|
-
WHERE (type = 'table'
|
|
745
|
-
AND name IN ${sql.in([...REQUIRED_TABLES, "effect_agent_message_deliveries", "effect_agent_recovery_checkpoints"])}
|
|
746
|
-
) OR (type = 'index' AND name IN ('effect_agent_submissions_nonterminal', 'effect_agent_records_subtree', 'effect_agent_message_deliveries_pending', 'effect_agent_records_outstanding', 'effect_agent_records_call', 'effect_agent_records_run_input', 'effect_agent_records_worker_input'))
|
|
747
|
-
ORDER BY name
|
|
748
|
-
`.pipe(Effect.mapError(storageError("verify storage tables")));
|
|
749
|
-
|
|
750
|
-
const required = yield* decodeRows(
|
|
751
|
-
Schema.Array(DoNameRow),
|
|
752
|
-
"sqlite_master",
|
|
753
|
-
"required_tables",
|
|
754
|
-
requiredRows,
|
|
755
|
-
);
|
|
756
|
-
|
|
757
|
-
if (required.length !== REQUIRED_TABLES.length + 9) {
|
|
758
|
-
return yield* DoStorageCompatibilityError.make({
|
|
759
|
-
actualVersion: CurrentDoStorageVersion,
|
|
760
|
-
supportedVersion: CurrentDoStorageVersion,
|
|
761
|
-
message:
|
|
762
|
-
"The Durable Object claims the current format but is missing required tables or its nonterminal index. Retain the original store for inspection.",
|
|
763
|
-
});
|
|
764
|
-
}
|
|
874
|
+
yield* verifyWorkerPredecessor(true);
|
|
765
875
|
|
|
766
876
|
return makeJournal(sql, failpoint, maxStoredValueBytes);
|
|
767
877
|
});
|
|
@@ -8,7 +8,23 @@ import { createMessageDeliveryTables } from "./message-delivery-schema.ts";
|
|
|
8
8
|
import { createRecoveryCheckpointTable } from "./recovery-checkpoint-schema.ts";
|
|
9
9
|
|
|
10
10
|
/** The current storage version recorded in `effect_agent_meta`. */
|
|
11
|
-
export const CurrentDoStorageVersion =
|
|
11
|
+
export const CurrentDoStorageVersion = 9;
|
|
12
|
+
|
|
13
|
+
/** One permanent destination inbox fence, including workers stopped before admission. */
|
|
14
|
+
export const createWorkerStops = Effect.gen(function* () {
|
|
15
|
+
const sql = yield* SqlClient.SqlClient;
|
|
16
|
+
|
|
17
|
+
yield* sql`CREATE TABLE effect_agent_worker_stops (thread_id TEXT PRIMARY KEY NOT NULL, terminal TEXT)`;
|
|
18
|
+
yield* sql`CREATE INDEX effect_agent_worker_starts ON effect_agent_message_deliveries(owner_thread_id,
|
|
19
|
+
json_extract(record_json, '$.envelope.workerAdmission.origin.worker.delegationId'),
|
|
20
|
+
json_extract(record_json, '$.envelope.workerAdmission.origin.worker.targetAgentId'), message_id)
|
|
21
|
+
WHERE message_id = json_extract(record_json, '$.envelope.workerAdmission.origin.firstMessageId')`;
|
|
22
|
+
yield* sql`CREATE INDEX effect_agent_worker_pending ON effect_agent_message_deliveries(owner_thread_id,
|
|
23
|
+
json_extract(record_json, '$.envelope.workerAdmission.origin.worker.threadId'), message_id)
|
|
24
|
+
WHERE state IN ('pending', 'parked') AND json_extract(record_json, '$.receipt') IS NULL`;
|
|
25
|
+
yield* sql`CREATE INDEX effect_agent_worker_execution ON effect_agent_canonical_records(thread_id,
|
|
26
|
+
json_extract(record_json, '$.payload._tag'), sequence) WHERE json_extract(record_json, '$.payload.runId') IS NOT NULL`;
|
|
27
|
+
});
|
|
12
28
|
|
|
13
29
|
/** Index only outstanding obligations, ordered by the recovery scan's stable cursor. */
|
|
14
30
|
export const createNonterminalIndex = Effect.gen(function* () {
|
|
@@ -272,6 +288,7 @@ export const doMigrations = SqliteMigrator.fromRecord({
|
|
|
272
288
|
yield* createMessageDeliveryTables;
|
|
273
289
|
yield* createMessageDeliveryPendingIndex;
|
|
274
290
|
yield* createRecoveryCheckpointTable;
|
|
291
|
+
yield* createWorkerStops;
|
|
275
292
|
yield* sql`
|
|
276
293
|
CREATE TABLE effect_agent_meta (
|
|
277
294
|
key TEXT PRIMARY KEY NOT NULL,
|