@backendkit-labs/bulkhead 0.1.0 → 0.1.1
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/chunk-LXXCDKHB.cjs +243 -0
- package/dist/chunk-LXXCDKHB.cjs.map +1 -0
- package/dist/chunk-SRWDZPTJ.js +236 -0
- package/dist/chunk-SRWDZPTJ.js.map +1 -0
- package/dist/index.cjs +18 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -222
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +17 -246
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.js +1 -230
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +90 -89
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
6
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
7
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
8
|
+
if (decorator = decorators[i])
|
|
9
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
10
|
+
if (kind && result) __defProp(target, key, result);
|
|
11
|
+
return result;
|
|
12
|
+
};
|
|
13
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
14
|
+
|
|
15
|
+
// src/bulkhead/bulkhead.ts
|
|
16
|
+
var BulkheadRejectedError = class extends Error {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "BulkheadRejectedError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var BulkheadTimeoutError = class extends Error {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "BulkheadTimeoutError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var Bulkhead = class {
|
|
29
|
+
constructor(config) {
|
|
30
|
+
this.config = config;
|
|
31
|
+
}
|
|
32
|
+
config;
|
|
33
|
+
activeCalls = 0;
|
|
34
|
+
nextId = 0;
|
|
35
|
+
queue = [];
|
|
36
|
+
totalCalls = 0;
|
|
37
|
+
successfulCalls = 0;
|
|
38
|
+
failedCalls = 0;
|
|
39
|
+
rejectedCalls = 0;
|
|
40
|
+
timedOutCalls = 0;
|
|
41
|
+
totalDurationMs = 0;
|
|
42
|
+
async execute(task) {
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
this.totalCalls++;
|
|
45
|
+
if (this.activeCalls < this.config.maxConcurrentCalls) {
|
|
46
|
+
return this.runTask(task, startTime);
|
|
47
|
+
}
|
|
48
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
49
|
+
if (this.config.rejectWhenFull) {
|
|
50
|
+
this.rejectedCalls++;
|
|
51
|
+
throw new BulkheadRejectedError(
|
|
52
|
+
`Bulkhead '${this.config.name}' is full. Active: ${this.activeCalls}, Queue: ${this.queue.length}, Max: ${this.config.maxConcurrentCalls}`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return this.waitAndRetry(task, startTime);
|
|
56
|
+
}
|
|
57
|
+
const entryId = this.nextId++;
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const timeoutId = setTimeout(() => {
|
|
60
|
+
const index = this.queue.findIndex((item) => item.id === entryId);
|
|
61
|
+
if (index !== -1) this.queue.splice(index, 1);
|
|
62
|
+
this.timedOutCalls++;
|
|
63
|
+
reject(
|
|
64
|
+
new BulkheadTimeoutError(
|
|
65
|
+
`Bulkhead '${this.config.name}' timeout after ${this.config.queueTimeoutMs}ms`
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}, this.config.queueTimeoutMs);
|
|
69
|
+
this.queue.push({
|
|
70
|
+
id: entryId,
|
|
71
|
+
task,
|
|
72
|
+
resolve: (value) => {
|
|
73
|
+
clearTimeout(timeoutId);
|
|
74
|
+
resolve(value);
|
|
75
|
+
},
|
|
76
|
+
reject: (reason) => {
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
reject(reason);
|
|
79
|
+
},
|
|
80
|
+
queuedAt: startTime
|
|
81
|
+
});
|
|
82
|
+
this.processQueue();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async runTask(task, startTime) {
|
|
86
|
+
this.activeCalls++;
|
|
87
|
+
try {
|
|
88
|
+
const result = await task();
|
|
89
|
+
this.successfulCalls++;
|
|
90
|
+
this.totalDurationMs += Date.now() - startTime;
|
|
91
|
+
return result;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.failedCalls++;
|
|
94
|
+
throw error;
|
|
95
|
+
} finally {
|
|
96
|
+
this.activeCalls--;
|
|
97
|
+
this.processQueue();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async waitAndRetry(task, startTime) {
|
|
101
|
+
const maxRetries = 3;
|
|
102
|
+
const baseDelay = 100;
|
|
103
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
104
|
+
if (this.activeCalls < this.config.maxConcurrentCalls) {
|
|
105
|
+
return this.runTask(task, startTime);
|
|
106
|
+
}
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)));
|
|
108
|
+
}
|
|
109
|
+
this.rejectedCalls++;
|
|
110
|
+
throw new BulkheadRejectedError(
|
|
111
|
+
`Bulkhead '${this.config.name}' rejected after ${maxRetries} retries`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
processQueue() {
|
|
115
|
+
while (this.activeCalls < this.config.maxConcurrentCalls && this.queue.length > 0) {
|
|
116
|
+
const next = this.queue.shift();
|
|
117
|
+
if (!next) break;
|
|
118
|
+
const waitTime = Date.now() - next.queuedAt;
|
|
119
|
+
if (waitTime > this.config.queueTimeoutMs) {
|
|
120
|
+
next.reject(new BulkheadTimeoutError(`Task timed out after ${waitTime}ms in queue`));
|
|
121
|
+
this.timedOutCalls++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
this.activeCalls++;
|
|
125
|
+
next.task().then((result) => {
|
|
126
|
+
this.successfulCalls++;
|
|
127
|
+
next.resolve(result);
|
|
128
|
+
}).catch((error) => {
|
|
129
|
+
this.failedCalls++;
|
|
130
|
+
next.reject(error);
|
|
131
|
+
}).finally(() => {
|
|
132
|
+
this.activeCalls--;
|
|
133
|
+
this.processQueue();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
getMetrics() {
|
|
138
|
+
return {
|
|
139
|
+
name: this.config.name,
|
|
140
|
+
activeCalls: this.activeCalls,
|
|
141
|
+
queuedCalls: this.queue.length,
|
|
142
|
+
maxConcurrentCalls: this.config.maxConcurrentCalls,
|
|
143
|
+
maxQueueSize: this.config.maxQueueSize,
|
|
144
|
+
totalCalls: this.totalCalls,
|
|
145
|
+
successfulCalls: this.successfulCalls,
|
|
146
|
+
failedCalls: this.failedCalls,
|
|
147
|
+
rejectedCalls: this.rejectedCalls,
|
|
148
|
+
timedOutCalls: this.timedOutCalls,
|
|
149
|
+
averageDurationMs: this.successfulCalls > 0 ? Math.round(this.totalDurationMs / this.successfulCalls) : 0
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
canAccept() {
|
|
153
|
+
return this.activeCalls < this.config.maxConcurrentCalls || this.queue.length < this.config.maxQueueSize;
|
|
154
|
+
}
|
|
155
|
+
resetMetrics() {
|
|
156
|
+
this.totalCalls = 0;
|
|
157
|
+
this.successfulCalls = 0;
|
|
158
|
+
this.failedCalls = 0;
|
|
159
|
+
this.rejectedCalls = 0;
|
|
160
|
+
this.timedOutCalls = 0;
|
|
161
|
+
this.totalDurationMs = 0;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/bulkhead/bulkhead.registry.ts
|
|
166
|
+
var DEFAULT_CONFIG = {
|
|
167
|
+
maxConcurrentCalls: 10,
|
|
168
|
+
maxQueueSize: 100,
|
|
169
|
+
queueTimeoutMs: 3e4,
|
|
170
|
+
rejectWhenFull: true
|
|
171
|
+
};
|
|
172
|
+
var BulkheadRegistry = class {
|
|
173
|
+
bulkheads = /* @__PURE__ */ new Map();
|
|
174
|
+
getOrCreate(options) {
|
|
175
|
+
if (!this.bulkheads.has(options.name)) {
|
|
176
|
+
const config = { ...DEFAULT_CONFIG, ...options, name: options.name };
|
|
177
|
+
this.bulkheads.set(options.name, new Bulkhead(config));
|
|
178
|
+
}
|
|
179
|
+
return this.bulkheads.get(options.name);
|
|
180
|
+
}
|
|
181
|
+
/** Per-client isolation — 5 concurrent, 20 queued */
|
|
182
|
+
getForClient(clientId, endpoint) {
|
|
183
|
+
const name = endpoint ? `client:${clientId}:${endpoint}` : `client:${clientId}`;
|
|
184
|
+
return this.getOrCreate({ name, maxConcurrentCalls: 5, maxQueueSize: 20 });
|
|
185
|
+
}
|
|
186
|
+
/** Service-level limiting — 20 concurrent, 200 queued */
|
|
187
|
+
getForService(serviceName) {
|
|
188
|
+
return this.getOrCreate({
|
|
189
|
+
name: `service:${serviceName}`,
|
|
190
|
+
maxConcurrentCalls: 20,
|
|
191
|
+
maxQueueSize: 200
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
/** Database connection limiting — 15 concurrent, 150 queued */
|
|
195
|
+
getForDatabase(schema) {
|
|
196
|
+
return this.getOrCreate({
|
|
197
|
+
name: `database:${schema}`,
|
|
198
|
+
maxConcurrentCalls: 15,
|
|
199
|
+
maxQueueSize: 150
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/** External HTTP calls — 8 concurrent, 50 queued, 10s timeout */
|
|
203
|
+
getForHttpExternal(serviceName) {
|
|
204
|
+
return this.getOrCreate({
|
|
205
|
+
name: `http:${serviceName}`,
|
|
206
|
+
maxConcurrentCalls: 8,
|
|
207
|
+
maxQueueSize: 50,
|
|
208
|
+
queueTimeoutMs: 1e4
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
getAllMetrics() {
|
|
212
|
+
const metrics = {};
|
|
213
|
+
for (const [name, bulkhead] of this.bulkheads) {
|
|
214
|
+
metrics[name] = bulkhead.getMetrics();
|
|
215
|
+
}
|
|
216
|
+
return metrics;
|
|
217
|
+
}
|
|
218
|
+
/** Returns bulkheads at or above 80% active capacity */
|
|
219
|
+
getOverloadedBulkheads() {
|
|
220
|
+
const overloaded = [];
|
|
221
|
+
for (const bulkhead of this.bulkheads.values()) {
|
|
222
|
+
const m = bulkhead.getMetrics();
|
|
223
|
+
if (m.activeCalls >= m.maxConcurrentCalls * 0.8) {
|
|
224
|
+
overloaded.push(m);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return overloaded;
|
|
228
|
+
}
|
|
229
|
+
resetAllMetrics() {
|
|
230
|
+
for (const bulkhead of this.bulkheads.values()) {
|
|
231
|
+
bulkhead.resetMetrics();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
exports.Bulkhead = Bulkhead;
|
|
237
|
+
exports.BulkheadRegistry = BulkheadRegistry;
|
|
238
|
+
exports.BulkheadRejectedError = BulkheadRejectedError;
|
|
239
|
+
exports.BulkheadTimeoutError = BulkheadTimeoutError;
|
|
240
|
+
exports.__decorateClass = __decorateClass;
|
|
241
|
+
exports.__decorateParam = __decorateParam;
|
|
242
|
+
//# sourceMappingURL=chunk-LXXCDKHB.cjs.map
|
|
243
|
+
//# sourceMappingURL=chunk-LXXCDKHB.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bulkhead/bulkhead.ts","../src/bulkhead/bulkhead.registry.ts"],"names":[],"mappings":"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","file":"chunk-LXXCDKHB.cjs","sourcesContent":["export interface BulkheadConfig {\n /** Max number of concurrent executions */\n maxConcurrentCalls: number;\n /** Max queue size for waiting tasks */\n maxQueueSize: number;\n /** Max time a task can wait in queue (ms) */\n queueTimeoutMs: number;\n /** Reject immediately when queue is full; if false, retries with backoff */\n rejectWhenFull: boolean;\n /** Identifier used in metrics and error messages */\n name: string;\n}\n\nexport interface BulkheadMetrics {\n name: string;\n activeCalls: number;\n queuedCalls: number;\n maxConcurrentCalls: number;\n maxQueueSize: number;\n totalCalls: number;\n successfulCalls: number;\n failedCalls: number;\n rejectedCalls: number;\n timedOutCalls: number;\n averageDurationMs: number;\n}\n\nexport class BulkheadRejectedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport class BulkheadTimeoutError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BulkheadTimeoutError';\n }\n}\n\nexport class Bulkhead {\n private activeCalls = 0;\n private nextId = 0;\n private queue: Array<{\n id: number;\n task: () => Promise<unknown>;\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n queuedAt: number;\n }> = [];\n\n private totalCalls = 0;\n private successfulCalls = 0;\n private failedCalls = 0;\n private rejectedCalls = 0;\n private timedOutCalls = 0;\n private totalDurationMs = 0;\n\n constructor(private readonly config: BulkheadConfig) {}\n\n async execute<T>(task: () => Promise<T>): Promise<T> {\n const startTime = Date.now();\n this.totalCalls++;\n\n if (this.activeCalls < this.config.maxConcurrentCalls) {\n return this.runTask(task, startTime);\n }\n\n if (this.queue.length >= this.config.maxQueueSize) {\n if (this.config.rejectWhenFull) {\n this.rejectedCalls++;\n throw new BulkheadRejectedError(\n `Bulkhead '${this.config.name}' is full. ` +\n `Active: ${this.activeCalls}, Queue: ${this.queue.length}, Max: ${this.config.maxConcurrentCalls}`,\n );\n }\n return this.waitAndRetry(task, startTime);\n }\n\n const entryId = this.nextId++;\n return new Promise<T>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n const index = this.queue.findIndex(item => item.id === entryId);\n if (index !== -1) this.queue.splice(index, 1);\n this.timedOutCalls++;\n reject(\n new BulkheadTimeoutError(\n `Bulkhead '${this.config.name}' timeout after ${this.config.queueTimeoutMs}ms`,\n ),\n );\n }, this.config.queueTimeoutMs);\n\n this.queue.push({\n id: entryId,\n task: task as () => Promise<unknown>,\n resolve: value => {\n clearTimeout(timeoutId);\n resolve(value as T);\n },\n reject: reason => {\n clearTimeout(timeoutId);\n reject(reason);\n },\n queuedAt: startTime,\n });\n\n this.processQueue();\n });\n }\n\n private async runTask<T>(task: () => Promise<T>, startTime: number): Promise<T> {\n this.activeCalls++;\n try {\n const result = await task();\n this.successfulCalls++;\n this.totalDurationMs += Date.now() - startTime;\n return result;\n } catch (error) {\n this.failedCalls++;\n throw error;\n } finally {\n this.activeCalls--;\n this.processQueue();\n }\n }\n\n private async waitAndRetry<T>(task: () => Promise<T>, startTime: number): Promise<T> {\n const maxRetries = 3;\n const baseDelay = 100;\n\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n if (this.activeCalls < this.config.maxConcurrentCalls) {\n return this.runTask(task, startTime);\n }\n await new Promise(resolve => setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)));\n }\n\n this.rejectedCalls++;\n throw new BulkheadRejectedError(\n `Bulkhead '${this.config.name}' rejected after ${maxRetries} retries`,\n );\n }\n\n private processQueue(): void {\n while (this.activeCalls < this.config.maxConcurrentCalls && this.queue.length > 0) {\n const next = this.queue.shift();\n if (!next) break;\n\n const waitTime = Date.now() - next.queuedAt;\n if (waitTime > this.config.queueTimeoutMs) {\n next.reject(new BulkheadTimeoutError(`Task timed out after ${waitTime}ms in queue`));\n this.timedOutCalls++;\n continue;\n }\n\n this.activeCalls++;\n next\n .task()\n .then(result => {\n this.successfulCalls++;\n next.resolve(result);\n })\n .catch(error => {\n this.failedCalls++;\n next.reject(error);\n })\n .finally(() => {\n this.activeCalls--;\n this.processQueue();\n });\n }\n }\n\n getMetrics(): BulkheadMetrics {\n return {\n name: this.config.name,\n activeCalls: this.activeCalls,\n queuedCalls: this.queue.length,\n maxConcurrentCalls: this.config.maxConcurrentCalls,\n maxQueueSize: this.config.maxQueueSize,\n totalCalls: this.totalCalls,\n successfulCalls: this.successfulCalls,\n failedCalls: this.failedCalls,\n rejectedCalls: this.rejectedCalls,\n timedOutCalls: this.timedOutCalls,\n averageDurationMs:\n this.successfulCalls > 0 ? Math.round(this.totalDurationMs / this.successfulCalls) : 0,\n };\n }\n\n canAccept(): boolean {\n return (\n this.activeCalls < this.config.maxConcurrentCalls ||\n this.queue.length < this.config.maxQueueSize\n );\n }\n\n resetMetrics(): void {\n this.totalCalls = 0;\n this.successfulCalls = 0;\n this.failedCalls = 0;\n this.rejectedCalls = 0;\n this.timedOutCalls = 0;\n this.totalDurationMs = 0;\n }\n}\n","import { Bulkhead, BulkheadConfig, BulkheadMetrics } from './bulkhead.js';\n\nexport interface BulkheadOptions extends Partial<BulkheadConfig> {\n name: string;\n}\n\nconst DEFAULT_CONFIG: Omit<BulkheadConfig, 'name'> = {\n maxConcurrentCalls: 10,\n maxQueueSize: 100,\n queueTimeoutMs: 30000,\n rejectWhenFull: true,\n};\n\nexport class BulkheadRegistry {\n private readonly bulkheads = new Map<string, Bulkhead>();\n\n getOrCreate(options: BulkheadOptions): Bulkhead {\n if (!this.bulkheads.has(options.name)) {\n const config: BulkheadConfig = { ...DEFAULT_CONFIG, ...options, name: options.name };\n this.bulkheads.set(options.name, new Bulkhead(config));\n }\n return this.bulkheads.get(options.name)!;\n }\n\n /** Per-client isolation — 5 concurrent, 20 queued */\n getForClient(clientId: string, endpoint?: string): Bulkhead {\n const name = endpoint ? `client:${clientId}:${endpoint}` : `client:${clientId}`;\n return this.getOrCreate({ name, maxConcurrentCalls: 5, maxQueueSize: 20 });\n }\n\n /** Service-level limiting — 20 concurrent, 200 queued */\n getForService(serviceName: string): Bulkhead {\n return this.getOrCreate({\n name: `service:${serviceName}`,\n maxConcurrentCalls: 20,\n maxQueueSize: 200,\n });\n }\n\n /** Database connection limiting — 15 concurrent, 150 queued */\n getForDatabase(schema: string): Bulkhead {\n return this.getOrCreate({\n name: `database:${schema}`,\n maxConcurrentCalls: 15,\n maxQueueSize: 150,\n });\n }\n\n /** External HTTP calls — 8 concurrent, 50 queued, 10s timeout */\n getForHttpExternal(serviceName: string): Bulkhead {\n return this.getOrCreate({\n name: `http:${serviceName}`,\n maxConcurrentCalls: 8,\n maxQueueSize: 50,\n queueTimeoutMs: 10000,\n });\n }\n\n getAllMetrics(): Record<string, BulkheadMetrics> {\n const metrics: Record<string, BulkheadMetrics> = {};\n for (const [name, bulkhead] of this.bulkheads) {\n metrics[name] = bulkhead.getMetrics();\n }\n return metrics;\n }\n\n /** Returns bulkheads at or above 80% active capacity */\n getOverloadedBulkheads(): BulkheadMetrics[] {\n const overloaded: BulkheadMetrics[] = [];\n for (const bulkhead of this.bulkheads.values()) {\n const m = bulkhead.getMetrics();\n if (m.activeCalls >= m.maxConcurrentCalls * 0.8) {\n overloaded.push(m);\n }\n }\n return overloaded;\n }\n\n resetAllMetrics(): void {\n for (const bulkhead of this.bulkheads.values()) {\n bulkhead.resetMetrics();\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
4
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
5
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
6
|
+
if (decorator = decorators[i])
|
|
7
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
8
|
+
if (kind && result) __defProp(target, key, result);
|
|
9
|
+
return result;
|
|
10
|
+
};
|
|
11
|
+
var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
|
|
12
|
+
|
|
13
|
+
// src/bulkhead/bulkhead.ts
|
|
14
|
+
var BulkheadRejectedError = class extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "BulkheadRejectedError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var BulkheadTimeoutError = class extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "BulkheadTimeoutError";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var Bulkhead = class {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
}
|
|
30
|
+
config;
|
|
31
|
+
activeCalls = 0;
|
|
32
|
+
nextId = 0;
|
|
33
|
+
queue = [];
|
|
34
|
+
totalCalls = 0;
|
|
35
|
+
successfulCalls = 0;
|
|
36
|
+
failedCalls = 0;
|
|
37
|
+
rejectedCalls = 0;
|
|
38
|
+
timedOutCalls = 0;
|
|
39
|
+
totalDurationMs = 0;
|
|
40
|
+
async execute(task) {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
this.totalCalls++;
|
|
43
|
+
if (this.activeCalls < this.config.maxConcurrentCalls) {
|
|
44
|
+
return this.runTask(task, startTime);
|
|
45
|
+
}
|
|
46
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
47
|
+
if (this.config.rejectWhenFull) {
|
|
48
|
+
this.rejectedCalls++;
|
|
49
|
+
throw new BulkheadRejectedError(
|
|
50
|
+
`Bulkhead '${this.config.name}' is full. Active: ${this.activeCalls}, Queue: ${this.queue.length}, Max: ${this.config.maxConcurrentCalls}`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return this.waitAndRetry(task, startTime);
|
|
54
|
+
}
|
|
55
|
+
const entryId = this.nextId++;
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const timeoutId = setTimeout(() => {
|
|
58
|
+
const index = this.queue.findIndex((item) => item.id === entryId);
|
|
59
|
+
if (index !== -1) this.queue.splice(index, 1);
|
|
60
|
+
this.timedOutCalls++;
|
|
61
|
+
reject(
|
|
62
|
+
new BulkheadTimeoutError(
|
|
63
|
+
`Bulkhead '${this.config.name}' timeout after ${this.config.queueTimeoutMs}ms`
|
|
64
|
+
)
|
|
65
|
+
);
|
|
66
|
+
}, this.config.queueTimeoutMs);
|
|
67
|
+
this.queue.push({
|
|
68
|
+
id: entryId,
|
|
69
|
+
task,
|
|
70
|
+
resolve: (value) => {
|
|
71
|
+
clearTimeout(timeoutId);
|
|
72
|
+
resolve(value);
|
|
73
|
+
},
|
|
74
|
+
reject: (reason) => {
|
|
75
|
+
clearTimeout(timeoutId);
|
|
76
|
+
reject(reason);
|
|
77
|
+
},
|
|
78
|
+
queuedAt: startTime
|
|
79
|
+
});
|
|
80
|
+
this.processQueue();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async runTask(task, startTime) {
|
|
84
|
+
this.activeCalls++;
|
|
85
|
+
try {
|
|
86
|
+
const result = await task();
|
|
87
|
+
this.successfulCalls++;
|
|
88
|
+
this.totalDurationMs += Date.now() - startTime;
|
|
89
|
+
return result;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
this.failedCalls++;
|
|
92
|
+
throw error;
|
|
93
|
+
} finally {
|
|
94
|
+
this.activeCalls--;
|
|
95
|
+
this.processQueue();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async waitAndRetry(task, startTime) {
|
|
99
|
+
const maxRetries = 3;
|
|
100
|
+
const baseDelay = 100;
|
|
101
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
102
|
+
if (this.activeCalls < this.config.maxConcurrentCalls) {
|
|
103
|
+
return this.runTask(task, startTime);
|
|
104
|
+
}
|
|
105
|
+
await new Promise((resolve) => setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)));
|
|
106
|
+
}
|
|
107
|
+
this.rejectedCalls++;
|
|
108
|
+
throw new BulkheadRejectedError(
|
|
109
|
+
`Bulkhead '${this.config.name}' rejected after ${maxRetries} retries`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
processQueue() {
|
|
113
|
+
while (this.activeCalls < this.config.maxConcurrentCalls && this.queue.length > 0) {
|
|
114
|
+
const next = this.queue.shift();
|
|
115
|
+
if (!next) break;
|
|
116
|
+
const waitTime = Date.now() - next.queuedAt;
|
|
117
|
+
if (waitTime > this.config.queueTimeoutMs) {
|
|
118
|
+
next.reject(new BulkheadTimeoutError(`Task timed out after ${waitTime}ms in queue`));
|
|
119
|
+
this.timedOutCalls++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
this.activeCalls++;
|
|
123
|
+
next.task().then((result) => {
|
|
124
|
+
this.successfulCalls++;
|
|
125
|
+
next.resolve(result);
|
|
126
|
+
}).catch((error) => {
|
|
127
|
+
this.failedCalls++;
|
|
128
|
+
next.reject(error);
|
|
129
|
+
}).finally(() => {
|
|
130
|
+
this.activeCalls--;
|
|
131
|
+
this.processQueue();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
getMetrics() {
|
|
136
|
+
return {
|
|
137
|
+
name: this.config.name,
|
|
138
|
+
activeCalls: this.activeCalls,
|
|
139
|
+
queuedCalls: this.queue.length,
|
|
140
|
+
maxConcurrentCalls: this.config.maxConcurrentCalls,
|
|
141
|
+
maxQueueSize: this.config.maxQueueSize,
|
|
142
|
+
totalCalls: this.totalCalls,
|
|
143
|
+
successfulCalls: this.successfulCalls,
|
|
144
|
+
failedCalls: this.failedCalls,
|
|
145
|
+
rejectedCalls: this.rejectedCalls,
|
|
146
|
+
timedOutCalls: this.timedOutCalls,
|
|
147
|
+
averageDurationMs: this.successfulCalls > 0 ? Math.round(this.totalDurationMs / this.successfulCalls) : 0
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
canAccept() {
|
|
151
|
+
return this.activeCalls < this.config.maxConcurrentCalls || this.queue.length < this.config.maxQueueSize;
|
|
152
|
+
}
|
|
153
|
+
resetMetrics() {
|
|
154
|
+
this.totalCalls = 0;
|
|
155
|
+
this.successfulCalls = 0;
|
|
156
|
+
this.failedCalls = 0;
|
|
157
|
+
this.rejectedCalls = 0;
|
|
158
|
+
this.timedOutCalls = 0;
|
|
159
|
+
this.totalDurationMs = 0;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// src/bulkhead/bulkhead.registry.ts
|
|
164
|
+
var DEFAULT_CONFIG = {
|
|
165
|
+
maxConcurrentCalls: 10,
|
|
166
|
+
maxQueueSize: 100,
|
|
167
|
+
queueTimeoutMs: 3e4,
|
|
168
|
+
rejectWhenFull: true
|
|
169
|
+
};
|
|
170
|
+
var BulkheadRegistry = class {
|
|
171
|
+
bulkheads = /* @__PURE__ */ new Map();
|
|
172
|
+
getOrCreate(options) {
|
|
173
|
+
if (!this.bulkheads.has(options.name)) {
|
|
174
|
+
const config = { ...DEFAULT_CONFIG, ...options, name: options.name };
|
|
175
|
+
this.bulkheads.set(options.name, new Bulkhead(config));
|
|
176
|
+
}
|
|
177
|
+
return this.bulkheads.get(options.name);
|
|
178
|
+
}
|
|
179
|
+
/** Per-client isolation — 5 concurrent, 20 queued */
|
|
180
|
+
getForClient(clientId, endpoint) {
|
|
181
|
+
const name = endpoint ? `client:${clientId}:${endpoint}` : `client:${clientId}`;
|
|
182
|
+
return this.getOrCreate({ name, maxConcurrentCalls: 5, maxQueueSize: 20 });
|
|
183
|
+
}
|
|
184
|
+
/** Service-level limiting — 20 concurrent, 200 queued */
|
|
185
|
+
getForService(serviceName) {
|
|
186
|
+
return this.getOrCreate({
|
|
187
|
+
name: `service:${serviceName}`,
|
|
188
|
+
maxConcurrentCalls: 20,
|
|
189
|
+
maxQueueSize: 200
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Database connection limiting — 15 concurrent, 150 queued */
|
|
193
|
+
getForDatabase(schema) {
|
|
194
|
+
return this.getOrCreate({
|
|
195
|
+
name: `database:${schema}`,
|
|
196
|
+
maxConcurrentCalls: 15,
|
|
197
|
+
maxQueueSize: 150
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/** External HTTP calls — 8 concurrent, 50 queued, 10s timeout */
|
|
201
|
+
getForHttpExternal(serviceName) {
|
|
202
|
+
return this.getOrCreate({
|
|
203
|
+
name: `http:${serviceName}`,
|
|
204
|
+
maxConcurrentCalls: 8,
|
|
205
|
+
maxQueueSize: 50,
|
|
206
|
+
queueTimeoutMs: 1e4
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
getAllMetrics() {
|
|
210
|
+
const metrics = {};
|
|
211
|
+
for (const [name, bulkhead] of this.bulkheads) {
|
|
212
|
+
metrics[name] = bulkhead.getMetrics();
|
|
213
|
+
}
|
|
214
|
+
return metrics;
|
|
215
|
+
}
|
|
216
|
+
/** Returns bulkheads at or above 80% active capacity */
|
|
217
|
+
getOverloadedBulkheads() {
|
|
218
|
+
const overloaded = [];
|
|
219
|
+
for (const bulkhead of this.bulkheads.values()) {
|
|
220
|
+
const m = bulkhead.getMetrics();
|
|
221
|
+
if (m.activeCalls >= m.maxConcurrentCalls * 0.8) {
|
|
222
|
+
overloaded.push(m);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return overloaded;
|
|
226
|
+
}
|
|
227
|
+
resetAllMetrics() {
|
|
228
|
+
for (const bulkhead of this.bulkheads.values()) {
|
|
229
|
+
bulkhead.resetMetrics();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export { Bulkhead, BulkheadRegistry, BulkheadRejectedError, BulkheadTimeoutError, __decorateClass, __decorateParam };
|
|
235
|
+
//# sourceMappingURL=chunk-SRWDZPTJ.js.map
|
|
236
|
+
//# sourceMappingURL=chunk-SRWDZPTJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bulkhead/bulkhead.ts","../src/bulkhead/bulkhead.registry.ts"],"names":[],"mappings":"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","file":"chunk-SRWDZPTJ.js","sourcesContent":["export interface BulkheadConfig {\n /** Max number of concurrent executions */\n maxConcurrentCalls: number;\n /** Max queue size for waiting tasks */\n maxQueueSize: number;\n /** Max time a task can wait in queue (ms) */\n queueTimeoutMs: number;\n /** Reject immediately when queue is full; if false, retries with backoff */\n rejectWhenFull: boolean;\n /** Identifier used in metrics and error messages */\n name: string;\n}\n\nexport interface BulkheadMetrics {\n name: string;\n activeCalls: number;\n queuedCalls: number;\n maxConcurrentCalls: number;\n maxQueueSize: number;\n totalCalls: number;\n successfulCalls: number;\n failedCalls: number;\n rejectedCalls: number;\n timedOutCalls: number;\n averageDurationMs: number;\n}\n\nexport class BulkheadRejectedError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BulkheadRejectedError';\n }\n}\n\nexport class BulkheadTimeoutError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'BulkheadTimeoutError';\n }\n}\n\nexport class Bulkhead {\n private activeCalls = 0;\n private nextId = 0;\n private queue: Array<{\n id: number;\n task: () => Promise<unknown>;\n resolve: (value: unknown) => void;\n reject: (reason: unknown) => void;\n queuedAt: number;\n }> = [];\n\n private totalCalls = 0;\n private successfulCalls = 0;\n private failedCalls = 0;\n private rejectedCalls = 0;\n private timedOutCalls = 0;\n private totalDurationMs = 0;\n\n constructor(private readonly config: BulkheadConfig) {}\n\n async execute<T>(task: () => Promise<T>): Promise<T> {\n const startTime = Date.now();\n this.totalCalls++;\n\n if (this.activeCalls < this.config.maxConcurrentCalls) {\n return this.runTask(task, startTime);\n }\n\n if (this.queue.length >= this.config.maxQueueSize) {\n if (this.config.rejectWhenFull) {\n this.rejectedCalls++;\n throw new BulkheadRejectedError(\n `Bulkhead '${this.config.name}' is full. ` +\n `Active: ${this.activeCalls}, Queue: ${this.queue.length}, Max: ${this.config.maxConcurrentCalls}`,\n );\n }\n return this.waitAndRetry(task, startTime);\n }\n\n const entryId = this.nextId++;\n return new Promise<T>((resolve, reject) => {\n const timeoutId = setTimeout(() => {\n const index = this.queue.findIndex(item => item.id === entryId);\n if (index !== -1) this.queue.splice(index, 1);\n this.timedOutCalls++;\n reject(\n new BulkheadTimeoutError(\n `Bulkhead '${this.config.name}' timeout after ${this.config.queueTimeoutMs}ms`,\n ),\n );\n }, this.config.queueTimeoutMs);\n\n this.queue.push({\n id: entryId,\n task: task as () => Promise<unknown>,\n resolve: value => {\n clearTimeout(timeoutId);\n resolve(value as T);\n },\n reject: reason => {\n clearTimeout(timeoutId);\n reject(reason);\n },\n queuedAt: startTime,\n });\n\n this.processQueue();\n });\n }\n\n private async runTask<T>(task: () => Promise<T>, startTime: number): Promise<T> {\n this.activeCalls++;\n try {\n const result = await task();\n this.successfulCalls++;\n this.totalDurationMs += Date.now() - startTime;\n return result;\n } catch (error) {\n this.failedCalls++;\n throw error;\n } finally {\n this.activeCalls--;\n this.processQueue();\n }\n }\n\n private async waitAndRetry<T>(task: () => Promise<T>, startTime: number): Promise<T> {\n const maxRetries = 3;\n const baseDelay = 100;\n\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n if (this.activeCalls < this.config.maxConcurrentCalls) {\n return this.runTask(task, startTime);\n }\n await new Promise(resolve => setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)));\n }\n\n this.rejectedCalls++;\n throw new BulkheadRejectedError(\n `Bulkhead '${this.config.name}' rejected after ${maxRetries} retries`,\n );\n }\n\n private processQueue(): void {\n while (this.activeCalls < this.config.maxConcurrentCalls && this.queue.length > 0) {\n const next = this.queue.shift();\n if (!next) break;\n\n const waitTime = Date.now() - next.queuedAt;\n if (waitTime > this.config.queueTimeoutMs) {\n next.reject(new BulkheadTimeoutError(`Task timed out after ${waitTime}ms in queue`));\n this.timedOutCalls++;\n continue;\n }\n\n this.activeCalls++;\n next\n .task()\n .then(result => {\n this.successfulCalls++;\n next.resolve(result);\n })\n .catch(error => {\n this.failedCalls++;\n next.reject(error);\n })\n .finally(() => {\n this.activeCalls--;\n this.processQueue();\n });\n }\n }\n\n getMetrics(): BulkheadMetrics {\n return {\n name: this.config.name,\n activeCalls: this.activeCalls,\n queuedCalls: this.queue.length,\n maxConcurrentCalls: this.config.maxConcurrentCalls,\n maxQueueSize: this.config.maxQueueSize,\n totalCalls: this.totalCalls,\n successfulCalls: this.successfulCalls,\n failedCalls: this.failedCalls,\n rejectedCalls: this.rejectedCalls,\n timedOutCalls: this.timedOutCalls,\n averageDurationMs:\n this.successfulCalls > 0 ? Math.round(this.totalDurationMs / this.successfulCalls) : 0,\n };\n }\n\n canAccept(): boolean {\n return (\n this.activeCalls < this.config.maxConcurrentCalls ||\n this.queue.length < this.config.maxQueueSize\n );\n }\n\n resetMetrics(): void {\n this.totalCalls = 0;\n this.successfulCalls = 0;\n this.failedCalls = 0;\n this.rejectedCalls = 0;\n this.timedOutCalls = 0;\n this.totalDurationMs = 0;\n }\n}\n","import { Bulkhead, BulkheadConfig, BulkheadMetrics } from './bulkhead.js';\n\nexport interface BulkheadOptions extends Partial<BulkheadConfig> {\n name: string;\n}\n\nconst DEFAULT_CONFIG: Omit<BulkheadConfig, 'name'> = {\n maxConcurrentCalls: 10,\n maxQueueSize: 100,\n queueTimeoutMs: 30000,\n rejectWhenFull: true,\n};\n\nexport class BulkheadRegistry {\n private readonly bulkheads = new Map<string, Bulkhead>();\n\n getOrCreate(options: BulkheadOptions): Bulkhead {\n if (!this.bulkheads.has(options.name)) {\n const config: BulkheadConfig = { ...DEFAULT_CONFIG, ...options, name: options.name };\n this.bulkheads.set(options.name, new Bulkhead(config));\n }\n return this.bulkheads.get(options.name)!;\n }\n\n /** Per-client isolation — 5 concurrent, 20 queued */\n getForClient(clientId: string, endpoint?: string): Bulkhead {\n const name = endpoint ? `client:${clientId}:${endpoint}` : `client:${clientId}`;\n return this.getOrCreate({ name, maxConcurrentCalls: 5, maxQueueSize: 20 });\n }\n\n /** Service-level limiting — 20 concurrent, 200 queued */\n getForService(serviceName: string): Bulkhead {\n return this.getOrCreate({\n name: `service:${serviceName}`,\n maxConcurrentCalls: 20,\n maxQueueSize: 200,\n });\n }\n\n /** Database connection limiting — 15 concurrent, 150 queued */\n getForDatabase(schema: string): Bulkhead {\n return this.getOrCreate({\n name: `database:${schema}`,\n maxConcurrentCalls: 15,\n maxQueueSize: 150,\n });\n }\n\n /** External HTTP calls — 8 concurrent, 50 queued, 10s timeout */\n getForHttpExternal(serviceName: string): Bulkhead {\n return this.getOrCreate({\n name: `http:${serviceName}`,\n maxConcurrentCalls: 8,\n maxQueueSize: 50,\n queueTimeoutMs: 10000,\n });\n }\n\n getAllMetrics(): Record<string, BulkheadMetrics> {\n const metrics: Record<string, BulkheadMetrics> = {};\n for (const [name, bulkhead] of this.bulkheads) {\n metrics[name] = bulkhead.getMetrics();\n }\n return metrics;\n }\n\n /** Returns bulkheads at or above 80% active capacity */\n getOverloadedBulkheads(): BulkheadMetrics[] {\n const overloaded: BulkheadMetrics[] = [];\n for (const bulkhead of this.bulkheads.values()) {\n const m = bulkhead.getMetrics();\n if (m.activeCalls >= m.maxConcurrentCalls * 0.8) {\n overloaded.push(m);\n }\n }\n return overloaded;\n }\n\n resetAllMetrics(): void {\n for (const bulkhead of this.bulkheads.values()) {\n bulkhead.resetMetrics();\n }\n }\n}\n"]}
|