@zlink-systems/framework 0.11.0 → 0.12.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/EndpointNotation.d.ts +5 -4
- package/dist/contracts/Configuration/EndpointNotation.js +5 -4
- package/dist/contracts/Configuration/InternalDefaults.d.ts +0 -1
- package/dist/contracts/Configuration/InternalDefaults.js +1 -2
- package/dist/contracts/Configuration/RegistrationNormalizers.js +1 -2
- package/dist/contracts/Configuration/RegistrationTypes.d.ts +2 -5
- package/dist/contracts/Configuration/RegistrationValidators.js +0 -1
- package/dist/contracts/Errors/ZLinkFrameworkException.d.ts +6 -7
- package/dist/contracts/Errors/ZLinkFrameworkException.js +6 -7
- package/dist/runtime/actors/actor-client.js +1 -1
- package/dist/runtime/actors/actor-mailbox.js +1 -5
- package/dist/runtime/backend/contracts/index.d.ts +2 -0
- package/dist/runtime/backend/mesh-completion-table.d.ts +1 -3
- package/dist/runtime/backend/mesh-completion-table.js +11 -15
- package/dist/runtime/backend/mesh-dispatch-pump.d.ts +11 -4
- package/dist/runtime/backend/mesh-dispatch-pump.js +201 -134
- package/dist/runtime/backend/node/node-backend-adapter-factory.d.ts +3 -4
- package/dist/runtime/backend/node/node-backend-adapter-factory.js +51 -37
- package/dist/runtime/backend/node/node-backend-adapter-support.d.ts +2 -2
- package/dist/runtime/backend/node/node-backend-adapter-support.js +2 -2
- package/dist/runtime/backend/node/node-dealer-channel-client-transport.js +2 -2
- package/dist/runtime/backend/node/node-event-loop-poller.d.ts +16 -0
- package/dist/runtime/backend/node/node-event-loop-poller.js +49 -0
- package/dist/runtime/backend/node/node-raw-binding-port.js +24 -41
- package/dist/runtime/backend/node/node-raw-mesh-backend.d.ts +8 -8
- package/dist/runtime/backend/node/node-raw-mesh-backend.js +100 -107
- package/dist/runtime/backend/node/node-socket-backend-adapter.d.ts +3 -2
- package/dist/runtime/backend/node/node-socket-backend-adapter.js +18 -67
- package/dist/runtime/backend/raw-binding-port.d.ts +1 -0
- package/dist/runtime/backend/runtime-values.d.ts +5 -1
- package/dist/runtime/channels/channel-dispatchers.js +2 -2
- package/dist/runtime/channels/channel-envelope.js +324 -121
- package/dist/runtime/channels/channel-multipart.d.ts +8 -1
- package/dist/runtime/channels/channel-outbound-operations.js +0 -2
- package/dist/runtime/channels/channel-receive-loops.d.ts +1 -3
- package/dist/runtime/channels/channel-receive-loops.js +25 -65
- package/dist/runtime/channels/channel-socket-registry.js +3 -1
- package/dist/runtime/channels/channel-transports.d.ts +2 -1
- package/dist/runtime/channels/channel-transports.js +12 -10
- package/dist/runtime/execution/serial-execution-queue.d.ts +5 -18
- package/dist/runtime/execution/serial-execution-queue.js +16 -80
- package/dist/runtime/foundation/event-loop-resources.d.ts +0 -3
- package/dist/runtime/foundation/event-loop-resources.js +2 -11
- package/dist/runtime/foundation/operation-registry.d.ts +2 -8
- package/dist/runtime/foundation/operation-registry.js +3 -21
- package/dist/runtime/foundation/raw-service-mesh-runtime.d.ts +19 -7
- package/dist/runtime/foundation/raw-service-mesh-runtime.js +56 -10
- package/dist/runtime/foundation/service-mailbox.d.ts +1 -7
- package/dist/runtime/foundation/service-mailbox.js +3 -2
- package/dist/runtime/foundation/service-runtime-contracts.d.ts +2 -1
- package/dist/runtime/foundation/service-stateful-registry.d.ts +1 -1
- package/dist/runtime/foundation/service-stateful-registry.js +2 -2
- package/dist/runtime/foundation/service-stateful-runtime.js +14 -14
- package/dist/runtime/framework-errors-internal.d.ts +4 -4
- package/dist/runtime/framework-errors-internal.js +8 -10
- package/dist/runtime/host/actor-placement-coordinator.js +1 -1
- package/dist/runtime/host/index.js +1 -1
- package/dist/runtime/host/service-relocation-host-runtime.js +0 -4
- package/dist/runtime/host/spot-address-transport.js +1 -1
- package/dist/runtime/host/user-spot-creation-coordinator.js +1 -1
- package/dist/runtime/spots/spot-node-runtime-manager.d.ts +0 -12
- package/dist/runtime/spots/spot-node-runtime-manager.js +6 -166
- package/dist/runtime/spots/spot-serial-turn-executor.js +1 -4
- package/dist/runtime/spots/spot-timer.d.ts +3 -0
- package/dist/runtime/spots/spot-timer.js +87 -18
- package/dist/runtime/streams/session-serial-executor.js +1 -3
- package/dist/runtime/streams/stream-session-runtime.d.ts +2 -2
- package/dist/runtime/streams/stream-session-runtime.js +13 -19
- package/dist/runtime/workers/index.d.ts +0 -1
- package/dist/runtime/workers/index.js +1 -9
- package/package.json +3 -3
- package/src/contracts/Configuration/EndpointNotation.ts +5 -4
- package/src/contracts/Configuration/InternalDefaults.ts +0 -1
- package/src/contracts/Configuration/RegistrationNormalizers.ts +2 -4
- package/src/contracts/Configuration/RegistrationTypes.ts +2 -5
- package/src/contracts/Configuration/RegistrationValidators.ts +0 -1
- package/src/contracts/Errors/ZLinkFrameworkException.ts +6 -7
- package/src/runtime/actors/actor-client.ts +1 -1
- package/src/runtime/actors/actor-mailbox.ts +1 -12
- package/src/runtime/backend/contracts/index.ts +2 -0
- package/src/runtime/backend/mesh-completion-table.ts +12 -23
- package/src/runtime/backend/mesh-dispatch-pump.ts +206 -138
- package/src/runtime/backend/node/node-backend-adapter-factory.ts +66 -35
- package/src/runtime/backend/node/node-backend-adapter-support.ts +4 -4
- package/src/runtime/backend/node/node-dealer-channel-client-transport.ts +2 -2
- package/src/runtime/backend/node/node-event-loop-poller.ts +55 -0
- package/src/runtime/backend/node/node-raw-binding-port.ts +30 -45
- package/src/runtime/backend/node/node-raw-mesh-backend.ts +97 -110
- package/src/runtime/backend/node/node-socket-backend-adapter.ts +23 -69
- package/src/runtime/backend/raw-binding-port.ts +1 -0
- package/src/runtime/backend/runtime-values.ts +7 -1
- package/src/runtime/channels/channel-dispatchers.ts +2 -2
- package/src/runtime/channels/channel-envelope.ts +379 -109
- package/src/runtime/channels/channel-multipart.ts +9 -1
- package/src/runtime/channels/channel-outbound-operations.ts +0 -1
- package/src/runtime/channels/channel-receive-loops.ts +24 -75
- package/src/runtime/channels/channel-socket-registry.ts +4 -1
- package/src/runtime/channels/channel-transports.ts +20 -10
- package/src/runtime/execution/serial-execution-queue.ts +16 -108
- package/src/runtime/foundation/event-loop-resources.ts +2 -11
- package/src/runtime/foundation/operation-registry.ts +2 -22
- package/src/runtime/foundation/raw-service-mesh-runtime.ts +74 -19
- package/src/runtime/foundation/service-mailbox.ts +3 -13
- package/src/runtime/foundation/service-runtime-contracts.ts +2 -1
- package/src/runtime/foundation/service-stateful-registry.ts +2 -2
- package/src/runtime/foundation/service-stateful-runtime.ts +14 -14
- package/src/runtime/framework-errors-internal.ts +8 -10
- package/src/runtime/host/actor-placement-coordinator.ts +1 -1
- package/src/runtime/host/index.ts +2 -1
- package/src/runtime/host/service-relocation-host-runtime.ts +0 -4
- package/src/runtime/host/spot-address-transport.ts +1 -1
- package/src/runtime/host/user-spot-creation-coordinator.ts +1 -1
- package/src/runtime/spots/spot-node-runtime-manager.ts +13 -233
- package/src/runtime/spots/spot-serial-turn-executor.ts +1 -8
- package/src/runtime/spots/spot-timer.ts +109 -18
- package/src/runtime/streams/session-serial-executor.ts +1 -7
- package/src/runtime/streams/stream-session-runtime.ts +13 -17
- package/src/runtime/workers/index.ts +2 -17
|
@@ -104,16 +104,13 @@ export interface ZLinkLocationRegistration {
|
|
|
104
104
|
/**
|
|
105
105
|
* CPU worker-thread pool settings. I/O workers do not consume this pool.
|
|
106
106
|
* `minThreads` keeps a warm baseline, `maxThreads` bounds active CPU jobs,
|
|
107
|
-
* `idleTimeoutMs` reclaims workers above the baseline
|
|
108
|
-
*
|
|
109
|
-
* JavaScript caller omits a field: `0`, `max(2, availableParallelism())`,
|
|
110
|
-
* `30000`, and `1024`, respectively.
|
|
107
|
+
* `idleTimeoutMs` reclaims workers above the baseline. The registration
|
|
108
|
+
* normalizer supplies defaults when a JavaScript caller omits a field.
|
|
111
109
|
*/
|
|
112
110
|
export interface ZLinkWorkerOptions {
|
|
113
111
|
readonly minThreads: number;
|
|
114
112
|
readonly maxThreads: number;
|
|
115
113
|
readonly idleTimeoutMs: number;
|
|
116
|
-
readonly maxQueueLength: number;
|
|
117
114
|
}
|
|
118
115
|
|
|
119
116
|
export interface ZLinkCodecSerializerRegistration {
|
|
@@ -97,7 +97,6 @@ function validateWorkerOptions(worker: ZLinkWorkerOptions | undefined): void {
|
|
|
97
97
|
requireNonNegativeInteger('Worker minThreads', worker.minThreads);
|
|
98
98
|
requirePositiveInteger('Worker maxThreads', worker.maxThreads);
|
|
99
99
|
requireNonNegativeInteger('Worker idleTimeoutMs', worker.idleTimeoutMs);
|
|
100
|
-
requirePositiveInteger('Worker maxQueueLength', worker.maxQueueLength);
|
|
101
100
|
if (worker.maxThreads < worker.minThreads) {
|
|
102
101
|
throw new ZLinkConfigurationException(
|
|
103
102
|
'Worker maxThreads must be greater than or equal to minThreads.'
|
|
@@ -6,13 +6,12 @@ export enum ZLinkFrameworkErrorKind {
|
|
|
6
6
|
NotConfigured = 3,
|
|
7
7
|
Rejected = 4,
|
|
8
8
|
Unavailable = 5,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
InternalFailure = 12
|
|
9
|
+
DeadlineExceeded = 6,
|
|
10
|
+
ShuttingDown = 7,
|
|
11
|
+
ProtocolError = 8,
|
|
12
|
+
InvalidOperation = 9,
|
|
13
|
+
DataLost = 10,
|
|
14
|
+
InternalFailure = 11
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export class ZLinkFrameworkException extends Error {
|
|
@@ -916,7 +916,7 @@ function submitted(): ZLinkSubmitResult {
|
|
|
916
916
|
// the reply comes from a remote target, so a fine framework failure code
|
|
917
917
|
// refines the coarse terminal, and a terminal-only conflict/busy is the
|
|
918
918
|
// target's owner/queue state (retryable stale/Unavailable), never a
|
|
919
|
-
// source-owned
|
|
919
|
+
// source-owned queue exhaustion. Backpressure never appears on this reply
|
|
920
920
|
// path. Internal kinds are preserved so the stale-actor re-resolve retry
|
|
921
921
|
// (isStaleActorError) keeps working.
|
|
922
922
|
function actorFailureCodeKind(
|
|
@@ -5,10 +5,6 @@ import {
|
|
|
5
5
|
type ZLinkSerialWorkOptions,
|
|
6
6
|
type ZLinkSerialWorkRecord
|
|
7
7
|
} from '../execution/serial-execution-queue';
|
|
8
|
-
import {
|
|
9
|
-
ZLinkFrameworkInternalErrorKind,
|
|
10
|
-
createInternalFrameworkException
|
|
11
|
-
} from '../framework-errors-internal';
|
|
12
8
|
import { runZLinkActorExecution } from './actor-execution-context';
|
|
13
9
|
import {
|
|
14
10
|
bindApplicationJobPermit,
|
|
@@ -25,14 +21,7 @@ export class ZLinkActorSerialExecutor {
|
|
|
25
21
|
) {
|
|
26
22
|
this.scheduler = new ZLinkSerialExecutionQueue(
|
|
27
23
|
(record) => this.runRecord(record),
|
|
28
|
-
|
|
29
|
-
...options,
|
|
30
|
-
capacityError: options?.capacityError ?? (() =>
|
|
31
|
-
createInternalFrameworkException(
|
|
32
|
-
ZLinkFrameworkInternalErrorKind.WorkerQueueFull,
|
|
33
|
-
'Actor execution queue capacity was exceeded.'
|
|
34
|
-
))
|
|
35
|
-
}
|
|
24
|
+
options
|
|
36
25
|
);
|
|
37
26
|
}
|
|
38
27
|
|
|
@@ -578,6 +578,8 @@ export interface ZLinkBackendSubscriberSocket extends ZLinkBackendConnectableSoc
|
|
|
578
578
|
|
|
579
579
|
export interface ZLinkBackendReadablePoller {
|
|
580
580
|
wait(timeoutMs: number): boolean;
|
|
581
|
+
waitForReadable(signal?: AbortSignal): Promise<boolean>;
|
|
582
|
+
markDrained(): void;
|
|
581
583
|
dispose(): void;
|
|
582
584
|
}
|
|
583
585
|
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { ZLinkBufferMessage as Message } from './runtime-message';
|
|
2
|
+
import type { Message as ZLinkMessage } from '../../contracts/Common/Message';
|
|
2
3
|
import type {
|
|
3
4
|
MeshOperationId,
|
|
4
5
|
ReceiveKindData,
|
|
5
6
|
ReceiveRecord
|
|
6
7
|
} from '../foundation/service-runtime-contracts';
|
|
7
8
|
import { operationIdentityKey } from '../foundation/operation-identity';
|
|
8
|
-
import {
|
|
9
|
-
ZLinkFrameworkErrorKind,
|
|
10
|
-
ZLinkFrameworkException
|
|
11
|
-
} from '../../contracts';
|
|
12
|
-
|
|
13
|
-
export const ZLINK_MESH_COMPLETION_CAPACITY = 4_096;
|
|
14
9
|
|
|
15
10
|
export interface ZLinkMeshCompletionDiagnostic {
|
|
16
11
|
readonly kind: 'unknownOrLate';
|
|
@@ -36,14 +31,7 @@ export class ZLinkMeshCompletionTable {
|
|
|
36
31
|
private readonly pending = new Map<string, PendingCompletion>();
|
|
37
32
|
private disposed = false;
|
|
38
33
|
|
|
39
|
-
constructor(
|
|
40
|
-
private readonly maxPendingOperations = ZLINK_MESH_COMPLETION_CAPACITY,
|
|
41
|
-
private readonly onDiagnostic?: (diagnostic: ZLinkMeshCompletionDiagnostic) => void
|
|
42
|
-
) {
|
|
43
|
-
if (!Number.isSafeInteger(maxPendingOperations) || maxPendingOperations <= 0) {
|
|
44
|
-
throw new RangeError('maxPendingOperations must be a positive safe integer.');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
34
|
+
constructor(private readonly onDiagnostic?: (diagnostic: ZLinkMeshCompletionDiagnostic) => void) {}
|
|
47
35
|
|
|
48
36
|
/**
|
|
49
37
|
* Submits and registers without yielding control to the event loop. Mesh
|
|
@@ -63,12 +51,6 @@ export class ZLinkMeshCompletionTable {
|
|
|
63
51
|
signal.reason ?? new DOMException('The operation was aborted.', 'AbortError')
|
|
64
52
|
);
|
|
65
53
|
}
|
|
66
|
-
if (this.pending.size >= this.maxPendingOperations) {
|
|
67
|
-
return Promise.reject(new ZLinkFrameworkException(
|
|
68
|
-
ZLinkFrameworkErrorKind.CapacityExceeded,
|
|
69
|
-
`Mesh completion capacity ${this.maxPendingOperations} is exhausted.`
|
|
70
|
-
));
|
|
71
|
-
}
|
|
72
54
|
const operationId = operation();
|
|
73
55
|
// The backend submission callback can synchronously re-enter `dispose()`.
|
|
74
56
|
// TypeScript's control-flow analysis cannot observe mutation through it.
|
|
@@ -117,7 +99,7 @@ export class ZLinkMeshCompletionTable {
|
|
|
117
99
|
this.pending.delete(key);
|
|
118
100
|
pending.removeAbort?.();
|
|
119
101
|
try {
|
|
120
|
-
pending.resolve(
|
|
102
|
+
pending.resolve(retainCompletion(record));
|
|
121
103
|
} catch (error) {
|
|
122
104
|
pending.reject(error);
|
|
123
105
|
}
|
|
@@ -144,16 +126,23 @@ export class ZLinkMeshCompletionTable {
|
|
|
144
126
|
|
|
145
127
|
}
|
|
146
128
|
|
|
147
|
-
function
|
|
129
|
+
function retainCompletion(record: ReceiveRecord): ZLinkMeshCompletion {
|
|
148
130
|
return {
|
|
149
131
|
terminalResult: record.terminalResult,
|
|
150
132
|
failureErrno: record.failureErrno,
|
|
151
133
|
operationKind: record.operationKind,
|
|
152
134
|
kindData: record.kindData,
|
|
153
|
-
parts: record.parts.map(
|
|
135
|
+
parts: record.parts.map(retainCompletionPart)
|
|
154
136
|
};
|
|
155
137
|
}
|
|
156
138
|
|
|
139
|
+
function retainCompletionPart(part: ZLinkMessage): Message {
|
|
140
|
+
// A runtime message already owns a managed Buffer and close() is a no-op.
|
|
141
|
+
// Binding-native messages can expose native storage through data(), which
|
|
142
|
+
// becomes invalid when their owner closes; materialize that one boundary copy.
|
|
143
|
+
return part instanceof Message ? part : Message.fromOwned(Buffer.from(part.data()));
|
|
144
|
+
}
|
|
145
|
+
|
|
157
146
|
export function closeMeshCompletion(completion: ZLinkMeshCompletion): void {
|
|
158
147
|
closeParts(completion.parts);
|
|
159
148
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { availableParallelism } from 'node:os';
|
|
2
3
|
import { ZLINK_BACKEND_RECV_DONT_WAIT } from './runtime-values';
|
|
3
4
|
import {
|
|
4
5
|
ReadyDomain,
|
|
6
|
+
type ReadyBatch,
|
|
5
7
|
type ReadyRecord,
|
|
8
|
+
type ReceiveBatch,
|
|
6
9
|
type ReceiveRecord
|
|
7
10
|
} from '../foundation/service-runtime-contracts';
|
|
8
11
|
import type { ZLinkBackendMeshNode } from './contracts';
|
|
@@ -13,9 +16,11 @@ import type {
|
|
|
13
16
|
} from '../application-jobs/contracts';
|
|
14
17
|
import { runWithApplicationJobPermit } from '../application-jobs/application-job-queue-scope';
|
|
15
18
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
19
|
+
const MESH_DISPATCH_YIELD_RECORDS = 16;
|
|
20
|
+
const MESH_DISPATCH_YIELD_INTERVAL_MS = 2;
|
|
18
21
|
const MESH_DISPATCH_LIFECYCLE_CLAIM_BUDGET = 4;
|
|
22
|
+
const MESH_DISPATCH_RECEIVE_CAPACITY = 64;
|
|
23
|
+
const MESH_DISPATCH_MAX_APPLICATION_WORKERS = Math.max(2, availableParallelism());
|
|
19
24
|
// Captured while the runtime module is loaded, before any application owner
|
|
20
25
|
// exists. A ready callback may run inside a Spot turn; the shared Mesh pump
|
|
21
26
|
// must enter like an independent receive-loop task, without inheriting that
|
|
@@ -52,13 +57,15 @@ class ZLinkMeshDispatchFailure extends Error {
|
|
|
52
57
|
|
|
53
58
|
export class ZLinkMeshDispatchPump {
|
|
54
59
|
private pendingDomains: number = ReadyDomain.None;
|
|
55
|
-
private scheduled = false;
|
|
56
60
|
private infrastructureScheduled = false;
|
|
57
61
|
private disposed = false;
|
|
58
|
-
private drainPromise?: Promise<void>;
|
|
59
62
|
private infrastructureDrainPromise?: Promise<void>;
|
|
60
63
|
private readonly activeDrains = new Set<Promise<void>>();
|
|
64
|
+
private readonly idleApplicationWorkers = new Set<() => void>();
|
|
65
|
+
private applicationWorkerCount = 0;
|
|
61
66
|
private readonly capacityStop = new AbortController();
|
|
67
|
+
private recordsSinceYield = 0;
|
|
68
|
+
private yieldStartedAtMs = 0;
|
|
62
69
|
|
|
63
70
|
constructor(
|
|
64
71
|
private readonly node: ZLinkBackendMeshNode,
|
|
@@ -67,12 +74,12 @@ export class ZLinkMeshDispatchPump {
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
start(): void {
|
|
77
|
+
if (this.applicationWorkerCount === 0) this.startApplicationWorker();
|
|
70
78
|
this.node.setReadyHandler((domains) => {
|
|
71
79
|
if (this.disposed) {
|
|
72
80
|
return ReadyDomain.None;
|
|
73
81
|
}
|
|
74
|
-
this.
|
|
75
|
-
this.schedule();
|
|
82
|
+
this.markReady(domains);
|
|
76
83
|
return domains;
|
|
77
84
|
});
|
|
78
85
|
}
|
|
@@ -84,43 +91,102 @@ export class ZLinkMeshDispatchPump {
|
|
|
84
91
|
this.disposed = true;
|
|
85
92
|
this.capacityStop.abort();
|
|
86
93
|
this.pendingDomains = ReadyDomain.None;
|
|
94
|
+
for (const wake of this.idleApplicationWorkers) wake();
|
|
95
|
+
this.idleApplicationWorkers.clear();
|
|
87
96
|
while (this.activeDrains.size > 0) {
|
|
88
97
|
await Promise.all([...this.activeDrains]);
|
|
89
98
|
}
|
|
90
99
|
}
|
|
91
100
|
|
|
92
|
-
private
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
private markReady(domains: number): void {
|
|
102
|
+
if (domains === ReadyDomain.None || this.disposed) return;
|
|
103
|
+
const previous = this.pendingDomains;
|
|
104
|
+
this.pendingDomains |= domains;
|
|
105
|
+
const startInfrastructure =
|
|
106
|
+
(domains & ReadyDomain.Infrastructure) !== 0
|
|
107
|
+
&& (previous & ReadyDomain.Infrastructure) === 0;
|
|
108
|
+
const startApplication =
|
|
109
|
+
(domains & ReadyDomain.Application) !== 0
|
|
110
|
+
&& (previous & ReadyDomain.Application) === 0;
|
|
111
|
+
if (startInfrastructure && startApplication) {
|
|
112
|
+
const startTurn = this.yieldIfNeeded() ?? Promise.resolve();
|
|
113
|
+
this.scheduleInfrastructure(startTurn);
|
|
97
114
|
return;
|
|
98
115
|
}
|
|
99
|
-
if (this.
|
|
116
|
+
if (startInfrastructure) this.scheduleInfrastructure();
|
|
117
|
+
if (startApplication) {
|
|
118
|
+
if ((domains & ReadyDomain.Infrastructure) !== 0) {
|
|
119
|
+
setImmediate(() => this.wakeApplicationWorker());
|
|
120
|
+
} else {
|
|
121
|
+
this.wakeApplicationWorker();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private wakeApplicationWorker(): void {
|
|
127
|
+
if (this.disposed || (this.pendingDomains & ReadyDomain.Application) === 0) return;
|
|
128
|
+
const idle = this.idleApplicationWorkers.values().next().value as (() => void) | undefined;
|
|
129
|
+
if (idle !== undefined) {
|
|
130
|
+
this.idleApplicationWorkers.delete(idle);
|
|
131
|
+
this.pendingDomains &= ~ReadyDomain.Application;
|
|
132
|
+
idle();
|
|
100
133
|
return;
|
|
101
134
|
}
|
|
102
|
-
this.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
135
|
+
if (this.applicationWorkerCount < MESH_DISPATCH_MAX_APPLICATION_WORKERS) {
|
|
136
|
+
this.startApplicationWorker();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private startApplicationWorker(): void {
|
|
141
|
+
this.applicationWorkerCount += 1;
|
|
142
|
+
let worker: Promise<void>;
|
|
143
|
+
worker = detachedMeshDispatchScope(() =>
|
|
144
|
+
runZLinkExecutionArea('application', () => this.runApplicationWorker()))
|
|
145
|
+
.finally(() => {
|
|
146
|
+
this.applicationWorkerCount -= 1;
|
|
147
|
+
this.activeDrains.delete(worker);
|
|
111
148
|
});
|
|
112
|
-
this.activeDrains.add(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
149
|
+
this.activeDrains.add(worker);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async runApplicationWorker(): Promise<void> {
|
|
153
|
+
// A worker claims one owner at a time so a suspended handler cannot hold
|
|
154
|
+
// unrelated owners. Each owner may supply up to 64 pre-admitted records.
|
|
155
|
+
const readyBatch = this.node.createReadyBatch(1);
|
|
156
|
+
const receiveBatch = this.node.createReceiveBatch(
|
|
157
|
+
MESH_DISPATCH_RECEIVE_CAPACITY, this.options.partCapacity ?? 256
|
|
158
|
+
);
|
|
159
|
+
try {
|
|
160
|
+
for (;;) {
|
|
161
|
+
await this.waitForApplicationReady();
|
|
162
|
+
if (this.disposed) return;
|
|
163
|
+
try {
|
|
164
|
+
await this.drainDomain(ReadyDomain.Application, readyBatch, receiveBatch);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.reportDispatchError(error);
|
|
121
167
|
}
|
|
122
168
|
}
|
|
123
|
-
}
|
|
169
|
+
} finally {
|
|
170
|
+
receiveBatch.close();
|
|
171
|
+
readyBatch.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private waitForApplicationReady(): Promise<void> {
|
|
176
|
+
if (this.disposed) return Promise.resolve();
|
|
177
|
+
if ((this.pendingDomains & ReadyDomain.Application) !== 0) {
|
|
178
|
+
this.pendingDomains &= ~ReadyDomain.Application;
|
|
179
|
+
return Promise.resolve();
|
|
180
|
+
}
|
|
181
|
+
return new Promise(resolve => this.idleApplicationWorkers.add(resolve));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private reportDispatchError(error: unknown): void {
|
|
185
|
+
if (error instanceof ZLinkMeshDispatchFailure) {
|
|
186
|
+
this.options.reportError?.(error.dispatchCause, error.context);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.options.reportError?.(error);
|
|
124
190
|
}
|
|
125
191
|
|
|
126
192
|
/**
|
|
@@ -130,13 +196,14 @@ export class ZLinkMeshDispatchPump {
|
|
|
130
196
|
* the infrastructure mailbox and prevent the relocation coordinator from
|
|
131
197
|
* completing the drain that releases the application work.
|
|
132
198
|
*/
|
|
133
|
-
private scheduleInfrastructure(): void {
|
|
199
|
+
private scheduleInfrastructure(startTurn?: Promise<void>): void {
|
|
134
200
|
if (this.infrastructureScheduled) {
|
|
135
201
|
return;
|
|
136
202
|
}
|
|
137
203
|
this.infrastructureScheduled = true;
|
|
138
|
-
const drain =
|
|
139
|
-
.then(() => detachedMeshDispatchScope(() =>
|
|
204
|
+
const drain = (startTurn ?? this.yieldIfNeeded() ?? Promise.resolve())
|
|
205
|
+
.then(() => detachedMeshDispatchScope(() =>
|
|
206
|
+
runZLinkExecutionArea('infrastructure', () => this.drainInfrastructure())))
|
|
140
207
|
.catch((error) => {
|
|
141
208
|
if (error instanceof ZLinkMeshDispatchFailure) {
|
|
142
209
|
this.options.reportError?.(error.dispatchCause, error.context);
|
|
@@ -158,44 +225,45 @@ export class ZLinkMeshDispatchPump {
|
|
|
158
225
|
});
|
|
159
226
|
}
|
|
160
227
|
|
|
161
|
-
private async drain(): Promise<void> {
|
|
162
|
-
while (!this.disposed) {
|
|
163
|
-
if ((this.pendingDomains & ReadyDomain.Application) === 0) {
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
this.pendingDomains &= ~ReadyDomain.Application;
|
|
167
|
-
await this.drainDomain(ReadyDomain.Application);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
228
|
private async drainInfrastructure(): Promise<void> {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
this.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
229
|
+
const readyBatch = this.node.createReadyBatch(
|
|
230
|
+
Math.min(this.options.readyCapacity ?? 32, MESH_DISPATCH_LIFECYCLE_CLAIM_BUDGET)
|
|
231
|
+
);
|
|
232
|
+
const receiveBatch = this.node.createReceiveBatch(
|
|
233
|
+
MESH_DISPATCH_RECEIVE_CAPACITY, this.options.partCapacity ?? 256
|
|
234
|
+
);
|
|
235
|
+
try {
|
|
236
|
+
while (!this.disposed) {
|
|
237
|
+
if ((this.pendingDomains & ReadyDomain.Infrastructure) === 0) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
this.pendingDomains &= ~ReadyDomain.Infrastructure;
|
|
241
|
+
if ((this.pendingDomains & ReadyDomain.Application) !== 0) {
|
|
242
|
+
setImmediate(() => this.wakeApplicationWorker());
|
|
243
|
+
}
|
|
244
|
+
const lifecycleBudgetExhausted = await this.drainDomain(
|
|
245
|
+
ReadyDomain.Infrastructure,
|
|
246
|
+
readyBatch,
|
|
247
|
+
receiveBatch,
|
|
248
|
+
MESH_DISPATCH_LIFECYCLE_CLAIM_BUDGET
|
|
249
|
+
);
|
|
250
|
+
if (lifecycleBudgetExhausted) {
|
|
251
|
+
this.pendingDomains |= ReadyDomain.Infrastructure;
|
|
252
|
+
await yieldToEventLoop();
|
|
253
|
+
}
|
|
183
254
|
}
|
|
255
|
+
} finally {
|
|
256
|
+
receiveBatch.close();
|
|
257
|
+
readyBatch.close();
|
|
184
258
|
}
|
|
185
259
|
}
|
|
186
260
|
|
|
187
|
-
private async drainDomain(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
const readyBatch = this.node.createReadyBatch(readyCapacity);
|
|
194
|
-
// One record per claim receive keeps one ordinary-ingress permit paired
|
|
195
|
-
// with exactly one dispatch turn. Terminal completion claims bypass it.
|
|
196
|
-
const receiveBatch = this.node.createReceiveBatch(1, this.options.partCapacity ?? 256);
|
|
197
|
-
let receiveBatchesSinceTimerYield = 0;
|
|
198
|
-
let timerYieldStartedAtMs = this.nowMs();
|
|
261
|
+
private async drainDomain(
|
|
262
|
+
domain: number,
|
|
263
|
+
readyBatch: ReadyBatch,
|
|
264
|
+
receiveBatch: ReceiveBatch,
|
|
265
|
+
claimBudget?: number
|
|
266
|
+
): Promise<boolean> {
|
|
199
267
|
let claimsDrained = 0;
|
|
200
268
|
try {
|
|
201
269
|
for (;;) {
|
|
@@ -210,59 +278,46 @@ export class ZLinkMeshDispatchPump {
|
|
|
210
278
|
return false;
|
|
211
279
|
}
|
|
212
280
|
if (domain === ReadyDomain.Application && drained.hasResidue) {
|
|
213
|
-
this.
|
|
281
|
+
this.markReady(ReadyDomain.Application);
|
|
214
282
|
}
|
|
215
283
|
for (let index = 0; index < drained.records.length; index += 1) {
|
|
216
284
|
const claim = readyBatch.takeClaim(index);
|
|
217
285
|
claimsDrained += 1;
|
|
286
|
+
const owner = drained.records[index];
|
|
287
|
+
// Raw ingress already attached one host permit to each record.
|
|
288
|
+
// A claim that reserves admission here may receive only that permit's record.
|
|
218
289
|
try {
|
|
219
|
-
|
|
290
|
+
const capacity = owner.ordinaryIngressPreAdmitted === true
|
|
291
|
+
? MESH_DISPATCH_RECEIVE_CAPACITY : 1;
|
|
220
292
|
for (;;) {
|
|
221
|
-
|
|
222
|
-
const claimPermit = owner.terminalCompletion === true
|
|
293
|
+
let claimPermit = owner.terminalCompletion === true
|
|
223
294
|
|| owner.ordinaryIngressPreAdmitted === true
|
|
224
295
|
|| domain === ReadyDomain.Infrastructure
|
|
225
296
|
? undefined
|
|
226
297
|
: await this.acquirePermit();
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const permit = record.applicationJobPermit ?? claimPermit;
|
|
241
|
-
if (owner.ordinaryIngressPreAdmitted === true && permit === undefined) {
|
|
242
|
-
record.releaseRetainedIngress?.();
|
|
243
|
-
throw new Error('Pre-admitted raw ingress record lost its Application Job Queue permit.');
|
|
298
|
+
try {
|
|
299
|
+
if (this.capacityStop.signal.aborted) return false;
|
|
300
|
+
receiveBatch.reset(capacity);
|
|
301
|
+
const received = claim.recvBatch(receiveBatch, ZLINK_BACKEND_RECV_DONT_WAIT);
|
|
302
|
+
if (!received.ok) break;
|
|
303
|
+
// This owner keeps its claim while it drains. Other owners and
|
|
304
|
+
// infrastructure may progress if one of its handlers suspends.
|
|
305
|
+
if (domain === ReadyDomain.Infrastructure) {
|
|
306
|
+
this.infrastructureScheduled = false;
|
|
307
|
+
this.infrastructureDrainPromise = undefined;
|
|
308
|
+
if ((this.pendingDomains & ReadyDomain.Infrastructure) !== 0) {
|
|
309
|
+
this.scheduleInfrastructure();
|
|
310
|
+
}
|
|
244
311
|
}
|
|
245
312
|
try {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
this.scheduled = false;
|
|
255
|
-
this.drainPromise = undefined;
|
|
256
|
-
if (domain === ReadyDomain.Infrastructure) {
|
|
257
|
-
this.infrastructureScheduled = false;
|
|
258
|
-
this.infrastructureDrainPromise = undefined;
|
|
259
|
-
}
|
|
260
|
-
if (this.pendingDomains !== ReadyDomain.None) {
|
|
261
|
-
this.schedule();
|
|
262
|
-
}
|
|
263
|
-
const dispatch = () => runZLinkExecutionArea(
|
|
264
|
-
domain === ReadyDomain.Infrastructure ? 'infrastructure' : 'application',
|
|
265
|
-
async () => {
|
|
313
|
+
for (const record of received.records) {
|
|
314
|
+
if (this.disposed) break;
|
|
315
|
+
const permit = record.applicationJobPermit ?? claimPermit;
|
|
316
|
+
if (owner.ordinaryIngressPreAdmitted === true && permit === undefined) {
|
|
317
|
+
throw new Error('Pre-admitted raw ingress record lost its Application Job Queue permit.');
|
|
318
|
+
}
|
|
319
|
+
this.recordsSinceYield += 1;
|
|
320
|
+
const dispatch = async () => {
|
|
266
321
|
try {
|
|
267
322
|
await this.options.dispatch(drained.records[index], record);
|
|
268
323
|
} catch (error) {
|
|
@@ -272,55 +327,67 @@ export class ZLinkMeshDispatchPump {
|
|
|
272
327
|
);
|
|
273
328
|
}
|
|
274
329
|
await record.onTerminalCompletion?.();
|
|
330
|
+
};
|
|
331
|
+
if (permit === undefined) {
|
|
332
|
+
await dispatch();
|
|
333
|
+
} else {
|
|
334
|
+
if (
|
|
335
|
+
domain === ReadyDomain.Application
|
|
336
|
+
&& record.applicationJobPermit === undefined
|
|
337
|
+
) permit.markApplicationQueued();
|
|
338
|
+
// The scope now owns this reservation, including failure
|
|
339
|
+
// and detached handler handoff. Only unused permits remain
|
|
340
|
+
// owned by the receive attempt's finally block.
|
|
341
|
+
if (permit === claimPermit) claimPermit = undefined;
|
|
342
|
+
await runWithApplicationJobPermit(permit, dispatch);
|
|
275
343
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
await dispatch();
|
|
279
|
-
} else {
|
|
280
|
-
if (
|
|
281
|
-
domain === ReadyDomain.Application
|
|
282
|
-
&& record.applicationJobPermit === undefined
|
|
283
|
-
) permit.markApplicationQueued();
|
|
284
|
-
await runWithApplicationJobPermit(permit, dispatch);
|
|
344
|
+
const yieldTurn = this.yieldIfNeeded();
|
|
345
|
+
if (yieldTurn !== undefined) await yieldTurn;
|
|
285
346
|
}
|
|
286
347
|
} finally {
|
|
287
|
-
|
|
288
|
-
|
|
348
|
+
// Batch ownership includes records whose dispatch never began,
|
|
349
|
+
// including on failure or shutdown in the middle of the batch.
|
|
350
|
+
for (const record of received.records) {
|
|
351
|
+
for (const part of record.parts) part.close();
|
|
352
|
+
record.releaseRetainedIngress?.();
|
|
289
353
|
}
|
|
290
|
-
record.releaseRetainedIngress?.();
|
|
291
354
|
}
|
|
355
|
+
} finally {
|
|
356
|
+
claimPermit?.releaseAfterInternalProcessing();
|
|
292
357
|
}
|
|
293
|
-
// A continuously readable owner must not keep the timers phase
|
|
294
|
-
// from running, but a timer turn for every single record adds a
|
|
295
|
-
// Promise, closure, and timer allocation to the hot path.
|
|
296
|
-
receiveBatchesSinceTimerYield += 1;
|
|
297
|
-
if (
|
|
298
|
-
receiveBatchesSinceTimerYield >= MESH_DISPATCH_TIMER_YIELD_BATCHES
|
|
299
|
-
|| this.nowMs() - timerYieldStartedAtMs >= MESH_DISPATCH_TIMER_YIELD_INTERVAL_MS
|
|
300
|
-
) {
|
|
301
|
-
receiveBatchesSinceTimerYield = 0;
|
|
302
|
-
await yieldToTimers();
|
|
303
|
-
timerYieldStartedAtMs = this.nowMs();
|
|
304
|
-
}
|
|
305
|
-
receiveBatch.reset();
|
|
306
358
|
}
|
|
307
359
|
} finally {
|
|
308
360
|
claim.release();
|
|
309
361
|
}
|
|
310
362
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
363
|
+
if (claimBudget !== undefined && claimsDrained >= claimBudget) {
|
|
364
|
+
this.wakeApplicationWorker();
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
// Empty ready claims also count as work, so they cannot monopolize
|
|
368
|
+
// the microtask queue without giving I/O and deadlines a turn.
|
|
369
|
+
this.recordsSinceYield += 1;
|
|
370
|
+
const yieldTurn = this.yieldIfNeeded();
|
|
371
|
+
if (yieldTurn !== undefined) await yieldTurn;
|
|
314
372
|
if (!drained.hasResidue) {
|
|
315
373
|
return false;
|
|
316
374
|
}
|
|
317
375
|
}
|
|
318
376
|
} finally {
|
|
319
|
-
|
|
320
|
-
readyBatch.close();
|
|
377
|
+
readyBatch.reset();
|
|
321
378
|
}
|
|
322
379
|
}
|
|
323
380
|
|
|
381
|
+
private yieldIfNeeded(): Promise<void> | undefined {
|
|
382
|
+
const nowMs = this.nowMs();
|
|
383
|
+
if (this.recordsSinceYield < MESH_DISPATCH_YIELD_RECORDS
|
|
384
|
+
&& nowMs - this.yieldStartedAtMs < MESH_DISPATCH_YIELD_INTERVAL_MS) return undefined;
|
|
385
|
+
this.recordsSinceYield = 0;
|
|
386
|
+
return yieldToTimers().then(() => {
|
|
387
|
+
this.yieldStartedAtMs = this.nowMs();
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
324
391
|
private nowMs(): number {
|
|
325
392
|
return this.options.monotonicNowMs?.() ?? performance.now();
|
|
326
393
|
}
|
|
@@ -358,6 +425,7 @@ function yieldToEventLoop(): Promise<void> {
|
|
|
358
425
|
return new Promise((resolve) => setImmediate(resolve));
|
|
359
426
|
}
|
|
360
427
|
|
|
428
|
+
|
|
361
429
|
function yieldToTimers(): Promise<void> {
|
|
362
430
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
363
431
|
}
|