@backendkit-labs/bulkhead 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/dist/index.cjs +229 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +73 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +224 -0
- package/dist/index.js.map +1 -0
- package/dist/nestjs/index.cjs +453 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +70 -0
- package/dist/nestjs/index.d.ts +70 -0
- package/dist/nestjs/index.js +450 -0
- package/dist/nestjs/index.js.map +1 -0
- package/package.json +89 -0
package/README.md
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# @backendkit-labs/bulkhead
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@backendkit-labs/bulkhead)
|
|
4
|
+
[](https://github.com/backendkit-dev/backendkit-monorepo/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](package.json)
|
|
7
|
+
|
|
8
|
+
> Bulkhead concurrency limiting for Node.js — inspired by Resilience4j. Framework-agnostic core with optional NestJS integration.
|
|
9
|
+
|
|
10
|
+
Prevents resource exhaustion and cascading failures by limiting how many operations run simultaneously on a given resource.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @backendkit-labs/bulkhead
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start — Framework-agnostic
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Bulkhead } from '@backendkit-labs/bulkhead';
|
|
26
|
+
|
|
27
|
+
const bulkhead = new Bulkhead({
|
|
28
|
+
name: 'payments',
|
|
29
|
+
maxConcurrentCalls: 10,
|
|
30
|
+
maxQueueSize: 50,
|
|
31
|
+
queueTimeoutMs: 5000,
|
|
32
|
+
rejectWhenFull: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = await bulkhead.execute(() => callPaymentApi());
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Core API
|
|
41
|
+
|
|
42
|
+
### `Bulkhead`
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const bulkhead = new Bulkhead(config);
|
|
46
|
+
|
|
47
|
+
// Execute a task — waits in queue if at capacity
|
|
48
|
+
await bulkhead.execute(async () => { ... });
|
|
49
|
+
|
|
50
|
+
// Check if capacity is available before executing
|
|
51
|
+
if (bulkhead.canAccept()) { ... }
|
|
52
|
+
|
|
53
|
+
// Current metrics snapshot
|
|
54
|
+
const metrics = bulkhead.getMetrics();
|
|
55
|
+
|
|
56
|
+
// Reset all counters
|
|
57
|
+
bulkhead.resetMetrics();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `BulkheadConfig`
|
|
61
|
+
|
|
62
|
+
| Property | Type | Description |
|
|
63
|
+
|----------|------|-------------|
|
|
64
|
+
| `name` | `string` | Identifier for metrics and error messages |
|
|
65
|
+
| `maxConcurrentCalls` | `number` | Max simultaneous executions |
|
|
66
|
+
| `maxQueueSize` | `number` | Max tasks waiting in queue |
|
|
67
|
+
| `queueTimeoutMs` | `number` | Max time a task can wait in queue (ms) |
|
|
68
|
+
| `rejectWhenFull` | `boolean` | Throw immediately when full; if `false`, retries with exponential backoff |
|
|
69
|
+
|
|
70
|
+
### `BulkheadMetrics`
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
{
|
|
74
|
+
name: string;
|
|
75
|
+
activeCalls: number;
|
|
76
|
+
queuedCalls: number;
|
|
77
|
+
maxConcurrentCalls: number;
|
|
78
|
+
maxQueueSize: number;
|
|
79
|
+
totalCalls: number;
|
|
80
|
+
successfulCalls: number;
|
|
81
|
+
failedCalls: number;
|
|
82
|
+
rejectedCalls: number;
|
|
83
|
+
timedOutCalls: number;
|
|
84
|
+
averageDurationMs: number;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Errors
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { BulkheadRejectedError, BulkheadTimeoutError } from '@backendkit-labs/bulkhead';
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await bulkhead.execute(task);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof BulkheadRejectedError) {
|
|
97
|
+
// Queue was full — task was not queued
|
|
98
|
+
}
|
|
99
|
+
if (error instanceof BulkheadTimeoutError) {
|
|
100
|
+
// Task waited too long in queue
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## BulkheadRegistry
|
|
108
|
+
|
|
109
|
+
Manages named bulkhead instances with sensible defaults for common resource types:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { BulkheadRegistry } from '@backendkit-labs/bulkhead';
|
|
113
|
+
|
|
114
|
+
const registry = new BulkheadRegistry();
|
|
115
|
+
|
|
116
|
+
// Custom
|
|
117
|
+
const bh = registry.getOrCreate({ name: 'my-service', maxConcurrentCalls: 15 });
|
|
118
|
+
|
|
119
|
+
// Pre-configured factory methods
|
|
120
|
+
const clientBh = registry.getForClient('client-123', '/api/orders'); // 5 concurrent, 20 queued
|
|
121
|
+
const serviceBh = registry.getForService('inventory-service'); // 20 concurrent, 200 queued
|
|
122
|
+
const dbBh = registry.getForDatabase('orders_schema'); // 15 concurrent, 150 queued
|
|
123
|
+
const externalBh = registry.getForHttpExternal('stripe-api'); // 8 concurrent, 50 queued, 10s timeout
|
|
124
|
+
|
|
125
|
+
// Observability
|
|
126
|
+
const all = registry.getAllMetrics();
|
|
127
|
+
const overloaded = registry.getOverloadedBulkheads(); // ≥80% active capacity
|
|
128
|
+
registry.resetAllMetrics();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| Method | Concurrent | Queue | Timeout |
|
|
132
|
+
|--------|-----------|-------|---------|
|
|
133
|
+
| `getForClient(id, endpoint?)` | 5 | 20 | 30s |
|
|
134
|
+
| `getForService(name)` | 20 | 200 | 30s |
|
|
135
|
+
| `getForDatabase(schema)` | 15 | 150 | 30s |
|
|
136
|
+
| `getForHttpExternal(name)` | 8 | 50 | 10s |
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## NestJS Integration
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm install @backendkit-labs/bulkhead
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Import `BulkheadModule` into your NestJS application:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { BulkheadModule } from '@backendkit-labs/bulkhead/nestjs';
|
|
150
|
+
|
|
151
|
+
@Module({
|
|
152
|
+
imports: [BulkheadModule],
|
|
153
|
+
})
|
|
154
|
+
export class AppModule {}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Guard — declarative per-route protection
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { UseBulkhead, BulkheadGuard } from '@backendkit-labs/bulkhead/nestjs';
|
|
161
|
+
|
|
162
|
+
@Controller('orders')
|
|
163
|
+
export class OrdersController {
|
|
164
|
+
// Shared service-level limit
|
|
165
|
+
@UseBulkhead({ name: 'orders-service' })
|
|
166
|
+
@UseGuards(BulkheadGuard)
|
|
167
|
+
@Get()
|
|
168
|
+
findAll() { ... }
|
|
169
|
+
|
|
170
|
+
// Per-client isolation (reads x-client-id header)
|
|
171
|
+
@UseBulkhead({ name: 'orders-create', perClient: true })
|
|
172
|
+
@UseGuards(BulkheadGuard)
|
|
173
|
+
@Post()
|
|
174
|
+
create() { ... }
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Returns `503 Service Unavailable` when at capacity.
|
|
179
|
+
|
|
180
|
+
### Interceptor — wraps handler execution inside the bulkhead
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { BulkheadInterceptor } from '@backendkit-labs/bulkhead/nestjs';
|
|
184
|
+
|
|
185
|
+
// Apply globally
|
|
186
|
+
app.useGlobalInterceptors(new BulkheadInterceptor(registry));
|
|
187
|
+
|
|
188
|
+
// Or per controller / route
|
|
189
|
+
@UseInterceptors(BulkheadInterceptor)
|
|
190
|
+
@Controller('reports')
|
|
191
|
+
export class ReportsController { ... }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Returns `503` on rejection, `408` on timeout.
|
|
195
|
+
|
|
196
|
+
### Middleware — global HTTP concurrency limit
|
|
197
|
+
|
|
198
|
+
Protects the entire service from being overwhelmed before requests even reach your handlers:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
import { HttpBulkheadMiddleware } from '@backendkit-labs/bulkhead/nestjs';
|
|
202
|
+
|
|
203
|
+
@Module({ imports: [BulkheadModule] })
|
|
204
|
+
export class AppModule implements NestModule {
|
|
205
|
+
configure(consumer: MiddlewareConsumer) {
|
|
206
|
+
consumer.apply(HttpBulkheadMiddleware).forRoutes('*');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Configure via environment variables:
|
|
212
|
+
|
|
213
|
+
| Variable | Default | Description |
|
|
214
|
+
|----------|---------|-------------|
|
|
215
|
+
| `HTTP_BULKHEAD_CONCURRENCY` | `50` | Max concurrent requests |
|
|
216
|
+
| `HTTP_BULKHEAD_MAX_QUEUE` | `100` | Max queued requests |
|
|
217
|
+
|
|
218
|
+
Returns `429 Too Many Requests` when the queue is full.
|
|
219
|
+
|
|
220
|
+
### Method Decorator
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
import { WithBulkhead } from '@backendkit-labs/bulkhead/nestjs';
|
|
224
|
+
|
|
225
|
+
@Injectable()
|
|
226
|
+
export class ReportService {
|
|
227
|
+
// Must have bulkheadRegistry injected
|
|
228
|
+
constructor(public readonly bulkheadRegistry: BulkheadRegistry) {}
|
|
229
|
+
|
|
230
|
+
@WithBulkhead({ name: 'report-generation', maxConcurrent: 3 })
|
|
231
|
+
async generateReport(id: string) { ... }
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Monitoring — BulkheadService
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { BulkheadService } from '@backendkit-labs/bulkhead/nestjs';
|
|
239
|
+
|
|
240
|
+
@Controller('health')
|
|
241
|
+
export class HealthController {
|
|
242
|
+
constructor(private readonly bulkheads: BulkheadService) {}
|
|
243
|
+
|
|
244
|
+
@Get('bulkheads')
|
|
245
|
+
getMetrics() {
|
|
246
|
+
return {
|
|
247
|
+
all: this.bulkheads.getAllMetrics(),
|
|
248
|
+
critical: this.bulkheads.getCriticalBulkheads(), // ≥90% active
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`BulkheadService` also logs a warning every 60 seconds when any bulkhead reaches 90%+ utilization.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Architecture
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
@backendkit-labs/bulkhead (core — no framework deps)
|
|
262
|
+
Bulkhead queue-based concurrency limiter
|
|
263
|
+
BulkheadRegistry named instances + factory methods
|
|
264
|
+
|
|
265
|
+
@backendkit-labs/bulkhead/nestjs (optional NestJS layer)
|
|
266
|
+
BulkheadModule NestJS module
|
|
267
|
+
BulkheadGuard @UseBulkhead() per-route decorator
|
|
268
|
+
BulkheadInterceptor wraps handler in execute()
|
|
269
|
+
HttpBulkheadMiddleware global HTTP request limiter
|
|
270
|
+
WithBulkhead method-level decorator
|
|
271
|
+
BulkheadService metrics + auto-monitoring
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## License
|
|
277
|
+
|
|
278
|
+
MIT — [BackendKit Labs](https://github.com/backendkit-dev)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bulkhead/bulkhead.ts
|
|
4
|
+
var BulkheadRejectedError = class extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "BulkheadRejectedError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var BulkheadTimeoutError = class extends Error {
|
|
11
|
+
constructor(message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "BulkheadTimeoutError";
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var Bulkhead = class {
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
config;
|
|
21
|
+
activeCalls = 0;
|
|
22
|
+
nextId = 0;
|
|
23
|
+
queue = [];
|
|
24
|
+
totalCalls = 0;
|
|
25
|
+
successfulCalls = 0;
|
|
26
|
+
failedCalls = 0;
|
|
27
|
+
rejectedCalls = 0;
|
|
28
|
+
timedOutCalls = 0;
|
|
29
|
+
totalDurationMs = 0;
|
|
30
|
+
async execute(task) {
|
|
31
|
+
const startTime = Date.now();
|
|
32
|
+
this.totalCalls++;
|
|
33
|
+
if (this.activeCalls < this.config.maxConcurrentCalls) {
|
|
34
|
+
return this.runTask(task, startTime);
|
|
35
|
+
}
|
|
36
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
37
|
+
if (this.config.rejectWhenFull) {
|
|
38
|
+
this.rejectedCalls++;
|
|
39
|
+
throw new BulkheadRejectedError(
|
|
40
|
+
`Bulkhead '${this.config.name}' is full. Active: ${this.activeCalls}, Queue: ${this.queue.length}, Max: ${this.config.maxConcurrentCalls}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return this.waitAndRetry(task, startTime);
|
|
44
|
+
}
|
|
45
|
+
const entryId = this.nextId++;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const timeoutId = setTimeout(() => {
|
|
48
|
+
const index = this.queue.findIndex((item) => item.id === entryId);
|
|
49
|
+
if (index !== -1) this.queue.splice(index, 1);
|
|
50
|
+
this.timedOutCalls++;
|
|
51
|
+
reject(
|
|
52
|
+
new BulkheadTimeoutError(
|
|
53
|
+
`Bulkhead '${this.config.name}' timeout after ${this.config.queueTimeoutMs}ms`
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
}, this.config.queueTimeoutMs);
|
|
57
|
+
this.queue.push({
|
|
58
|
+
id: entryId,
|
|
59
|
+
task,
|
|
60
|
+
resolve: (value) => {
|
|
61
|
+
clearTimeout(timeoutId);
|
|
62
|
+
resolve(value);
|
|
63
|
+
},
|
|
64
|
+
reject: (reason) => {
|
|
65
|
+
clearTimeout(timeoutId);
|
|
66
|
+
reject(reason);
|
|
67
|
+
},
|
|
68
|
+
queuedAt: startTime
|
|
69
|
+
});
|
|
70
|
+
this.processQueue();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async runTask(task, startTime) {
|
|
74
|
+
this.activeCalls++;
|
|
75
|
+
try {
|
|
76
|
+
const result = await task();
|
|
77
|
+
this.successfulCalls++;
|
|
78
|
+
this.totalDurationMs += Date.now() - startTime;
|
|
79
|
+
return result;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.failedCalls++;
|
|
82
|
+
throw error;
|
|
83
|
+
} finally {
|
|
84
|
+
this.activeCalls--;
|
|
85
|
+
this.processQueue();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async waitAndRetry(task, startTime) {
|
|
89
|
+
const maxRetries = 3;
|
|
90
|
+
const baseDelay = 100;
|
|
91
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
92
|
+
if (this.activeCalls < this.config.maxConcurrentCalls) {
|
|
93
|
+
return this.runTask(task, startTime);
|
|
94
|
+
}
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, baseDelay * Math.pow(2, attempt - 1)));
|
|
96
|
+
}
|
|
97
|
+
this.rejectedCalls++;
|
|
98
|
+
throw new BulkheadRejectedError(
|
|
99
|
+
`Bulkhead '${this.config.name}' rejected after ${maxRetries} retries`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
processQueue() {
|
|
103
|
+
while (this.activeCalls < this.config.maxConcurrentCalls && this.queue.length > 0) {
|
|
104
|
+
const next = this.queue.shift();
|
|
105
|
+
if (!next) break;
|
|
106
|
+
const waitTime = Date.now() - next.queuedAt;
|
|
107
|
+
if (waitTime > this.config.queueTimeoutMs) {
|
|
108
|
+
next.reject(new BulkheadTimeoutError(`Task timed out after ${waitTime}ms in queue`));
|
|
109
|
+
this.timedOutCalls++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
this.activeCalls++;
|
|
113
|
+
next.task().then((result) => {
|
|
114
|
+
this.successfulCalls++;
|
|
115
|
+
next.resolve(result);
|
|
116
|
+
}).catch((error) => {
|
|
117
|
+
this.failedCalls++;
|
|
118
|
+
next.reject(error);
|
|
119
|
+
}).finally(() => {
|
|
120
|
+
this.activeCalls--;
|
|
121
|
+
this.processQueue();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
getMetrics() {
|
|
126
|
+
return {
|
|
127
|
+
name: this.config.name,
|
|
128
|
+
activeCalls: this.activeCalls,
|
|
129
|
+
queuedCalls: this.queue.length,
|
|
130
|
+
maxConcurrentCalls: this.config.maxConcurrentCalls,
|
|
131
|
+
maxQueueSize: this.config.maxQueueSize,
|
|
132
|
+
totalCalls: this.totalCalls,
|
|
133
|
+
successfulCalls: this.successfulCalls,
|
|
134
|
+
failedCalls: this.failedCalls,
|
|
135
|
+
rejectedCalls: this.rejectedCalls,
|
|
136
|
+
timedOutCalls: this.timedOutCalls,
|
|
137
|
+
averageDurationMs: this.successfulCalls > 0 ? Math.round(this.totalDurationMs / this.successfulCalls) : 0
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
canAccept() {
|
|
141
|
+
return this.activeCalls < this.config.maxConcurrentCalls || this.queue.length < this.config.maxQueueSize;
|
|
142
|
+
}
|
|
143
|
+
resetMetrics() {
|
|
144
|
+
this.totalCalls = 0;
|
|
145
|
+
this.successfulCalls = 0;
|
|
146
|
+
this.failedCalls = 0;
|
|
147
|
+
this.rejectedCalls = 0;
|
|
148
|
+
this.timedOutCalls = 0;
|
|
149
|
+
this.totalDurationMs = 0;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// src/bulkhead/bulkhead.registry.ts
|
|
154
|
+
var DEFAULT_CONFIG = {
|
|
155
|
+
maxConcurrentCalls: 10,
|
|
156
|
+
maxQueueSize: 100,
|
|
157
|
+
queueTimeoutMs: 3e4,
|
|
158
|
+
rejectWhenFull: true
|
|
159
|
+
};
|
|
160
|
+
var BulkheadRegistry = class {
|
|
161
|
+
bulkheads = /* @__PURE__ */ new Map();
|
|
162
|
+
getOrCreate(options) {
|
|
163
|
+
if (!this.bulkheads.has(options.name)) {
|
|
164
|
+
const config = { ...DEFAULT_CONFIG, ...options, name: options.name };
|
|
165
|
+
this.bulkheads.set(options.name, new Bulkhead(config));
|
|
166
|
+
}
|
|
167
|
+
return this.bulkheads.get(options.name);
|
|
168
|
+
}
|
|
169
|
+
/** Per-client isolation — 5 concurrent, 20 queued */
|
|
170
|
+
getForClient(clientId, endpoint) {
|
|
171
|
+
const name = endpoint ? `client:${clientId}:${endpoint}` : `client:${clientId}`;
|
|
172
|
+
return this.getOrCreate({ name, maxConcurrentCalls: 5, maxQueueSize: 20 });
|
|
173
|
+
}
|
|
174
|
+
/** Service-level limiting — 20 concurrent, 200 queued */
|
|
175
|
+
getForService(serviceName) {
|
|
176
|
+
return this.getOrCreate({
|
|
177
|
+
name: `service:${serviceName}`,
|
|
178
|
+
maxConcurrentCalls: 20,
|
|
179
|
+
maxQueueSize: 200
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** Database connection limiting — 15 concurrent, 150 queued */
|
|
183
|
+
getForDatabase(schema) {
|
|
184
|
+
return this.getOrCreate({
|
|
185
|
+
name: `database:${schema}`,
|
|
186
|
+
maxConcurrentCalls: 15,
|
|
187
|
+
maxQueueSize: 150
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** External HTTP calls — 8 concurrent, 50 queued, 10s timeout */
|
|
191
|
+
getForHttpExternal(serviceName) {
|
|
192
|
+
return this.getOrCreate({
|
|
193
|
+
name: `http:${serviceName}`,
|
|
194
|
+
maxConcurrentCalls: 8,
|
|
195
|
+
maxQueueSize: 50,
|
|
196
|
+
queueTimeoutMs: 1e4
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
getAllMetrics() {
|
|
200
|
+
const metrics = {};
|
|
201
|
+
for (const [name, bulkhead] of this.bulkheads) {
|
|
202
|
+
metrics[name] = bulkhead.getMetrics();
|
|
203
|
+
}
|
|
204
|
+
return metrics;
|
|
205
|
+
}
|
|
206
|
+
/** Returns bulkheads at or above 80% active capacity */
|
|
207
|
+
getOverloadedBulkheads() {
|
|
208
|
+
const overloaded = [];
|
|
209
|
+
for (const bulkhead of this.bulkheads.values()) {
|
|
210
|
+
const m = bulkhead.getMetrics();
|
|
211
|
+
if (m.activeCalls >= m.maxConcurrentCalls * 0.8) {
|
|
212
|
+
overloaded.push(m);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return overloaded;
|
|
216
|
+
}
|
|
217
|
+
resetAllMetrics() {
|
|
218
|
+
for (const bulkhead of this.bulkheads.values()) {
|
|
219
|
+
bulkhead.resetMetrics();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
exports.Bulkhead = Bulkhead;
|
|
225
|
+
exports.BulkheadRegistry = BulkheadRegistry;
|
|
226
|
+
exports.BulkheadRejectedError = BulkheadRejectedError;
|
|
227
|
+
exports.BulkheadTimeoutError = BulkheadTimeoutError;
|
|
228
|
+
//# sourceMappingURL=index.cjs.map
|
|
229
|
+
//# sourceMappingURL=index.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":"index.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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
interface BulkheadConfig {
|
|
2
|
+
/** Max number of concurrent executions */
|
|
3
|
+
maxConcurrentCalls: number;
|
|
4
|
+
/** Max queue size for waiting tasks */
|
|
5
|
+
maxQueueSize: number;
|
|
6
|
+
/** Max time a task can wait in queue (ms) */
|
|
7
|
+
queueTimeoutMs: number;
|
|
8
|
+
/** Reject immediately when queue is full; if false, retries with backoff */
|
|
9
|
+
rejectWhenFull: boolean;
|
|
10
|
+
/** Identifier used in metrics and error messages */
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
interface BulkheadMetrics {
|
|
14
|
+
name: string;
|
|
15
|
+
activeCalls: number;
|
|
16
|
+
queuedCalls: number;
|
|
17
|
+
maxConcurrentCalls: number;
|
|
18
|
+
maxQueueSize: number;
|
|
19
|
+
totalCalls: number;
|
|
20
|
+
successfulCalls: number;
|
|
21
|
+
failedCalls: number;
|
|
22
|
+
rejectedCalls: number;
|
|
23
|
+
timedOutCalls: number;
|
|
24
|
+
averageDurationMs: number;
|
|
25
|
+
}
|
|
26
|
+
declare class BulkheadRejectedError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
declare class BulkheadTimeoutError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
declare class Bulkhead {
|
|
33
|
+
private readonly config;
|
|
34
|
+
private activeCalls;
|
|
35
|
+
private nextId;
|
|
36
|
+
private queue;
|
|
37
|
+
private totalCalls;
|
|
38
|
+
private successfulCalls;
|
|
39
|
+
private failedCalls;
|
|
40
|
+
private rejectedCalls;
|
|
41
|
+
private timedOutCalls;
|
|
42
|
+
private totalDurationMs;
|
|
43
|
+
constructor(config: BulkheadConfig);
|
|
44
|
+
execute<T>(task: () => Promise<T>): Promise<T>;
|
|
45
|
+
private runTask;
|
|
46
|
+
private waitAndRetry;
|
|
47
|
+
private processQueue;
|
|
48
|
+
getMetrics(): BulkheadMetrics;
|
|
49
|
+
canAccept(): boolean;
|
|
50
|
+
resetMetrics(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface BulkheadOptions extends Partial<BulkheadConfig> {
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
declare class BulkheadRegistry {
|
|
57
|
+
private readonly bulkheads;
|
|
58
|
+
getOrCreate(options: BulkheadOptions): Bulkhead;
|
|
59
|
+
/** Per-client isolation — 5 concurrent, 20 queued */
|
|
60
|
+
getForClient(clientId: string, endpoint?: string): Bulkhead;
|
|
61
|
+
/** Service-level limiting — 20 concurrent, 200 queued */
|
|
62
|
+
getForService(serviceName: string): Bulkhead;
|
|
63
|
+
/** Database connection limiting — 15 concurrent, 150 queued */
|
|
64
|
+
getForDatabase(schema: string): Bulkhead;
|
|
65
|
+
/** External HTTP calls — 8 concurrent, 50 queued, 10s timeout */
|
|
66
|
+
getForHttpExternal(serviceName: string): Bulkhead;
|
|
67
|
+
getAllMetrics(): Record<string, BulkheadMetrics>;
|
|
68
|
+
/** Returns bulkheads at or above 80% active capacity */
|
|
69
|
+
getOverloadedBulkheads(): BulkheadMetrics[];
|
|
70
|
+
resetAllMetrics(): void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { Bulkhead, type BulkheadConfig, type BulkheadMetrics, type BulkheadOptions, BulkheadRegistry, BulkheadRejectedError, BulkheadTimeoutError };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
interface BulkheadConfig {
|
|
2
|
+
/** Max number of concurrent executions */
|
|
3
|
+
maxConcurrentCalls: number;
|
|
4
|
+
/** Max queue size for waiting tasks */
|
|
5
|
+
maxQueueSize: number;
|
|
6
|
+
/** Max time a task can wait in queue (ms) */
|
|
7
|
+
queueTimeoutMs: number;
|
|
8
|
+
/** Reject immediately when queue is full; if false, retries with backoff */
|
|
9
|
+
rejectWhenFull: boolean;
|
|
10
|
+
/** Identifier used in metrics and error messages */
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
interface BulkheadMetrics {
|
|
14
|
+
name: string;
|
|
15
|
+
activeCalls: number;
|
|
16
|
+
queuedCalls: number;
|
|
17
|
+
maxConcurrentCalls: number;
|
|
18
|
+
maxQueueSize: number;
|
|
19
|
+
totalCalls: number;
|
|
20
|
+
successfulCalls: number;
|
|
21
|
+
failedCalls: number;
|
|
22
|
+
rejectedCalls: number;
|
|
23
|
+
timedOutCalls: number;
|
|
24
|
+
averageDurationMs: number;
|
|
25
|
+
}
|
|
26
|
+
declare class BulkheadRejectedError extends Error {
|
|
27
|
+
constructor(message: string);
|
|
28
|
+
}
|
|
29
|
+
declare class BulkheadTimeoutError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
declare class Bulkhead {
|
|
33
|
+
private readonly config;
|
|
34
|
+
private activeCalls;
|
|
35
|
+
private nextId;
|
|
36
|
+
private queue;
|
|
37
|
+
private totalCalls;
|
|
38
|
+
private successfulCalls;
|
|
39
|
+
private failedCalls;
|
|
40
|
+
private rejectedCalls;
|
|
41
|
+
private timedOutCalls;
|
|
42
|
+
private totalDurationMs;
|
|
43
|
+
constructor(config: BulkheadConfig);
|
|
44
|
+
execute<T>(task: () => Promise<T>): Promise<T>;
|
|
45
|
+
private runTask;
|
|
46
|
+
private waitAndRetry;
|
|
47
|
+
private processQueue;
|
|
48
|
+
getMetrics(): BulkheadMetrics;
|
|
49
|
+
canAccept(): boolean;
|
|
50
|
+
resetMetrics(): void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface BulkheadOptions extends Partial<BulkheadConfig> {
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
declare class BulkheadRegistry {
|
|
57
|
+
private readonly bulkheads;
|
|
58
|
+
getOrCreate(options: BulkheadOptions): Bulkhead;
|
|
59
|
+
/** Per-client isolation — 5 concurrent, 20 queued */
|
|
60
|
+
getForClient(clientId: string, endpoint?: string): Bulkhead;
|
|
61
|
+
/** Service-level limiting — 20 concurrent, 200 queued */
|
|
62
|
+
getForService(serviceName: string): Bulkhead;
|
|
63
|
+
/** Database connection limiting — 15 concurrent, 150 queued */
|
|
64
|
+
getForDatabase(schema: string): Bulkhead;
|
|
65
|
+
/** External HTTP calls — 8 concurrent, 50 queued, 10s timeout */
|
|
66
|
+
getForHttpExternal(serviceName: string): Bulkhead;
|
|
67
|
+
getAllMetrics(): Record<string, BulkheadMetrics>;
|
|
68
|
+
/** Returns bulkheads at or above 80% active capacity */
|
|
69
|
+
getOverloadedBulkheads(): BulkheadMetrics[];
|
|
70
|
+
resetAllMetrics(): void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { Bulkhead, type BulkheadConfig, type BulkheadMetrics, type BulkheadOptions, BulkheadRegistry, BulkheadRejectedError, BulkheadTimeoutError };
|