@sekiban/dcb-runtime 0.2.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/LICENSE +86 -0
- package/README.md +13 -0
- package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
- package/dist/allocator/AllocatorDurableObject.js +658 -0
- package/dist/allocator/IssuanceLedger.d.ts +188 -0
- package/dist/allocator/IssuanceLedger.js +608 -0
- package/dist/allocator/IssuanceReconciler.d.ts +41 -0
- package/dist/allocator/IssuanceReconciler.js +191 -0
- package/dist/allocator/OrderClock.d.ts +41 -0
- package/dist/allocator/OrderClock.js +92 -0
- package/dist/allocator/SortableUniqueId.d.ts +46 -0
- package/dist/allocator/SortableUniqueId.js +156 -0
- package/dist/allocator/types.d.ts +52 -0
- package/dist/allocator/types.js +1 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
- package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
- package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
- package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
- package/dist/bootstrap/OperatorBootstrap.js +104 -0
- package/dist/bootstrap/manifest.d.ts +9 -0
- package/dist/bootstrap/manifest.js +115 -0
- package/dist/bootstrap/types.d.ts +75 -0
- package/dist/bootstrap/types.js +1 -0
- package/dist/cli/OperatorRepairCli.d.ts +11 -0
- package/dist/cli/OperatorRepairCli.js +108 -0
- package/dist/cloudflare.d.ts +164 -0
- package/dist/cloudflare.js +19487 -0
- package/dist/commit/CommitWorker.d.ts +101 -0
- package/dist/commit/CommitWorker.js +921 -0
- package/dist/commit/types.d.ts +25 -0
- package/dist/commit/types.js +13 -0
- package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
- package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
- package/dist/completeness/types.d.ts +85 -0
- package/dist/completeness/types.js +6 -0
- package/dist/composition.d.ts +123 -0
- package/dist/composition.js +237 -0
- package/dist/cosmos.d.ts +11 -0
- package/dist/cosmos.js +1411 -0
- package/dist/d1-mv.d.ts +8 -0
- package/dist/d1-mv.js +2001 -0
- package/dist/d1.d.ts +16 -0
- package/dist/d1.js +3418 -0
- package/dist/diagnostics/G60DurableHop.d.ts +99 -0
- package/dist/diagnostics/G60DurableHop.js +248 -0
- package/dist/diagnostics/G65Admission.d.ts +23 -0
- package/dist/diagnostics/G65Admission.js +43 -0
- package/dist/diagnostics/G65DirectRing.d.ts +33 -0
- package/dist/diagnostics/G65DirectRing.js +85 -0
- package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
- package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
- package/dist/downstream/DeliveryCore.d.ts +127 -0
- package/dist/downstream/DeliveryCore.js +304 -0
- package/dist/downstream/Doorbell.d.ts +43 -0
- package/dist/downstream/Doorbell.js +137 -0
- package/dist/downstream/DownstreamAdapter.d.ts +52 -0
- package/dist/downstream/DownstreamAdapter.js +299 -0
- package/dist/downstream/ExclusionLookup.d.ts +36 -0
- package/dist/downstream/ExclusionLookup.js +50 -0
- package/dist/downstream/InconsistencyDetector.d.ts +27 -0
- package/dist/downstream/InconsistencyDetector.js +82 -0
- package/dist/downstream/OutboxDrain.d.ts +35 -0
- package/dist/downstream/OutboxDrain.js +126 -0
- package/dist/downstream/types.d.ts +77 -0
- package/dist/downstream/types.js +64 -0
- package/dist/eventIdentity.d.ts +30 -0
- package/dist/eventIdentity.js +49 -0
- package/dist/eventRecord.d.ts +26 -0
- package/dist/eventRecord.js +53 -0
- package/dist/g42-probe.d.ts +9 -0
- package/dist/g42-probe.js +9 -0
- package/dist/http/SerializedQueryWorker.d.ts +58 -0
- package/dist/http/SerializedQueryWorker.js +497 -0
- package/dist/http/commitResponse.d.ts +46 -0
- package/dist/http/commitResponse.js +80 -0
- package/dist/http/testServiceId.d.ts +17 -0
- package/dist/http/testServiceId.js +14 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +19926 -0
- package/dist/journal/JournalDurableObject.d.ts +30 -0
- package/dist/journal/JournalDurableObject.js +481 -0
- package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
- package/dist/journal/JournalFirstTouchProbe.js +375 -0
- package/dist/journal/types.d.ts +116 -0
- package/dist/journal/types.js +52 -0
- package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
- package/dist/mv/MaterializedViewCatchUp.js +353 -0
- package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
- package/dist/mv/MaterializedViewRuntime.js +328 -0
- package/dist/mv/MaterializedViewStore.d.ts +257 -0
- package/dist/mv/MaterializedViewStore.js +875 -0
- package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
- package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
- package/dist/mv/index.d.ts +8 -0
- package/dist/mv/index.js +2357 -0
- package/dist/projection/LiveProjectionWorker.d.ts +86 -0
- package/dist/projection/LiveProjectionWorker.js +240 -0
- package/dist/projection/ProjectionRuntime.d.ts +113 -0
- package/dist/projection/ProjectionRuntime.js +346 -0
- package/dist/projection/ProjectorRegistry.d.ts +51 -0
- package/dist/projection/ProjectorRegistry.js +116 -0
- package/dist/query/ProjectionQueryStore.d.ts +102 -0
- package/dist/query/ProjectionQueryStore.js +185 -0
- package/dist/query/QueryRegistry.d.ts +22 -0
- package/dist/query/QueryRegistry.js +36 -0
- package/dist/read/SerializedReadWorker.d.ts +36 -0
- package/dist/read/SerializedReadWorker.js +225 -0
- package/dist/repair/RepairWorker.d.ts +72 -0
- package/dist/repair/RepairWorker.js +461 -0
- package/dist/safeWindow.d.ts +20 -0
- package/dist/safeWindow.js +36 -0
- package/dist/scope/ControlRouteScope.d.ts +22 -0
- package/dist/scope/ControlRouteScope.js +36 -0
- package/dist/scope/ScopeName.d.ts +34 -0
- package/dist/scope/ScopeName.js +67 -0
- package/dist/service/ServiceIdentityProvider.d.ts +35 -0
- package/dist/service/ServiceIdentityProvider.js +56 -0
- package/dist/store/CosmosEventStore.d.ts +126 -0
- package/dist/store/CosmosEventStore.js +950 -0
- package/dist/store/D1EventStore.d.ts +111 -0
- package/dist/store/D1EventStore.js +1111 -0
- package/dist/store/PostgresEventStore.d.ts +35 -0
- package/dist/store/PostgresEventStore.js +811 -0
- package/dist/store/provider.d.ts +25 -0
- package/dist/store/provider.js +32 -0
- package/dist/store/types.d.ts +205 -0
- package/dist/store/types.js +18 -0
- package/dist/tag/EventDigest.d.ts +40 -0
- package/dist/tag/EventDigest.js +158 -0
- package/dist/tag/TagDurableObject.d.ts +286 -0
- package/dist/tag/TagDurableObject.js +4071 -0
- package/dist/tag/TagSqlMeasurement.d.ts +26 -0
- package/dist/tag/TagSqlMeasurement.js +44 -0
- package/dist/tag/TagSqlSchema.d.ts +29 -0
- package/dist/tag/TagSqlSchema.js +211 -0
- package/dist/tag/types.d.ts +173 -0
- package/dist/tag/types.js +3 -0
- package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
- package/dist/tagstate/TagStateDurableObject.js +499 -0
- package/dist/trace/AttributionRatio.d.ts +12 -0
- package/dist/trace/AttributionRatio.js +68 -0
- package/dist/trace/CloudflareTracing.d.ts +7 -0
- package/dist/trace/CloudflareTracing.js +9 -0
- package/dist/trace/CommitTrace.d.ts +405 -0
- package/dist/trace/CommitTrace.js +1008 -0
- package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
- package/dist/trace/CommitTraceConsoleSink.js +114 -0
- package/dist/trace/CommitTraceVerifier.d.ts +22 -0
- package/dist/trace/CommitTraceVerifier.js +295 -0
- package/dist/trace/ObservationStream.d.ts +118 -0
- package/dist/trace/ObservationStream.js +148 -0
- package/package.json +71 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { IssuanceLedgerError, ISSUANCE_COUNT_KEY, ISSUANCE_MIGRATION_KEY, applyTargetResolution, buildCertificateSnapshot, completeMigrationProofInTransaction, countUnresolvedEntries, pageLegacyInventoryInTransaction, parseCandidateMembership, parseTargetResolutionEvidence, registerIssuanceInTransaction, registrationProbe, syncIssuanceRecoveryAlarm, envelopeKey, } from "./IssuanceLedger";
|
|
2
|
+
import { reconcileIssuanceBatch, verifyTargetResolutionEvidence } from "./IssuanceReconciler";
|
|
3
|
+
import { allocateOrderRange, diagnosticAllocatedAt, systemOrderClock, OrderClockReadError, } from "./OrderClock";
|
|
4
|
+
import { assertSortableUniqueId, SortableUniqueIdError } from "./SortableUniqueId";
|
|
5
|
+
import { correlationIdForAttempt, DurableObjectActivation, enterNativeActorHandleSpan, enterNativeCommitSpan, noOpNativeTracing, } from "../trace/CommitTrace";
|
|
6
|
+
import { scopeIdFor } from "../scope/ScopeName";
|
|
7
|
+
import { beginDurableObjectHandlerObservation } from "../trace/ObservationStream";
|
|
8
|
+
const STATE_KEY = "allocator-state";
|
|
9
|
+
const ATTEMPT_KEY_PREFIX = "attempt:";
|
|
10
|
+
const ROLLBACK_WARNING_WINDOW_MS = 1000n;
|
|
11
|
+
class AllocationTransactionFault extends Error {
|
|
12
|
+
}
|
|
13
|
+
function json(body, status = 200) {
|
|
14
|
+
return new Response(JSON.stringify(body), {
|
|
15
|
+
status,
|
|
16
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function error(status, code, message) {
|
|
20
|
+
return json({ error: message, code }, status);
|
|
21
|
+
}
|
|
22
|
+
function isObject(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function isNonEmptyString(value) {
|
|
26
|
+
return typeof value === "string" && value.length > 0;
|
|
27
|
+
}
|
|
28
|
+
function isNonNegativeInteger(value) {
|
|
29
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
30
|
+
}
|
|
31
|
+
function attemptKey(attemptId) {
|
|
32
|
+
return `${ATTEMPT_KEY_PREFIX}${attemptId}`;
|
|
33
|
+
}
|
|
34
|
+
function currentState(allocatorLineageId) {
|
|
35
|
+
return { schemaVersion: 5, allocatorLineageId, allocatedWatermark: null, bootstrapSeed: null, lastRollbackWarningFingerprint: null };
|
|
36
|
+
}
|
|
37
|
+
/** G32 is a fresh allocator namespace; an old durable state is never upgraded. */
|
|
38
|
+
function assertG32State(state) {
|
|
39
|
+
if (state === undefined)
|
|
40
|
+
return;
|
|
41
|
+
if (state.schemaVersion !== 5 || !isNonEmptyString(state.allocatorLineageId)) {
|
|
42
|
+
throw new SortableUniqueIdError("SUID_INVALID", "G32 allocator requires a fresh 30-digit state namespace");
|
|
43
|
+
}
|
|
44
|
+
if (state.allocatedWatermark !== null)
|
|
45
|
+
assertSortableUniqueId(state.allocatedWatermark);
|
|
46
|
+
if (state.bootstrapSeed !== null)
|
|
47
|
+
assertSortableUniqueId(state.bootstrapSeed.highWatermark);
|
|
48
|
+
}
|
|
49
|
+
function seedFrom(value) {
|
|
50
|
+
if (!isObject(value) || !isNonEmptyString(value.importId) || !isNonNegativeInteger(value.leaseEpoch) || !isNonEmptyString(value.highWatermark))
|
|
51
|
+
return { error: "importId, leaseEpoch, and highWatermark are required" };
|
|
52
|
+
try {
|
|
53
|
+
decodeSuid(value.highWatermark);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { error: "highWatermark must be an allocator SUID" };
|
|
57
|
+
}
|
|
58
|
+
return { value: { importId: value.importId, leaseEpoch: value.leaseEpoch, highWatermark: value.highWatermark } };
|
|
59
|
+
}
|
|
60
|
+
function newAllocatorLineageId() {
|
|
61
|
+
// A fresh token is generated when the allocator has no durable state, so a
|
|
62
|
+
// namespace recreation cannot silently reuse the previous lineage.
|
|
63
|
+
return crypto.randomUUID();
|
|
64
|
+
}
|
|
65
|
+
export function decodeSuid(suid) {
|
|
66
|
+
return assertSortableUniqueId(suid).ticks;
|
|
67
|
+
}
|
|
68
|
+
export function nextSuids(watermark, count, clockTick) {
|
|
69
|
+
return [...allocateOrderRange(watermark, count, clockTick).suids];
|
|
70
|
+
}
|
|
71
|
+
function allocateFrom(value) {
|
|
72
|
+
if (!isObject(value) || !isNonEmptyString(value.attemptId) || !Array.isArray(value.candidates)) {
|
|
73
|
+
return { error: "attemptId and candidates are required" };
|
|
74
|
+
}
|
|
75
|
+
if (value.candidates.length === 0) {
|
|
76
|
+
return { error: "an allocation requires at least one candidate" };
|
|
77
|
+
}
|
|
78
|
+
const candidates = [];
|
|
79
|
+
const candidateMembership = new Map();
|
|
80
|
+
for (const rawCandidate of value.candidates) {
|
|
81
|
+
if (!isObject(rawCandidate) ||
|
|
82
|
+
!isNonNegativeInteger(rawCandidate.candidateIndex) ||
|
|
83
|
+
!isNonEmptyString(rawCandidate.eventId)) {
|
|
84
|
+
return { error: "each candidate needs a non-negative candidateIndex and non-empty eventId" };
|
|
85
|
+
}
|
|
86
|
+
const membership = parseCandidateMembership(rawCandidate);
|
|
87
|
+
if (membership !== undefined) {
|
|
88
|
+
candidateMembership.set(rawCandidate.candidateIndex, membership);
|
|
89
|
+
}
|
|
90
|
+
candidates.push({
|
|
91
|
+
candidateIndex: rawCandidate.candidateIndex,
|
|
92
|
+
eventId: rawCandidate.eventId,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const ordered = [...candidates].sort((left, right) => left.candidateIndex - right.candidateIndex);
|
|
96
|
+
if (new Set(ordered.map((candidate) => candidate.candidateIndex)).size !== ordered.length) {
|
|
97
|
+
return { error: "candidateIndex values must be unique" };
|
|
98
|
+
}
|
|
99
|
+
if (new Set(ordered.map((candidate) => candidate.eventId)).size !== ordered.length) {
|
|
100
|
+
return { error: "eventId values must be unique within an attempt" };
|
|
101
|
+
}
|
|
102
|
+
if (!ordered.every((candidate, index) => candidate.candidateIndex === index)) {
|
|
103
|
+
return { error: "candidateIndex values must form a complete zero-based sequence" };
|
|
104
|
+
}
|
|
105
|
+
const faultInjection = value.faultInjection;
|
|
106
|
+
if (faultInjection !== undefined && faultInjection !== "between-vector-and-watermark") {
|
|
107
|
+
return { error: "unsupported faultInjection" };
|
|
108
|
+
}
|
|
109
|
+
const serviceId = isNonEmptyString(value.serviceId) ? value.serviceId : undefined;
|
|
110
|
+
const bootstrapCommandId = isNonEmptyString(value.bootstrapCommandId) ? value.bootstrapCommandId : undefined;
|
|
111
|
+
const bootstrapEpoch = isNonNegativeInteger(value.bootstrapEpoch) ? value.bootstrapEpoch : undefined;
|
|
112
|
+
return {
|
|
113
|
+
value: {
|
|
114
|
+
attemptId: value.attemptId,
|
|
115
|
+
candidates: ordered,
|
|
116
|
+
candidateMembership,
|
|
117
|
+
faultInjection,
|
|
118
|
+
...(serviceId !== undefined ? { serviceId } : {}),
|
|
119
|
+
...(bootstrapCommandId !== undefined ? { bootstrapCommandId } : {}),
|
|
120
|
+
...(bootstrapEpoch !== undefined ? { bootstrapEpoch } : {}),
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* A single service-wide allocator. Every allocation uses this one Durable
|
|
126
|
+
* Object instance, so the transaction that writes an attempt vector and the
|
|
127
|
+
* allocated watermark is serialized and atomic.
|
|
128
|
+
*/
|
|
129
|
+
export class AllocatorDurableObject {
|
|
130
|
+
ctx;
|
|
131
|
+
env;
|
|
132
|
+
orderClock;
|
|
133
|
+
nativeTracing;
|
|
134
|
+
/** Observation-only; it is never serialized into Durable Object storage. */
|
|
135
|
+
activation = new DurableObjectActivation();
|
|
136
|
+
constructor(ctx, env, orderClock = systemOrderClock, nativeTracing = noOpNativeTracing) {
|
|
137
|
+
this.ctx = ctx;
|
|
138
|
+
this.env = env;
|
|
139
|
+
this.orderClock = orderClock;
|
|
140
|
+
this.nativeTracing = nativeTracing;
|
|
141
|
+
}
|
|
142
|
+
async alarm() {
|
|
143
|
+
const serviceId = await this.serviceIdFromState();
|
|
144
|
+
try {
|
|
145
|
+
if (serviceId !== undefined && this.env?.TAG !== undefined) {
|
|
146
|
+
await reconcileIssuanceBatch(this.ctx.storage, { TAG: this.env.TAG }, serviceId, async (evidence) => {
|
|
147
|
+
try {
|
|
148
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
149
|
+
await applyTargetResolution(txn, evidence);
|
|
150
|
+
});
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await this.rearmIssuanceRecoveryIfNeeded();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async rearmIssuanceRecoveryIfNeeded() {
|
|
164
|
+
await syncIssuanceRecoveryAlarm(this.ctx.storage, Date.now());
|
|
165
|
+
}
|
|
166
|
+
async serviceIdFromState() {
|
|
167
|
+
const state = await this.ctx.storage.get(STATE_KEY);
|
|
168
|
+
return state?.serviceId;
|
|
169
|
+
}
|
|
170
|
+
async fetch(request) {
|
|
171
|
+
// Must run before the first await in every handler admission.
|
|
172
|
+
const activation = this.activation.beginHandler();
|
|
173
|
+
const observation = beginDurableObjectHandlerObservation("ALLOCATOR", activation);
|
|
174
|
+
const url = new URL(request.url);
|
|
175
|
+
if (request.method === "POST" && url.pathname === "/allocate") {
|
|
176
|
+
// The native actor wrapper needs the same request identity as the
|
|
177
|
+
// allocation handler. Decode it once: teeing a DO request for tracing
|
|
178
|
+
// and then reading the original leaves a second stream alive past the
|
|
179
|
+
// response boundary under concurrent workerd test isolates.
|
|
180
|
+
let body;
|
|
181
|
+
return enterNativeActorHandleSpan(this.nativeTracing, { actorClass: "ALLOCATOR", actorKey: "allocator", activation, observation }, async () => {
|
|
182
|
+
try {
|
|
183
|
+
body = await request.json();
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const parsed = allocateFrom(body);
|
|
189
|
+
return parsed.value?.serviceId === undefined
|
|
190
|
+
? undefined
|
|
191
|
+
: { attemptId: parsed.value.attemptId, serviceId: parsed.value.serviceId };
|
|
192
|
+
}, () => body === undefined
|
|
193
|
+
? error(400, "invalid_allocation", "Request body must be JSON")
|
|
194
|
+
: this.allocate(body, activation, observation, request.headers.get("x-sdt-g77-suppress-recovery-alarm") === "1"));
|
|
195
|
+
}
|
|
196
|
+
if (request.method === "GET" && url.pathname === "/state") {
|
|
197
|
+
const state = await this.ctx.storage.transaction(async (txn) => {
|
|
198
|
+
const stored = await txn.get(STATE_KEY);
|
|
199
|
+
if (stored !== undefined) {
|
|
200
|
+
assertG32State(stored);
|
|
201
|
+
return { ...stored, bootstrapSeed: stored.bootstrapSeed ?? null, lastRollbackWarningFingerprint: stored.lastRollbackWarningFingerprint ?? null };
|
|
202
|
+
}
|
|
203
|
+
const initialized = currentState(newAllocatorLineageId());
|
|
204
|
+
await txn.put(STATE_KEY, initialized);
|
|
205
|
+
return initialized;
|
|
206
|
+
});
|
|
207
|
+
return json(state);
|
|
208
|
+
}
|
|
209
|
+
if (request.method === "GET" && url.pathname.startsWith("/attempts/")) {
|
|
210
|
+
return enterNativeActorHandleSpan(this.nativeTracing, { actorClass: "ALLOCATOR", actorKey: "allocator", activation, observation }, async () => undefined, async () => {
|
|
211
|
+
let attemptId;
|
|
212
|
+
try {
|
|
213
|
+
attemptId = decodeURIComponent(url.pathname.slice("/attempts/".length));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return error(400, "invalid_attempt_id", "Attempt ID must be URI encoded");
|
|
217
|
+
}
|
|
218
|
+
if (attemptId.length === 0) {
|
|
219
|
+
return error(400, "invalid_attempt_id", "Attempt ID is required");
|
|
220
|
+
}
|
|
221
|
+
observation.markFirstStorageRead();
|
|
222
|
+
const vector = await this.ctx.storage.get(attemptKey(attemptId));
|
|
223
|
+
if (vector === undefined) {
|
|
224
|
+
return error(404, "allocation_not_found", "No durable allocation exists for this attempt");
|
|
225
|
+
}
|
|
226
|
+
if (vector.allocatorLineageId !== undefined && vector.allocatorLineageId.length > 0) {
|
|
227
|
+
return json(vector);
|
|
228
|
+
}
|
|
229
|
+
const state = await this.ctx.storage.get(STATE_KEY);
|
|
230
|
+
return json({ ...vector, allocatorLineageId: state?.allocatorLineageId || newAllocatorLineageId() });
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (request.method === "POST" && url.pathname === "/seed-after")
|
|
234
|
+
return this.seedAfter(request);
|
|
235
|
+
if (request.method === "GET" && url.pathname === "/__internal/g77/capabilities") {
|
|
236
|
+
return json({
|
|
237
|
+
issuanceLedger: true,
|
|
238
|
+
certificateRoute: true,
|
|
239
|
+
resolveRoute: true,
|
|
240
|
+
reconcilerRoute: true,
|
|
241
|
+
migrationCut: true,
|
|
242
|
+
legacyInventoryRoute: true,
|
|
243
|
+
migrationProofRoute: true,
|
|
244
|
+
reconcileNowRoute: true,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (request.method === "GET" && url.pathname.startsWith("/__internal/g77/registration/")) {
|
|
248
|
+
return this.registrationStatus(url.pathname);
|
|
249
|
+
}
|
|
250
|
+
if (request.method === "GET" && url.pathname === "/__internal/g77/certificate") {
|
|
251
|
+
return this.certificateSnapshot(request);
|
|
252
|
+
}
|
|
253
|
+
if (request.method === "POST" && url.pathname === "/__internal/g77/resolve-target") {
|
|
254
|
+
return this.resolveTarget(request);
|
|
255
|
+
}
|
|
256
|
+
if (request.method === "POST" && url.pathname === "/__internal/g77/migration-cut") {
|
|
257
|
+
return this.installMigrationCut(request);
|
|
258
|
+
}
|
|
259
|
+
if (request.method === "POST" && url.pathname === "/__internal/g77/legacy-inventory-page") {
|
|
260
|
+
return this.pageLegacyInventory(request);
|
|
261
|
+
}
|
|
262
|
+
if (request.method === "POST" && url.pathname === "/__internal/g77/migration-proof") {
|
|
263
|
+
return this.completeMigrationProof(request);
|
|
264
|
+
}
|
|
265
|
+
if (request.method === "GET" && url.pathname === "/__internal/g77/recovery-alarm") {
|
|
266
|
+
return json({
|
|
267
|
+
alarmAt: await this.ctx.storage.getAlarm(),
|
|
268
|
+
armedAt: await this.ctx.storage.get("issuance:recovery-alarm-armed-at") ?? null,
|
|
269
|
+
unresolvedCount: await countUnresolvedEntries(this.ctx.storage),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (request.method === "POST" && url.pathname === "/__internal/g77/reconcile-now") {
|
|
273
|
+
return this.reconcileNow();
|
|
274
|
+
}
|
|
275
|
+
return error(404, "allocator_route_not_found", "Allocator route was not found");
|
|
276
|
+
}
|
|
277
|
+
async registrationStatus(pathname) {
|
|
278
|
+
const parts = pathname.split("/");
|
|
279
|
+
const candidateIndex = Number(parts.at(-1));
|
|
280
|
+
const attemptId = decodeURIComponent(parts.at(-2) ?? "");
|
|
281
|
+
if (!isNonEmptyString(attemptId) || !Number.isInteger(candidateIndex)) {
|
|
282
|
+
return error(400, "invalid_registration_probe", "Invalid registration probe path");
|
|
283
|
+
}
|
|
284
|
+
const probe = await this.ctx.storage.transaction(async (txn) => registrationProbe(txn, attemptId, candidateIndex));
|
|
285
|
+
return json(probe);
|
|
286
|
+
}
|
|
287
|
+
async certificateSnapshot(request) {
|
|
288
|
+
const requestedServiceId = new URL(request.url).searchParams.get("serviceId");
|
|
289
|
+
if (!isNonEmptyString(requestedServiceId)) {
|
|
290
|
+
return error(400, "invalid_certificate_request", "serviceId is required");
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
const certificate = await this.ctx.storage.transaction(async (txn) => {
|
|
294
|
+
const state = await txn.get(STATE_KEY);
|
|
295
|
+
if (state === undefined)
|
|
296
|
+
throw new IssuanceLedgerError("allocator state missing");
|
|
297
|
+
const boundServiceId = state.serviceId;
|
|
298
|
+
if (boundServiceId !== undefined && boundServiceId !== requestedServiceId) {
|
|
299
|
+
throw new IssuanceLedgerError("certificate service scope does not match allocator binding");
|
|
300
|
+
}
|
|
301
|
+
const serviceId = boundServiceId ?? requestedServiceId;
|
|
302
|
+
return buildCertificateSnapshot(txn, txn, {
|
|
303
|
+
serviceId,
|
|
304
|
+
allocatorLineageId: state.allocatorLineageId,
|
|
305
|
+
allocatedWatermark: state.allocatedWatermark,
|
|
306
|
+
generatedAt: Date.now(),
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
return json(certificate);
|
|
310
|
+
}
|
|
311
|
+
catch (failure) {
|
|
312
|
+
if (failure instanceof IssuanceLedgerError) {
|
|
313
|
+
return error(409, "certificate_unavailable", failure.message);
|
|
314
|
+
}
|
|
315
|
+
return error(500, "certificate_failure", "Certificate snapshot failed");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async resolveTarget(request) {
|
|
319
|
+
let body;
|
|
320
|
+
try {
|
|
321
|
+
body = await request.json();
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return error(400, "invalid_resolution", "JSON required");
|
|
325
|
+
}
|
|
326
|
+
let evidence;
|
|
327
|
+
try {
|
|
328
|
+
evidence = parseTargetResolutionEvidence(body);
|
|
329
|
+
}
|
|
330
|
+
catch (failure) {
|
|
331
|
+
if (failure instanceof IssuanceLedgerError) {
|
|
332
|
+
return error(409, "resolution_rejected", failure.message);
|
|
333
|
+
}
|
|
334
|
+
return error(400, "invalid_resolution", "Invalid resolution body");
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const serviceId = await this.serviceIdFromState();
|
|
338
|
+
if (serviceId === undefined || this.env?.TAG === undefined) {
|
|
339
|
+
return error(409, "resolution_rejected", "Tag verification prerequisites are unavailable");
|
|
340
|
+
}
|
|
341
|
+
const envelope = await this.ctx.storage.get(envelopeKey(evidence.attemptId, evidence.candidateIndex));
|
|
342
|
+
if (envelope === undefined) {
|
|
343
|
+
return error(409, "resolution_rejected", "unknown issuance envelope");
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await verifyTargetResolutionEvidence({ TAG: this.env.TAG }, serviceId, evidence, envelope);
|
|
347
|
+
}
|
|
348
|
+
catch (verificationFailure) {
|
|
349
|
+
const message = verificationFailure instanceof Error
|
|
350
|
+
? verificationFailure.message
|
|
351
|
+
: "tag observation verification failed";
|
|
352
|
+
return error(409, "resolution_rejected", message);
|
|
353
|
+
}
|
|
354
|
+
const result = await this.ctx.storage.transaction(async (txn) => applyTargetResolution(txn, evidence));
|
|
355
|
+
return json(result);
|
|
356
|
+
}
|
|
357
|
+
catch (failure) {
|
|
358
|
+
if (failure instanceof IssuanceLedgerError) {
|
|
359
|
+
return error(409, "resolution_rejected", failure.message);
|
|
360
|
+
}
|
|
361
|
+
return error(500, "resolution_failure", "Target resolution failed");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async pageLegacyInventory(request) {
|
|
365
|
+
let body;
|
|
366
|
+
try {
|
|
367
|
+
body = await request.json();
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return error(400, "invalid_inventory_page", "JSON required");
|
|
371
|
+
}
|
|
372
|
+
const pageSize = isObject(body) && typeof body.pageSize === "number" ? body.pageSize : 8;
|
|
373
|
+
const faultInjection = isObject(body) && body.faultInjection === "after-cursor-persist"
|
|
374
|
+
? "after-cursor-persist"
|
|
375
|
+
: undefined;
|
|
376
|
+
try {
|
|
377
|
+
const result = await this.ctx.storage.transaction(async (txn) => pageLegacyInventoryInTransaction(txn, { pageSize, faultInjection }, this.ctx.storage));
|
|
378
|
+
return json(result);
|
|
379
|
+
}
|
|
380
|
+
catch (failure) {
|
|
381
|
+
if (failure instanceof IssuanceLedgerError) {
|
|
382
|
+
return error(503, "inventory_page_crash", failure.message);
|
|
383
|
+
}
|
|
384
|
+
return error(500, "inventory_page_failure", "Legacy inventory page failed");
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async completeMigrationProof(request) {
|
|
388
|
+
let body;
|
|
389
|
+
try {
|
|
390
|
+
body = await request.json();
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return error(400, "invalid_migration_proof", "JSON required");
|
|
394
|
+
}
|
|
395
|
+
if (!isObject(body) || !isNonEmptyString(body.migrationProofId)) {
|
|
396
|
+
return error(400, "invalid_migration_proof", "migrationProofId is required");
|
|
397
|
+
}
|
|
398
|
+
const boundEvidence = isObject(body.boundEvidence) ? body.boundEvidence : {};
|
|
399
|
+
try {
|
|
400
|
+
await this.ctx.storage.transaction(async (txn) => completeMigrationProofInTransaction(txn, {
|
|
401
|
+
migrationProofId: body.migrationProofId,
|
|
402
|
+
boundEvidence,
|
|
403
|
+
}));
|
|
404
|
+
return json({ completed: true, migrationProofId: body.migrationProofId });
|
|
405
|
+
}
|
|
406
|
+
catch (failure) {
|
|
407
|
+
if (failure instanceof IssuanceLedgerError) {
|
|
408
|
+
return error(409, "migration_proof_rejected", failure.message);
|
|
409
|
+
}
|
|
410
|
+
return error(500, "migration_proof_failure", "Migration proof completion failed");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async reconcileNow() {
|
|
414
|
+
const serviceId = await this.serviceIdFromState();
|
|
415
|
+
if (serviceId === undefined || this.env?.TAG === undefined) {
|
|
416
|
+
return error(409, "reconcile_unavailable", "Reconciliation prerequisites are unavailable");
|
|
417
|
+
}
|
|
418
|
+
const result = await reconcileIssuanceBatch(this.ctx.storage, { TAG: this.env.TAG }, serviceId, async (evidence) => {
|
|
419
|
+
try {
|
|
420
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
421
|
+
await applyTargetResolution(txn, evidence);
|
|
422
|
+
});
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}, { bypassNeverContactedGrace: true });
|
|
429
|
+
return json(result);
|
|
430
|
+
}
|
|
431
|
+
async installMigrationCut(request) {
|
|
432
|
+
let body;
|
|
433
|
+
try {
|
|
434
|
+
body = await request.json();
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return error(400, "invalid_migration_cut", "JSON required");
|
|
438
|
+
}
|
|
439
|
+
const cutAt = isObject(body) && typeof body.cutAt === "number" ? body.cutAt : Date.now();
|
|
440
|
+
await this.ctx.storage.transaction(async (txn) => {
|
|
441
|
+
const existing = await txn.get(ISSUANCE_MIGRATION_KEY);
|
|
442
|
+
if (existing !== undefined)
|
|
443
|
+
return;
|
|
444
|
+
await txn.put(ISSUANCE_MIGRATION_KEY, {
|
|
445
|
+
cutInstalledAt: cutAt,
|
|
446
|
+
legacyInventoryCursor: null,
|
|
447
|
+
inventoryComplete: false,
|
|
448
|
+
migrationProofId: null,
|
|
449
|
+
});
|
|
450
|
+
const existingCount = await txn.get(ISSUANCE_COUNT_KEY);
|
|
451
|
+
if (existingCount === undefined || existingCount === null) {
|
|
452
|
+
await txn.put(ISSUANCE_COUNT_KEY, 0);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return json({ installed: true, cutAt });
|
|
456
|
+
}
|
|
457
|
+
async allocate(body, activation, observation, suppressRecoveryAlarm = false) {
|
|
458
|
+
const parsed = allocateFrom(body);
|
|
459
|
+
if (parsed.value === undefined) {
|
|
460
|
+
return error(400, "invalid_allocation", parsed.error ?? "Invalid allocation request");
|
|
461
|
+
}
|
|
462
|
+
const input = parsed.value;
|
|
463
|
+
if (input.serviceId !== undefined &&
|
|
464
|
+
input.bootstrapCommandId !== undefined &&
|
|
465
|
+
input.bootstrapEpoch !== undefined &&
|
|
466
|
+
this.env?.BOOTSTRAP !== undefined) {
|
|
467
|
+
const admitted = await enterNativeCommitSpan(this.nativeTracing, "allocator.bootstrap.finalize", {
|
|
468
|
+
schema: "sdt.commit/v1",
|
|
469
|
+
face: "accepted",
|
|
470
|
+
rowId: "S09",
|
|
471
|
+
correlationId: correlationIdForAttempt(input.attemptId),
|
|
472
|
+
attemptId: input.attemptId,
|
|
473
|
+
serviceId: input.serviceId,
|
|
474
|
+
actorClass: "ALLOCATOR",
|
|
475
|
+
actorKey: `allocator:${input.serviceId}`,
|
|
476
|
+
activation,
|
|
477
|
+
operation: "allocator.bootstrap.finalize",
|
|
478
|
+
kind: "nested",
|
|
479
|
+
}, async () => {
|
|
480
|
+
const url = new URL("https://allocator.internal/command/finalize");
|
|
481
|
+
url.searchParams.set("__serviceId", input.serviceId);
|
|
482
|
+
const fetch = () => this.env.BOOTSTRAP.get(scopeIdFor(this.env.BOOTSTRAP, {
|
|
483
|
+
serviceId: input.serviceId,
|
|
484
|
+
doClass: "bootstrap",
|
|
485
|
+
identity: "coordinator",
|
|
486
|
+
})).fetch(new Request(url, {
|
|
487
|
+
method: "POST",
|
|
488
|
+
headers: { "content-type": "application/json" },
|
|
489
|
+
body: JSON.stringify({ commandId: input.bootstrapCommandId, leaseEpoch: input.bootstrapEpoch }),
|
|
490
|
+
}));
|
|
491
|
+
return observation === undefined ? fetch() : observation.subrequest(fetch);
|
|
492
|
+
});
|
|
493
|
+
if (!admitted.ok)
|
|
494
|
+
return error(409, "bootstrap_command_rejected", "bootstrap fencing epoch rejects allocation");
|
|
495
|
+
}
|
|
496
|
+
try {
|
|
497
|
+
observation?.markFirstStorageRead();
|
|
498
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
499
|
+
const existing = await txn.get(attemptKey(input.attemptId));
|
|
500
|
+
const persistedState = await txn.get(STATE_KEY);
|
|
501
|
+
assertG32State(persistedState);
|
|
502
|
+
const lineage = persistedState?.allocatorLineageId || newAllocatorLineageId();
|
|
503
|
+
if (existing !== undefined) {
|
|
504
|
+
// Never upgrade an old attempt vector in place. The new and old
|
|
505
|
+
// lexical domains cannot coexist and a replay must stay byte exact.
|
|
506
|
+
for (const candidate of existing.candidates)
|
|
507
|
+
assertSortableUniqueId(candidate.suid);
|
|
508
|
+
if (persistedState?.allocatedWatermark !== null && persistedState?.allocatedWatermark !== undefined) {
|
|
509
|
+
assertSortableUniqueId(persistedState.allocatedWatermark);
|
|
510
|
+
}
|
|
511
|
+
if (!isNonEmptyString(existing.allocatorLineageId)) {
|
|
512
|
+
throw new SortableUniqueIdError("SUID_INVALID", "G32 allocator refuses a pre-cutover attempt vector");
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
vector: existing,
|
|
516
|
+
created: false,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
const state = persistedState === undefined
|
|
520
|
+
? currentState(lineage)
|
|
521
|
+
: { ...persistedState, allocatorLineageId: lineage, schemaVersion: 5, bootstrapSeed: persistedState.bootstrapSeed ?? null, lastRollbackWarningFingerprint: persistedState.lastRollbackWarningFingerprint ?? null };
|
|
522
|
+
// This is intentionally before the first transaction write. A clock
|
|
523
|
+
// failure therefore cannot leave a vector, watermark, or warning fact.
|
|
524
|
+
let clockTick;
|
|
525
|
+
try {
|
|
526
|
+
clockTick = this.orderClock.tick();
|
|
527
|
+
}
|
|
528
|
+
catch (error) {
|
|
529
|
+
throw error instanceof OrderClockReadError
|
|
530
|
+
? error
|
|
531
|
+
: new OrderClockReadError("Order clock failed before allocation write", { cause: error });
|
|
532
|
+
}
|
|
533
|
+
const range = allocateOrderRange(state.allocatedWatermark, input.candidates.length, clockTick);
|
|
534
|
+
const rollback = range.observedTicks !== null && range.physicalTicks < range.observedTicks;
|
|
535
|
+
// The warning key is deliberately independent of the newly allocated
|
|
536
|
+
// watermark. A rollback observed repeatedly in one clock window is
|
|
537
|
+
// one operational fact, not a new fact for every monotone allocation.
|
|
538
|
+
const warningWindow = (clockTick / ROLLBACK_WARNING_WINDOW_MS).toString();
|
|
539
|
+
const warningFingerprint = `rollback:${lineage}:${warningWindow}`;
|
|
540
|
+
const shouldWarn = rollback && state.lastRollbackWarningFingerprint !== warningFingerprint;
|
|
541
|
+
const candidates = input.candidates.map((candidate, index) => ({
|
|
542
|
+
...candidate,
|
|
543
|
+
suid: range.suids[index],
|
|
544
|
+
}));
|
|
545
|
+
const vector = {
|
|
546
|
+
attemptId: input.attemptId,
|
|
547
|
+
allocatorLineageId: lineage,
|
|
548
|
+
candidates,
|
|
549
|
+
// Diagnostic presentation only; ordering is the ordinal above.
|
|
550
|
+
allocatedAt: diagnosticAllocatedAt(range.base),
|
|
551
|
+
};
|
|
552
|
+
const migration = await txn.get(ISSUANCE_MIGRATION_KEY);
|
|
553
|
+
if (migration !== undefined) {
|
|
554
|
+
if (input.candidateMembership.size === 0) {
|
|
555
|
+
throw new IssuanceLedgerError("post-cut allocation requires canonical target membership");
|
|
556
|
+
}
|
|
557
|
+
if (input.serviceId === undefined) {
|
|
558
|
+
throw new IssuanceLedgerError("post-cut allocation requires service identity for issuance registration");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const updatedState = {
|
|
562
|
+
schemaVersion: 5,
|
|
563
|
+
allocatorLineageId: lineage,
|
|
564
|
+
allocatedWatermark: range.watermark,
|
|
565
|
+
bootstrapSeed: state.bootstrapSeed ?? null,
|
|
566
|
+
lastRollbackWarningFingerprint: shouldWarn ? warningFingerprint : state.lastRollbackWarningFingerprint ?? null,
|
|
567
|
+
...(input.serviceId !== undefined ? { serviceId: input.serviceId } : state.serviceId !== undefined ? { serviceId: state.serviceId } : {}),
|
|
568
|
+
};
|
|
569
|
+
await txn.put(attemptKey(input.attemptId), vector);
|
|
570
|
+
if (input.candidateMembership.size > 0 && input.serviceId !== undefined) {
|
|
571
|
+
await registerIssuanceInTransaction(txn, {
|
|
572
|
+
serviceId: input.serviceId,
|
|
573
|
+
allocatorLineageId: lineage,
|
|
574
|
+
attemptId: input.attemptId,
|
|
575
|
+
candidates,
|
|
576
|
+
candidateMembership: input.candidateMembership,
|
|
577
|
+
migration,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (input.faultInjection === "between-vector-and-watermark") {
|
|
581
|
+
throw new AllocationTransactionFault("Simulated interruption before transaction commit");
|
|
582
|
+
}
|
|
583
|
+
await txn.put(STATE_KEY, updatedState);
|
|
584
|
+
return {
|
|
585
|
+
vector,
|
|
586
|
+
created: true,
|
|
587
|
+
...(shouldWarn ? {
|
|
588
|
+
rollbackWarning: {
|
|
589
|
+
tick: range.physicalTicks.toString(),
|
|
590
|
+
watermark: state.allocatedWatermark,
|
|
591
|
+
serviceId: input.serviceId ?? "unknown-service",
|
|
592
|
+
allocatorLineageId: lineage,
|
|
593
|
+
},
|
|
594
|
+
} : {}),
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
if (result.rollbackWarning !== undefined) {
|
|
598
|
+
try {
|
|
599
|
+
console.warn(JSON.stringify({ type: "allocator_clock_rollback", ...result.rollbackWarning }));
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
// Warning transport is observational; it must never change the
|
|
603
|
+
// already committed allocation result.
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (result.created && input.candidateMembership.size > 0 && !suppressRecoveryAlarm) {
|
|
607
|
+
await syncIssuanceRecoveryAlarm(this.ctx.storage, Date.now());
|
|
608
|
+
}
|
|
609
|
+
return json(result.vector, result.created ? 201 : 200);
|
|
610
|
+
}
|
|
611
|
+
catch (failure) {
|
|
612
|
+
if (failure instanceof AllocationTransactionFault) {
|
|
613
|
+
return error(503, "simulated_allocation_crash", "Simulated interruption before durable allocation transaction commit");
|
|
614
|
+
}
|
|
615
|
+
if (failure instanceof OrderClockReadError) {
|
|
616
|
+
return error(503, "allocator_order_clock_failed", failure.message);
|
|
617
|
+
}
|
|
618
|
+
if (failure instanceof SortableUniqueIdError) {
|
|
619
|
+
return error(409, "allocator_suid_invalid", failure.message);
|
|
620
|
+
}
|
|
621
|
+
if (failure instanceof IssuanceLedgerError) {
|
|
622
|
+
return error(400, "issuance_registration_rejected", failure.message);
|
|
623
|
+
}
|
|
624
|
+
return error(500, "allocator_failure", "Allocator could not persist the full allocation vector");
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/** One transaction: only a never-used allocator can establish bootstrap successor state. */
|
|
628
|
+
async seedAfter(request) {
|
|
629
|
+
let body;
|
|
630
|
+
try {
|
|
631
|
+
body = await request.json();
|
|
632
|
+
}
|
|
633
|
+
catch {
|
|
634
|
+
return error(400, "invalid_bootstrap_seed", "Request body must be JSON");
|
|
635
|
+
}
|
|
636
|
+
const parsed = seedFrom(body);
|
|
637
|
+
if (parsed.value === undefined)
|
|
638
|
+
return error(400, "invalid_bootstrap_seed", parsed.error ?? "Invalid bootstrap seed");
|
|
639
|
+
const input = parsed.value;
|
|
640
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
641
|
+
const stored = await txn.get(STATE_KEY);
|
|
642
|
+
assertG32State(stored);
|
|
643
|
+
const state = stored === undefined ? currentState(newAllocatorLineageId()) : { ...stored, bootstrapSeed: stored.bootstrapSeed ?? null, lastRollbackWarningFingerprint: stored.lastRollbackWarningFingerprint ?? null };
|
|
644
|
+
const seed = state.bootstrapSeed;
|
|
645
|
+
if (seed !== null) {
|
|
646
|
+
if (seed.importId === input.importId && seed.leaseEpoch === input.leaseEpoch && seed.highWatermark === input.highWatermark)
|
|
647
|
+
return { status: 200, body: state };
|
|
648
|
+
return { status: 409, body: { code: "allocator_seed_rejected", error: "allocator already seeded" } };
|
|
649
|
+
}
|
|
650
|
+
if (state.allocatedWatermark !== null)
|
|
651
|
+
return { status: 409, body: { code: "allocator_seed_rejected", error: "allocator already allocating" } };
|
|
652
|
+
const updated = { ...state, allocatedWatermark: input.highWatermark, bootstrapSeed: input };
|
|
653
|
+
await txn.put(STATE_KEY, updated);
|
|
654
|
+
return { status: 201, body: updated };
|
|
655
|
+
});
|
|
656
|
+
return json(result.body, result.status);
|
|
657
|
+
}
|
|
658
|
+
}
|