@hardlydifficult/worker-server 1.0.4 → 1.0.6
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/README.md +300 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hardlydifficult/worker-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
WebSocket-based worker server with health monitoring, request routing, and load balancing.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -15,218 +15,388 @@ import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
|
15
15
|
|
|
16
16
|
const server = new WorkerServer({
|
|
17
17
|
port: 8080,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
authToken: "my-secret-token", // optional
|
|
19
|
+
logger: console, // optional, defaults to no-op
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
+
// Handle worker registrations and messages
|
|
22
23
|
server.onWorkerConnected((worker) => {
|
|
23
|
-
console.log(`Worker
|
|
24
|
+
console.log(`Worker ${worker.name} (${worker.id}) connected`);
|
|
24
25
|
});
|
|
25
26
|
|
|
26
|
-
server.onWorkerDisconnected((worker,
|
|
27
|
-
console.log(`Worker
|
|
28
|
-
if (pendingRequests.size > 0) {
|
|
29
|
-
console.log(`Rescheduling ${pendingRequests.size} pending requests`);
|
|
30
|
-
}
|
|
27
|
+
server.onWorkerDisconnected((worker, pendingRequestIds) => {
|
|
28
|
+
console.log(`Worker ${worker.id} disconnected with ${pendingRequestIds.size} pending requests`);
|
|
31
29
|
});
|
|
32
30
|
|
|
33
31
|
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
34
|
-
console.log(`Work
|
|
32
|
+
console.log(`Work completed: ${message.requestId}`);
|
|
35
33
|
});
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
// Start the server
|
|
36
|
+
await server.start();
|
|
37
|
+
|
|
38
|
+
// Get an available worker supporting a model
|
|
39
|
+
const worker = server.getAvailableWorker("gpt-4");
|
|
40
|
+
if (worker) {
|
|
41
|
+
server.send(worker.id, { type: "work_request", requestId: "req-1" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Stop when done
|
|
45
|
+
await server.stop();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Core Concepts
|
|
49
|
+
|
|
50
|
+
### Worker Lifecycle Management
|
|
51
|
+
|
|
52
|
+
The server handles worker connections, registrations, and disconnections with automatic health monitoring.
|
|
53
|
+
|
|
54
|
+
#### Registration and Authentication
|
|
55
|
+
|
|
56
|
+
Workers connect via WebSocket and send a `worker_registration` message with an optional `authToken`. If the server is configured with `authToken`, the worker must provide a matching token.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
60
|
+
|
|
61
|
+
const server = new WorkerServer({
|
|
62
|
+
port: 8080,
|
|
63
|
+
authToken: "secret",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
server.onWorkerConnected((worker) => {
|
|
67
|
+
console.log(`Worker registered: ${worker.id}`);
|
|
44
68
|
});
|
|
45
69
|
|
|
46
70
|
await server.start();
|
|
47
|
-
console.log("Worker server running on port 8080");
|
|
48
71
|
```
|
|
49
72
|
|
|
50
|
-
|
|
73
|
+
#### Heartbeat and Health Monitoring
|
|
51
74
|
|
|
52
|
-
|
|
75
|
+
Workers must send periodic `heartbeat` messages. The server tracks the last heartbeat timestamp and closes connections that miss the timeout.
|
|
53
76
|
|
|
54
|
-
|
|
77
|
+
```typescript
|
|
78
|
+
const server = new WorkerServer({
|
|
79
|
+
port: 8080,
|
|
80
|
+
heartbeatTimeoutMs: 60_000, // Missed for 60s → unhealthy
|
|
81
|
+
healthCheckIntervalMs: 10_000, // Check every 10s
|
|
82
|
+
heartbeatIntervalMs: 15_000, // Communicate 15s interval to workers
|
|
83
|
+
});
|
|
84
|
+
```
|
|
55
85
|
|
|
56
|
-
|
|
86
|
+
| Option | Default | Description |
|
|
87
|
+
|--------|---------|-------------|
|
|
88
|
+
| `heartbeatTimeoutMs` | 60000 | Time before worker is marked unhealthy |
|
|
89
|
+
| `healthCheckIntervalMs` | 10000 | Frequency of health checks |
|
|
90
|
+
| `heartbeatIntervalMs` | 15000 | Heartbeat interval communicated to workers |
|
|
57
91
|
|
|
58
|
-
|
|
92
|
+
#### Disconnection Handling
|
|
59
93
|
|
|
60
|
-
|
|
94
|
+
When a worker disconnects, the `onWorkerDisconnected` handler is called with its pending request IDs.
|
|
61
95
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
```typescript
|
|
97
|
+
server.onWorkerDisconnected((worker, pendingRequestIds) => {
|
|
98
|
+
// Reassign pending requests as needed
|
|
99
|
+
console.log(`Pending: ${[...pendingRequestIds].join(", ")}`);
|
|
100
|
+
});
|
|
101
|
+
```
|
|
66
102
|
|
|
67
|
-
|
|
103
|
+
### Request Tracking and Load Balancing
|
|
68
104
|
|
|
69
|
-
|
|
105
|
+
Requests are tracked per-worker to avoid overloading and to support category-specific limits.
|
|
70
106
|
|
|
71
|
-
|
|
107
|
+
#### Request Lifecycle
|
|
72
108
|
|
|
73
|
-
|
|
109
|
+
Track a request as assigned to a worker, then release it when complete.
|
|
74
110
|
|
|
75
111
|
```typescript
|
|
76
|
-
|
|
112
|
+
// Assign request to worker
|
|
113
|
+
const worker = server.getAvailableWorker("gpt-4");
|
|
114
|
+
if (worker) {
|
|
115
|
+
server.trackRequest(worker.id, "req-1");
|
|
116
|
+
server.send(worker.id, { type: "work_request", requestId: "req-1" });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Worker sends completion message
|
|
120
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
121
|
+
server.releaseRequest(message.requestId, { incrementCompleted: true });
|
|
122
|
+
});
|
|
77
123
|
```
|
|
78
124
|
|
|
79
|
-
|
|
80
|
-
|--------|------|---------|-------------|
|
|
81
|
-
| `port` | `number` | — | HTTP + WebSocket server port |
|
|
82
|
-
| `authToken` | `string` | `undefined` | Optional token required for worker registration |
|
|
83
|
-
| `heartbeatTimeoutMs` | `number` | `60000` | Milliseconds before a missed heartbeat marks a worker unhealthy |
|
|
84
|
-
| `healthCheckIntervalMs` | `number` | `10000` | Interval (ms) for health checks |
|
|
85
|
-
| `heartbeatIntervalMs` | `number` | `15000` | Heartbeat interval communicated to workers |
|
|
86
|
-
| `logger` | `WorkerServerLogger` | `undefined` | Logger instance (defaults to no-op) |
|
|
125
|
+
#### Available Worker Selection
|
|
87
126
|
|
|
88
|
-
|
|
127
|
+
Workers are selected based on capacity and model support.
|
|
89
128
|
|
|
90
129
|
```typescript
|
|
91
|
-
//
|
|
92
|
-
|
|
130
|
+
// Get least-loaded worker supporting a model
|
|
131
|
+
const worker = server.getAvailableWorker("gpt-4");
|
|
132
|
+
// → least-loaded worker that supports "gpt-4"
|
|
133
|
+
|
|
134
|
+
// Get any available worker (model-agnostic)
|
|
135
|
+
const anyWorker = server.getAnyAvailableWorker();
|
|
136
|
+
// → any worker (Available or Busy status)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Workers are marked `Busy` when `activeRequests >= maxConcurrentRequests`.
|
|
140
|
+
|
|
141
|
+
#### Per-Category Concurrency Limits
|
|
142
|
+
|
|
143
|
+
Workers can define per-category limits in their capabilities. The pool enforces these when `trackRequest` is called with a category.
|
|
93
144
|
|
|
94
|
-
|
|
95
|
-
|
|
145
|
+
```typescript
|
|
146
|
+
// Worker capabilities include:
|
|
147
|
+
{
|
|
148
|
+
models: [{ modelId: "gpt-4", ... }],
|
|
149
|
+
maxConcurrentRequests: 5,
|
|
150
|
+
concurrencyLimits: {
|
|
151
|
+
inference: 2,
|
|
152
|
+
embedding: 4,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
96
155
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
type: string,
|
|
100
|
-
handler: WorkerMessageHandler<T>
|
|
101
|
-
): () => void;
|
|
156
|
+
// Track request in a category
|
|
157
|
+
server.trackRequest(worker.id, "req-1", "inference");
|
|
102
158
|
```
|
|
103
159
|
|
|
104
|
-
|
|
160
|
+
### Message Routing
|
|
161
|
+
|
|
162
|
+
Messages are routed by the `type` field to registered handlers.
|
|
105
163
|
|
|
106
164
|
```typescript
|
|
107
|
-
|
|
108
|
-
|
|
165
|
+
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
166
|
+
console.log(`Worker ${worker.id} completed ${message.requestId}`);
|
|
167
|
+
});
|
|
109
168
|
|
|
110
|
-
|
|
111
|
-
|
|
169
|
+
server.onWorkerMessage("metrics", (worker, message) => {
|
|
170
|
+
console.log(`Worker ${worker.id} metrics:`, message.metrics);
|
|
171
|
+
});
|
|
112
172
|
```
|
|
113
173
|
|
|
114
|
-
|
|
174
|
+
Returns an unsubscribe function:
|
|
115
175
|
|
|
116
176
|
```typescript
|
|
117
|
-
|
|
118
|
-
|
|
177
|
+
const unsubscribe = server.onWorkerMessage("status", handler);
|
|
178
|
+
// later...
|
|
179
|
+
unsubscribe();
|
|
180
|
+
```
|
|
119
181
|
|
|
120
|
-
|
|
121
|
-
getAnyAvailableWorker(): WorkerInfo | null;
|
|
182
|
+
### Sending Messages
|
|
122
183
|
|
|
123
|
-
|
|
124
|
-
|
|
184
|
+
#### Targeted Send
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const success = server.send(workerId, { type: "ping" });
|
|
188
|
+
// Returns false if worker not found or WebSocket not open
|
|
189
|
+
```
|
|
125
190
|
|
|
126
|
-
|
|
127
|
-
getAvailableWorkerCount(): number;
|
|
191
|
+
#### Broadcast
|
|
128
192
|
|
|
129
|
-
|
|
130
|
-
|
|
193
|
+
```typescript
|
|
194
|
+
server.broadcast({ type: "shutdown" });
|
|
195
|
+
// Sends to all connected workers with open sockets
|
|
131
196
|
```
|
|
132
197
|
|
|
133
|
-
|
|
198
|
+
### Server Extensibility
|
|
199
|
+
|
|
200
|
+
#### Additional WebSocket Endpoints
|
|
134
201
|
|
|
135
202
|
```typescript
|
|
136
|
-
//
|
|
137
|
-
|
|
203
|
+
// Create a custom WebSocket endpoint
|
|
204
|
+
server.addWebSocketEndpoint("/ws/metrics", (ws) => {
|
|
205
|
+
ws.on("message", (data) => {
|
|
206
|
+
console.log("Metrics client message:", data.toString());
|
|
207
|
+
});
|
|
208
|
+
});
|
|
138
209
|
|
|
139
|
-
//
|
|
140
|
-
releaseRequest(
|
|
141
|
-
requestId: string,
|
|
142
|
-
options?: { incrementCompleted?: boolean }
|
|
143
|
-
): void;
|
|
210
|
+
// Clients connect to ws://localhost:8080/ws/metrics
|
|
144
211
|
```
|
|
145
212
|
|
|
146
|
-
#### HTTP
|
|
213
|
+
#### HTTP Handlers
|
|
147
214
|
|
|
148
215
|
```typescript
|
|
149
|
-
|
|
150
|
-
|
|
216
|
+
server.addHttpHandler(async (req, res) => {
|
|
217
|
+
if (req.url === "/health") {
|
|
218
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
219
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return false; // continue to next handler or 404
|
|
223
|
+
});
|
|
151
224
|
|
|
152
|
-
//
|
|
153
|
-
addWebSocketEndpoint(
|
|
154
|
-
path: string,
|
|
155
|
-
handler: WebSocketConnectionHandler
|
|
156
|
-
): void;
|
|
225
|
+
// Custom HTTP responses take precedence over 404
|
|
157
226
|
```
|
|
158
227
|
|
|
159
|
-
|
|
228
|
+
## Public API
|
|
229
|
+
|
|
230
|
+
### `WorkerServer`
|
|
231
|
+
|
|
232
|
+
Main server class for managing worker connections.
|
|
233
|
+
|
|
234
|
+
| Method | Description |
|
|
235
|
+
|--------|-------------|
|
|
236
|
+
| `onWorkerConnected(handler)` | Register handler for worker registration events |
|
|
237
|
+
| `onWorkerDisconnected(handler)` | Register handler for worker disconnection events |
|
|
238
|
+
| `onWorkerMessage(type, handler)` | Register handler for a specific message type |
|
|
239
|
+
| `send(workerId, message)` | Send a JSON message to a specific worker |
|
|
240
|
+
| `broadcast(message)` | Broadcast a JSON message to all workers |
|
|
241
|
+
| `getAvailableWorker(model, category?)` | Get least-loaded worker supporting model |
|
|
242
|
+
| `getAnyAvailableWorker()` | Get any available/Busy worker |
|
|
243
|
+
| `getWorkerCount()` | Total connected worker count |
|
|
244
|
+
| `getAvailableWorkerCount()` | Available worker count |
|
|
245
|
+
| `getWorkerInfo()` | Get public info about all workers |
|
|
246
|
+
| `trackRequest(workerId, requestId, category?)` | Track request as in-progress |
|
|
247
|
+
| `releaseRequest(requestId, options?)` | Release tracked request |
|
|
248
|
+
| `addHttpHandler(handler)` | Add HTTP request handler |
|
|
249
|
+
| `addWebSocketEndpoint(path, handler)` | Add custom WebSocket endpoint |
|
|
250
|
+
| `start()` | Start HTTP + WebSocket server |
|
|
251
|
+
| `stop()` | Stop server and close all connections |
|
|
252
|
+
|
|
253
|
+
### `WorkerPool`
|
|
254
|
+
|
|
255
|
+
Internal pool manager with public helpers.
|
|
256
|
+
|
|
257
|
+
| Method | Description |
|
|
258
|
+
|--------|-------------|
|
|
259
|
+
| `add(worker)` | Add a connected worker to the pool |
|
|
260
|
+
| `remove(id)` | Remove worker by ID |
|
|
261
|
+
| `get(id)` | Get worker by ID |
|
|
262
|
+
| `has(id)` | Check if worker is in pool |
|
|
263
|
+
| `getAvailableWorker(model, category?)` | Get available worker by model |
|
|
264
|
+
| `getAnyAvailableWorker()` | Get any available/Busy worker |
|
|
265
|
+
| `getCount()` | Total worker count |
|
|
266
|
+
| `getAvailableCount()` | Available worker count |
|
|
267
|
+
| `getWorkerInfoList()` | Get public info for all workers |
|
|
268
|
+
| `checkHealth(timeoutMs)` | Check worker health and return dead IDs |
|
|
269
|
+
| `send(workerId, message)` | Send message to worker |
|
|
270
|
+
| `broadcast(message)` | Broadcast to all workers |
|
|
271
|
+
| `closeAll()` | Close all worker connections |
|
|
272
|
+
|
|
273
|
+
### `toWorkerInfo(worker)`
|
|
274
|
+
|
|
275
|
+
Converts internal `ConnectedWorker` to public `WorkerInfo`.
|
|
160
276
|
|
|
161
277
|
```typescript
|
|
162
|
-
|
|
163
|
-
start(): Promise<void>;
|
|
278
|
+
import { toWorkerInfo, type ConnectedWorker } from "@hardlydifficult/worker-server";
|
|
164
279
|
|
|
165
|
-
|
|
166
|
-
|
|
280
|
+
const internal: ConnectedWorker = /* ... */;
|
|
281
|
+
const publicInfo = toWorkerInfo(internal);
|
|
282
|
+
// No websocket reference in publicInfo
|
|
167
283
|
```
|
|
168
284
|
|
|
169
|
-
###
|
|
285
|
+
### Types
|
|
286
|
+
|
|
287
|
+
| Type | Description |
|
|
288
|
+
|------|-------------|
|
|
289
|
+
| `WorkerStatus` | `available`, `busy`, `draining`, `unhealthy` |
|
|
290
|
+
| `ModelInfo` | Model capabilities and metadata |
|
|
291
|
+
| `WorkerCapabilities` | Worker capacity, models, and concurrency limits |
|
|
292
|
+
| `WorkerInfo` | Public worker state |
|
|
293
|
+
| `ConnectedWorker` | Internal state (includes WebSocket) |
|
|
294
|
+
| `WorkerServerOptions` | Configuration for `WorkerServer` |
|
|
295
|
+
| `WorkerServerLogger` | Logger interface |
|
|
296
|
+
| `HttpRequestHandler` | HTTP request handler type |
|
|
297
|
+
| `WorkerMessageHandler<T>` | Typed message handler |
|
|
298
|
+
| `WorkerConnectedHandler` | Worker connected event handler |
|
|
299
|
+
| `WorkerDisconnectedHandler` | Worker disconnected event handler |
|
|
300
|
+
| `WebSocketConnectionHandler` | Custom WebSocket endpoint handler |
|
|
301
|
+
|
|
302
|
+
### Constants and Defaults
|
|
170
303
|
|
|
171
|
-
|
|
304
|
+
Default timeouts (milliseconds):
|
|
172
305
|
|
|
173
306
|
```typescript
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
readonly capabilities: WorkerCapabilities;
|
|
179
|
-
readonly sessionId: string;
|
|
180
|
-
readonly connectedAt: Date;
|
|
181
|
-
readonly lastHeartbeat: Date;
|
|
182
|
-
readonly activeRequests: number;
|
|
183
|
-
readonly completedRequests: number;
|
|
184
|
-
readonly pendingRequestIds: ReadonlySet<string>;
|
|
185
|
-
readonly categoryActiveRequests: ReadonlyMap<string, number>;
|
|
307
|
+
{
|
|
308
|
+
heartbeatTimeoutMs: 60_000,
|
|
309
|
+
healthCheckIntervalMs: 10_000,
|
|
310
|
+
heartbeatIntervalMs: 15_000,
|
|
186
311
|
}
|
|
187
312
|
```
|
|
188
313
|
|
|
189
|
-
###
|
|
314
|
+
### Utility Functions
|
|
190
315
|
|
|
191
|
-
|
|
316
|
+
#### `safeCompare(a, b)`
|
|
317
|
+
|
|
318
|
+
Timing-safe string comparison.
|
|
192
319
|
|
|
193
320
|
```typescript
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
321
|
+
import { safeCompare } from "@hardlydifficult/worker-server";
|
|
322
|
+
|
|
323
|
+
const isValid = safeCompare(inputToken, secretToken);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Appendix
|
|
327
|
+
|
|
328
|
+
### Worker Registration Protocol
|
|
329
|
+
|
|
330
|
+
Workers send:
|
|
331
|
+
|
|
332
|
+
```json
|
|
333
|
+
{
|
|
334
|
+
"type": "worker_registration",
|
|
335
|
+
"workerId": "worker-1",
|
|
336
|
+
"workerName": "My Worker",
|
|
337
|
+
"capabilities": {
|
|
338
|
+
"models": [{
|
|
339
|
+
"modelId": "gpt-4",
|
|
340
|
+
"displayName": "GPT-4",
|
|
341
|
+
"maxContextTokens": 8192,
|
|
342
|
+
"maxOutputTokens": 4096,
|
|
343
|
+
"supportsStreaming": true
|
|
344
|
+
}],
|
|
345
|
+
"maxConcurrentRequests": 5,
|
|
346
|
+
"concurrencyLimits": {
|
|
347
|
+
"inference": 2,
|
|
348
|
+
"embedding": 4
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
"authToken": "optional"
|
|
199
352
|
}
|
|
200
353
|
```
|
|
201
354
|
|
|
202
|
-
|
|
355
|
+
Server responds:
|
|
203
356
|
|
|
204
|
-
|
|
357
|
+
```json
|
|
358
|
+
{
|
|
359
|
+
"type": "worker_registration_ack",
|
|
360
|
+
"success": true,
|
|
361
|
+
"sessionId": "uuid",
|
|
362
|
+
"heartbeatIntervalMs": 15000
|
|
363
|
+
}
|
|
364
|
+
```
|
|
205
365
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
366
|
+
### Worker Heartbeat Protocol
|
|
367
|
+
|
|
368
|
+
Workers send:
|
|
369
|
+
|
|
370
|
+
```json
|
|
371
|
+
{
|
|
372
|
+
"type": "heartbeat",
|
|
373
|
+
"workerId": "worker-1",
|
|
374
|
+
"timestamp": "2024-01-01T00:00:00.000Z"
|
|
215
375
|
}
|
|
216
376
|
```
|
|
217
377
|
|
|
218
|
-
|
|
378
|
+
Server responds:
|
|
219
379
|
|
|
220
|
-
|
|
380
|
+
```json
|
|
381
|
+
{
|
|
382
|
+
"type": "heartbeat_ack",
|
|
383
|
+
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
384
|
+
"nextHeartbeatDeadline": "2024-01-01T00:01:00.000Z"
|
|
385
|
+
}
|
|
386
|
+
```
|
|
221
387
|
|
|
222
|
-
|
|
223
|
-
|--------|-------------|
|
|
224
|
-
| `available` | Worker is idle and can accept new requests |
|
|
225
|
-
| `busy` | Worker is at max concurrent requests but may accept model-agnostic tasks |
|
|
226
|
-
| `draining` | Worker is being gracefully decommissioned |
|
|
227
|
-
| `unhealthy` | Worker has missed heartbeats and is presumed degraded |
|
|
388
|
+
### Status Transitions
|
|
228
389
|
|
|
229
|
-
|
|
390
|
+
- `Available` → `Busy` when `activeRequests >= maxConcurrentRequests`
|
|
391
|
+
- `Busy` → `Available` when `activeRequests < maxConcurrentRequests`
|
|
392
|
+
- Any → `Unhealthy` on heartbeat timeout
|
|
393
|
+
- `Unhealthy` → `Available/Busy` on heartbeat recovery
|
|
394
|
+
|
|
395
|
+
### Concurrent Request Tracking
|
|
396
|
+
|
|
397
|
+
The pool tracks requests per-worker and per-category (if provided). It automatically decrements the category count when releasing a tracked request.
|
|
398
|
+
|
|
399
|
+
### Worker Protocol Summary
|
|
230
400
|
|
|
231
401
|
Workers communicate using JSON messages with a `type` field:
|
|
232
402
|
|
|
@@ -237,10 +407,4 @@ Workers communicate using JSON messages with a `type` field:
|
|
|
237
407
|
| `heartbeat` | Worker → Server | Periodic health check |
|
|
238
408
|
| `heartbeat_ack` | Server → Worker | Acknowledgment with next deadline |
|
|
239
409
|
|
|
240
|
-
All other message types are routed to registered handlers via `onWorkerMessage()`.
|
|
241
|
-
|
|
242
|
-
## Health Monitoring
|
|
243
|
-
|
|
244
|
-
- Workers missing heartbeats for `heartbeatTimeoutMs` are marked `unhealthy`
|
|
245
|
-
- Workers missing heartbeats for `3 × heartbeatTimeoutMs` are disconnected
|
|
246
|
-
- Health checks run every `healthCheckIntervalMs`
|
|
410
|
+
All other message types are routed to registered handlers via `onWorkerMessage()`.
|