@hardlydifficult/worker-server 1.0.5 → 1.0.7
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 +206 -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
|
|
|
@@ -13,197 +13,209 @@ npm install @hardlydifficult/worker-server
|
|
|
13
13
|
```typescript
|
|
14
14
|
import { WorkerServer } from "@hardlydifficult/worker-server";
|
|
15
15
|
|
|
16
|
-
const server = new WorkerServer({
|
|
17
|
-
port: 8080,
|
|
18
|
-
heartbeatTimeoutMs: 60000, // 60 seconds
|
|
19
|
-
healthCheckIntervalMs: 10000, // 10 seconds
|
|
20
|
-
});
|
|
16
|
+
const server = new WorkerServer({ port: 8080 });
|
|
21
17
|
|
|
22
18
|
server.onWorkerConnected((worker) => {
|
|
23
|
-
console.log(`Worker
|
|
19
|
+
console.log(`Worker ${worker.name} connected (${worker.id})`);
|
|
24
20
|
});
|
|
25
21
|
|
|
26
|
-
server.onWorkerDisconnected((worker,
|
|
27
|
-
console.log(`Worker
|
|
28
|
-
if (pendingRequests.size > 0) {
|
|
29
|
-
console.log(`Rescheduling ${pendingRequests.size} pending requests`);
|
|
30
|
-
}
|
|
22
|
+
server.onWorkerDisconnected((worker, pending) => {
|
|
23
|
+
console.log(`Worker ${worker.name} disconnected with ${pending.size} pending requests`);
|
|
31
24
|
});
|
|
32
25
|
|
|
33
26
|
server.onWorkerMessage("work_complete", (worker, message) => {
|
|
34
|
-
console.log(`
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
server.addHttpHandler(async (req, res) => {
|
|
38
|
-
if (req.url === "/health") {
|
|
39
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
40
|
-
res.end(JSON.stringify({ status: "ok" }));
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
27
|
+
console.log(`Worker ${worker.id} completed request ${message.requestId}`);
|
|
44
28
|
});
|
|
45
29
|
|
|
46
30
|
await server.start();
|
|
47
|
-
console.log("
|
|
31
|
+
console.log("Server listening on port 8080");
|
|
48
32
|
```
|
|
49
33
|
|
|
50
|
-
## Core
|
|
34
|
+
## Core Components
|
|
51
35
|
|
|
52
|
-
###
|
|
53
|
-
|
|
54
|
-
Workers connect via WebSocket, register with capabilities (supported models, concurrency limits), and send periodic heartbeats. The server tracks their status (available, busy, draining, unhealthy) and makes routing decisions based on load and model compatibility.
|
|
55
|
-
|
|
56
|
-
### Request Tracking
|
|
57
|
-
|
|
58
|
-
Requests are tracked per-worker and can be released when complete. Optional categories enable per-category concurrency limits when workers declare `concurrencyLimits` in their capabilities.
|
|
36
|
+
### WorkerServer
|
|
59
37
|
|
|
60
|
-
|
|
38
|
+
WebSocket server managing remote worker connections with health checks, message routing, and pool management.
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
- Model support (exact or substring match)
|
|
64
|
-
- Current load (least-loaded algorithm)
|
|
65
|
-
- Category-specific limits (when `category` is provided)
|
|
40
|
+
#### Lifecycle Management
|
|
66
41
|
|
|
67
|
-
|
|
42
|
+
```typescript
|
|
43
|
+
const server = new WorkerServer({ port: 8080, authToken: "secret" });
|
|
68
44
|
|
|
69
|
-
|
|
45
|
+
// Start the server
|
|
46
|
+
await server.start();
|
|
70
47
|
|
|
71
|
-
|
|
48
|
+
// Stop the server gracefully
|
|
49
|
+
await server.stop();
|
|
50
|
+
```
|
|
72
51
|
|
|
73
|
-
####
|
|
52
|
+
#### Registration Handlers
|
|
74
53
|
|
|
75
54
|
```typescript
|
|
76
|
-
|
|
77
|
-
|
|
55
|
+
// Called when a worker successfully registers
|
|
56
|
+
const unsubscribeConnected = server.onWorkerConnected((worker) => {
|
|
57
|
+
console.log(`Worker connected: ${worker.name}`);
|
|
58
|
+
});
|
|
78
59
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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) |
|
|
60
|
+
// Called when a worker disconnects
|
|
61
|
+
const unsubscribeDisconnected = server.onWorkerDisconnected((worker, pending) => {
|
|
62
|
+
console.log(`Worker disconnected with ${pending.size} pending requests`);
|
|
63
|
+
});
|
|
64
|
+
```
|
|
87
65
|
|
|
88
|
-
####
|
|
66
|
+
#### Message Handling
|
|
89
67
|
|
|
90
68
|
```typescript
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
onWorkerDisconnected(handler: WorkerDisconnectedHandler): () => void;
|
|
69
|
+
// Register handlers for domain-specific messages by type
|
|
70
|
+
server.onWorkerMessage("work_request", (worker, message) => {
|
|
71
|
+
// Process work request from worker
|
|
72
|
+
});
|
|
96
73
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
handler: WorkerMessageHandler<T>
|
|
101
|
-
): () => void;
|
|
74
|
+
server.onWorkerMessage("status_update", (worker, message) => {
|
|
75
|
+
// Handle status updates from worker
|
|
76
|
+
});
|
|
102
77
|
```
|
|
103
78
|
|
|
104
|
-
####
|
|
79
|
+
#### Sending Messages
|
|
105
80
|
|
|
106
81
|
```typescript
|
|
107
|
-
// Send
|
|
108
|
-
send(
|
|
82
|
+
// Send to a specific worker
|
|
83
|
+
const success = server.send("worker-1", { type: "stop", reason: "shutdown" });
|
|
109
84
|
|
|
110
85
|
// Broadcast to all connected workers
|
|
111
|
-
broadcast(
|
|
86
|
+
server.broadcast({ type: "maintenance_start" });
|
|
112
87
|
```
|
|
113
88
|
|
|
114
89
|
#### Pool Queries
|
|
115
90
|
|
|
116
91
|
```typescript
|
|
117
|
-
// Get least-loaded worker supporting
|
|
118
|
-
|
|
92
|
+
// Get least-loaded worker supporting a specific model
|
|
93
|
+
const worker = server.getAvailableWorker("sonnet-3.5");
|
|
119
94
|
|
|
120
95
|
// Get any available worker (model-agnostic)
|
|
121
|
-
getAnyAvailableWorker()
|
|
96
|
+
const anyWorker = server.getAnyAvailableWorker();
|
|
97
|
+
|
|
98
|
+
// Get all worker info
|
|
99
|
+
const workers = server.getWorkerInfo(); // Returns WorkerInfo[]
|
|
100
|
+
```
|
|
122
101
|
|
|
123
|
-
|
|
124
|
-
getWorkerCount(): number;
|
|
102
|
+
#### Request Tracking
|
|
125
103
|
|
|
126
|
-
|
|
127
|
-
|
|
104
|
+
```typescript
|
|
105
|
+
// Track a request assigned to a worker
|
|
106
|
+
server.trackRequest("worker-1", "req-123", "inference");
|
|
128
107
|
|
|
129
|
-
//
|
|
130
|
-
|
|
108
|
+
// Release a completed request
|
|
109
|
+
server.releaseRequest("req-123", { incrementCompleted: true });
|
|
131
110
|
```
|
|
132
111
|
|
|
133
|
-
|
|
112
|
+
### WorkerPool
|
|
113
|
+
|
|
114
|
+
Manages worker lifecycle, request tracking, health checks, and message routing.
|
|
115
|
+
|
|
116
|
+
#### Worker Status
|
|
117
|
+
|
|
118
|
+
Workers transition automatically between states:
|
|
119
|
+
- `available`: Ready to accept new requests
|
|
120
|
+
- `busy`: At maximum concurrent request capacity
|
|
121
|
+
- `draining`: In the process of shutting down
|
|
122
|
+
- `unhealthy`: Heartbeat missed beyond timeout threshold
|
|
123
|
+
|
|
124
|
+
#### Pool Operations
|
|
134
125
|
|
|
135
126
|
```typescript
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
import { WorkerPool, WorkerStatus, type ConnectedWorker } from "@hardlydifficult/worker-server";
|
|
128
|
+
|
|
129
|
+
const pool = new WorkerPool();
|
|
130
|
+
|
|
131
|
+
// Get the least-loaded available worker supporting a model
|
|
132
|
+
const worker = pool.getAvailableWorker("sonnet-3.5", "inference");
|
|
133
|
+
|
|
134
|
+
// Get any available or busy worker
|
|
135
|
+
const anyWorker = pool.getAnyAvailableWorker();
|
|
136
|
+
|
|
137
|
+
// Get count statistics
|
|
138
|
+
const total = pool.getCount();
|
|
139
|
+
const available = pool.getAvailableCount();
|
|
138
140
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
)
|
|
141
|
+
// Get worker info (without WebSocket reference)
|
|
142
|
+
const workers = pool.getWorkerInfoList();
|
|
143
|
+
|
|
144
|
+
// Track and release requests
|
|
145
|
+
pool.trackRequest("worker-1", "req-123");
|
|
146
|
+
pool.releaseRequest("req-123");
|
|
147
|
+
|
|
148
|
+
// Check for unhealthy workers
|
|
149
|
+
const deadWorkerIds = pool.checkHealth(60_000); // 60-second timeout
|
|
150
|
+
|
|
151
|
+
// Send and broadcast messages
|
|
152
|
+
pool.send("worker-1", { type: "ping" });
|
|
153
|
+
pool.broadcast({ type: "shutdown" });
|
|
144
154
|
```
|
|
145
155
|
|
|
146
|
-
|
|
156
|
+
### ConnectionHandler
|
|
157
|
+
|
|
158
|
+
Handles WebSocket connection lifecycle and protocol message routing.
|
|
159
|
+
|
|
160
|
+
#### Message Routing
|
|
147
161
|
|
|
148
162
|
```typescript
|
|
149
|
-
|
|
150
|
-
addHttpHandler(handler: HttpRequestHandler): void;
|
|
163
|
+
import { ConnectionHandler } from "@hardlydifficult/worker-server";
|
|
151
164
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
165
|
+
const handler = new ConnectionHandler(pool, config, logger);
|
|
166
|
+
|
|
167
|
+
// Register handlers for custom message types
|
|
168
|
+
const unregister = handler.onMessage("custom_type", (worker, message) => {
|
|
169
|
+
console.log(`Received from ${worker.id}:`, message);
|
|
170
|
+
});
|
|
157
171
|
```
|
|
158
172
|
|
|
159
|
-
####
|
|
173
|
+
#### Event Handlers
|
|
160
174
|
|
|
161
175
|
```typescript
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
handler.onWorkerConnected((worker) => {
|
|
177
|
+
console.log("Worker connected:", worker.id);
|
|
178
|
+
});
|
|
164
179
|
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
handler.onWorkerDisconnected((worker, pending) => {
|
|
181
|
+
console.log("Worker disconnected with pending:", pending.size);
|
|
182
|
+
});
|
|
167
183
|
```
|
|
168
184
|
|
|
185
|
+
## Types and Interfaces
|
|
186
|
+
|
|
169
187
|
### WorkerInfo
|
|
170
188
|
|
|
171
|
-
Public
|
|
189
|
+
Public worker metadata exposed to consumers:
|
|
172
190
|
|
|
173
191
|
```typescript
|
|
174
192
|
interface WorkerInfo {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
id: string;
|
|
194
|
+
name: string;
|
|
195
|
+
status: WorkerStatus;
|
|
196
|
+
capabilities: WorkerCapabilities;
|
|
197
|
+
sessionId: string;
|
|
198
|
+
connectedAt: Date;
|
|
199
|
+
lastHeartbeat: Date;
|
|
200
|
+
activeRequests: number;
|
|
201
|
+
completedRequests: number;
|
|
202
|
+
pendingRequestIds: ReadonlySet<string>;
|
|
203
|
+
categoryActiveRequests: ReadonlyMap<string, number>;
|
|
186
204
|
}
|
|
187
205
|
```
|
|
188
206
|
|
|
189
207
|
### WorkerCapabilities
|
|
190
208
|
|
|
191
|
-
Describes a worker
|
|
209
|
+
Describes what a worker can do:
|
|
192
210
|
|
|
193
211
|
```typescript
|
|
194
212
|
interface WorkerCapabilities {
|
|
195
213
|
models: ModelInfo[];
|
|
196
214
|
maxConcurrentRequests: number;
|
|
197
215
|
metadata?: Record<string, unknown>;
|
|
198
|
-
concurrencyLimits?: Record<string, number>;
|
|
216
|
+
concurrencyLimits?: Record<string, number>; // per-category limits
|
|
199
217
|
}
|
|
200
|
-
```
|
|
201
218
|
|
|
202
|
-
### ModelInfo
|
|
203
|
-
|
|
204
|
-
Describes a supported model:
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
219
|
interface ModelInfo {
|
|
208
220
|
modelId: string;
|
|
209
221
|
displayName: string;
|
|
@@ -215,32 +227,90 @@ interface ModelInfo {
|
|
|
215
227
|
}
|
|
216
228
|
```
|
|
217
229
|
|
|
218
|
-
###
|
|
230
|
+
### Configuration Options
|
|
219
231
|
|
|
220
|
-
|
|
232
|
+
```typescript
|
|
233
|
+
interface WorkerServerOptions {
|
|
234
|
+
port: number;
|
|
235
|
+
authToken?: string;
|
|
236
|
+
heartbeatTimeoutMs?: number; // default: 60000
|
|
237
|
+
healthCheckIntervalMs?: number; // default: 10000
|
|
238
|
+
heartbeatIntervalMs?: number; // default: 15000
|
|
239
|
+
logger?: WorkerServerLogger;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
221
242
|
|
|
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 |
|
|
243
|
+
### Logger Interface
|
|
228
244
|
|
|
229
|
-
|
|
245
|
+
```typescript
|
|
246
|
+
interface WorkerServerLogger {
|
|
247
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
248
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
249
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
250
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
251
|
+
}
|
|
252
|
+
```
|
|
230
253
|
|
|
231
|
-
|
|
254
|
+
## Advanced Features
|
|
232
255
|
|
|
233
|
-
|
|
234
|
-
|---------|-----------|-------------|
|
|
235
|
-
| `worker_registration` | Worker → Server | Register with capabilities |
|
|
236
|
-
| `worker_registration_ack` | Server → Worker | Acknowledgment with session ID |
|
|
237
|
-
| `heartbeat` | Worker → Server | Periodic health check |
|
|
238
|
-
| `heartbeat_ack` | Server → Worker | Acknowledgment with next deadline |
|
|
256
|
+
### HTTP Endpoints
|
|
239
257
|
|
|
240
|
-
|
|
258
|
+
Custom HTTP handlers can be added:
|
|
241
259
|
|
|
242
|
-
|
|
260
|
+
```typescript
|
|
261
|
+
server.addHttpHandler(async (req, res) => {
|
|
262
|
+
if (req.url === "/health") {
|
|
263
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
264
|
+
res.end(JSON.stringify({ status: "ok" }));
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
return false; // continue to next handler
|
|
268
|
+
});
|
|
269
|
+
```
|
|
243
270
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
271
|
+
### Custom WebSocket Endpoints
|
|
272
|
+
|
|
273
|
+
Additional WebSocket paths can be handled:
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
server.addWebSocketEndpoint("/ws/admin", (ws) => {
|
|
277
|
+
ws.on("message", (data) => {
|
|
278
|
+
// Handle admin WebSocket messages
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Authentication
|
|
284
|
+
|
|
285
|
+
Optionally require authentication tokens from workers:
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const server = new WorkerServer({
|
|
289
|
+
port: 8080,
|
|
290
|
+
authToken: "your-secret-token"
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Workers must send registration with matching authToken
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Load Balancing with Category Limits
|
|
297
|
+
|
|
298
|
+
Workers can declare per-category concurrency limits:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
const capabilities = {
|
|
302
|
+
models: [{ modelId: "sonnet", ... }],
|
|
303
|
+
maxConcurrentRequests: 10,
|
|
304
|
+
concurrencyLimits: {
|
|
305
|
+
inference: 5, // max 5 concurrent inference requests
|
|
306
|
+
embeddings: 2 // max 2 concurrent embedding requests
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Requests are then tracked by category:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
server.trackRequest("worker-1", "req-1", "inference");
|
|
315
|
+
server.releaseRequest("req-1"); // category looked up automatically
|
|
316
|
+
```
|