@zlink-systems/framework 0.10.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 +6 -1
- package/dist/runtime/backend/runtime-values.js +6 -0
- 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 +14 -1
- package/dist/runtime/channels/channel-transports.d.ts +2 -1
- package/dist/runtime/channels/channel-transports.js +12 -10
- package/dist/runtime/channels/client-server-location-runtime.js +15 -3
- 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/locations/location-store-repository.d.ts +1 -0
- package/dist/runtime/locations/location-store-repository.js +64 -75
- 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 +9 -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 +13 -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 +17 -1
- package/src/runtime/channels/channel-transports.ts +20 -10
- package/src/runtime/channels/client-server-location-runtime.ts +16 -2
- 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/locations/location-store-repository.ts +118 -80
- 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
|
@@ -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
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Context,
|
|
3
3
|
CoreHwmBudgetSnapshot,
|
|
4
|
-
Received,
|
|
5
4
|
TopicMessage
|
|
6
5
|
} from '@zlink-systems/zlink';
|
|
7
6
|
import type { ZLinkCoreHwmOptions } from '../../../contracts/Configuration';
|
|
@@ -11,6 +10,7 @@ import type {
|
|
|
11
10
|
ZLinkBackendDealerSocket,
|
|
12
11
|
ZLinkBackendPublisherSocket,
|
|
13
12
|
ZLinkBackendReadablePoller,
|
|
13
|
+
ZLinkBackendReceived,
|
|
14
14
|
ZLinkBackendRouterSocket,
|
|
15
15
|
ZLinkBackendSocket,
|
|
16
16
|
ZLinkBackendSocketMonitor,
|
|
@@ -24,18 +24,15 @@ import type {
|
|
|
24
24
|
import {
|
|
25
25
|
closeWithBusyRetry,
|
|
26
26
|
isContextTerminatedError,
|
|
27
|
-
isPollerInterruptedError,
|
|
28
27
|
zlink,
|
|
29
28
|
type ZLinkBindingModule
|
|
30
29
|
} from './node-backend-adapter-support';
|
|
31
30
|
import { wrapMonitorSocket } from './node-monitor-backend-adapter';
|
|
32
|
-
import { wrapSocket } from './node-socket-backend-adapter';
|
|
31
|
+
import { nodeEventLoopPollerOf, wrapSocket } from './node-socket-backend-adapter';
|
|
33
32
|
import { ZLinkNodeMeshBackendAdapter } from './node-mesh-backend-adapter';
|
|
34
33
|
|
|
35
|
-
export { isDisconnectRouteNotFoundError } from './node-socket-backend-adapter';
|
|
36
|
-
|
|
37
34
|
export class ZLinkNodeBackendAdapterFactory implements ZLinkBackendAdapterFactory {
|
|
38
|
-
createReceived():
|
|
35
|
+
createReceived(): ZLinkBackendReceived {
|
|
39
36
|
return new zlink.Received();
|
|
40
37
|
}
|
|
41
38
|
|
|
@@ -70,11 +67,17 @@ class ZLinkNodeChannelBackendAdapter implements ZLinkChannelBackendAdapter {
|
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
createDealerSocket(context: ZLinkBackendContext): ZLinkBackendDealerSocket {
|
|
73
|
-
return wrapSocket(
|
|
70
|
+
return wrapSocket(
|
|
71
|
+
zlink.createDealerSocket(asNodeContext(context)),
|
|
72
|
+
true
|
|
73
|
+
) as unknown as ZLinkBackendDealerSocket;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
createRouterSocket(context: ZLinkBackendContext): ZLinkBackendRouterSocket {
|
|
77
|
-
return wrapSocket(
|
|
77
|
+
return wrapSocket(
|
|
78
|
+
zlink.createRouterSocket(asNodeContext(context)),
|
|
79
|
+
true
|
|
80
|
+
) as unknown as ZLinkBackendRouterSocket;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
createPublisherSocket(context: ZLinkBackendContext): ZLinkBackendPublisherSocket {
|
|
@@ -82,7 +85,10 @@ class ZLinkNodeChannelBackendAdapter implements ZLinkChannelBackendAdapter {
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
createSubscriberSocket(context: ZLinkBackendContext): ZLinkBackendSubscriberSocket {
|
|
85
|
-
return wrapSocket(
|
|
88
|
+
return wrapSocket(
|
|
89
|
+
zlink.createSubSocket(asNodeContext(context)),
|
|
90
|
+
false
|
|
91
|
+
) as unknown as ZLinkBackendSubscriberSocket;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
createReadablePoller(socket: ZLinkBackendSubscriberSocket): ZLinkBackendReadablePoller {
|
|
@@ -94,7 +100,7 @@ class ZLinkNodeStreamBackendAdapter implements ZLinkStreamBackendAdapter {
|
|
|
94
100
|
createStreamSocket(context: ZLinkBackendContext): ZLinkBackendStreamSocket {
|
|
95
101
|
const socket = zlink.createStreamSocket(asNodeContext(context));
|
|
96
102
|
socket.options.recvMode = zlink.StreamRecvMode.Packet;
|
|
97
|
-
return wrapSocket(socket) as unknown as ZLinkBackendStreamSocket;
|
|
103
|
+
return wrapSocket(socket, true) as unknown as ZLinkBackendStreamSocket;
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
createStreamPacket(): ZLinkBackendStreamPacket {
|
|
@@ -167,38 +173,63 @@ function asNodeContext(context: ZLinkBackendContext): Context {
|
|
|
167
173
|
function createNodeReadablePoller(
|
|
168
174
|
socket: { readonly nativeInstance: unknown }
|
|
169
175
|
): ZLinkBackendReadablePoller {
|
|
170
|
-
const
|
|
171
|
-
const events = zlink.createPollEvents(1);
|
|
172
|
-
try {
|
|
173
|
-
poller.add(socket.nativeInstance as never, [zlink.PollEventFlag.PollIn], 0);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
events.close();
|
|
176
|
-
poller.close();
|
|
177
|
-
throw error;
|
|
178
|
-
}
|
|
176
|
+
const eventLoopPoller = nodeEventLoopPollerOf(socket);
|
|
179
177
|
let disposed = false;
|
|
178
|
+
let readable = false;
|
|
179
|
+
let pending: {
|
|
180
|
+
readonly promise: Promise<boolean>;
|
|
181
|
+
readonly resolve: (readable: boolean) => void;
|
|
182
|
+
readonly signal?: AbortSignal;
|
|
183
|
+
readonly onAbort?: () => void;
|
|
184
|
+
} | undefined;
|
|
185
|
+
|
|
186
|
+
const settlePending = (value: boolean): void => {
|
|
187
|
+
const current = pending;
|
|
188
|
+
if (current === undefined) return;
|
|
189
|
+
pending = undefined;
|
|
190
|
+
if (current.signal !== undefined && current.onAbort !== undefined) {
|
|
191
|
+
current.signal.removeEventListener('abort', current.onAbort);
|
|
192
|
+
}
|
|
193
|
+
current.resolve(value);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
eventLoopPoller.setReadableHandler(() => {
|
|
197
|
+
if (disposed) return;
|
|
198
|
+
readable = true;
|
|
199
|
+
settlePending(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
180
202
|
return {
|
|
181
|
-
wait(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
wait(_timeoutMs: number): boolean {
|
|
204
|
+
return !disposed && readable;
|
|
205
|
+
},
|
|
206
|
+
waitForReadable(signal?: AbortSignal): Promise<boolean> {
|
|
207
|
+
if (disposed || signal?.aborted === true) return Promise.resolve(false);
|
|
208
|
+
if (readable) return Promise.resolve(true);
|
|
209
|
+
if (pending !== undefined) return pending.promise;
|
|
210
|
+
|
|
211
|
+
let resolvePending!: (value: boolean) => void;
|
|
212
|
+
const promise = new Promise<boolean>((resolve) => {
|
|
213
|
+
resolvePending = resolve;
|
|
214
|
+
});
|
|
215
|
+
const onAbort = signal === undefined
|
|
216
|
+
? undefined
|
|
217
|
+
: (): void => settlePending(false);
|
|
218
|
+
pending = { promise, resolve: resolvePending, signal, onAbort };
|
|
219
|
+
if (signal !== undefined && onAbort !== undefined) {
|
|
220
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
188
221
|
}
|
|
222
|
+
return promise;
|
|
223
|
+
},
|
|
224
|
+
markDrained(): void {
|
|
225
|
+
readable = false;
|
|
189
226
|
},
|
|
190
227
|
dispose(): void {
|
|
191
228
|
if (disposed) return;
|
|
192
229
|
disposed = true;
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
events.close();
|
|
198
|
-
} finally {
|
|
199
|
-
poller.close();
|
|
200
|
-
}
|
|
201
|
-
}
|
|
230
|
+
readable = false;
|
|
231
|
+
settlePending(false);
|
|
232
|
+
eventLoopPoller.setReadableHandler(() => {});
|
|
202
233
|
}
|
|
203
234
|
};
|
|
204
235
|
}
|
|
@@ -30,7 +30,7 @@ interface ZLinkBindingReplySubmitOperation {
|
|
|
30
30
|
|
|
31
31
|
interface ZLinkBindingAsyncSendSubmitOperation {
|
|
32
32
|
message(message: unknown): ZLinkBindingAsyncSendSubmitOperation;
|
|
33
|
-
submit():
|
|
33
|
+
submit(): import('@zlink-systems/zlink').SendSubmission;
|
|
34
34
|
submit_sync(): void;
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -41,7 +41,7 @@ export interface ZLinkBindingRequestOperation {
|
|
|
41
41
|
interface ZLinkBindingRequestSubmitOperation {
|
|
42
42
|
message(message: unknown): ZLinkBindingRequestSubmitOperation;
|
|
43
43
|
timeout(timeoutMs: number): ZLinkBindingRequestSubmitOperation;
|
|
44
|
-
submit():
|
|
44
|
+
submit(): import('@zlink-systems/zlink').RequestSubmission;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function isBindingNotFound(error: unknown): boolean {
|
|
@@ -113,7 +113,7 @@ export async function submitBindingAsyncSend(
|
|
|
113
113
|
current = current === undefined ? operation.message(nativePart) : current.message(nativePart);
|
|
114
114
|
}
|
|
115
115
|
current ??= operation.message(Buffer.alloc(0));
|
|
116
|
-
await current.submit();
|
|
116
|
+
await current.submit().admitted;
|
|
117
117
|
} catch (error) {
|
|
118
118
|
throw translateBindingResultError(error);
|
|
119
119
|
}
|
|
@@ -156,7 +156,7 @@ export async function submitBindingRequest(
|
|
|
156
156
|
if (timeoutMs !== undefined) {
|
|
157
157
|
current = current.timeout(timeoutMs);
|
|
158
158
|
}
|
|
159
|
-
return await current.submit();
|
|
159
|
+
return await current.submit().reply;
|
|
160
160
|
} catch (error) {
|
|
161
161
|
throw translateBindingResultError(error);
|
|
162
162
|
}
|