@zlink-systems/framework 0.30.2 → 0.30.3
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/.tsbuildinfo +1 -1
- package/dist/runtime/execution/index.js +7 -9
- package/dist/runtime/execution/state-lane.js +34 -11
- package/dist/runtime/locations/auto-connect-loop.d.ts +2 -0
- package/dist/runtime/locations/auto-connect-loop.js +15 -2
- package/dist/runtime/spots/index.js +0 -3
- package/dist/runtime/spots/spot-serial-turn-executor.js +10 -4
- package/package.json +2 -2
- package/src/runtime/execution/index.ts +16 -14
- package/src/runtime/execution/state-lane.ts +53 -14
- package/src/runtime/locations/auto-connect-loop.ts +18 -2
- package/src/runtime/spots/index.ts +0 -6
- package/src/runtime/spots/spot-serial-turn-executor.ts +11 -5
|
@@ -192,6 +192,11 @@ function isCurrentZLinkSpotSerialTurn(executor) {
|
|
|
192
192
|
current.executor === executor &&
|
|
193
193
|
executor.isActiveTurn(current.turn, current.turnId));
|
|
194
194
|
}
|
|
195
|
+
function createExecutionBarrierAdmissionException(seal, message) {
|
|
196
|
+
return (0, framework_errors_internal_1.createInternalFrameworkException)(seal.kind === 'close'
|
|
197
|
+
? framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestTargetNotFound
|
|
198
|
+
: framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.SpotMoving, message);
|
|
199
|
+
}
|
|
195
200
|
/**
|
|
196
201
|
* Seals application-lane admission and waits for every active Promise turn to
|
|
197
202
|
* finish. A seal generation prevents a stale abort from reopening a newer
|
|
@@ -215,14 +220,9 @@ class ZLinkExecutionBarrier {
|
|
|
215
220
|
}
|
|
216
221
|
enter() {
|
|
217
222
|
if (this.committedSeal !== undefined) {
|
|
218
|
-
throw (
|
|
219
|
-
? framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected
|
|
220
|
-
: framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.SpotMoving, 'ZLink execution barrier is committed.');
|
|
223
|
+
throw createExecutionBarrierAdmissionException(this.committedSeal, 'ZLink execution barrier is committed.');
|
|
221
224
|
}
|
|
222
225
|
if (this.currentSeal !== undefined) {
|
|
223
|
-
if (this.currentSeal.kind === 'close') {
|
|
224
|
-
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected, 'ZLink execution barrier is sealed for Close.');
|
|
225
|
-
}
|
|
226
226
|
return new Promise((resolve, reject) => {
|
|
227
227
|
this.admissionWaiters.push({ resolve, reject });
|
|
228
228
|
});
|
|
@@ -302,9 +302,7 @@ class ZLinkExecutionBarrier {
|
|
|
302
302
|
return false;
|
|
303
303
|
this.currentSeal = undefined;
|
|
304
304
|
this.committedSeal = seal;
|
|
305
|
-
const error = (
|
|
306
|
-
? framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected
|
|
307
|
-
: framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.SpotMoving, 'ZLink execution barrier is committed.');
|
|
305
|
+
const error = createExecutionBarrierAdmissionException(seal, 'ZLink execution barrier is committed.');
|
|
308
306
|
for (const waiter of this.admissionWaiters)
|
|
309
307
|
waiter.reject(error);
|
|
310
308
|
this.admissionWaiters.length = 0;
|
|
@@ -18,6 +18,10 @@ function startOutsideStateLane(work) {
|
|
|
18
18
|
}
|
|
19
19
|
const currentLaneStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
20
20
|
const diagnosticPending = isStructuralGuardEnabled() ? new Map() : undefined;
|
|
21
|
+
const diagnosticLaneIds = isStructuralGuardEnabled()
|
|
22
|
+
? new WeakMap()
|
|
23
|
+
: undefined;
|
|
24
|
+
let nextDiagnosticLane = 0;
|
|
21
25
|
let nextDiagnosticWork = 0;
|
|
22
26
|
const diagnosticTurnStorage = diagnosticPending === undefined ? undefined : new node_async_hooks_1.AsyncLocalStorage();
|
|
23
27
|
/**
|
|
@@ -132,15 +136,24 @@ if (isStructuralGuardEnabled()) {
|
|
|
132
136
|
});
|
|
133
137
|
const run = ZLinkStateLane.prototype.run;
|
|
134
138
|
ZLinkStateLane.prototype.run = function (work) {
|
|
135
|
-
const
|
|
136
|
-
|
|
139
|
+
const release = registerDiagnosticWork(`${diagnosticLaneName(this)} ${diagnosticWorkName(work)}`);
|
|
140
|
+
let completion;
|
|
141
|
+
try {
|
|
142
|
+
completion = run.call(this, () => runDiagnosticTurn(this, work, release.description));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
release();
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
void completion.then(release, release);
|
|
149
|
+
return guardStateLaneCompletion(completion, `another state lane completion [${release.description}]`, this);
|
|
137
150
|
};
|
|
138
151
|
const tryPost = ZLinkStateLane.prototype.tryPost;
|
|
139
152
|
ZLinkStateLane.prototype.tryPost = function (work) {
|
|
140
|
-
const release = registerDiagnosticWork(
|
|
153
|
+
const release = registerDiagnosticWork(`${diagnosticLaneName(this)} post ${diagnosticWorkName(work)}`);
|
|
141
154
|
const accepted = tryPost.call(this, () => {
|
|
142
155
|
try {
|
|
143
|
-
const result = runDiagnosticTurn(this, work);
|
|
156
|
+
const result = runDiagnosticTurn(this, work, release.description);
|
|
144
157
|
if (result instanceof Promise)
|
|
145
158
|
return result.finally(release);
|
|
146
159
|
release();
|
|
@@ -156,8 +169,8 @@ if (isStructuralGuardEnabled()) {
|
|
|
156
169
|
return accepted;
|
|
157
170
|
};
|
|
158
171
|
}
|
|
159
|
-
function runDiagnosticTurn(lane, work) {
|
|
160
|
-
const turn = { lane, active: true };
|
|
172
|
+
function runDiagnosticTurn(lane, work, description) {
|
|
173
|
+
const turn = { lane, description, active: true };
|
|
161
174
|
return diagnosticTurnStorage.run(turn, () => {
|
|
162
175
|
try {
|
|
163
176
|
const result = work();
|
|
@@ -183,7 +196,8 @@ function guardStateLaneCompletion(completion, label, owner) {
|
|
|
183
196
|
return (onFulfilled, onRejected) => {
|
|
184
197
|
const current = ZLinkStateLane.current;
|
|
185
198
|
if (current !== undefined && current !== owner) {
|
|
186
|
-
throw new Error(`A state lane turn cannot wait for ${label}
|
|
199
|
+
throw new Error(`A state lane turn cannot wait for ${label}. ` +
|
|
200
|
+
`current turn: ${diagnosticTurnStorage?.getStore()?.description ?? UNKNOWN_DIAGNOSTIC_CALLER}`);
|
|
187
201
|
}
|
|
188
202
|
return target.then(onFulfilled, onRejected);
|
|
189
203
|
};
|
|
@@ -210,16 +224,25 @@ function debugPendingWorkNames() {
|
|
|
210
224
|
function diagnosticWorkName(work) {
|
|
211
225
|
return work.name.length > 0 ? work.name : '<anonymous>';
|
|
212
226
|
}
|
|
227
|
+
function diagnosticLaneName(lane) {
|
|
228
|
+
let id = diagnosticLaneIds.get(lane);
|
|
229
|
+
if (id === undefined) {
|
|
230
|
+
id = ++nextDiagnosticLane;
|
|
231
|
+
diagnosticLaneIds.set(lane, id);
|
|
232
|
+
}
|
|
233
|
+
return `state lane #${id}`;
|
|
234
|
+
}
|
|
213
235
|
function registerDiagnosticWork(name) {
|
|
214
236
|
if (diagnosticPending === undefined)
|
|
215
|
-
return () => undefined;
|
|
237
|
+
return Object.assign(() => undefined, { description: name });
|
|
216
238
|
const id = ++nextDiagnosticWork;
|
|
217
239
|
const frames = new Error().stack?.split('\n').slice(2) ?? [];
|
|
218
240
|
const caller = frames
|
|
219
241
|
.find((frame) => !frame.includes('state-lane.') && !frame.includes('serial-execution-queue.'))
|
|
220
242
|
?.trim() ?? UNKNOWN_DIAGNOSTIC_CALLER;
|
|
221
|
-
|
|
222
|
-
|
|
243
|
+
const description = `${name} ${caller}`;
|
|
244
|
+
diagnosticPending.set(id, description);
|
|
245
|
+
return Object.assign(() => {
|
|
223
246
|
diagnosticPending.delete(id);
|
|
224
|
-
};
|
|
247
|
+
}, { description });
|
|
225
248
|
}
|
|
@@ -26,6 +26,7 @@ export declare class ZLinkAutoConnectLoop {
|
|
|
26
26
|
private lastStamp?;
|
|
27
27
|
private lastLiveOwnerSetVersion?;
|
|
28
28
|
private lastTickFailed;
|
|
29
|
+
private inFlightTick;
|
|
29
30
|
constructor(options: ZLinkAutoConnectLoopOptions);
|
|
30
31
|
start(signal?: AbortSignal): Promise<void>;
|
|
31
32
|
requestPeerReconcile(meshName: string, _peerRid: string): Promise<void>;
|
|
@@ -33,6 +34,7 @@ export declare class ZLinkAutoConnectLoop {
|
|
|
33
34
|
prepareTransportShutdown(): Promise<void>;
|
|
34
35
|
finishTransportShutdown(signal?: AbortSignal): Promise<void>;
|
|
35
36
|
tick(signal?: AbortSignal): Promise<void>;
|
|
37
|
+
private enqueueTick;
|
|
36
38
|
private tickCore;
|
|
37
39
|
private runReconcile;
|
|
38
40
|
private scheduleNext;
|
|
@@ -17,6 +17,8 @@ class ZLinkAutoConnectLoop {
|
|
|
17
17
|
lastStamp;
|
|
18
18
|
lastLiveOwnerSetVersion;
|
|
19
19
|
lastTickFailed = false;
|
|
20
|
+
// This settled tail orders whole operations. The tick fields below are touched only inside it.
|
|
21
|
+
inFlightTick = Promise.resolve();
|
|
20
22
|
constructor(options) {
|
|
21
23
|
this.reconciler = options.reconciler;
|
|
22
24
|
this.options = { ...Options_1.zlinkRuntimeDefaultLocationOptions, ...options.options };
|
|
@@ -45,7 +47,7 @@ class ZLinkAutoConnectLoop {
|
|
|
45
47
|
if (meshName !== this.meshName || controller === undefined)
|
|
46
48
|
return;
|
|
47
49
|
// A fresh descriptor scan uses the same RID ordering and connection owner as a periodic tick.
|
|
48
|
-
await this.
|
|
50
|
+
await this.enqueueTick(async () => {
|
|
49
51
|
if (!controller.signal.aborted)
|
|
50
52
|
await this.runReconcile(controller.signal);
|
|
51
53
|
});
|
|
@@ -64,13 +66,24 @@ class ZLinkAutoConnectLoop {
|
|
|
64
66
|
this.clearTimer(this.timer);
|
|
65
67
|
this.timer = undefined;
|
|
66
68
|
}
|
|
69
|
+
await this.inFlightTick;
|
|
67
70
|
this.reconciler.disconnectPeers();
|
|
68
71
|
}
|
|
69
72
|
async finishTransportShutdown(signal) {
|
|
70
73
|
await this.reconciler.unpublishLocal(signal);
|
|
71
74
|
}
|
|
72
75
|
async tick(signal) {
|
|
73
|
-
|
|
76
|
+
const controller = this.controller;
|
|
77
|
+
await this.enqueueTick(async () => {
|
|
78
|
+
if (controller?.signal.aborted !== true)
|
|
79
|
+
await this.tickCore(signal ?? controller?.signal);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
enqueueTick(operation) {
|
|
83
|
+
const completion = this.inFlightTick.then(operation);
|
|
84
|
+
// The caller receives the failure; the successor still owns its place in the order.
|
|
85
|
+
this.inFlightTick = completion.then(() => undefined, () => undefined);
|
|
86
|
+
return completion;
|
|
74
87
|
}
|
|
75
88
|
async tickCore(signal) {
|
|
76
89
|
const lastTickFailed = this.lastTickFailed;
|
|
@@ -874,9 +874,6 @@ class DefaultZLinkSpotManager {
|
|
|
874
874
|
async admitActorJoin(spotId, actor, request, commit, signal, leaveSource, contentType = 'application/json') {
|
|
875
875
|
const meshName = this.activations.resolveUnique(spotId)?.meshName;
|
|
876
876
|
this.options.admission?.requireRequest('Actor join admission', meshName);
|
|
877
|
-
if (meshName !== undefined && this.isSpotClosing(meshName, spotId)) {
|
|
878
|
-
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestRejected, `Spot '${String(spotId)}' is closing.`);
|
|
879
|
-
}
|
|
880
877
|
return await this.actorMembership.admitActorJoin(spotId, actor, request, commit, signal, leaveSource, contentType);
|
|
881
878
|
}
|
|
882
879
|
async leaveActor(spotId, actor, signal) {
|
|
@@ -146,11 +146,14 @@ class ZLinkSpotSerialTurnExecutor {
|
|
|
146
146
|
}
|
|
147
147
|
/** Admits membership work and holds the lifecycle FIFO until its result settles. */
|
|
148
148
|
executeLifecycleOperation(operation) {
|
|
149
|
-
|
|
150
|
-
try {
|
|
149
|
+
const requireAdmission = () => {
|
|
151
150
|
if (this.lifecycleAdmissionClosed) {
|
|
152
|
-
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.
|
|
151
|
+
throw (0, framework_errors_internal_1.createInternalFrameworkException)(framework_errors_internal_1.ZLinkFrameworkInternalErrorKind.RequestTargetNotFound, 'Spot incarnation is closing (committed Close).');
|
|
153
152
|
}
|
|
153
|
+
};
|
|
154
|
+
let entry;
|
|
155
|
+
try {
|
|
156
|
+
requireAdmission();
|
|
154
157
|
entry = this.executionBarrier?.enter();
|
|
155
158
|
}
|
|
156
159
|
catch (error) {
|
|
@@ -158,7 +161,10 @@ class ZLinkSpotSerialTurnExecutor {
|
|
|
158
161
|
}
|
|
159
162
|
const submit = (claim) => {
|
|
160
163
|
try {
|
|
161
|
-
return this.scheduler.submitLifecycleOperation(
|
|
164
|
+
return this.scheduler.submitLifecycleOperation(() => {
|
|
165
|
+
requireAdmission();
|
|
166
|
+
return operation();
|
|
167
|
+
});
|
|
162
168
|
}
|
|
163
169
|
catch (error) {
|
|
164
170
|
return Promise.reject(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zlink-systems/framework",
|
|
3
|
-
"version": "0.30.
|
|
3
|
+
"version": "0.30.3",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@opentelemetry/api": "^1.9.1",
|
|
21
21
|
"@opentelemetry/api-logs": "^0.221.0",
|
|
22
|
-
"@zlink-systems/stream-wire": "0.30.
|
|
22
|
+
"@zlink-systems/stream-wire": "0.30.3",
|
|
23
23
|
"@zlink-systems/zlink": "1.20.0"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -270,6 +270,18 @@ export interface ZLinkExecutionBarrierSeal {
|
|
|
270
270
|
readonly kind: 'close' | 'relocation' | 'activation';
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
function createExecutionBarrierAdmissionException(
|
|
274
|
+
seal: ZLinkExecutionBarrierSeal,
|
|
275
|
+
message: string
|
|
276
|
+
) {
|
|
277
|
+
return createInternalFrameworkException(
|
|
278
|
+
seal.kind === 'close'
|
|
279
|
+
? ZLinkFrameworkInternalErrorKind.RequestTargetNotFound
|
|
280
|
+
: ZLinkFrameworkInternalErrorKind.SpotMoving,
|
|
281
|
+
message
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
273
285
|
export interface ZLinkExecutionBarrierClaim {
|
|
274
286
|
release(): void;
|
|
275
287
|
/** The turn yielded; it no longer runs until resume() admits it again. */
|
|
@@ -308,20 +320,12 @@ export class ZLinkExecutionBarrier {
|
|
|
308
320
|
|
|
309
321
|
enter(): ZLinkExecutionBarrierClaim | Promise<ZLinkExecutionBarrierClaim> {
|
|
310
322
|
if (this.committedSeal !== undefined) {
|
|
311
|
-
throw
|
|
312
|
-
this.committedSeal
|
|
313
|
-
? ZLinkFrameworkInternalErrorKind.RequestRejected
|
|
314
|
-
: ZLinkFrameworkInternalErrorKind.SpotMoving,
|
|
323
|
+
throw createExecutionBarrierAdmissionException(
|
|
324
|
+
this.committedSeal,
|
|
315
325
|
'ZLink execution barrier is committed.'
|
|
316
326
|
);
|
|
317
327
|
}
|
|
318
328
|
if (this.currentSeal !== undefined) {
|
|
319
|
-
if (this.currentSeal.kind === 'close') {
|
|
320
|
-
throw createInternalFrameworkException(
|
|
321
|
-
ZLinkFrameworkInternalErrorKind.RequestRejected,
|
|
322
|
-
'ZLink execution barrier is sealed for Close.'
|
|
323
|
-
);
|
|
324
|
-
}
|
|
325
329
|
return new Promise<ZLinkExecutionBarrierClaim>((resolve, reject) => {
|
|
326
330
|
this.admissionWaiters.push({ resolve, reject });
|
|
327
331
|
});
|
|
@@ -400,10 +404,8 @@ export class ZLinkExecutionBarrier {
|
|
|
400
404
|
if (!this.isCurrent(seal)) return false;
|
|
401
405
|
this.currentSeal = undefined;
|
|
402
406
|
this.committedSeal = seal;
|
|
403
|
-
const error =
|
|
404
|
-
seal
|
|
405
|
-
? ZLinkFrameworkInternalErrorKind.RequestRejected
|
|
406
|
-
: ZLinkFrameworkInternalErrorKind.SpotMoving,
|
|
407
|
+
const error = createExecutionBarrierAdmissionException(
|
|
408
|
+
seal,
|
|
407
409
|
'ZLink execution barrier is committed.'
|
|
408
410
|
);
|
|
409
411
|
for (const waiter of this.admissionWaiters) waiter.reject(error);
|
|
@@ -5,6 +5,7 @@ type ZLinkStateLaneWork<T> = () => Promise<T> | T;
|
|
|
5
5
|
|
|
6
6
|
interface ZLinkStateLaneTurn {
|
|
7
7
|
readonly lane: ZLinkStateLane;
|
|
8
|
+
readonly description: string;
|
|
8
9
|
active: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
@@ -21,6 +22,10 @@ export function startOutsideStateLane<T>(work: () => T): T {
|
|
|
21
22
|
|
|
22
23
|
const currentLaneStorage = new AsyncLocalStorage<ZLinkStateLane>();
|
|
23
24
|
const diagnosticPending = isStructuralGuardEnabled() ? new Map<number, string>() : undefined;
|
|
25
|
+
const diagnosticLaneIds = isStructuralGuardEnabled()
|
|
26
|
+
? new WeakMap<ZLinkStateLane, number>()
|
|
27
|
+
: undefined;
|
|
28
|
+
let nextDiagnosticLane = 0;
|
|
24
29
|
let nextDiagnosticWork = 0;
|
|
25
30
|
const diagnosticTurnStorage =
|
|
26
31
|
diagnosticPending === undefined ? undefined : new AsyncLocalStorage<ZLinkStateLaneTurn>();
|
|
@@ -152,10 +157,22 @@ if (isStructuralGuardEnabled()) {
|
|
|
152
157
|
this: ZLinkStateLane,
|
|
153
158
|
work: ZLinkStateLaneWork<T>
|
|
154
159
|
): Promise<T> {
|
|
155
|
-
const
|
|
160
|
+
const release = registerDiagnosticWork(
|
|
161
|
+
`${diagnosticLaneName(this)} ${diagnosticWorkName(work)}`
|
|
162
|
+
);
|
|
163
|
+
let completion: Promise<T>;
|
|
164
|
+
try {
|
|
165
|
+
completion = run.call(this, () =>
|
|
166
|
+
runDiagnosticTurn(this, work, release.description)
|
|
167
|
+
) as Promise<T>;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
release();
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
void completion.then(release, release);
|
|
156
173
|
return guardStateLaneCompletion(
|
|
157
|
-
|
|
158
|
-
|
|
174
|
+
completion,
|
|
175
|
+
`another state lane completion [${release.description}]`,
|
|
159
176
|
this
|
|
160
177
|
);
|
|
161
178
|
};
|
|
@@ -165,10 +182,12 @@ if (isStructuralGuardEnabled()) {
|
|
|
165
182
|
this: ZLinkStateLane,
|
|
166
183
|
work: ZLinkStateLaneWork<void>
|
|
167
184
|
): boolean {
|
|
168
|
-
const release = registerDiagnosticWork(
|
|
185
|
+
const release = registerDiagnosticWork(
|
|
186
|
+
`${diagnosticLaneName(this)} post ${diagnosticWorkName(work)}`
|
|
187
|
+
);
|
|
169
188
|
const accepted = tryPost.call(this, () => {
|
|
170
189
|
try {
|
|
171
|
-
const result = runDiagnosticTurn(this, work);
|
|
190
|
+
const result = runDiagnosticTurn(this, work, release.description);
|
|
172
191
|
if (result instanceof Promise) return result.finally(release);
|
|
173
192
|
release();
|
|
174
193
|
return result;
|
|
@@ -182,8 +201,12 @@ if (isStructuralGuardEnabled()) {
|
|
|
182
201
|
};
|
|
183
202
|
}
|
|
184
203
|
|
|
185
|
-
function runDiagnosticTurn<T>(
|
|
186
|
-
|
|
204
|
+
function runDiagnosticTurn<T>(
|
|
205
|
+
lane: ZLinkStateLane,
|
|
206
|
+
work: ZLinkStateLaneWork<T>,
|
|
207
|
+
description: string
|
|
208
|
+
): Promise<T> | T {
|
|
209
|
+
const turn: ZLinkStateLaneTurn = { lane, description, active: true };
|
|
187
210
|
return diagnosticTurnStorage!.run(turn, () => {
|
|
188
211
|
try {
|
|
189
212
|
const result = work();
|
|
@@ -213,7 +236,10 @@ export function guardStateLaneCompletion<T>(
|
|
|
213
236
|
return (onFulfilled: (value: T) => unknown, onRejected: (error: unknown) => unknown) => {
|
|
214
237
|
const current = ZLinkStateLane.current;
|
|
215
238
|
if (current !== undefined && current !== owner) {
|
|
216
|
-
throw new Error(
|
|
239
|
+
throw new Error(
|
|
240
|
+
`A state lane turn cannot wait for ${label}. ` +
|
|
241
|
+
`current turn: ${diagnosticTurnStorage?.getStore()?.description ?? UNKNOWN_DIAGNOSTIC_CALLER}`
|
|
242
|
+
);
|
|
217
243
|
}
|
|
218
244
|
return target.then(onFulfilled, onRejected);
|
|
219
245
|
};
|
|
@@ -246,16 +272,29 @@ export function diagnosticWorkName(work: { readonly name: string }): string {
|
|
|
246
272
|
return work.name.length > 0 ? work.name : '<anonymous>';
|
|
247
273
|
}
|
|
248
274
|
|
|
249
|
-
function
|
|
250
|
-
|
|
275
|
+
function diagnosticLaneName(lane: ZLinkStateLane): string {
|
|
276
|
+
let id = diagnosticLaneIds!.get(lane);
|
|
277
|
+
if (id === undefined) {
|
|
278
|
+
id = ++nextDiagnosticLane;
|
|
279
|
+
diagnosticLaneIds!.set(lane, id);
|
|
280
|
+
}
|
|
281
|
+
return `state lane #${id}`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function registerDiagnosticWork(name: string): (() => void) & { readonly description: string } {
|
|
285
|
+
if (diagnosticPending === undefined) return Object.assign(() => undefined, { description: name });
|
|
251
286
|
const id = ++nextDiagnosticWork;
|
|
252
287
|
const frames = new Error().stack?.split('\n').slice(2) ?? [];
|
|
253
288
|
const caller =
|
|
254
289
|
frames
|
|
255
290
|
.find((frame) => !frame.includes('state-lane.') && !frame.includes('serial-execution-queue.'))
|
|
256
291
|
?.trim() ?? UNKNOWN_DIAGNOSTIC_CALLER;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
292
|
+
const description = `${name} ${caller}`;
|
|
293
|
+
diagnosticPending.set(id, description);
|
|
294
|
+
return Object.assign(
|
|
295
|
+
() => {
|
|
296
|
+
diagnosticPending.delete(id);
|
|
297
|
+
},
|
|
298
|
+
{ description }
|
|
299
|
+
);
|
|
261
300
|
}
|
|
@@ -32,6 +32,8 @@ export class ZLinkAutoConnectLoop {
|
|
|
32
32
|
private lastStamp?: bigint;
|
|
33
33
|
private lastLiveOwnerSetVersion?: number;
|
|
34
34
|
private lastTickFailed = false;
|
|
35
|
+
// This settled tail orders whole operations. The tick fields below are touched only inside it.
|
|
36
|
+
private inFlightTick: Promise<void> = Promise.resolve();
|
|
35
37
|
|
|
36
38
|
constructor(options: ZLinkAutoConnectLoopOptions) {
|
|
37
39
|
this.reconciler = options.reconciler;
|
|
@@ -59,7 +61,7 @@ export class ZLinkAutoConnectLoop {
|
|
|
59
61
|
const controller = this.controller;
|
|
60
62
|
if (meshName !== this.meshName || controller === undefined) return;
|
|
61
63
|
// A fresh descriptor scan uses the same RID ordering and connection owner as a periodic tick.
|
|
62
|
-
await this.
|
|
64
|
+
await this.enqueueTick(async () => {
|
|
63
65
|
if (!controller.signal.aborted) await this.runReconcile(controller.signal);
|
|
64
66
|
});
|
|
65
67
|
}
|
|
@@ -79,6 +81,7 @@ export class ZLinkAutoConnectLoop {
|
|
|
79
81
|
this.clearTimer(this.timer);
|
|
80
82
|
this.timer = undefined;
|
|
81
83
|
}
|
|
84
|
+
await this.inFlightTick;
|
|
82
85
|
this.reconciler.disconnectPeers();
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -87,7 +90,20 @@ export class ZLinkAutoConnectLoop {
|
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
async tick(signal?: AbortSignal): Promise<void> {
|
|
90
|
-
|
|
93
|
+
const controller = this.controller;
|
|
94
|
+
await this.enqueueTick(async () => {
|
|
95
|
+
if (controller?.signal.aborted !== true) await this.tickCore(signal ?? controller?.signal);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private enqueueTick(operation: () => Promise<void>): Promise<void> {
|
|
100
|
+
const completion = this.inFlightTick.then(operation);
|
|
101
|
+
// The caller receives the failure; the successor still owns its place in the order.
|
|
102
|
+
this.inFlightTick = completion.then(
|
|
103
|
+
() => undefined,
|
|
104
|
+
() => undefined
|
|
105
|
+
);
|
|
106
|
+
return completion;
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
private async tickCore(signal?: AbortSignal): Promise<void> {
|
|
@@ -1521,12 +1521,6 @@ export class DefaultZLinkSpotManager {
|
|
|
1521
1521
|
): Promise<ZLinkSpotActorJoinResult> {
|
|
1522
1522
|
const meshName = this.activations.resolveUnique(spotId)?.meshName;
|
|
1523
1523
|
this.options.admission?.requireRequest('Actor join admission', meshName);
|
|
1524
|
-
if (meshName !== undefined && this.isSpotClosing(meshName, spotId)) {
|
|
1525
|
-
throw createInternalFrameworkException(
|
|
1526
|
-
ZLinkFrameworkInternalErrorKind.RequestRejected,
|
|
1527
|
-
`Spot '${String(spotId)}' is closing.`
|
|
1528
|
-
);
|
|
1529
|
-
}
|
|
1530
1524
|
return await this.actorMembership.admitActorJoin(
|
|
1531
1525
|
spotId,
|
|
1532
1526
|
actor,
|
|
@@ -217,21 +217,27 @@ export class ZLinkSpotSerialTurnExecutor {
|
|
|
217
217
|
|
|
218
218
|
/** Admits membership work and holds the lifecycle FIFO until its result settles. */
|
|
219
219
|
executeLifecycleOperation<T>(operation: () => Promise<T> | T): Promise<T> {
|
|
220
|
-
|
|
221
|
-
try {
|
|
220
|
+
const requireAdmission = (): void => {
|
|
222
221
|
if (this.lifecycleAdmissionClosed) {
|
|
223
222
|
throw createInternalFrameworkException(
|
|
224
|
-
ZLinkFrameworkInternalErrorKind.
|
|
225
|
-
'Spot
|
|
223
|
+
ZLinkFrameworkInternalErrorKind.RequestTargetNotFound,
|
|
224
|
+
'Spot incarnation is closing (committed Close).'
|
|
226
225
|
);
|
|
227
226
|
}
|
|
227
|
+
};
|
|
228
|
+
let entry: ZLinkExecutionBarrierClaim | Promise<ZLinkExecutionBarrierClaim> | undefined;
|
|
229
|
+
try {
|
|
230
|
+
requireAdmission();
|
|
228
231
|
entry = this.executionBarrier?.enter();
|
|
229
232
|
} catch (error) {
|
|
230
233
|
return Promise.reject(error);
|
|
231
234
|
}
|
|
232
235
|
const submit = (claim?: ZLinkExecutionBarrierClaim): Promise<T> => {
|
|
233
236
|
try {
|
|
234
|
-
return this.scheduler.submitLifecycleOperation(
|
|
237
|
+
return this.scheduler.submitLifecycleOperation(() => {
|
|
238
|
+
requireAdmission();
|
|
239
|
+
return operation();
|
|
240
|
+
});
|
|
235
241
|
} catch (error) {
|
|
236
242
|
return Promise.reject(error);
|
|
237
243
|
} finally {
|