@zlink-systems/framework 0.28.0 → 0.29.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/.tsbuildinfo +1 -1
- package/dist/contracts/Configuration/Builders.d.ts +2 -0
- package/dist/contracts/Configuration/InternalDefaults.d.ts +3 -0
- package/dist/contracts/Configuration/InternalDefaults.js +4 -1
- package/dist/contracts/Configuration/RegistrationBuilders.js +9 -0
- package/dist/contracts/Configuration/RegistrationNormalizers.d.ts +1 -0
- package/dist/contracts/Configuration/RegistrationNormalizers.js +16 -0
- package/dist/contracts/Configuration/RegistrationTypes.d.ts +3 -0
- package/dist/runtime/actors/actor-mailbox.d.ts +2 -2
- package/dist/runtime/actors/actor-mailbox.js +2 -2
- package/dist/runtime/backend/node/node-raw-mesh-backend.js +1 -3
- package/dist/runtime/execution/serial-execution-queue.d.ts +12 -3
- package/dist/runtime/execution/serial-execution-queue.js +116 -28
- package/dist/runtime/foundation/operation-registry.d.ts +3 -2
- package/dist/runtime/foundation/operation-registry.js +5 -1
- package/dist/runtime/foundation/service-authority-payload-codec.d.ts +0 -2
- package/dist/runtime/foundation/service-authority-payload-codec.js +0 -43
- package/dist/runtime/foundation/service-stateful-registry.d.ts +4 -3
- package/dist/runtime/foundation/service-stateful-registry.js +5 -2
- package/dist/runtime/foundation/service-stateful-runtime.d.ts +1 -8
- package/dist/runtime/foundation/service-stateful-runtime.js +78 -126
- package/dist/runtime/foundation/service-stateful-wire-codec.d.ts +0 -11
- package/dist/runtime/foundation/service-stateful-wire-codec.js +1 -23
- package/dist/runtime/foundation/service-topology-registry.d.ts +5 -0
- package/dist/runtime/foundation/service-topology-registry.js +16 -17
- package/dist/runtime/host/instance-activation-authority.d.ts +0 -1
- package/dist/runtime/host/instance-activation-authority.js +46 -97
- package/dist/runtime/host/spot-address-transport.js +1 -1
- package/dist/runtime/host/stateful-authority-route-runtime.d.ts +1 -0
- package/dist/runtime/host/stateful-authority-route-runtime.js +17 -9
- package/dist/runtime/locations/domain-store-contract.d.ts +1 -0
- package/dist/runtime/locations/in-memory-authority-store.d.ts +2 -0
- package/dist/runtime/locations/in-memory-authority-store.js +22 -8
- package/dist/runtime/locations/in-memory-location-store.d.ts +1 -0
- package/dist/runtime/locations/in-memory-location-store.js +3 -0
- package/dist/runtime/locations/internal-store-contracts.d.ts +1 -1
- package/dist/runtime/locations/location-store-repository.d.ts +1 -0
- package/dist/runtime/locations/location-store-repository.js +92 -49
- package/dist/runtime/spots/spot-serial-executor.js +4 -1
- package/dist/runtime/spots/spot-serial-turn-executor.d.ts +3 -0
- package/dist/runtime/spots/spot-serial-turn-executor.js +5 -0
- package/dist/runtime/streams/index.d.ts +0 -1
- package/dist/runtime/streams/index.js +4 -3
- package/dist/runtime/streams/protocol.js +0 -9
- package/dist/runtime/streams/session-context.d.ts +0 -4
- package/dist/runtime/streams/session-context.js +0 -26
- package/dist/runtime/streams/session-serial-executor.d.ts +0 -1
- package/dist/runtime/streams/session-serial-executor.js +0 -3
- package/dist/runtime/streams/stream-message-utils.d.ts +0 -1
- package/dist/runtime/streams/stream-message-utils.js +0 -20
- package/dist/runtime/streams/stream-session-runtime.d.ts +6 -3
- package/dist/runtime/streams/stream-session-runtime.js +40 -85
- package/package.json +3 -3
- package/src/contracts/Configuration/Builders.ts +2 -0
- package/src/contracts/Configuration/InternalDefaults.ts +4 -0
- package/src/contracts/Configuration/RegistrationBuilders.ts +13 -0
- package/src/contracts/Configuration/RegistrationNormalizers.ts +25 -0
- package/src/contracts/Configuration/RegistrationTypes.ts +3 -0
- package/src/runtime/actors/actor-mailbox.ts +7 -2
- package/src/runtime/backend/node/node-raw-mesh-backend.ts +1 -7
- package/src/runtime/execution/serial-execution-queue.ts +117 -29
- package/src/runtime/foundation/operation-registry.ts +16 -9
- package/src/runtime/foundation/service-authority-payload-codec.ts +0 -46
- package/src/runtime/foundation/service-stateful-registry.ts +10 -5
- package/src/runtime/foundation/service-stateful-runtime.ts +103 -181
- package/src/runtime/foundation/service-stateful-wire-codec.ts +0 -25
- package/src/runtime/foundation/service-topology-registry.ts +30 -20
- package/src/runtime/host/instance-activation-authority.ts +58 -125
- package/src/runtime/host/spot-address-transport.ts +1 -1
- package/src/runtime/host/stateful-authority-route-runtime.ts +22 -9
- package/src/runtime/locations/domain-store-contract.ts +5 -0
- package/src/runtime/locations/in-memory-authority-store.ts +42 -8
- package/src/runtime/locations/in-memory-location-store.ts +8 -0
- package/src/runtime/locations/internal-store-contracts.ts +1 -0
- package/src/runtime/locations/location-store-repository.ts +123 -64
- package/src/runtime/protocol/service_wire_pilot_codec.generated.ts +1 -1
- package/src/runtime/spots/spot-serial-executor.ts +4 -1
- package/src/runtime/spots/spot-serial-turn-executor.ts +10 -0
- package/src/runtime/streams/index.ts +3 -1
- package/src/runtime/streams/protocol.ts +0 -13
- package/src/runtime/streams/session-context.ts +0 -32
- package/src/runtime/streams/session-serial-executor.ts +0 -8
- package/src/runtime/streams/stream-message-utils.ts +1 -27
- package/src/runtime/streams/stream-session-runtime.ts +54 -97
- package/dist/runtime/streams/session-requests.d.ts +0 -25
- package/dist/runtime/streams/session-requests.js +0 -101
- package/src/runtime/streams/session-requests.ts +0 -110
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
ZLinkAuthorityStore,
|
|
17
17
|
ZLinkObjectCreationStore
|
|
18
18
|
} from '../locations/internal-store-contracts';
|
|
19
|
-
import { ZLinkFrameworkException } from '../../contracts';
|
|
19
|
+
import { ZLinkFrameworkErrorKind, ZLinkFrameworkException } from '../../contracts';
|
|
20
20
|
import {
|
|
21
21
|
decodeServiceInstanceAuthorityPayload,
|
|
22
22
|
decodeServiceReadySpotAuthority,
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
|
|
47
47
|
const CREATION_REQUEST_RETENTION_MS = 24 * 60 * 60 * 1_000;
|
|
48
48
|
const MAX_CREATION_REQUEST_BYTES = 1024 * 1024;
|
|
49
|
-
const ACTIVATION_JOIN_POLL_INTERVAL_MS = 10;
|
|
50
49
|
|
|
51
50
|
interface PendingReservation {
|
|
52
51
|
readonly reservationId: string;
|
|
@@ -115,87 +114,81 @@ export class ZLinkInstanceActivationAuthority implements ServiceAsyncInstanceAct
|
|
|
115
114
|
}
|
|
116
115
|
let reserved: Awaited<ReturnType<ZLinkObjectCreationStore['reserve']>>;
|
|
117
116
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
{
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
117
|
+
reserved = await this.options.store.reserve(
|
|
118
|
+
{
|
|
119
|
+
key: { kind: 'instance_spot', globalId: target.targetSpotId },
|
|
120
|
+
intent: {
|
|
121
|
+
stableType: target.stableType,
|
|
122
|
+
requestContentReference: stored.reference.value,
|
|
123
|
+
requestSha256: requestHash,
|
|
124
|
+
requestEncodedSize: BigInt(requestBytes.byteLength)
|
|
125
|
+
},
|
|
126
|
+
target: {
|
|
127
|
+
meshName: this.options.meshName,
|
|
128
|
+
nodeRid: target.targetNodeRid,
|
|
129
|
+
nodeLifecycleGeneration: target.targetNodeGeneration,
|
|
130
|
+
owner
|
|
131
|
+
},
|
|
132
|
+
creatingPayload: encodeServiceInstanceAuthorityPayload({
|
|
133
|
+
state: 'coldActivating',
|
|
134
|
+
stableType: target.stableType,
|
|
135
|
+
spotId: target.targetSpotId,
|
|
136
|
+
ownerId: owner.ownerId,
|
|
137
|
+
ownerLeaseGeneration: owner.leaseGeneration,
|
|
138
|
+
ownerMeshName: this.options.meshName,
|
|
139
|
+
ownerNodeRid: target.targetNodeRid,
|
|
140
|
+
ownerNodeGeneration: target.targetNodeGeneration
|
|
141
|
+
}),
|
|
142
|
+
capacity: {
|
|
143
|
+
actors: 0,
|
|
144
|
+
spots: 1,
|
|
145
|
+
spotType: {
|
|
146
|
+
objectKind: 'instance_spot',
|
|
136
147
|
stableType: target.stableType,
|
|
137
|
-
|
|
138
|
-
ownerId: owner.ownerId,
|
|
139
|
-
ownerLeaseGeneration: owner.leaseGeneration,
|
|
140
|
-
ownerMeshName: this.options.meshName,
|
|
141
|
-
ownerNodeRid: target.targetNodeRid,
|
|
142
|
-
ownerNodeGeneration: target.targetNodeGeneration
|
|
143
|
-
}),
|
|
144
|
-
capacity: {
|
|
145
|
-
actors: 0,
|
|
146
|
-
spots: 1,
|
|
147
|
-
spotType: {
|
|
148
|
-
objectKind: 'instance_spot',
|
|
149
|
-
stableType: target.stableType,
|
|
150
|
-
count: 1
|
|
151
|
-
}
|
|
148
|
+
count: 1
|
|
152
149
|
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.options.metrics?.recordInstanceSpotClaimConflict(
|
|
159
|
-
this.options.meshName,
|
|
160
|
-
target.stableType,
|
|
161
|
-
'already_exists'
|
|
162
|
-
);
|
|
163
|
-
await this.deleteOrphan(stored.reference);
|
|
164
|
-
return readyExisting(reserved.current, target);
|
|
165
|
-
}
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
undefined,
|
|
153
|
+
activation.deadlineUnixMs
|
|
154
|
+
);
|
|
166
155
|
} catch (error) {
|
|
167
156
|
await this.deleteOrphan(stored.reference);
|
|
168
157
|
throw error;
|
|
169
158
|
}
|
|
170
|
-
if (
|
|
171
|
-
reserved.kind === 'conflict' &&
|
|
172
|
-
reserved.current.kind === 'snapshot' &&
|
|
173
|
-
reserved.current.allocation.objectKind === 'instance_spot' &&
|
|
174
|
-
reserved.current.allocation.stableType === target.stableType &&
|
|
175
|
-
reserved.current.allocation.state === 'reserved'
|
|
176
|
-
) {
|
|
177
|
-
this.options.metrics?.recordInstanceSpotClaimConflict(
|
|
178
|
-
this.options.meshName,
|
|
179
|
-
target.stableType,
|
|
180
|
-
'activation_in_progress'
|
|
181
|
-
);
|
|
182
|
-
await this.deleteOrphan(stored.reference);
|
|
183
|
-
return await this.awaitConcurrentActivation(target, activation.deadlineUnixMs);
|
|
184
|
-
}
|
|
185
159
|
if (reserved.kind !== 'reserved') {
|
|
160
|
+
const conflictReason =
|
|
161
|
+
reserved.kind === 'alreadyExists'
|
|
162
|
+
? 'already_exists'
|
|
163
|
+
: reserved.kind === 'conflict' &&
|
|
164
|
+
reserved.current.kind === 'snapshot' &&
|
|
165
|
+
reserved.current.allocation.objectKind === 'instance_spot' &&
|
|
166
|
+
reserved.current.allocation.stableType === target.stableType &&
|
|
167
|
+
reserved.current.allocation.state === 'reserved'
|
|
168
|
+
? 'activation_in_progress'
|
|
169
|
+
: reserved.kind;
|
|
186
170
|
this.options.metrics?.recordInstanceSpotClaimConflict(
|
|
187
171
|
this.options.meshName,
|
|
188
172
|
target.stableType,
|
|
189
|
-
|
|
173
|
+
conflictReason
|
|
190
174
|
);
|
|
191
175
|
await this.deleteOrphan(stored.reference);
|
|
176
|
+
if (reserved.kind === 'typeMismatch') {
|
|
177
|
+
throw createInternalFrameworkException(
|
|
178
|
+
ZLinkFrameworkInternalErrorKind.SpotTypeMismatch,
|
|
179
|
+
`Instance activation reservation failed: ${reserved.kind}.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
192
182
|
if (reserved.kind === 'placementCapacityExhausted') {
|
|
193
183
|
throw createInternalFrameworkException(
|
|
194
184
|
ZLinkFrameworkInternalErrorKind.PlacementCapacityExhausted,
|
|
195
185
|
`Instance Spot placement capacity is exhausted for '${String(target.targetSpotId)}'.`
|
|
196
186
|
);
|
|
197
187
|
}
|
|
198
|
-
throw new
|
|
188
|
+
throw new ZLinkFrameworkException(
|
|
189
|
+
ZLinkFrameworkErrorKind.Unavailable,
|
|
190
|
+
`Instance activation reservation failed: ${reserved.kind}.`
|
|
191
|
+
);
|
|
199
192
|
}
|
|
200
193
|
const reservation: ServiceInstanceActivationReservation = {
|
|
201
194
|
attempt: reserved.creating.objectGeneration,
|
|
@@ -455,34 +448,6 @@ export class ZLinkInstanceActivationAuthority implements ServiceAsyncInstanceAct
|
|
|
455
448
|
}
|
|
456
449
|
}
|
|
457
450
|
|
|
458
|
-
private async awaitConcurrentActivation(
|
|
459
|
-
target: ServiceInstanceActivationTarget,
|
|
460
|
-
deadlineUnixMs: bigint
|
|
461
|
-
): Promise<Extract<ServiceInstanceAuthorityRead, { readonly kind: 'ready' }>> {
|
|
462
|
-
const key = authorityKey(target.targetSpotId);
|
|
463
|
-
const deadlineMs = performance.now() + Number(deadlineUnixMs - BigInt(Date.now()));
|
|
464
|
-
while (performance.now() <= deadlineMs) {
|
|
465
|
-
const current = await this.options.store.readAuthority(key);
|
|
466
|
-
if (current.kind === 'snapshot') {
|
|
467
|
-
if (
|
|
468
|
-
current.allocation.objectKind !== 'instance_spot' ||
|
|
469
|
-
current.allocation.stableType !== target.stableType
|
|
470
|
-
) {
|
|
471
|
-
throw new Error('Concurrent Instance activation resolved to a different Spot type.');
|
|
472
|
-
}
|
|
473
|
-
if (current.allocation.state === 'active') {
|
|
474
|
-
return readyExisting(current, target);
|
|
475
|
-
}
|
|
476
|
-
} else {
|
|
477
|
-
throw new Error('Concurrent Instance activation ended before the Ready barrier.');
|
|
478
|
-
}
|
|
479
|
-
await waitForActivationJoin(deadlineMs);
|
|
480
|
-
}
|
|
481
|
-
throw new Error(
|
|
482
|
-
`Concurrent Instance activation for '${target.targetSpotId}' did not reach Ready before deadline.`
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
451
|
private requirePending(reservation: ServiceInstanceActivationReservation): PendingReservation {
|
|
487
452
|
const pending = this.pending.get(reservation.token);
|
|
488
453
|
if (pending === undefined || pending.creating.objectGeneration !== reservation.attempt) {
|
|
@@ -539,38 +504,6 @@ function readyRead(
|
|
|
539
504
|
return { kind: 'ready', route: routeFromSnapshot(snapshot), authority: snapshot };
|
|
540
505
|
}
|
|
541
506
|
|
|
542
|
-
function readyExisting(
|
|
543
|
-
snapshot: ZLinkAuthoritySnapshot,
|
|
544
|
-
target: ServiceInstanceActivationTarget
|
|
545
|
-
): Extract<ServiceInstanceAuthorityRead, { readonly kind: 'ready' }> {
|
|
546
|
-
const decoded =
|
|
547
|
-
decodeServiceReadySpotAuthority(snapshot.payload) ??
|
|
548
|
-
decodeServiceClosingSpotAuthority(snapshot.payload);
|
|
549
|
-
if (
|
|
550
|
-
decoded?.kind !== 'instance_spot' ||
|
|
551
|
-
decoded.stableType !== target.stableType ||
|
|
552
|
-
decoded.spotId !== target.targetSpotId ||
|
|
553
|
-
snapshot.allocation.objectKind !== 'instance_spot' ||
|
|
554
|
-
snapshot.allocation.state !== 'active' ||
|
|
555
|
-
decoded.ownerId !== snapshot.ownerId ||
|
|
556
|
-
decoded.ownerLeaseGeneration !== snapshot.ownerLeaseGeneration ||
|
|
557
|
-
decoded.ownerMeshName !== snapshot.allocation.descriptor.meshName ||
|
|
558
|
-
decoded.ownerNodeGeneration !== snapshot.allocation.descriptorLifecycleGeneration ||
|
|
559
|
-
!routingIdsEqual(decoded.ownerNodeRid, snapshot.allocation.descriptor.rid)
|
|
560
|
-
) {
|
|
561
|
-
throw new Error('Existing Instance authority is not a matching Ready allocation.');
|
|
562
|
-
}
|
|
563
|
-
return { kind: 'ready', route: routeFromSnapshot(snapshot), authority: snapshot };
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async function waitForActivationJoin(deadlineMs: number): Promise<void> {
|
|
567
|
-
const remaining = deadlineMs - performance.now();
|
|
568
|
-
if (remaining <= 0) return;
|
|
569
|
-
await new Promise<void>((resolve) => {
|
|
570
|
-
setTimeout(resolve, Math.min(ACTIVATION_JOIN_POLL_INTERVAL_MS, remaining));
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
|
|
574
507
|
function requireCommitIdentity(
|
|
575
508
|
target: ServiceInstanceActivationTarget,
|
|
576
509
|
reservation: ServiceInstanceActivationReservation,
|
|
@@ -576,7 +576,7 @@ export class ZLinkHostSpotAddressTransport implements ZLinkSpotAddressTransport
|
|
|
576
576
|
if (stableType === undefined) {
|
|
577
577
|
if (distinctTypes.length === 0) return { kind: 'unsupported' };
|
|
578
578
|
throw createInternalFrameworkException(
|
|
579
|
-
ZLinkFrameworkInternalErrorKind.
|
|
579
|
+
ZLinkFrameworkInternalErrorKind.InvalidOperation,
|
|
580
580
|
'Instance Spot type is required when multiple types are registered.'
|
|
581
581
|
);
|
|
582
582
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeAuthorityKey } from '../locations/authority-key-codec';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import type {
|
|
3
4
|
ZLinkAuthorityKey,
|
|
@@ -175,12 +176,25 @@ export class ZLinkStatefulAuthorityRouteRuntime {
|
|
|
175
176
|
|
|
176
177
|
const status = sink.status();
|
|
177
178
|
for (const pending of snapshot.pending) {
|
|
179
|
+
if (pending.targetMeshName !== meshName) continue;
|
|
178
180
|
if (
|
|
179
|
-
pending.targetMeshName === meshName &&
|
|
180
181
|
routingIdsEqual(status.routingId, pending.targetNodeRid) &&
|
|
181
182
|
status.lifecycleGeneration === pending.targetNodeGeneration
|
|
182
183
|
) {
|
|
183
184
|
await this.recoverPendingInstanceActivation(sink, pending, signal);
|
|
185
|
+
} else {
|
|
186
|
+
const release = this.options.creationStore?.releaseEndedReservation;
|
|
187
|
+
if (release === undefined)
|
|
188
|
+
throw new Error('Reservation lifecycle recovery requires a Location repository.');
|
|
189
|
+
if (
|
|
190
|
+
await release.call(
|
|
191
|
+
this.options.creationStore,
|
|
192
|
+
encodeAuthorityKey('instance_spot', pending.spotId),
|
|
193
|
+
pending.pending.storeVersion,
|
|
194
|
+
signal
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
await this.clearRecoveryRoot(pending.pending.requestReference, signal);
|
|
184
198
|
}
|
|
185
199
|
}
|
|
186
200
|
|
|
@@ -386,14 +400,7 @@ export class ZLinkStatefulAuthorityRouteRuntime {
|
|
|
386
400
|
signal
|
|
387
401
|
);
|
|
388
402
|
if (aborted.kind === 'aborted' || aborted.kind === 'alreadyAborted') {
|
|
389
|
-
|
|
390
|
-
await this.options.relocationStore?.delete(
|
|
391
|
-
relocationBlobReference(recovery.pending.requestReference)
|
|
392
|
-
);
|
|
393
|
-
} catch {
|
|
394
|
-
// The authority no longer publishes this root. Retention owns a
|
|
395
|
-
// failed best-effort orphan cleanup.
|
|
396
|
-
}
|
|
403
|
+
await this.clearRecoveryRoot(recovery.pending.requestReference, signal);
|
|
397
404
|
}
|
|
398
405
|
return;
|
|
399
406
|
}
|
|
@@ -401,6 +408,12 @@ export class ZLinkStatefulAuthorityRouteRuntime {
|
|
|
401
408
|
return;
|
|
402
409
|
}
|
|
403
410
|
|
|
411
|
+
private async clearRecoveryRoot(reference: string, signal?: AbortSignal): Promise<void> {
|
|
412
|
+
if (this.options.relocationStore === undefined)
|
|
413
|
+
throw new Error('Instance activation recovery requires a Relocation Store.');
|
|
414
|
+
await this.options.relocationStore.delete(relocationBlobReference(reference), signal);
|
|
415
|
+
}
|
|
416
|
+
|
|
404
417
|
private async readRecoveryEnvelope(
|
|
405
418
|
recovery: ServiceActivationRecoveryState,
|
|
406
419
|
signal?: AbortSignal
|
|
@@ -140,6 +140,11 @@ export interface ZLinkDomainLocationStore {
|
|
|
140
140
|
request: ZLinkObjectCreationCompleteRequest,
|
|
141
141
|
signal?: AbortSignal
|
|
142
142
|
): Promise<ZLinkObjectCreationCompleteResult>;
|
|
143
|
+
releaseEndedReservation?(
|
|
144
|
+
key: ZLinkAuthorityKey,
|
|
145
|
+
expectedStoreVersion: string,
|
|
146
|
+
signal?: AbortSignal
|
|
147
|
+
): Promise<boolean>;
|
|
143
148
|
abort(
|
|
144
149
|
request: ZLinkObjectAbortRequest,
|
|
145
150
|
signal?: AbortSignal,
|
|
@@ -276,6 +276,42 @@ export class ZLinkInMemoryAuthorityStore {
|
|
|
276
276
|
return BigInt(removed);
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
async releaseEndedReservation(
|
|
280
|
+
key: ZLinkAuthorityKey,
|
|
281
|
+
expectedStoreVersion: string,
|
|
282
|
+
signal?: AbortSignal
|
|
283
|
+
): Promise<boolean> {
|
|
284
|
+
signal?.throwIfAborted();
|
|
285
|
+
return this.tryReleaseEndedReservation(key, expectedStoreVersion);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private tryReleaseEndedReservation(
|
|
289
|
+
key: ZLinkAuthorityKey,
|
|
290
|
+
expectedStoreVersion: string
|
|
291
|
+
): boolean {
|
|
292
|
+
const current = this.rows.get(key.value);
|
|
293
|
+
if (
|
|
294
|
+
current === undefined ||
|
|
295
|
+
current.snapshot.storeVersion.value !== expectedStoreVersion ||
|
|
296
|
+
current.snapshot.allocation.state !== 'reserved' ||
|
|
297
|
+
current.creation === undefined ||
|
|
298
|
+
this.validation.isTargetLive(
|
|
299
|
+
current.snapshot.allocation.descriptor,
|
|
300
|
+
current.snapshot.allocation.descriptorLifecycleGeneration,
|
|
301
|
+
{
|
|
302
|
+
ownerId: current.snapshot.ownerId,
|
|
303
|
+
leaseGeneration: current.snapshot.ownerLeaseGeneration
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
return false;
|
|
308
|
+
this.adjustCapacity(this.pendingCapacity, current.snapshot.allocation, -1);
|
|
309
|
+
this.rows.delete(key.value);
|
|
310
|
+
this.creationTerminals.set(current.creation.reservationId, 'aborted');
|
|
311
|
+
this.scanRevision++;
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
279
315
|
async reserve(
|
|
280
316
|
request: ZLinkObjectReserveRequest,
|
|
281
317
|
signal?: AbortSignal
|
|
@@ -297,15 +333,13 @@ export class ZLinkInMemoryAuthorityStore {
|
|
|
297
333
|
if (current.snapshot.allocation.state === 'active') {
|
|
298
334
|
return { kind: 'alreadyExists', current: this.snapshot(current.snapshot) };
|
|
299
335
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
336
|
+
if (
|
|
337
|
+
!this.tryReleaseEndedReservation(
|
|
338
|
+
encodeAuthorityKey(request.key.kind, request.key.globalId),
|
|
339
|
+
current.snapshot.storeVersion.value
|
|
340
|
+
)
|
|
341
|
+
)
|
|
303
342
|
return { kind: 'conflict', current: this.read(key) };
|
|
304
|
-
}
|
|
305
|
-
this.adjustCapacity(this.pendingCapacity, current.snapshot.allocation, -1);
|
|
306
|
-
this.rows.delete(key);
|
|
307
|
-
this.creationTerminals.set(current.creation.reservationId, 'aborted');
|
|
308
|
-
this.scanRevision++;
|
|
309
343
|
}
|
|
310
344
|
const target = creationTarget(request);
|
|
311
345
|
if (
|
|
@@ -201,6 +201,14 @@ export class ZLinkInMemoryLocationStore
|
|
|
201
201
|
return this.authority.readCreationTerminal(operation, signal);
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
async releaseEndedReservation(
|
|
205
|
+
key: ZLinkAuthorityKey,
|
|
206
|
+
expectedStoreVersion: string,
|
|
207
|
+
signal?: AbortSignal
|
|
208
|
+
): Promise<boolean> {
|
|
209
|
+
return this.authority.releaseEndedReservation(key, expectedStoreVersion, signal);
|
|
210
|
+
}
|
|
211
|
+
|
|
204
212
|
async abort(
|
|
205
213
|
request: ZLinkObjectAbortRequest,
|
|
206
214
|
signal?: AbortSignal
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ServiceRelocationAuthorityPayloadCodec } from '../foundation/service-relocation-runtime';
|
|
1
2
|
import { UINT64_MAX } from '@zlink-systems/stream-wire';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { zlinkRuntimeDefaultLocationOptions } from '../../contracts/Locations/Options';
|
|
@@ -17,6 +18,8 @@ import {
|
|
|
17
18
|
type ZLinkStoreScanCursor,
|
|
18
19
|
type ZLinkStoreVersion,
|
|
19
20
|
type ZLinkStoreWriteRequest,
|
|
21
|
+
ZLinkFrameworkException,
|
|
22
|
+
ZLinkFrameworkErrorKind,
|
|
20
23
|
ZLinkFrameworkRuntimeState,
|
|
21
24
|
ZLinkObjectRole
|
|
22
25
|
} from '../../contracts';
|
|
@@ -204,7 +207,30 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
204
207
|
private readonly nowProvider: () => Date = () => new Date()
|
|
205
208
|
) {
|
|
206
209
|
super(nowProvider);
|
|
207
|
-
|
|
210
|
+
// Classify only failures of the opaque SPI, before interpreting its records.
|
|
211
|
+
this.provider = {
|
|
212
|
+
async read(key, signal) {
|
|
213
|
+
try {
|
|
214
|
+
return await provider.read(key, signal);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
throw locationStoreProviderFailure(error, signal);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
async write(request, signal) {
|
|
220
|
+
try {
|
|
221
|
+
return await provider.write(request, signal);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
throw locationStoreProviderFailure(error, signal);
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
async scan(request, signal) {
|
|
227
|
+
try {
|
|
228
|
+
return await provider.scan(request, signal);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
throw locationStoreProviderFailure(error, signal);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
};
|
|
208
234
|
this.aggregateInventory = new ZLinkAggregateInventoryStore(this.provider);
|
|
209
235
|
}
|
|
210
236
|
|
|
@@ -1099,6 +1125,77 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1099
1125
|
}
|
|
1100
1126
|
}
|
|
1101
1127
|
|
|
1128
|
+
override async releaseEndedReservation(
|
|
1129
|
+
key: ZLinkAuthorityKey,
|
|
1130
|
+
expectedStoreVersion: string,
|
|
1131
|
+
signal?: AbortSignal
|
|
1132
|
+
): Promise<boolean> {
|
|
1133
|
+
const rowKey = authorityKey(key.value);
|
|
1134
|
+
const current = await this.provider.read(rowKey, signal);
|
|
1135
|
+
if (current.kind === 'missing' || current.value.version.value !== expectedStoreVersion)
|
|
1136
|
+
return false;
|
|
1137
|
+
const record = decodeAuthorityRecord(current.value.bytes);
|
|
1138
|
+
const snapshot = record.snapshot;
|
|
1139
|
+
if (
|
|
1140
|
+
snapshot.allocation.state !== 'reserved' ||
|
|
1141
|
+
snapshot.pendingCreation === undefined ||
|
|
1142
|
+
record.aggregate !== undefined ||
|
|
1143
|
+
hasRelocationAuthority(snapshot.payload)
|
|
1144
|
+
)
|
|
1145
|
+
return false;
|
|
1146
|
+
const leaseKey = ownerKey(snapshot.ownerId);
|
|
1147
|
+
const descriptorKey = meshKey(
|
|
1148
|
+
snapshot.allocation.descriptor.meshName,
|
|
1149
|
+
snapshot.allocation.descriptor.rid
|
|
1150
|
+
);
|
|
1151
|
+
const capacityRowKey = capacityKey(
|
|
1152
|
+
snapshot.allocation.descriptor.meshName,
|
|
1153
|
+
String(snapshot.allocation.descriptor.rid)
|
|
1154
|
+
);
|
|
1155
|
+
const [lease, descriptor, capacity] = await Promise.all([
|
|
1156
|
+
this.provider.read(leaseKey, signal),
|
|
1157
|
+
this.provider.read(descriptorKey, signal),
|
|
1158
|
+
this.provider.read(capacityRowKey, signal)
|
|
1159
|
+
]);
|
|
1160
|
+
if (lease.kind === 'found' && lease.value.expiresAt === undefined)
|
|
1161
|
+
throw new Error('Location Store owner lease record is invalid.');
|
|
1162
|
+
if (
|
|
1163
|
+
sameLiveOwner(lease, snapshot) &&
|
|
1164
|
+
descriptor.kind === 'found' &&
|
|
1165
|
+
reviveMeshDescriptor(
|
|
1166
|
+
decodeCanonicalDescriptorRecord<ZLinkMeshNodeDescriptor>(descriptor.value.bytes).descriptor
|
|
1167
|
+
).lifecycleGeneration === snapshot.allocation.descriptorLifecycleGeneration
|
|
1168
|
+
)
|
|
1169
|
+
return false;
|
|
1170
|
+
const mutations: ZLinkStoreWriteRequest['mutations'][number][] = [
|
|
1171
|
+
{ kind: 'delete', key: rowKey }
|
|
1172
|
+
];
|
|
1173
|
+
if (capacity.kind === 'found') {
|
|
1174
|
+
const stored = decodeJson<CapacityRecord>(capacity.value.bytes);
|
|
1175
|
+
mutations.push({
|
|
1176
|
+
kind: 'put',
|
|
1177
|
+
key: capacityRowKey,
|
|
1178
|
+
bytes: encodeJson({
|
|
1179
|
+
active: stored.active,
|
|
1180
|
+
pending: subtractCapacity(stored.pending, snapshot.allocation.capacity)
|
|
1181
|
+
} satisfies CapacityRecord)
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const result = await this.provider.write(
|
|
1185
|
+
{
|
|
1186
|
+
conditions: [
|
|
1187
|
+
{ kind: 'version', key: rowKey, expected: current.value.version },
|
|
1188
|
+
conditionFor(leaseKey, lease),
|
|
1189
|
+
conditionFor(descriptorKey, descriptor),
|
|
1190
|
+
conditionFor(capacityRowKey, capacity)
|
|
1191
|
+
],
|
|
1192
|
+
mutations
|
|
1193
|
+
},
|
|
1194
|
+
signal
|
|
1195
|
+
);
|
|
1196
|
+
return result.kind === 'applied';
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1102
1199
|
override async reserve(
|
|
1103
1200
|
request: ZLinkObjectReserveRequest,
|
|
1104
1201
|
signal?: AbortSignal,
|
|
@@ -1130,62 +1227,13 @@ export class ZLinkLocationStoreRepository extends ZLinkInMemoryLocationStore {
|
|
|
1130
1227
|
return { kind: 'alreadyExists', current: snapshot };
|
|
1131
1228
|
}
|
|
1132
1229
|
if (
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
}
|
|
1140
|
-
// The row is Reserved by another owner. An unfinished creation
|
|
1141
|
-
// whose owner lease ended is cancellable, so reclaim it and retry
|
|
1142
|
-
// instead of blocking the key forever.
|
|
1143
|
-
const staleOwnerKey = ownerKey(snapshot.ownerId);
|
|
1144
|
-
const staleCapacityKey = capacityKey(
|
|
1145
|
-
snapshot.allocation.descriptor.meshName,
|
|
1146
|
-
String(snapshot.allocation.descriptor.rid)
|
|
1147
|
-
);
|
|
1148
|
-
const [staleOwnerRead, staleCapacityRead] = await Promise.all([
|
|
1149
|
-
this.provider.read(staleOwnerKey, signal),
|
|
1150
|
-
this.provider.read(staleCapacityKey, signal)
|
|
1151
|
-
]);
|
|
1152
|
-
if (staleOwnerRead.kind === 'found' && staleOwnerRead.value.expiresAt === undefined) {
|
|
1153
|
-
// A lease is always written with a positive TTL, so a missing
|
|
1154
|
-
// expiry is a corrupt record. Reclaiming deletes authority state,
|
|
1155
|
-
// so refuse rather than read the absence as expiry.
|
|
1156
|
-
throw new Error('Location Store owner lease record is invalid.');
|
|
1157
|
-
}
|
|
1158
|
-
if (sameLiveOwner(staleOwnerRead, record.snapshot)) {
|
|
1230
|
+
!(await this.releaseEndedReservation(
|
|
1231
|
+
encodedAuthorityKey,
|
|
1232
|
+
current.value.version.value,
|
|
1233
|
+
signal
|
|
1234
|
+
))
|
|
1235
|
+
)
|
|
1159
1236
|
return { kind: 'conflict', current: snapshot };
|
|
1160
|
-
}
|
|
1161
|
-
const staleCapacity =
|
|
1162
|
-
staleCapacityRead.kind === 'missing'
|
|
1163
|
-
? emptyCapacityRecord()
|
|
1164
|
-
: decodeJson<CapacityRecord>(staleCapacityRead.value.bytes);
|
|
1165
|
-
await this.provider.write(
|
|
1166
|
-
{
|
|
1167
|
-
conditions: [
|
|
1168
|
-
{ kind: 'version', key: rowKey, expected: current.value.version },
|
|
1169
|
-
versionCondition(staleOwnerKey, staleOwnerRead),
|
|
1170
|
-
conditionFor(staleCapacityKey, staleCapacityRead)
|
|
1171
|
-
],
|
|
1172
|
-
mutations: [
|
|
1173
|
-
{ kind: 'delete', key: rowKey },
|
|
1174
|
-
{
|
|
1175
|
-
kind: 'put',
|
|
1176
|
-
key: staleCapacityKey,
|
|
1177
|
-
bytes: encodeJson({
|
|
1178
|
-
active: staleCapacity.active,
|
|
1179
|
-
pending: subtractCapacity(
|
|
1180
|
-
staleCapacity.pending,
|
|
1181
|
-
record.snapshot.allocation.capacity
|
|
1182
|
-
)
|
|
1183
|
-
} satisfies CapacityRecord)
|
|
1184
|
-
}
|
|
1185
|
-
]
|
|
1186
|
-
},
|
|
1187
|
-
signal
|
|
1188
|
-
);
|
|
1189
1237
|
continue;
|
|
1190
1238
|
}
|
|
1191
1239
|
const [descriptorRead, leaseRead] = await Promise.all([
|
|
@@ -4037,15 +4085,10 @@ function sameCreationTarget(
|
|
|
4037
4085
|
);
|
|
4038
4086
|
}
|
|
4039
4087
|
|
|
4040
|
-
const
|
|
4088
|
+
const relocationAuthorityCodec = new ServiceRelocationAuthorityPayloadCodec();
|
|
4041
4089
|
|
|
4042
|
-
function
|
|
4043
|
-
return (
|
|
4044
|
-
payload.byteLength >= CANONICAL_AUTHORITY_MAGIC.byteLength &&
|
|
4045
|
-
Buffer.from(payload.buffer, payload.byteOffset, CANONICAL_AUTHORITY_MAGIC.byteLength).equals(
|
|
4046
|
-
CANONICAL_AUTHORITY_MAGIC
|
|
4047
|
-
)
|
|
4048
|
-
);
|
|
4090
|
+
function hasRelocationAuthority(payload: Uint8Array): boolean {
|
|
4091
|
+
return relocationAuthorityCodec.read(payload) !== undefined;
|
|
4049
4092
|
}
|
|
4050
4093
|
|
|
4051
4094
|
function sameLiveOwner(lease: ZLinkStoreReadResult, snapshot: StoredAuthoritySnapshot): boolean {
|
|
@@ -4441,3 +4484,19 @@ function throwIfOperationExpired(signal?: AbortSignal, deadlineUnixMs?: bigint):
|
|
|
4441
4484
|
throw new DOMException('Operation deadline exceeded.', 'TimeoutError');
|
|
4442
4485
|
}
|
|
4443
4486
|
}
|
|
4487
|
+
|
|
4488
|
+
// Store SPI §6: preserve caller failures and classify provider failures once.
|
|
4489
|
+
function locationStoreProviderFailure(error: unknown, signal?: AbortSignal): unknown {
|
|
4490
|
+
if (
|
|
4491
|
+
error instanceof ZLinkFrameworkException ||
|
|
4492
|
+
error instanceof TypeError ||
|
|
4493
|
+
error instanceof RangeError ||
|
|
4494
|
+
signal?.aborted === true
|
|
4495
|
+
)
|
|
4496
|
+
return error;
|
|
4497
|
+
return new ZLinkFrameworkException(
|
|
4498
|
+
ZLinkFrameworkErrorKind.Unavailable,
|
|
4499
|
+
'Location Store provider is unavailable.',
|
|
4500
|
+
error
|
|
4501
|
+
);
|
|
4502
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// <auto-generated> DO NOT EDIT. schema-sha256:
|
|
1
|
+
// <auto-generated> DO NOT EDIT. schema-sha256: 818fcd2f767793be8b5f626e5ac688807e671e546af2b11786e6fbaa02010ba5
|
|
2
2
|
export interface ServiceWireRouteFence { readonly id: string; readonly generation: bigint; readonly targetNodeRid: Uint8Array; readonly targetNodeGeneration: bigint; readonly expectedAuthorityOwnerGeneration: bigint; readonly expectedOwnerLeaseGeneration: bigint; }
|
|
3
3
|
export interface ApplicationPayloadEnvelopeV1 { readonly packetName: string; readonly contentType: string; readonly payload: Uint8Array; }
|
|
4
4
|
export interface ActorJoin28 { readonly correlation: bigint; readonly actor: ServiceWireRouteFence; readonly entry: boolean; readonly targetSpot: ServiceWireRouteFence; readonly payload?: ApplicationPayloadEnvelopeV1; }
|
|
@@ -134,7 +134,10 @@ export class ZLinkSpotSerialExecutor {
|
|
|
134
134
|
this.throwIfClosing();
|
|
135
135
|
let executor = this.actorExecutors.get(actorId);
|
|
136
136
|
if (executor === undefined) {
|
|
137
|
-
executor =
|
|
137
|
+
executor =
|
|
138
|
+
this.executionMode === ZLinkUserSpotExecutionMode.SpotWide
|
|
139
|
+
? this.spotSerial.createActorExecutor(actorId, this.sourceSpotId, this.actorOptions)
|
|
140
|
+
: new ZLinkActorSerialExecutor(actorId, this.sourceSpotId, this.actorOptions);
|
|
138
141
|
this.actorExecutors.set(actorId, executor);
|
|
139
142
|
}
|
|
140
143
|
return executor;
|