@intx/hub-sessions 0.3.0 → 0.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/dist/agent-repo.d.ts +14 -2
- package/dist/agent-repo.js +17 -4
- package/dist/agent-state-kind.js +14 -63
- package/dist/asset-service.js +14 -10
- package/dist/credential-push.d.ts +48 -4
- package/dist/credential-push.js +138 -6
- package/dist/event-collector-registry.d.ts +2 -1
- package/dist/event-collector-registry.js +38 -9
- package/dist/event-collector.d.ts +11 -1
- package/dist/event-collector.js +36 -3
- package/dist/hub-session-lookups.d.ts +1 -1
- package/dist/hub-session-lookups.js +68 -72
- package/dist/hub-session-orchestrator.d.ts +2 -3
- package/dist/hub-session-orchestrator.js +13 -12
- package/dist/index.d.ts +7 -6
- package/dist/index.js +7 -6
- package/dist/reconciliation-scheduler.d.ts +14 -0
- package/dist/reconciliation-scheduler.js +55 -0
- package/dist/repo-store/index.d.ts +1 -0
- package/dist/repo-store/index.js +1 -0
- package/dist/repo-store/user-principal-gate.d.ts +26 -0
- package/dist/repo-store/user-principal-gate.js +78 -0
- package/dist/session-service.d.ts +66 -121
- package/dist/session-service.js +444 -411
- package/dist/sidecar-allocation/capability-policy.d.ts +27 -0
- package/dist/sidecar-allocation/capability-policy.js +124 -0
- package/dist/sidecar-allocation/contracts.d.ts +29 -6
- package/dist/sidecar-allocation/contracts.js +7 -2
- package/dist/sidecar-allocation/index.d.ts +4 -3
- package/dist/sidecar-allocation/index.js +3 -2
- package/dist/sidecar-allocation/operation.d.ts +10 -0
- package/dist/sidecar-allocation/operation.js +54 -0
- package/dist/sidecar-allocation/plugin-registry.d.ts +16 -3
- package/dist/sidecar-allocation/plugin-registry.js +36 -12
- package/dist/sidecar-allocation/reconciler.d.ts +16 -4
- package/dist/sidecar-allocation/reconciler.js +486 -92
- package/dist/skill-kind.js +8 -62
- package/dist/substrate.d.ts +1 -1
- package/dist/substrate.js +1 -1
- package/dist/workflow-allocation-service.d.ts +21 -15
- package/dist/workflow-allocation-service.js +440 -125
- package/dist/workflow-dispatch-service.d.ts +4 -2
- package/dist/workflow-dispatch-service.js +89 -26
- package/dist/workflow-kind.d.ts +12 -0
- package/dist/workflow-kind.js +17 -60
- package/dist/workflow-probe-gate.d.ts +99 -27
- package/dist/workflow-probe-gate.js +196 -21
- package/dist/workflow-run-kind.d.ts +112 -19
- package/dist/workflow-run-kind.js +626 -210
- package/dist/workflow-run-restore.d.ts +1 -0
- package/dist/workflow-run-restore.js +5 -1
- package/dist/workflow-source-pins.d.ts +8 -0
- package/dist/workflow-source-pins.js +14 -0
- package/dist/ws/index.d.ts +1 -1
- package/dist/ws/index.js +1 -1
- package/dist/ws/pending-tracker.d.ts +93 -0
- package/dist/ws/pending-tracker.js +132 -0
- package/dist/ws/sidecar-events.d.ts +43 -29
- package/dist/ws/sidecar-events.js +0 -2
- package/dist/ws/sidecar-handler.d.ts +122 -85
- package/dist/ws/sidecar-handler.js +925 -878
- package/dist/ws/sidecar-handler.test-helpers.d.ts +38 -0
- package/dist/ws/sidecar-handler.test-helpers.js +95 -0
- package/dist/ws/sidecar-token-authenticator.js +37 -23
- package/package.json +13 -13
- package/dist/sidecar-allocation/placement-policy.d.ts +0 -11
- package/dist/sidecar-allocation/placement-policy.js +0 -21
|
@@ -2,9 +2,20 @@ import { type } from "arktype";
|
|
|
2
2
|
import { sha256 } from "@intx/crypto";
|
|
3
3
|
import { getLogger } from "@intx/log";
|
|
4
4
|
import { hexEncode } from "@intx/types";
|
|
5
|
+
import { SidecarIdentityValidationError } from "../ws/sidecar-handler.js";
|
|
5
6
|
import { SessionLaunchError } from "../session-service.js";
|
|
7
|
+
import { DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY } from "../reconciliation-scheduler.js";
|
|
6
8
|
import { DestroySidecarResult, EnsureSidecarResult, } from "./contracts.js";
|
|
9
|
+
import { DEFAULT_SIDECAR_OPERATION_TIMEOUT_MS, runSidecarOperation, SidecarOperationTimeoutError, } from "./operation.js";
|
|
7
10
|
const logger = getLogger(["hub", "sidecar-allocation"]);
|
|
11
|
+
class ReconciliationLeaseLostError extends Error {
|
|
12
|
+
constructor(allocationId, cause) {
|
|
13
|
+
super(`Reconciliation lease lost for allocation ${allocationId}`, {
|
|
14
|
+
cause,
|
|
15
|
+
});
|
|
16
|
+
this.name = "ReconciliationLeaseLostError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
const DEFAULT_LEASE_DURATION_MS = 60_000;
|
|
9
20
|
const DEFAULT_CONNECT_TIMEOUT_MS = 120_000;
|
|
10
21
|
const MAX_RETRY_BACKOFF_ATTEMPT = 5;
|
|
@@ -28,12 +39,38 @@ function parseDestroyResult(value) {
|
|
|
28
39
|
}
|
|
29
40
|
return result;
|
|
30
41
|
}
|
|
31
|
-
export function createSidecarAllocationReconciler({ allocationStore, plugins, router, hubWebSocketUrl, onReady, enableAutomaticReplacementRecovery = false, leaseDurationMs = DEFAULT_LEASE_DURATION_MS, connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS, retryDelayMs = defaultRetryDelay, now = () => new Date(), createSidecarId = () => `sc_${randomHex(16)}`, createToken = () => `intx_sc_${randomHex(32)}`, createLeaseId = () => `lease_${randomHex(16)}`, }) {
|
|
42
|
+
export function createSidecarAllocationReconciler({ allocationStore, plugins, router, hubWebSocketUrl, onInitializationRecovery, onReady, enableAutomaticReplacementRecovery = false, leaseDurationMs = DEFAULT_LEASE_DURATION_MS, connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MS, operationTimeoutMs = DEFAULT_SIDECAR_OPERATION_TIMEOUT_MS, maxConcurrentClaims = DEFAULT_SIDECAR_ALLOCATION_CONCURRENCY, retryDelayMs = defaultRetryDelay, now = () => new Date(), createSidecarId = () => `sc_${randomHex(16)}`, createToken = () => `intx_sc_${randomHex(32)}`, createLeaseId = () => `lease_${randomHex(16)}`, }) {
|
|
32
43
|
if (leaseDurationMs <= 0)
|
|
33
44
|
throw new Error("leaseDurationMs must be positive");
|
|
34
45
|
if (connectTimeoutMs <= 0) {
|
|
35
46
|
throw new Error("connectTimeoutMs must be positive");
|
|
36
47
|
}
|
|
48
|
+
if (!Number.isSafeInteger(operationTimeoutMs) || operationTimeoutMs <= 0) {
|
|
49
|
+
throw new Error("operationTimeoutMs must be a positive integer");
|
|
50
|
+
}
|
|
51
|
+
if (!Number.isSafeInteger(maxConcurrentClaims) || maxConcurrentClaims <= 0) {
|
|
52
|
+
throw new Error("maxConcurrentClaims must be a positive integer");
|
|
53
|
+
}
|
|
54
|
+
const activeAllocations = new Map();
|
|
55
|
+
function trackAllocation(allocation) {
|
|
56
|
+
const active = activeAllocations.get(allocation.id);
|
|
57
|
+
if (active === undefined)
|
|
58
|
+
return;
|
|
59
|
+
if (active.allocation.generation !== allocation.generation)
|
|
60
|
+
active.pendingConnect = null;
|
|
61
|
+
active.allocation = allocation;
|
|
62
|
+
}
|
|
63
|
+
async function finishReconciliation(allocationId, apply) {
|
|
64
|
+
const active = activeAllocations.get(allocationId);
|
|
65
|
+
if (active === undefined)
|
|
66
|
+
throw new ReconciliationLeaseLostError(allocationId);
|
|
67
|
+
const target = { allocationId, generation: active.allocation.generation };
|
|
68
|
+
await queueReconciliationStep(target, async () => {
|
|
69
|
+
active.controller.signal.throwIfAborted();
|
|
70
|
+
await apply(active.pendingConnect?.generation === target.generation &&
|
|
71
|
+
active.allocation.status === "allocated");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
37
74
|
function provisionerFor(allocation) {
|
|
38
75
|
const provisioner = plugins.getProvisioner(allocation.provisionerId);
|
|
39
76
|
if (provisioner === null ||
|
|
@@ -47,50 +84,122 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
47
84
|
function retryAt(attempt) {
|
|
48
85
|
return new Date(now().getTime() + retryDelayMs(attempt));
|
|
49
86
|
}
|
|
50
|
-
async function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
57
|
-
}, Math.max(1, Math.floor(leaseDurationMs / 3)));
|
|
87
|
+
async function withReconciliationLease(allocation, leaseId, operationName, operation, timeoutMs) {
|
|
88
|
+
trackAllocation(allocation);
|
|
89
|
+
const active = activeAllocations.get(allocation.id);
|
|
90
|
+
if (active === undefined)
|
|
91
|
+
throw new ReconciliationLeaseLostError(allocation.id);
|
|
92
|
+
const controller = active.controller;
|
|
58
93
|
try {
|
|
59
|
-
return await
|
|
94
|
+
return await runSidecarOperation(operationName, timeoutMs, async (signal) => {
|
|
95
|
+
if (!(await runSidecarOperation("Reconciliation lease validation", operationTimeoutMs, async () => {
|
|
96
|
+
try {
|
|
97
|
+
return await trackAllocationQuery(allocation.id, () => allocationStore.isReconciliationLeaseCurrent(allocation.id, allocation.generation, leaseId));
|
|
98
|
+
}
|
|
99
|
+
catch (cause) {
|
|
100
|
+
throw new ReconciliationLeaseLostError(allocation.id, cause);
|
|
101
|
+
}
|
|
102
|
+
}, signal))) {
|
|
103
|
+
throw new ReconciliationLeaseLostError(allocation.id);
|
|
104
|
+
}
|
|
105
|
+
if (performance.now() >= active.leaseDeadline) {
|
|
106
|
+
controller.abort(new ReconciliationLeaseLostError(allocation.id));
|
|
107
|
+
}
|
|
108
|
+
signal.throwIfAborted();
|
|
109
|
+
return operation({ signal, leaseId });
|
|
110
|
+
}, controller.signal);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
controller.signal.throwIfAborted();
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// A timed-out lookup still occupies its database connection. Exclude its
|
|
118
|
+
// allocation until it settles so retries cannot accumulate duplicate reads.
|
|
119
|
+
const pendingAllocationQueries = new Map();
|
|
120
|
+
async function trackAllocationQuery(allocationId, query) {
|
|
121
|
+
const pending = pendingAllocationQueries.get(allocationId) ?? { count: 0 };
|
|
122
|
+
pending.count += 1;
|
|
123
|
+
pendingAllocationQueries.set(allocationId, pending);
|
|
124
|
+
try {
|
|
125
|
+
return await query();
|
|
60
126
|
}
|
|
61
127
|
finally {
|
|
62
|
-
|
|
128
|
+
pending.count -= 1;
|
|
129
|
+
if (pending.count === 0)
|
|
130
|
+
pendingAllocationQueries.delete(allocationId);
|
|
63
131
|
}
|
|
64
132
|
}
|
|
65
|
-
async function
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
async function isSidecarReady(allocation) {
|
|
134
|
+
const active = activeAllocations.get(allocation.id);
|
|
135
|
+
if (active === undefined)
|
|
136
|
+
throw new ReconciliationLeaseLostError(allocation.id);
|
|
137
|
+
return runSidecarOperation("Sidecar readiness", operationTimeoutMs, () => trackAllocationQuery(allocation.id, () => router.isAllocatedSidecarReady({
|
|
138
|
+
allocationId: allocation.id,
|
|
139
|
+
generation: allocation.generation,
|
|
140
|
+
})), active.controller.signal);
|
|
141
|
+
}
|
|
142
|
+
async function replaceAfterFailure(allocation, leaseId, code, message, { onlyIfInitializationIncomplete = false, } = {}) {
|
|
143
|
+
const initializationCheck = onlyIfInitializationIncomplete
|
|
144
|
+
? {
|
|
145
|
+
onlyIfInitializationIncomplete: true,
|
|
146
|
+
...(allocation.initializationLeaseId !== undefined
|
|
147
|
+
? {
|
|
148
|
+
expectedInitializationLeaseId: allocation.initializationLeaseId,
|
|
149
|
+
}
|
|
150
|
+
: {}),
|
|
78
151
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
152
|
+
: {};
|
|
153
|
+
let shouldRetryInitialization = false;
|
|
154
|
+
await queueReconciliationStep({ allocationId: allocation.id, generation: allocation.generation }, async () => {
|
|
155
|
+
const updated = allocation.status === "allocated" &&
|
|
156
|
+
!enableAutomaticReplacementRecovery
|
|
157
|
+
? await allocationStore.beginUnrecoverableRelease({
|
|
158
|
+
...initializationCheck,
|
|
159
|
+
allocationId: allocation.id,
|
|
160
|
+
expectedGeneration: allocation.generation,
|
|
161
|
+
expectedLeaseId: leaseId,
|
|
162
|
+
failureCode: code,
|
|
163
|
+
failureMessage: `Automatic recovery is disabled: ${message}`,
|
|
164
|
+
now: now(),
|
|
165
|
+
})
|
|
166
|
+
: await allocationStore.beginReplacement({
|
|
167
|
+
...initializationCheck,
|
|
168
|
+
allocationId: allocation.id,
|
|
169
|
+
expectedStatus: allocation.status === "allocated"
|
|
170
|
+
? "allocated"
|
|
171
|
+
: "provisioning",
|
|
172
|
+
expectedGeneration: allocation.generation,
|
|
173
|
+
expectedLeaseId: leaseId,
|
|
174
|
+
nextAttemptAt: retryAt(allocation.ensureAttempts + allocation.destroyAttempts),
|
|
175
|
+
failureCode: code,
|
|
176
|
+
failureMessage: message,
|
|
177
|
+
now: now(),
|
|
178
|
+
});
|
|
179
|
+
if (updated !== null) {
|
|
180
|
+
// A late commit still advances the fence before this queued work
|
|
181
|
+
// settles and its allocation becomes eligible for another claim.
|
|
182
|
+
router.fenceAllocation(updated.id, updated.generation);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
shouldRetryInitialization = onlyIfInitializationIncomplete;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
if (shouldRetryInitialization)
|
|
189
|
+
await retryInitialization(allocation, leaseId);
|
|
190
|
+
}
|
|
191
|
+
async function retryInitialization(allocation, leaseId) {
|
|
192
|
+
// A publication or unsent rollback may have committed since the claim.
|
|
193
|
+
// Retry the ordinary callback, including initialization and dispatch requeue.
|
|
194
|
+
// If ownership was lost instead, this lease-guarded update changes nothing.
|
|
195
|
+
await finishReconciliation(allocation.id, () => allocationStore.scheduleRetry({
|
|
82
196
|
allocationId: allocation.id,
|
|
83
|
-
expectedStatus:
|
|
197
|
+
expectedStatus: "allocated",
|
|
84
198
|
expectedGeneration: allocation.generation,
|
|
85
199
|
expectedLeaseId: leaseId,
|
|
86
|
-
nextAttemptAt:
|
|
87
|
-
failureCode: code,
|
|
88
|
-
failureMessage: message,
|
|
200
|
+
nextAttemptAt: now(),
|
|
89
201
|
now: now(),
|
|
90
|
-
});
|
|
91
|
-
if (replaced !== null) {
|
|
92
|
-
router.fenceAllocation(replaced.id, replaced.generation);
|
|
93
|
-
}
|
|
202
|
+
}));
|
|
94
203
|
}
|
|
95
204
|
async function waitUntilReady(allocation, leaseId, connectionAlreadyReady = false) {
|
|
96
205
|
const target = {
|
|
@@ -99,26 +208,49 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
99
208
|
};
|
|
100
209
|
const deadline = allocation.connectDeadline;
|
|
101
210
|
const remaining = deadline === undefined ? 0 : deadline.getTime() - now().getTime();
|
|
211
|
+
// A stalled identity query is not evidence that the worker missed its
|
|
212
|
+
// connection deadline. Let it retry without releasing the generation.
|
|
213
|
+
const connectionReady = connectionAlreadyReady || (await isSidecarReady(allocation));
|
|
102
214
|
try {
|
|
103
|
-
if (!
|
|
104
|
-
|
|
105
|
-
|
|
215
|
+
if (!connectionReady) {
|
|
216
|
+
await withReconciliationLease(allocation, leaseId, "Sidecar connection", () => trackAllocationQuery(allocation.id, () => router.waitForAllocatedSidecar(target, Math.max(0, remaining), (validation) => {
|
|
217
|
+
// The waiter can expire before a notification lookup settles.
|
|
218
|
+
// Retain its exclusion and capacity independently of the wait.
|
|
219
|
+
void trackAllocationQuery(allocation.id, () => validation).catch(() => undefined);
|
|
220
|
+
})), operationTimeoutMs);
|
|
106
221
|
}
|
|
107
222
|
}
|
|
108
223
|
catch (error) {
|
|
224
|
+
// Let the router report connection expiry. Our outer deadline can expire
|
|
225
|
+
// during lease or identity validation without establishing worker loss.
|
|
226
|
+
// A failed identity lookup is likewise inconclusive: the worker may be
|
|
227
|
+
// healthy behind it, so retry instead of releasing the generation.
|
|
228
|
+
if (error instanceof ReconciliationLeaseLostError ||
|
|
229
|
+
error instanceof SidecarOperationTimeoutError ||
|
|
230
|
+
error instanceof SidecarIdentityValidationError)
|
|
231
|
+
throw error;
|
|
109
232
|
await replaceAfterFailure(allocation, leaseId, "sidecar_connect_failed", error instanceof Error ? error.message : String(error));
|
|
110
233
|
return;
|
|
111
234
|
}
|
|
235
|
+
await queueReconciliationStep(target, async () => {
|
|
236
|
+
const active = activeAllocations.get(allocation.id);
|
|
237
|
+
if (active?.allocation.generation !== allocation.generation)
|
|
238
|
+
throw new ReconciliationLeaseLostError(allocation.id);
|
|
239
|
+
active.controller.signal.throwIfAborted();
|
|
240
|
+
active.pendingConnect = null;
|
|
241
|
+
});
|
|
112
242
|
if (onReady !== undefined) {
|
|
113
243
|
try {
|
|
114
|
-
await
|
|
244
|
+
await withReconciliationLease(allocation, leaseId, "Workflow initialization", (context) => trackAllocationQuery(allocation.id, () => onReady(allocation, context)));
|
|
115
245
|
}
|
|
116
246
|
catch (error) {
|
|
247
|
+
if (error instanceof ReconciliationLeaseLostError)
|
|
248
|
+
throw error;
|
|
117
249
|
if (error instanceof SessionLaunchError && error.leakedAgent) {
|
|
118
|
-
await replaceAfterFailure(allocation, leaseId, "sidecar_initialization_uncertain", error.message);
|
|
250
|
+
await replaceAfterFailure(allocation, leaseId, "sidecar_initialization_uncertain", error.message, { onlyIfInitializationIncomplete: true });
|
|
119
251
|
return;
|
|
120
252
|
}
|
|
121
|
-
await allocationStore.scheduleRetry({
|
|
253
|
+
await finishReconciliation(allocation.id, () => allocationStore.scheduleRetry({
|
|
122
254
|
allocationId: allocation.id,
|
|
123
255
|
expectedStatus: "allocated",
|
|
124
256
|
expectedGeneration: allocation.generation,
|
|
@@ -131,16 +263,31 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
131
263
|
message: error instanceof Error ? error.message : String(error),
|
|
132
264
|
},
|
|
133
265
|
now: now(),
|
|
134
|
-
});
|
|
266
|
+
}));
|
|
135
267
|
return;
|
|
136
268
|
}
|
|
137
269
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
270
|
+
// A connect that lands during initialization schedules an immediate
|
|
271
|
+
// follow-up even on success: the new socket may be a restarted worker
|
|
272
|
+
// with an empty inventory (takeover suppresses the disconnect event), in
|
|
273
|
+
// which case the follow-up redeploys and restores it. When the worker is
|
|
274
|
+
// unchanged the follow-up is a no-op: deployReadyAllocation returns early
|
|
275
|
+
// once the workflow is active and its key is recorded.
|
|
276
|
+
await finishReconciliation(allocation.id, (pendingConnect) => pendingConnect
|
|
277
|
+
? allocationStore.scheduleRetry({
|
|
278
|
+
allocationId: allocation.id,
|
|
279
|
+
expectedStatus: "allocated",
|
|
280
|
+
expectedGeneration: allocation.generation,
|
|
281
|
+
expectedLeaseId: leaseId,
|
|
282
|
+
nextAttemptAt: now(),
|
|
283
|
+
now: now(),
|
|
284
|
+
})
|
|
285
|
+
: allocationStore.markConnectionReady({
|
|
286
|
+
allocationId: allocation.id,
|
|
287
|
+
generation: allocation.generation,
|
|
288
|
+
expectedLeaseId: leaseId,
|
|
289
|
+
now: now(),
|
|
290
|
+
}));
|
|
144
291
|
}
|
|
145
292
|
async function acceptEnsure(allocation, leaseId, provisioner, token) {
|
|
146
293
|
if (allocation.sidecarId === undefined) {
|
|
@@ -149,7 +296,8 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
149
296
|
const sidecarId = allocation.sidecarId;
|
|
150
297
|
let result;
|
|
151
298
|
try {
|
|
152
|
-
result = parseEnsureResult(await
|
|
299
|
+
result = parseEnsureResult(await withReconciliationLease(allocation, leaseId, "Sidecar ensure", ({ signal }) => provisioner.ensure({
|
|
300
|
+
signal,
|
|
153
301
|
allocationId: allocation.id,
|
|
154
302
|
generation: allocation.generation,
|
|
155
303
|
tenantId: allocation.tenantId,
|
|
@@ -157,15 +305,17 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
157
305
|
sidecarId,
|
|
158
306
|
token,
|
|
159
307
|
hubWebSocketUrl,
|
|
160
|
-
})));
|
|
308
|
+
}), operationTimeoutMs));
|
|
161
309
|
}
|
|
162
310
|
catch (error) {
|
|
311
|
+
if (error instanceof ReconciliationLeaseLostError)
|
|
312
|
+
throw error;
|
|
163
313
|
await replaceAfterFailure(allocation, leaseId, "ensure_failed", error instanceof Error ? error.message : String(error));
|
|
164
314
|
return;
|
|
165
315
|
}
|
|
166
316
|
if (result.kind === "rejected") {
|
|
167
317
|
if (!result.retryable) {
|
|
168
|
-
await allocationStore.failWithoutInfrastructure({
|
|
318
|
+
const failed = await allocationStore.failWithoutInfrastructure({
|
|
169
319
|
allocationId: allocation.id,
|
|
170
320
|
expectedStatus: "provisioning",
|
|
171
321
|
expectedGeneration: allocation.generation,
|
|
@@ -174,6 +324,12 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
174
324
|
expectedLeaseId: leaseId,
|
|
175
325
|
now: now(),
|
|
176
326
|
});
|
|
327
|
+
if (failed !== null) {
|
|
328
|
+
router.retireAllocation({
|
|
329
|
+
allocationId: failed.id,
|
|
330
|
+
generation: failed.generation,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
177
333
|
return;
|
|
178
334
|
}
|
|
179
335
|
await replaceAfterFailure(allocation, leaseId, result.code, result.message);
|
|
@@ -189,11 +345,8 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
189
345
|
now: now(),
|
|
190
346
|
});
|
|
191
347
|
if (allocated !== null) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
generation: allocated.generation,
|
|
195
|
-
};
|
|
196
|
-
if (await router.isAllocatedSidecarReady(target)) {
|
|
348
|
+
trackAllocation(allocated);
|
|
349
|
+
if (await isSidecarReady(allocated)) {
|
|
197
350
|
await waitUntilReady(allocated, leaseId, true);
|
|
198
351
|
return;
|
|
199
352
|
}
|
|
@@ -201,10 +354,19 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
201
354
|
// transitions. Do not hold the single reconciliation loop for the full
|
|
202
355
|
// connection timeout: park this lease at its persisted deadline and let
|
|
203
356
|
// sidecar.allocated.connected wake it immediately when the worker arrives.
|
|
204
|
-
await
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
357
|
+
await finishReconciliation(allocated.id, (pendingConnect) => pendingConnect
|
|
358
|
+
? allocationStore.scheduleRetry({
|
|
359
|
+
allocationId: allocated.id,
|
|
360
|
+
expectedStatus: "allocated",
|
|
361
|
+
expectedGeneration: allocated.generation,
|
|
362
|
+
expectedLeaseId: leaseId,
|
|
363
|
+
nextAttemptAt: now(),
|
|
364
|
+
now: now(),
|
|
365
|
+
})
|
|
366
|
+
: allocationStore.parkReconciliation(allocated.id, leaseId, {
|
|
367
|
+
kind: "await-connection",
|
|
368
|
+
fallbackNextAttemptAt: retryAt(MAX_RETRY_BACKOFF_ATTEMPT),
|
|
369
|
+
}));
|
|
208
370
|
}
|
|
209
371
|
}
|
|
210
372
|
async function bindAndEnsure(allocation, leaseId, provisioner, replacement) {
|
|
@@ -232,6 +394,7 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
232
394
|
});
|
|
233
395
|
if (bound === null)
|
|
234
396
|
return;
|
|
397
|
+
trackAllocation(bound);
|
|
235
398
|
router.fenceAllocation(bound.id, bound.generation);
|
|
236
399
|
await acceptEnsure(bound, leaseId, provisioner, token);
|
|
237
400
|
}
|
|
@@ -240,47 +403,77 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
240
403
|
allocation.status !== "releasing") {
|
|
241
404
|
throw new Error(`Cannot retry destroy while allocation ${allocation.id} is ${allocation.status}`);
|
|
242
405
|
}
|
|
243
|
-
|
|
406
|
+
const status = allocation.status;
|
|
407
|
+
await finishReconciliation(allocation.id, () => allocationStore.scheduleRetry({
|
|
244
408
|
allocationId: allocation.id,
|
|
245
|
-
expectedStatus:
|
|
409
|
+
expectedStatus: status,
|
|
246
410
|
expectedGeneration: allocation.generation,
|
|
247
411
|
nextAttemptAt: retryAt(allocation.destroyAttempts),
|
|
248
412
|
expectedLeaseId: leaseId,
|
|
249
413
|
attempt: "destroy",
|
|
250
414
|
now: now(),
|
|
251
|
-
});
|
|
415
|
+
}));
|
|
252
416
|
}
|
|
253
417
|
async function destroyCurrent(allocation, leaseId, provisioner) {
|
|
254
418
|
if (allocation.sidecarId === undefined)
|
|
255
419
|
return true;
|
|
256
420
|
const sidecarId = allocation.sidecarId;
|
|
421
|
+
let result;
|
|
257
422
|
try {
|
|
258
|
-
|
|
423
|
+
result = parseDestroyResult(await withReconciliationLease(allocation, leaseId, "Sidecar destroy", ({ signal }) => provisioner.destroy({
|
|
424
|
+
signal,
|
|
259
425
|
allocationId: allocation.id,
|
|
260
426
|
generation: allocation.generation,
|
|
261
427
|
sidecarId,
|
|
262
428
|
...(allocation.externalRef !== undefined
|
|
263
429
|
? { externalRef: allocation.externalRef }
|
|
264
430
|
: {}),
|
|
265
|
-
})));
|
|
266
|
-
if (result.kind === "destroyed")
|
|
267
|
-
return true;
|
|
431
|
+
}), operationTimeoutMs));
|
|
268
432
|
}
|
|
269
433
|
catch (error) {
|
|
434
|
+
if (error instanceof ReconciliationLeaseLostError)
|
|
435
|
+
throw error;
|
|
270
436
|
logger.warn `Destroy failed for allocation ${allocation.id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
437
|
+
await retryDestroy(allocation, leaseId);
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
if (result.kind === "destroyed")
|
|
441
|
+
return true;
|
|
442
|
+
if (!result.retryable) {
|
|
443
|
+
const failed = await allocationStore.markDestroyFailed({
|
|
444
|
+
allocationId: allocation.id,
|
|
445
|
+
expectedGeneration: allocation.generation,
|
|
446
|
+
expectedLeaseId: leaseId,
|
|
447
|
+
code: result.code,
|
|
448
|
+
message: result.message,
|
|
449
|
+
now: now(),
|
|
450
|
+
});
|
|
451
|
+
if (failed !== null) {
|
|
452
|
+
router.retireAllocation({
|
|
453
|
+
allocationId: failed.id,
|
|
454
|
+
generation: failed.generation,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return false;
|
|
271
458
|
}
|
|
272
459
|
await retryDestroy(allocation, leaseId);
|
|
273
460
|
return false;
|
|
274
461
|
}
|
|
275
462
|
async function reconcile(allocation, leaseId) {
|
|
276
463
|
router.fenceAllocation(allocation.id, allocation.generation);
|
|
277
|
-
if (allocation.status === "released" ||
|
|
464
|
+
if (allocation.status === "released" ||
|
|
465
|
+
allocation.status === "failed" ||
|
|
466
|
+
allocation.status === "destroy_failed") {
|
|
278
467
|
return;
|
|
279
468
|
}
|
|
469
|
+
if (allocation.status === "allocated" &&
|
|
470
|
+
onInitializationRecovery !== undefined) {
|
|
471
|
+
await withReconciliationLease(allocation, leaseId, "Sender deployment recovery", (context) => trackAllocationQuery(allocation.id, () => onInitializationRecovery(allocation, context)), operationTimeoutMs);
|
|
472
|
+
}
|
|
280
473
|
const provisioner = provisionerFor(allocation);
|
|
281
474
|
if (provisioner === null) {
|
|
282
475
|
if (allocation.status === "pending") {
|
|
283
|
-
await allocationStore.failWithoutInfrastructure({
|
|
476
|
+
const failed = await allocationStore.failWithoutInfrastructure({
|
|
284
477
|
allocationId: allocation.id,
|
|
285
478
|
expectedStatus: "pending",
|
|
286
479
|
expectedGeneration: allocation.generation,
|
|
@@ -289,16 +482,23 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
289
482
|
expectedLeaseId: leaseId,
|
|
290
483
|
now: now(),
|
|
291
484
|
});
|
|
485
|
+
if (failed !== null) {
|
|
486
|
+
router.retireAllocation({
|
|
487
|
+
allocationId: failed.id,
|
|
488
|
+
generation: failed.generation,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
292
491
|
}
|
|
293
492
|
else {
|
|
294
|
-
|
|
493
|
+
const status = allocation.status;
|
|
494
|
+
await finishReconciliation(allocation.id, () => allocationStore.scheduleRetry({
|
|
295
495
|
allocationId: allocation.id,
|
|
296
|
-
expectedStatus:
|
|
496
|
+
expectedStatus: status,
|
|
297
497
|
expectedGeneration: allocation.generation,
|
|
298
498
|
nextAttemptAt: retryAt(allocation.ensureAttempts + allocation.destroyAttempts),
|
|
299
499
|
expectedLeaseId: leaseId,
|
|
300
500
|
now: now(),
|
|
301
|
-
});
|
|
501
|
+
}));
|
|
302
502
|
}
|
|
303
503
|
return;
|
|
304
504
|
}
|
|
@@ -312,6 +512,10 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
312
512
|
await replaceAfterFailure(allocation, leaseId, "ensure_outcome_unknown", "Hub restarted before sidecar provisioning acceptance was recorded");
|
|
313
513
|
return;
|
|
314
514
|
case "allocated":
|
|
515
|
+
if (allocation.initializationLeaseId !== undefined) {
|
|
516
|
+
await replaceAfterFailure(allocation, leaseId, "sidecar_initialization_uncertain", "A previous initialization attempt did not record completion", { onlyIfInitializationIncomplete: true });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
315
519
|
await waitUntilReady(allocation, leaseId);
|
|
316
520
|
return;
|
|
317
521
|
case "replacing":
|
|
@@ -319,16 +523,23 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
319
523
|
return;
|
|
320
524
|
await bindAndEnsure(allocation, leaseId, provisioner, true);
|
|
321
525
|
return;
|
|
322
|
-
case "releasing":
|
|
526
|
+
case "releasing": {
|
|
323
527
|
if (!(await destroyCurrent(allocation, leaseId, provisioner)))
|
|
324
528
|
return;
|
|
325
|
-
await allocationStore.markReleased({
|
|
529
|
+
const released = await allocationStore.markReleased({
|
|
326
530
|
allocationId: allocation.id,
|
|
327
531
|
generation: allocation.generation,
|
|
328
532
|
expectedLeaseId: leaseId,
|
|
329
533
|
now: now(),
|
|
330
534
|
});
|
|
535
|
+
if (released !== null) {
|
|
536
|
+
router.retireAllocation({
|
|
537
|
+
allocationId: released.id,
|
|
538
|
+
generation: released.generation,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
331
541
|
return;
|
|
542
|
+
}
|
|
332
543
|
default: {
|
|
333
544
|
const exhaustive = allocation.status;
|
|
334
545
|
throw new Error(`Allocation ${allocation.id} has unhandled status ${String(exhaustive)}`);
|
|
@@ -355,16 +566,72 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
355
566
|
}
|
|
356
567
|
}
|
|
357
568
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
569
|
+
const connectionEvents = new Map();
|
|
570
|
+
function queueConnectionEvent(target, apply) {
|
|
571
|
+
const previous = connectionEvents.get(target.allocationId) ?? Promise.resolve();
|
|
572
|
+
const pending = previous.catch(() => undefined).then(apply);
|
|
573
|
+
connectionEvents.set(target.allocationId, pending);
|
|
574
|
+
const settled = () => {
|
|
575
|
+
if (connectionEvents.get(target.allocationId) === pending)
|
|
576
|
+
connectionEvents.delete(target.allocationId);
|
|
577
|
+
};
|
|
578
|
+
void pending.then(settled, settled);
|
|
579
|
+
return pending;
|
|
580
|
+
}
|
|
581
|
+
async function queueReconciliationStep(target, apply) {
|
|
582
|
+
const active = activeAllocations.get(target.allocationId);
|
|
583
|
+
if (active?.allocation.generation !== target.generation)
|
|
584
|
+
throw new ReconciliationLeaseLostError(target.allocationId);
|
|
585
|
+
try {
|
|
586
|
+
await runSidecarOperation("Allocation connection events", operationTimeoutMs, (signal) => queueConnectionEvent(target, async () => {
|
|
587
|
+
signal.throwIfAborted();
|
|
588
|
+
await apply();
|
|
589
|
+
}), active.controller.signal);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (error instanceof SidecarOperationTimeoutError) {
|
|
593
|
+
// Stop this lease without queuing another write behind the same stalled
|
|
594
|
+
// event. Keep the actual operation queued until it settles, preserving
|
|
595
|
+
// ordering with reconnects and excluding this allocation from new claims.
|
|
596
|
+
const cancelled = new ReconciliationLeaseLostError(target.allocationId, error);
|
|
597
|
+
active.controller.abort(cancelled);
|
|
598
|
+
throw cancelled;
|
|
599
|
+
}
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
function noteDisconnect(target, leaseInvalidated = false) {
|
|
604
|
+
const active = activeAllocations.get(target.allocationId);
|
|
605
|
+
if (active?.allocation.generation !== target.generation)
|
|
606
|
+
return;
|
|
607
|
+
active.pendingConnect = null;
|
|
608
|
+
if (leaseInvalidated || active.allocation.status === "allocated") {
|
|
609
|
+
active.controller.abort(new ReconciliationLeaseLostError(target.allocationId));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function noteConnect(target) {
|
|
613
|
+
const active = activeAllocations.get(target.allocationId);
|
|
614
|
+
if (active?.allocation.generation === target.generation)
|
|
615
|
+
active.pendingConnect = target;
|
|
616
|
+
}
|
|
617
|
+
function handleDisconnect(target) {
|
|
618
|
+
noteDisconnect(target);
|
|
619
|
+
return queueConnectionEvent(target, async () => {
|
|
620
|
+
const disconnected = await allocationStore.markConnectionLost({
|
|
621
|
+
allocationId: target.allocationId,
|
|
622
|
+
generation: target.generation,
|
|
623
|
+
connectDeadline: new Date(now().getTime() + connectTimeoutMs),
|
|
624
|
+
now: now(),
|
|
625
|
+
});
|
|
626
|
+
noteDisconnect(target, disconnected !== null);
|
|
364
627
|
});
|
|
365
628
|
}
|
|
366
|
-
|
|
367
|
-
|
|
629
|
+
function handleConnected(target) {
|
|
630
|
+
noteConnect(target);
|
|
631
|
+
return queueConnectionEvent(target, async () => {
|
|
632
|
+
await allocationStore.wakeReconciliation(target.allocationId, target.generation);
|
|
633
|
+
noteConnect(target);
|
|
634
|
+
});
|
|
368
635
|
}
|
|
369
636
|
async function repairUnscheduledConnections() {
|
|
370
637
|
for (const allocation of await allocationStore.listActive()) {
|
|
@@ -378,8 +645,17 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
378
645
|
allocationId: allocation.id,
|
|
379
646
|
generation: allocation.generation,
|
|
380
647
|
};
|
|
381
|
-
|
|
382
|
-
|
|
648
|
+
try {
|
|
649
|
+
if (await router.isAllocatedSidecarReady(target))
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
// Unknown readiness is not absence. Leave the allocation for the next
|
|
654
|
+
// repair sweep instead of scheduling a reconnect the worker may hold.
|
|
655
|
+
if (error instanceof SidecarIdentityValidationError)
|
|
656
|
+
continue;
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
383
659
|
try {
|
|
384
660
|
await allocationStore.scheduleReconnectIfUnscheduled({
|
|
385
661
|
...target,
|
|
@@ -393,23 +669,141 @@ export function createSidecarAllocationReconciler({ allocationStore, plugins, ro
|
|
|
393
669
|
}
|
|
394
670
|
}
|
|
395
671
|
}
|
|
672
|
+
let admittedClaims = 0;
|
|
396
673
|
async function reconcileNext() {
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
674
|
+
const retainedAllocations = new Set([
|
|
675
|
+
...connectionEvents.keys(),
|
|
676
|
+
...pendingAllocationQueries.keys(),
|
|
677
|
+
]);
|
|
678
|
+
// An active claim already owns capacity for its allocation's pending work.
|
|
679
|
+
for (const allocationId of activeAllocations.keys())
|
|
680
|
+
retainedAllocations.delete(allocationId);
|
|
681
|
+
if (admittedClaims + retainedAllocations.size >= maxConcurrentClaims)
|
|
403
682
|
return false;
|
|
683
|
+
admittedClaims += 1;
|
|
684
|
+
let claim;
|
|
404
685
|
try {
|
|
686
|
+
const leaseId = createLeaseId();
|
|
687
|
+
const claimStartedAt = performance.now();
|
|
688
|
+
const allocation = await runSidecarOperation("Sidecar allocation claim", operationTimeoutMs, () => {
|
|
689
|
+
claim = allocationStore.claimNextReconcilable({
|
|
690
|
+
leaseId,
|
|
691
|
+
leaseDurationMs,
|
|
692
|
+
excludedAllocationIds: [
|
|
693
|
+
...new Set([
|
|
694
|
+
...activeAllocations.keys(),
|
|
695
|
+
...connectionEvents.keys(),
|
|
696
|
+
...pendingAllocationQueries.keys(),
|
|
697
|
+
]),
|
|
698
|
+
],
|
|
699
|
+
});
|
|
700
|
+
return claim;
|
|
701
|
+
});
|
|
702
|
+
if (allocation === null)
|
|
703
|
+
return false;
|
|
704
|
+
return await reconcileClaim(allocation, leaseId, claimStartedAt);
|
|
705
|
+
}
|
|
706
|
+
finally {
|
|
707
|
+
const release = () => {
|
|
708
|
+
admittedClaims -= 1;
|
|
709
|
+
};
|
|
710
|
+
// Keep the reservation through the claim-to-reconciliation handoff. A
|
|
711
|
+
// timed-out claim still owns capacity until its database query settles.
|
|
712
|
+
if (claim === undefined)
|
|
713
|
+
release();
|
|
714
|
+
else
|
|
715
|
+
void claim.then(release, release);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function reconcileClaim(allocation, leaseId, claimStartedAt) {
|
|
719
|
+
// Local work may have started since this claim's exclusion snapshot. Let
|
|
720
|
+
// its lease expire without adding another database write behind that work.
|
|
721
|
+
if (connectionEvents.has(allocation.id) ||
|
|
722
|
+
activeAllocations.has(allocation.id) ||
|
|
723
|
+
pendingAllocationQueries.has(allocation.id))
|
|
724
|
+
return true;
|
|
725
|
+
const active = {
|
|
726
|
+
allocation,
|
|
727
|
+
controller: new AbortController(),
|
|
728
|
+
leaseDeadline: claimStartedAt + leaseDurationMs,
|
|
729
|
+
pendingConnect: null,
|
|
730
|
+
};
|
|
731
|
+
activeAllocations.set(allocation.id, active);
|
|
732
|
+
let finished = false;
|
|
733
|
+
let renewing = false;
|
|
734
|
+
const renew = async () => {
|
|
735
|
+
if (finished || renewing || active.controller.signal.aborted)
|
|
736
|
+
return;
|
|
737
|
+
renewing = true;
|
|
738
|
+
const startedAt = performance.now();
|
|
739
|
+
try {
|
|
740
|
+
const renewed = await trackAllocationQuery(allocation.id, () => allocationStore.extendReconciliationLease(allocation.id, leaseId, leaseDurationMs));
|
|
741
|
+
if (finished || active.controller.signal.aborted)
|
|
742
|
+
return;
|
|
743
|
+
if (!renewed || performance.now() >= active.leaseDeadline) {
|
|
744
|
+
active.controller.abort(new ReconciliationLeaseLostError(allocation.id));
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// The database grants the lease during the request. Counting from its
|
|
748
|
+
// start avoids extending ownership by the response's transit time.
|
|
749
|
+
active.leaseDeadline = startedAt + leaseDurationMs;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
if (!finished && !active.controller.signal.aborted) {
|
|
754
|
+
active.controller.abort(new ReconciliationLeaseLostError(allocation.id, error));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
renewing = false;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
// One heartbeat covers the whole claim, including database transitions
|
|
762
|
+
// between provider calls. Short stages must not keep postponing renewal.
|
|
763
|
+
const heartbeat = setInterval(() => {
|
|
764
|
+
void renew();
|
|
765
|
+
}, Math.max(1, Math.floor(leaseDurationMs / 3)));
|
|
766
|
+
let expiryTimer;
|
|
767
|
+
const checkLeaseExpiry = () => {
|
|
768
|
+
if (active.controller.signal.aborted)
|
|
769
|
+
return;
|
|
770
|
+
const remaining = active.leaseDeadline - performance.now();
|
|
771
|
+
if (remaining <= 0) {
|
|
772
|
+
active.controller.abort(new ReconciliationLeaseLostError(allocation.id));
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
expiryTimer = setTimeout(checkLeaseExpiry, Math.ceil(remaining));
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
checkLeaseExpiry();
|
|
779
|
+
try {
|
|
780
|
+
active.controller.signal.throwIfAborted();
|
|
405
781
|
await reconcile(allocation, leaseId);
|
|
406
782
|
}
|
|
407
783
|
catch (error) {
|
|
784
|
+
if (error instanceof ReconciliationLeaseLostError) {
|
|
785
|
+
const cause = error.cause;
|
|
786
|
+
if (cause === undefined) {
|
|
787
|
+
logger.info `Allocation ${allocation.id} reconciliation stopped: lease ${leaseId} is no longer current`;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
logger.warn `Allocation ${allocation.id} reconciliation stopped because lease ${leaseId} could not be confirmed: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
791
|
+
}
|
|
792
|
+
// The durable schedule survives the claim. Stop renewing and let the
|
|
793
|
+
// lease expire; handling a lease failure must not require another write.
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
408
796
|
logger.error `Allocation ${allocation.id} reconciliation failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
409
|
-
await allocationStore.parkReconciliation(allocation.id, leaseId, {
|
|
797
|
+
await finishReconciliation(allocation.id, () => allocationStore.parkReconciliation(allocation.id, leaseId, {
|
|
410
798
|
kind: "retry-after-error",
|
|
411
799
|
notBefore: retryAt(MAX_RETRY_BACKOFF_ATTEMPT),
|
|
412
|
-
});
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
finally {
|
|
803
|
+
finished = true;
|
|
804
|
+
clearInterval(heartbeat);
|
|
805
|
+
clearTimeout(expiryTimer);
|
|
806
|
+
activeAllocations.delete(allocation.id);
|
|
413
807
|
}
|
|
414
808
|
return true;
|
|
415
809
|
}
|