@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
package/src/bounds.mjs
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Bounds checking and enforcement for capacity-limited channels
|
|
3
|
+
* @module kgc-runtime/bounds
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bounds configuration schema
|
|
11
|
+
*/
|
|
12
|
+
const BoundsSchema = z.object({
|
|
13
|
+
max_files_touched: z.number().int().positive().optional(),
|
|
14
|
+
max_bytes_changed: z.number().int().positive().optional(),
|
|
15
|
+
max_tool_ops: z.number().int().positive().optional(),
|
|
16
|
+
max_runtime_ms: z.number().int().positive().optional(),
|
|
17
|
+
max_graph_rewrites: z.number().int().positive().optional(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Operation schema
|
|
22
|
+
*/
|
|
23
|
+
const OperationSchema = z.object({
|
|
24
|
+
type: z.string(),
|
|
25
|
+
files: z.number().int().nonnegative().optional(),
|
|
26
|
+
bytes: z.number().int().nonnegative().optional(),
|
|
27
|
+
ops: z.number().int().nonnegative().optional(),
|
|
28
|
+
ms: z.number().int().nonnegative().optional(),
|
|
29
|
+
rewrites: z.number().int().nonnegative().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Receipt schema
|
|
34
|
+
*/
|
|
35
|
+
const ReceiptSchema = z.object({
|
|
36
|
+
admit: z.boolean(),
|
|
37
|
+
receipt_id: z.string().uuid(),
|
|
38
|
+
timestamp: z.number().int().positive(),
|
|
39
|
+
bound_used: z.string().optional(),
|
|
40
|
+
reason: z.string().optional(),
|
|
41
|
+
bound_violated: z.string().optional(),
|
|
42
|
+
parent_receipt_id: z.string().uuid().optional(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Bounds checker - enforces capacity limits during execution
|
|
47
|
+
*
|
|
48
|
+
* @class
|
|
49
|
+
*/
|
|
50
|
+
export class BoundsChecker {
|
|
51
|
+
/**
|
|
52
|
+
* Create a bounds checker
|
|
53
|
+
*
|
|
54
|
+
* @param {object} bounds - Bound limits
|
|
55
|
+
* @param {number} [bounds.max_files_touched] - Maximum files that can be touched
|
|
56
|
+
* @param {number} [bounds.max_bytes_changed] - Maximum bytes that can be changed
|
|
57
|
+
* @param {number} [bounds.max_tool_ops] - Maximum tool operations
|
|
58
|
+
* @param {number} [bounds.max_runtime_ms] - Maximum runtime in milliseconds
|
|
59
|
+
* @param {number} [bounds.max_graph_rewrites] - Maximum graph rewrites
|
|
60
|
+
*/
|
|
61
|
+
constructor(bounds) {
|
|
62
|
+
this.bounds = BoundsSchema.parse(bounds);
|
|
63
|
+
|
|
64
|
+
/** @type {object} Current usage tracking */
|
|
65
|
+
this.usage = {
|
|
66
|
+
files_touched: 0,
|
|
67
|
+
bytes_changed: 0,
|
|
68
|
+
tool_ops: 0,
|
|
69
|
+
runtime_ms: 0,
|
|
70
|
+
graph_rewrites: 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Check if operation can be executed within bounds
|
|
76
|
+
*
|
|
77
|
+
* @param {object} operation - Operation to check
|
|
78
|
+
* @param {string} operation.type - Operation type
|
|
79
|
+
* @param {number} [operation.files] - Files to touch
|
|
80
|
+
* @param {number} [operation.bytes] - Bytes to change
|
|
81
|
+
* @param {number} [operation.ops] - Tool operations
|
|
82
|
+
* @param {number} [operation.ms] - Runtime milliseconds
|
|
83
|
+
* @param {number} [operation.rewrites] - Graph rewrites
|
|
84
|
+
* @returns {boolean} True if operation can be executed
|
|
85
|
+
*/
|
|
86
|
+
canExecute(operation) {
|
|
87
|
+
const op = OperationSchema.parse(operation);
|
|
88
|
+
|
|
89
|
+
// Check files bound
|
|
90
|
+
if (this.bounds.max_files_touched !== undefined && op.files !== undefined) {
|
|
91
|
+
if (this.usage.files_touched + op.files > this.bounds.max_files_touched) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check bytes bound
|
|
97
|
+
if (this.bounds.max_bytes_changed !== undefined && op.bytes !== undefined) {
|
|
98
|
+
if (this.usage.bytes_changed + op.bytes > this.bounds.max_bytes_changed) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check ops bound
|
|
104
|
+
if (this.bounds.max_tool_ops !== undefined && op.ops !== undefined) {
|
|
105
|
+
if (this.usage.tool_ops + op.ops > this.bounds.max_tool_ops) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check runtime bound
|
|
111
|
+
if (this.bounds.max_runtime_ms !== undefined && op.ms !== undefined) {
|
|
112
|
+
if (this.usage.runtime_ms + op.ms > this.bounds.max_runtime_ms) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check rewrites bound
|
|
118
|
+
if (this.bounds.max_graph_rewrites !== undefined && op.rewrites !== undefined) {
|
|
119
|
+
if (this.usage.graph_rewrites + op.rewrites > this.bounds.max_graph_rewrites) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Record operation execution and update usage
|
|
129
|
+
*
|
|
130
|
+
* @param {object} operation - Operation that was executed
|
|
131
|
+
* @param {string} operation.type - Operation type
|
|
132
|
+
* @param {number} [operation.files] - Files touched
|
|
133
|
+
* @param {number} [operation.bytes] - Bytes changed
|
|
134
|
+
* @param {number} [operation.ops] - Tool operations executed
|
|
135
|
+
* @param {number} [operation.ms] - Runtime consumed
|
|
136
|
+
* @param {number} [operation.rewrites] - Graph rewrites performed
|
|
137
|
+
* @returns {void}
|
|
138
|
+
*/
|
|
139
|
+
recordOperation(operation) {
|
|
140
|
+
const op = OperationSchema.parse(operation);
|
|
141
|
+
|
|
142
|
+
if (op.files !== undefined) {
|
|
143
|
+
this.usage.files_touched += op.files;
|
|
144
|
+
}
|
|
145
|
+
if (op.bytes !== undefined) {
|
|
146
|
+
this.usage.bytes_changed += op.bytes;
|
|
147
|
+
}
|
|
148
|
+
if (op.ops !== undefined) {
|
|
149
|
+
this.usage.tool_ops += op.ops;
|
|
150
|
+
}
|
|
151
|
+
if (op.ms !== undefined) {
|
|
152
|
+
this.usage.runtime_ms += op.ms;
|
|
153
|
+
}
|
|
154
|
+
if (op.rewrites !== undefined) {
|
|
155
|
+
this.usage.graph_rewrites += op.rewrites;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get current usage statistics
|
|
161
|
+
*
|
|
162
|
+
* @returns {object} Current usage
|
|
163
|
+
* @returns {number} .files_touched - Files touched so far
|
|
164
|
+
* @returns {number} .bytes_changed - Bytes changed so far
|
|
165
|
+
* @returns {number} .tool_ops - Tool operations executed so far
|
|
166
|
+
* @returns {number} .runtime_ms - Runtime consumed so far
|
|
167
|
+
* @returns {number} .graph_rewrites - Graph rewrites performed so far
|
|
168
|
+
*/
|
|
169
|
+
getUsage() {
|
|
170
|
+
return { ...this.usage };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Enforce bounds on an operation and generate receipt
|
|
176
|
+
*
|
|
177
|
+
* @param {object} operation - Operation to check
|
|
178
|
+
* @param {string} operation.type - Operation type
|
|
179
|
+
* @param {number} [operation.files] - Files to touch
|
|
180
|
+
* @param {number} [operation.bytes] - Bytes to change
|
|
181
|
+
* @param {number} [operation.ops] - Tool operations
|
|
182
|
+
* @param {number} [operation.ms] - Runtime milliseconds
|
|
183
|
+
* @param {number} [operation.rewrites] - Graph rewrites
|
|
184
|
+
* @param {object} bounds - Bound limits
|
|
185
|
+
* @param {number} [bounds.max_files_touched] - Maximum files
|
|
186
|
+
* @param {number} [bounds.max_bytes_changed] - Maximum bytes
|
|
187
|
+
* @param {number} [bounds.max_tool_ops] - Maximum operations
|
|
188
|
+
* @param {number} [bounds.max_runtime_ms] - Maximum runtime
|
|
189
|
+
* @param {number} [bounds.max_graph_rewrites] - Maximum rewrites
|
|
190
|
+
* @param {object} [options] - Additional options
|
|
191
|
+
* @param {string} [options.parent_receipt_id] - Parent receipt for chaining
|
|
192
|
+
* @returns {object} Receipt with admission decision
|
|
193
|
+
* @returns {boolean} .admit - Whether operation is admitted
|
|
194
|
+
* @returns {string} .receipt_id - Unique receipt identifier
|
|
195
|
+
* @returns {number} .timestamp - Receipt generation timestamp
|
|
196
|
+
* @returns {string} [.bound_used] - Bound that was checked (on admit)
|
|
197
|
+
* @returns {string} [.reason] - Rejection reason (on deny)
|
|
198
|
+
* @returns {string} [.bound_violated] - Violated bound name (on deny)
|
|
199
|
+
* @returns {string} [.parent_receipt_id] - Parent receipt if chained
|
|
200
|
+
*/
|
|
201
|
+
export function enforceBounds(operation, bounds, options = {}) {
|
|
202
|
+
const op = OperationSchema.parse(operation);
|
|
203
|
+
const bnds = BoundsSchema.parse(bounds);
|
|
204
|
+
|
|
205
|
+
const receipt = {
|
|
206
|
+
receipt_id: randomUUID(),
|
|
207
|
+
timestamp: Date.now(),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Include parent receipt if provided
|
|
211
|
+
if (options.parent_receipt_id) {
|
|
212
|
+
receipt.parent_receipt_id = options.parent_receipt_id;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Check files bound
|
|
216
|
+
if (bnds.max_files_touched !== undefined && op.files !== undefined) {
|
|
217
|
+
if (op.files > bnds.max_files_touched) {
|
|
218
|
+
return ReceiptSchema.parse({
|
|
219
|
+
...receipt,
|
|
220
|
+
admit: false,
|
|
221
|
+
reason: `exceeded max_files: requested ${op.files}, limit ${bnds.max_files_touched}`,
|
|
222
|
+
bound_violated: 'files',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check bytes bound
|
|
228
|
+
if (bnds.max_bytes_changed !== undefined && op.bytes !== undefined) {
|
|
229
|
+
if (op.bytes > bnds.max_bytes_changed) {
|
|
230
|
+
return ReceiptSchema.parse({
|
|
231
|
+
...receipt,
|
|
232
|
+
admit: false,
|
|
233
|
+
reason: `exceeded max_bytes: requested ${op.bytes}, limit ${bnds.max_bytes_changed}`,
|
|
234
|
+
bound_violated: 'bytes',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check ops bound
|
|
240
|
+
if (bnds.max_tool_ops !== undefined && op.ops !== undefined) {
|
|
241
|
+
if (op.ops > bnds.max_tool_ops) {
|
|
242
|
+
return ReceiptSchema.parse({
|
|
243
|
+
...receipt,
|
|
244
|
+
admit: false,
|
|
245
|
+
reason: `exceeded max_tool_ops: requested ${op.ops}, limit ${bnds.max_tool_ops}`,
|
|
246
|
+
bound_violated: 'ops',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check runtime bound
|
|
252
|
+
if (bnds.max_runtime_ms !== undefined && op.ms !== undefined) {
|
|
253
|
+
if (op.ms > bnds.max_runtime_ms) {
|
|
254
|
+
return ReceiptSchema.parse({
|
|
255
|
+
...receipt,
|
|
256
|
+
admit: false,
|
|
257
|
+
reason: `exceeded max_runtime: requested ${op.ms}ms, limit ${bnds.max_runtime_ms}ms`,
|
|
258
|
+
bound_violated: 'runtime',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check rewrites bound
|
|
264
|
+
if (bnds.max_graph_rewrites !== undefined && op.rewrites !== undefined) {
|
|
265
|
+
if (op.rewrites > bnds.max_graph_rewrites) {
|
|
266
|
+
return ReceiptSchema.parse({
|
|
267
|
+
...receipt,
|
|
268
|
+
admit: false,
|
|
269
|
+
reason: `exceeded max_graph_rewrites: requested ${op.rewrites}, limit ${bnds.max_graph_rewrites}`,
|
|
270
|
+
bound_violated: 'rewrites',
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Determine which bound was used (for receipt tracking)
|
|
276
|
+
let bound_used = 'none';
|
|
277
|
+
if (op.files !== undefined) bound_used = 'files';
|
|
278
|
+
else if (op.bytes !== undefined) bound_used = 'bytes';
|
|
279
|
+
else if (op.ops !== undefined) bound_used = 'ops';
|
|
280
|
+
else if (op.ms !== undefined) bound_used = 'runtime';
|
|
281
|
+
else if (op.rewrites !== undefined) bound_used = 'rewrites';
|
|
282
|
+
|
|
283
|
+
// All checks passed - admit operation
|
|
284
|
+
return ReceiptSchema.parse({
|
|
285
|
+
...receipt,
|
|
286
|
+
admit: true,
|
|
287
|
+
bound_used,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Bulkhead Isolation Pattern
|
|
3
|
+
* Resource pools with separate limits to prevent cascade failures
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { generateReceipt } from './receipt.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bulkhead configuration schema
|
|
11
|
+
*/
|
|
12
|
+
export const BulkheadConfigSchema = z.object({
|
|
13
|
+
name: z.string(),
|
|
14
|
+
maxConcurrent: z.number().positive(),
|
|
15
|
+
maxQueueSize: z.number().nonnegative(),
|
|
16
|
+
timeout: z.number().positive().optional(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {z.infer<typeof BulkheadConfigSchema>} BulkheadConfig
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bulkhead statistics
|
|
25
|
+
* @typedef {{
|
|
26
|
+
* name: string,
|
|
27
|
+
* active: number,
|
|
28
|
+
* queued: number,
|
|
29
|
+
* completed: number,
|
|
30
|
+
* rejected: number,
|
|
31
|
+
* timedOut: number,
|
|
32
|
+
* maxConcurrent: number,
|
|
33
|
+
* }} BulkheadStats
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bulkhead isolation manager for resource pools
|
|
38
|
+
* Prevents cascade failures by isolating resources
|
|
39
|
+
*/
|
|
40
|
+
export class BulkheadManager {
|
|
41
|
+
/**
|
|
42
|
+
* @param {BulkheadConfig} config - Bulkhead configuration
|
|
43
|
+
*/
|
|
44
|
+
constructor(config) {
|
|
45
|
+
this.config = BulkheadConfigSchema.parse(config);
|
|
46
|
+
this.active = new Set();
|
|
47
|
+
this.queue = [];
|
|
48
|
+
this.stats = {
|
|
49
|
+
name: config.name,
|
|
50
|
+
active: 0,
|
|
51
|
+
queued: 0,
|
|
52
|
+
completed: 0,
|
|
53
|
+
rejected: 0,
|
|
54
|
+
timedOut: 0,
|
|
55
|
+
maxConcurrent: config.maxConcurrent,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Execute function with bulkhead protection
|
|
61
|
+
* @template T
|
|
62
|
+
* @param {() => Promise<T>} fn - Async function to execute
|
|
63
|
+
* @param {object} [options] - Execution options
|
|
64
|
+
* @param {number} [options.priority=0] - Execution priority (higher = sooner)
|
|
65
|
+
* @returns {Promise<T>} Function result
|
|
66
|
+
*/
|
|
67
|
+
async execute(fn, options = {}) {
|
|
68
|
+
const priority = options.priority ?? 0;
|
|
69
|
+
|
|
70
|
+
// Check if at capacity
|
|
71
|
+
if (this.active.size >= this.config.maxConcurrent) {
|
|
72
|
+
// Check queue capacity
|
|
73
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
74
|
+
this.stats.rejected++;
|
|
75
|
+
const error = new Error(`Bulkhead '${this.config.name}' queue full (max: ${this.config.maxQueueSize})`);
|
|
76
|
+
error.code = 'BULKHEAD_QUEUE_FULL';
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Add to queue
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
this.queue.push({ fn, resolve, reject, priority });
|
|
83
|
+
this.queue.sort((a, b) => b.priority - a.priority); // Higher priority first
|
|
84
|
+
this.stats.queued = this.queue.length;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Execute immediately
|
|
89
|
+
return this._executeImmediate(fn);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Execute function immediately (internal)
|
|
94
|
+
* @template T
|
|
95
|
+
* @param {() => Promise<T>} fn - Function to execute
|
|
96
|
+
* @returns {Promise<T>} Function result
|
|
97
|
+
* @private
|
|
98
|
+
*/
|
|
99
|
+
async _executeImmediate(fn) {
|
|
100
|
+
const taskId = Symbol('task');
|
|
101
|
+
this.active.add(taskId);
|
|
102
|
+
this.stats.active = this.active.size;
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
// Apply timeout if configured
|
|
106
|
+
const result = this.config.timeout
|
|
107
|
+
? await this._withTimeout(fn(), this.config.timeout)
|
|
108
|
+
: await fn();
|
|
109
|
+
|
|
110
|
+
this.stats.completed++;
|
|
111
|
+
return result;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (error.code === 'BULKHEAD_TIMEOUT') {
|
|
114
|
+
this.stats.timedOut++;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
} finally {
|
|
118
|
+
this.active.delete(taskId);
|
|
119
|
+
this.stats.active = this.active.size;
|
|
120
|
+
this._processQueue();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Process queued tasks
|
|
126
|
+
* @private
|
|
127
|
+
*/
|
|
128
|
+
_processQueue() {
|
|
129
|
+
while (this.queue.length > 0 && this.active.size < this.config.maxConcurrent) {
|
|
130
|
+
const task = this.queue.shift();
|
|
131
|
+
this.stats.queued = this.queue.length;
|
|
132
|
+
|
|
133
|
+
this._executeImmediate(task.fn)
|
|
134
|
+
.then(task.resolve)
|
|
135
|
+
.catch(task.reject);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Wrap promise with timeout
|
|
141
|
+
* @template T
|
|
142
|
+
* @param {Promise<T>} promise - Promise to wrap
|
|
143
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
144
|
+
* @returns {Promise<T>} Promise with timeout
|
|
145
|
+
* @private
|
|
146
|
+
*/
|
|
147
|
+
async _withTimeout(promise, timeout) {
|
|
148
|
+
return Promise.race([
|
|
149
|
+
promise,
|
|
150
|
+
new Promise((_, reject) => {
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
const error = new Error(`Bulkhead '${this.config.name}' operation timed out after ${timeout}ms`);
|
|
153
|
+
error.code = 'BULKHEAD_TIMEOUT';
|
|
154
|
+
reject(error);
|
|
155
|
+
}, timeout);
|
|
156
|
+
}),
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get current statistics
|
|
162
|
+
* @returns {BulkheadStats} Current statistics
|
|
163
|
+
*/
|
|
164
|
+
getStats() {
|
|
165
|
+
return { ...this.stats };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Clear queue (reject all pending)
|
|
170
|
+
* @returns {number} Number of tasks rejected
|
|
171
|
+
*/
|
|
172
|
+
clearQueue() {
|
|
173
|
+
const count = this.queue.length;
|
|
174
|
+
const error = new Error(`Bulkhead '${this.config.name}' queue cleared`);
|
|
175
|
+
error.code = 'BULKHEAD_CLEARED';
|
|
176
|
+
|
|
177
|
+
for (const task of this.queue) {
|
|
178
|
+
task.reject(error);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.queue = [];
|
|
182
|
+
this.stats.queued = 0;
|
|
183
|
+
this.stats.rejected += count;
|
|
184
|
+
|
|
185
|
+
return count;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Wait for all active tasks to complete
|
|
190
|
+
* @returns {Promise<void>}
|
|
191
|
+
*/
|
|
192
|
+
async drain() {
|
|
193
|
+
while (this.active.size > 0 || this.queue.length > 0) {
|
|
194
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generate receipt for bulkhead operation
|
|
200
|
+
* @param {string} operation - Operation name
|
|
201
|
+
* @param {Record<string, any>} inputs - Operation inputs
|
|
202
|
+
* @param {Record<string, any>} outputs - Operation outputs
|
|
203
|
+
* @returns {Promise<import('./receipt.mjs').Receipt>} Receipt
|
|
204
|
+
*/
|
|
205
|
+
async generateReceipt(operation, inputs, outputs) {
|
|
206
|
+
return generateReceipt(
|
|
207
|
+
`bulkhead:${this.config.name}:${operation}`,
|
|
208
|
+
{ ...inputs, bulkhead: this.config.name },
|
|
209
|
+
{ ...outputs, stats: this.getStats() }
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Multi-bulkhead coordinator for managing multiple resource pools
|
|
216
|
+
*/
|
|
217
|
+
export class BulkheadCoordinator {
|
|
218
|
+
constructor() {
|
|
219
|
+
/** @type {Map<string, BulkheadManager>} */
|
|
220
|
+
this.bulkheads = new Map();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Register a bulkhead
|
|
225
|
+
* @param {BulkheadConfig} config - Bulkhead configuration
|
|
226
|
+
* @returns {BulkheadManager} Created bulkhead
|
|
227
|
+
*/
|
|
228
|
+
register(config) {
|
|
229
|
+
const bulkhead = new BulkheadManager(config);
|
|
230
|
+
this.bulkheads.set(config.name, bulkhead);
|
|
231
|
+
return bulkhead;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get bulkhead by name
|
|
236
|
+
* @param {string} name - Bulkhead name
|
|
237
|
+
* @returns {BulkheadManager | undefined} Bulkhead manager
|
|
238
|
+
*/
|
|
239
|
+
get(name) {
|
|
240
|
+
return this.bulkheads.get(name);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Execute on named bulkhead
|
|
245
|
+
* @template T
|
|
246
|
+
* @param {string} name - Bulkhead name
|
|
247
|
+
* @param {() => Promise<T>} fn - Function to execute
|
|
248
|
+
* @param {object} [options] - Execution options
|
|
249
|
+
* @returns {Promise<T>} Function result
|
|
250
|
+
*/
|
|
251
|
+
async execute(name, fn, options) {
|
|
252
|
+
const bulkhead = this.bulkheads.get(name);
|
|
253
|
+
if (!bulkhead) {
|
|
254
|
+
throw new Error(`Bulkhead '${name}' not found`);
|
|
255
|
+
}
|
|
256
|
+
return bulkhead.execute(fn, options);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get statistics for all bulkheads
|
|
261
|
+
* @returns {Record<string, BulkheadStats>} All statistics
|
|
262
|
+
*/
|
|
263
|
+
getAllStats() {
|
|
264
|
+
const stats = {};
|
|
265
|
+
for (const [name, bulkhead] of this.bulkheads.entries()) {
|
|
266
|
+
stats[name] = bulkhead.getStats();
|
|
267
|
+
}
|
|
268
|
+
return stats;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Drain all bulkheads
|
|
273
|
+
* @returns {Promise<void>}
|
|
274
|
+
*/
|
|
275
|
+
async drainAll() {
|
|
276
|
+
await Promise.all(
|
|
277
|
+
Array.from(this.bulkheads.values()).map(b => b.drain())
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|