actly 1.2.0 → 1.3.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/CHANGELOG.md +234 -0
- package/MIGRATION.md +141 -0
- package/README.md +326 -218
- package/SECURITY.md +56 -0
- package/dist/core/act.cjs +406 -220
- package/dist/core/act.d.ts +6 -84
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +405 -222
- package/dist/core/executor.cjs +0 -27
- package/dist/core/executor.d.ts +0 -41
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +0 -28
- package/dist/core/health.cjs +146 -17
- package/dist/core/health.d.ts +20 -5
- package/dist/core/health.d.ts.map +1 -1
- package/dist/core/health.js +140 -18
- package/dist/core/shutdown.cjs +19 -8
- package/dist/core/shutdown.d.ts +1 -4
- package/dist/core/shutdown.d.ts.map +1 -1
- package/dist/core/shutdown.js +18 -9
- package/dist/core/tenant.cjs +61 -19
- package/dist/core/tenant.d.ts +5 -10
- package/dist/core/tenant.d.ts.map +1 -1
- package/dist/core/tenant.js +61 -20
- package/dist/errors.cjs +70 -103
- package/dist/errors.d.ts +33 -90
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +66 -103
- package/dist/index.cjs +25 -22
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -15
- package/dist/observability.cjs +0 -39
- package/dist/observability.d.ts +19 -78
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +0 -40
- package/dist/policies/bulkhead.cjs +44 -4
- package/dist/policies/bulkhead.d.ts +0 -0
- package/dist/policies/bulkhead.d.ts.map +1 -1
- package/dist/policies/bulkhead.js +44 -5
- package/dist/policies/cache.cjs +22 -142
- package/dist/policies/cache.d.ts +0 -46
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +22 -143
- package/dist/policies/circuitBreaker.cjs +125 -31
- package/dist/policies/circuitBreaker.d.ts +0 -0
- package/dist/policies/circuitBreaker.d.ts.map +1 -1
- package/dist/policies/circuitBreaker.js +125 -32
- package/dist/policies/dedupe.cjs +19 -66
- package/dist/policies/dedupe.d.ts +0 -23
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +19 -67
- package/dist/policies/noop.cjs +6 -0
- package/dist/policies/noop.d.ts +3 -0
- package/dist/policies/noop.d.ts.map +1 -0
- package/dist/policies/noop.js +3 -0
- package/dist/policies/rateLimit.cjs +15 -5
- package/dist/policies/rateLimit.d.ts +0 -0
- package/dist/policies/rateLimit.d.ts.map +1 -1
- package/dist/policies/rateLimit.js +15 -6
- package/dist/policies/retry.cjs +100 -65
- package/dist/policies/retry.d.ts +0 -27
- package/dist/policies/retry.d.ts.map +1 -1
- package/dist/policies/retry.js +100 -66
- package/dist/policies/timeout.cjs +32 -60
- package/dist/policies/timeout.d.ts +0 -13
- package/dist/policies/timeout.d.ts.map +1 -1
- package/dist/policies/timeout.js +32 -71
- package/dist/stores/base.cjs +0 -10
- package/dist/stores/base.d.ts +4 -67
- package/dist/stores/base.d.ts.map +1 -1
- package/dist/stores/base.js +0 -11
- package/dist/stores/memory.cjs +75 -88
- package/dist/stores/memory.d.ts +7 -74
- package/dist/stores/memory.d.ts.map +1 -1
- package/dist/stores/memory.js +75 -89
- package/dist/testing/index.cjs +56 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +52 -0
- package/dist/types/index.cjs +0 -1
- package/dist/types/index.d.ts +17 -307
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +0 -2
- package/dist/utils/abort.cjs +14 -111
- package/dist/utils/abort.d.ts +3 -74
- package/dist/utils/abort.d.ts.map +1 -1
- package/dist/utils/abort.js +14 -112
- package/dist/utils/abortPool.cjs +1 -2
- package/dist/utils/abortPool.d.ts +0 -0
- package/dist/utils/abortPool.d.ts.map +1 -1
- package/dist/utils/abortPool.js +1 -3
- package/dist/utils/backoff.cjs +3 -21
- package/dist/utils/backoff.d.ts +0 -11
- package/dist/utils/backoff.d.ts.map +1 -1
- package/dist/utils/backoff.js +3 -22
- package/dist/utils/decorator.cjs +44 -0
- package/dist/utils/decorator.d.ts +3 -0
- package/dist/utils/decorator.d.ts.map +1 -0
- package/dist/utils/decorator.js +41 -0
- package/dist/utils/key.cjs +13 -45
- package/dist/utils/key.d.ts +0 -27
- package/dist/utils/key.d.ts.map +1 -1
- package/dist/utils/key.js +13 -46
- package/dist/utils/limits.cjs +9 -27
- package/dist/utils/limits.d.ts +9 -27
- package/dist/utils/limits.d.ts.map +1 -1
- package/dist/utils/limits.js +9 -28
- package/dist/utils/safeCall.cjs +28 -0
- package/dist/utils/safeCall.d.ts +2 -0
- package/dist/utils/safeCall.d.ts.map +1 -0
- package/dist/utils/safeCall.js +25 -0
- package/dist/utils/sanitize.cjs +22 -1
- package/dist/utils/sanitize.d.ts +0 -0
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +22 -2
- package/dist/utils/validate.cjs +81 -18
- package/dist/utils/validate.d.ts +6 -19
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +81 -19
- package/package.json +38 -4
- package/dist/core/act.js.map +0 -1
- package/dist/core/executor.js.map +0 -1
- package/dist/core/health.js.map +0 -1
- package/dist/core/shutdown.js.map +0 -1
- package/dist/core/tenant.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observability.js.map +0 -1
- package/dist/policies/bulkhead.js.map +0 -1
- package/dist/policies/cache.js.map +0 -1
- package/dist/policies/circuitBreaker.js.map +0 -1
- package/dist/policies/dedupe.js.map +0 -1
- package/dist/policies/rateLimit.js.map +0 -1
- package/dist/policies/retry.js.map +0 -1
- package/dist/policies/timeout.js.map +0 -1
- package/dist/state/store.cjs +0 -14
- package/dist/state/store.d.ts +0 -11
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -11
- package/dist/state/store.js.map +0 -1
- package/dist/stores/base.js.map +0 -1
- package/dist/stores/memory.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/abort.js.map +0 -1
- package/dist/utils/abortPool.js.map +0 -1
- package/dist/utils/backoff.js.map +0 -1
- package/dist/utils/key.js.map +0 -1
- package/dist/utils/limits.js.map +0 -1
- package/dist/utils/sanitize.js.map +0 -1
- package/dist/utils/validate.js.map +0 -1
package/dist/observability.d.ts
CHANGED
|
@@ -1,73 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Eight event types covering the entire lifecycle of an `act()` call.
|
|
5
|
-
*
|
|
6
|
-
* # Contract
|
|
7
|
-
*
|
|
8
|
-
* When `options.observability` is `null`/`undefined` (the common case),
|
|
9
|
-
* no event objects are allocated and no function calls are made. The
|
|
10
|
-
* hot path is a single null-check per policy decision.
|
|
11
|
-
*
|
|
12
|
-
* When hooks ARE registered, events are allocated lazily — only when the
|
|
13
|
-
* corresponding event actually fires. A cache hit doesn't allocate an
|
|
14
|
-
* `onRetry` event, for example.
|
|
15
|
-
*
|
|
16
|
-
* # Event shape
|
|
17
|
-
*
|
|
18
|
-
* Every event carries `key`, `traceId`, and `timestamp` for correlation.
|
|
19
|
-
* Event-specific fields are on the same object (no nesting) for flat
|
|
20
|
-
* destructuring in user code.
|
|
21
|
-
*
|
|
22
|
-
* # Ordering
|
|
23
|
-
*
|
|
24
|
-
* For a successful fresh call with retries:
|
|
25
|
-
* onAttempt (1) → onAttempt (2) → onRetry (1→2) → onAttempt (3) → onFinalSuccess
|
|
26
|
-
*
|
|
27
|
-
* For a cache hit:
|
|
28
|
-
* onCacheHit → onFinalSuccess
|
|
29
|
-
*
|
|
30
|
-
* For a dedupe joiner:
|
|
31
|
-
* onDedupeJoin → onFinalSuccess (or onFinalFailure)
|
|
32
|
-
*
|
|
33
|
-
* For a timeout:
|
|
34
|
-
* onAttempt → onTimeout → onFinalFailure
|
|
35
|
-
*/
|
|
36
|
-
import type { ActSource } from './types/index.js';
|
|
37
|
-
/** Stable discriminator for telemetry. */
|
|
38
|
-
export type ActlyEventType = 'attempt' | 'retry' | 'cache-hit' | 'cache-miss' | 'dedupe-join' | 'timeout' | 'final-success' | 'final-failure';
|
|
39
|
-
/** Common fields on every event. */
|
|
1
|
+
import type { ActSource, ActlyFailedBy } from './types/index.js';
|
|
2
|
+
export type ActlyEventType = 'attempt' | 'retry' | 'cache-hit' | 'cache-miss' | 'dedupe-join' | 'timeout' | 'final-success' | 'final-failure' | 'backpressure' | 'watchdog';
|
|
40
3
|
export interface ActlyEventBase {
|
|
41
|
-
/** The `key` passed to `act()`. */
|
|
42
4
|
readonly key: string;
|
|
43
|
-
/** Auto-generated trace ID (or user-supplied via `options.traceId`). */
|
|
44
5
|
readonly traceId: string;
|
|
45
|
-
/** Event timestamp (ms since epoch). */
|
|
46
6
|
readonly timestamp: number;
|
|
47
|
-
/** Discriminator for switch statements. */
|
|
48
7
|
readonly type: ActlyEventType;
|
|
49
8
|
}
|
|
50
9
|
export interface AttemptEvent extends ActlyEventBase {
|
|
51
10
|
readonly type: 'attempt';
|
|
52
|
-
/** 1-based attempt number. */
|
|
53
11
|
readonly attempt: number;
|
|
54
|
-
/** Duration of this attempt in ms (set after attempt settles). */
|
|
55
12
|
readonly durationMs?: number;
|
|
56
|
-
/** Error from this attempt, if it failed. */
|
|
57
13
|
readonly error?: unknown;
|
|
58
14
|
}
|
|
59
15
|
export interface RetryEvent extends ActlyEventBase {
|
|
60
16
|
readonly type: 'retry';
|
|
61
|
-
/** The attempt that just failed. */
|
|
62
17
|
readonly attempt: number;
|
|
63
|
-
/** The delay (ms) before the next attempt. */
|
|
64
18
|
readonly delayMs: number;
|
|
65
|
-
/** Error that triggered the retry. */
|
|
66
19
|
readonly error: unknown;
|
|
67
20
|
}
|
|
68
21
|
export interface CacheHitEvent extends ActlyEventBase {
|
|
69
22
|
readonly type: 'cache-hit';
|
|
70
|
-
/** Age of the cached value in ms. */
|
|
71
23
|
readonly ageMs: number;
|
|
72
24
|
}
|
|
73
25
|
export interface CacheMissEvent extends ActlyEventBase {
|
|
@@ -75,41 +27,40 @@ export interface CacheMissEvent extends ActlyEventBase {
|
|
|
75
27
|
}
|
|
76
28
|
export interface DedupeJoinEvent extends ActlyEventBase {
|
|
77
29
|
readonly type: 'dedupe-join';
|
|
78
|
-
/** This caller's position in the joiner queue (1 = first joiner). */
|
|
79
30
|
readonly joinerPosition: number;
|
|
80
31
|
}
|
|
81
32
|
export interface TimeoutEvent extends ActlyEventBase {
|
|
82
33
|
readonly type: 'timeout';
|
|
83
|
-
/** Which deadline fired. */
|
|
84
34
|
readonly kind: 'per-attempt' | 'total';
|
|
85
|
-
/** The configured ms. */
|
|
86
35
|
readonly ms: number;
|
|
87
36
|
}
|
|
88
37
|
export interface FinalSuccessEvent extends ActlyEventBase {
|
|
89
38
|
readonly type: 'final-success';
|
|
90
|
-
/** Where the value came from. */
|
|
91
39
|
readonly source: ActSource;
|
|
92
|
-
/** Total attempts made (0 for cache hit). */
|
|
93
40
|
readonly attempts: number;
|
|
94
|
-
/** Total wall-clock duration of the act() call. */
|
|
95
41
|
readonly durationMs: number;
|
|
96
42
|
}
|
|
97
43
|
export interface FinalFailureEvent extends ActlyEventBase {
|
|
98
44
|
readonly type: 'final-failure';
|
|
99
|
-
/** Total attempts made. */
|
|
100
45
|
readonly attempts: number;
|
|
101
|
-
/** Total wall-clock duration of the act() call. */
|
|
102
46
|
readonly durationMs: number;
|
|
103
|
-
|
|
104
|
-
readonly failedBy: 'abort' | 'timeout' | 'total-timeout' | 'retry-exhausted' | 'fn-error' | 'validation';
|
|
105
|
-
/** The final error. */
|
|
47
|
+
readonly failedBy: ActlyFailedBy;
|
|
106
48
|
readonly error: unknown;
|
|
107
49
|
}
|
|
108
|
-
export type ActlyEvent = AttemptEvent | RetryEvent | CacheHitEvent | CacheMissEvent | DedupeJoinEvent | TimeoutEvent | FinalSuccessEvent | FinalFailureEvent;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
50
|
+
export type ActlyEvent = AttemptEvent | RetryEvent | CacheHitEvent | CacheMissEvent | DedupeJoinEvent | TimeoutEvent | FinalSuccessEvent | FinalFailureEvent | BackpressureEvent | WatchdogEvent;
|
|
51
|
+
export interface BackpressureEvent extends ActlyEventBase {
|
|
52
|
+
readonly type: 'backpressure';
|
|
53
|
+
readonly source: 'bulkhead';
|
|
54
|
+
readonly queueLength: number;
|
|
55
|
+
readonly maxConcurrent: number;
|
|
56
|
+
readonly maxQueueSize: number;
|
|
57
|
+
readonly utilization: number;
|
|
58
|
+
}
|
|
59
|
+
export interface WatchdogEvent extends ActlyEventBase {
|
|
60
|
+
readonly type: 'watchdog';
|
|
61
|
+
readonly elapsedMs: number;
|
|
62
|
+
readonly scope: string;
|
|
63
|
+
}
|
|
113
64
|
export interface ObservabilityHooks {
|
|
114
65
|
onAttempt?: (event: AttemptEvent) => void;
|
|
115
66
|
onRetry?: (event: RetryEvent) => void;
|
|
@@ -119,24 +70,14 @@ export interface ObservabilityHooks {
|
|
|
119
70
|
onTimeout?: (event: TimeoutEvent) => void;
|
|
120
71
|
onFinalSuccess?: (event: FinalSuccessEvent) => void;
|
|
121
72
|
onFinalFailure?: (event: FinalFailureEvent) => void;
|
|
73
|
+
onBackpressure?: (event: BackpressureEvent) => void;
|
|
74
|
+
onWatchdog?: (event: WatchdogEvent) => void;
|
|
122
75
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Internal: thread observability through the policy chain without changing
|
|
125
|
-
* every policy's signature. We attach it to `PolicyContext` as an optional
|
|
126
|
-
* field — policies that emit events check for its presence.
|
|
127
|
-
*
|
|
128
|
-
* `traceId` is also stored here so policies can include it in events.
|
|
129
|
-
*/
|
|
130
76
|
export interface ObservabilityContext {
|
|
131
77
|
traceId: string;
|
|
132
78
|
hooks: ObservabilityHooks;
|
|
133
|
-
/** Counter for dedupe joiner position. Per-key, but we approximate by call. */
|
|
134
79
|
joinerCounter: number;
|
|
135
80
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Quick null-check helper. Policies call this once at decision points; if
|
|
138
|
-
* it returns false, no further observability work is done.
|
|
139
|
-
*/
|
|
140
81
|
export declare function hasObservers(ctx: {
|
|
141
82
|
observability?: ObservabilityContext;
|
|
142
83
|
}): ctx is {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"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"}
|
package/dist/observability.js
CHANGED
|
@@ -1,43 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Observability hooks.
|
|
3
|
-
*
|
|
4
|
-
* Eight event types covering the entire lifecycle of an `act()` call.
|
|
5
|
-
*
|
|
6
|
-
* # Contract
|
|
7
|
-
*
|
|
8
|
-
* When `options.observability` is `null`/`undefined` (the common case),
|
|
9
|
-
* no event objects are allocated and no function calls are made. The
|
|
10
|
-
* hot path is a single null-check per policy decision.
|
|
11
|
-
*
|
|
12
|
-
* When hooks ARE registered, events are allocated lazily — only when the
|
|
13
|
-
* corresponding event actually fires. A cache hit doesn't allocate an
|
|
14
|
-
* `onRetry` event, for example.
|
|
15
|
-
*
|
|
16
|
-
* # Event shape
|
|
17
|
-
*
|
|
18
|
-
* Every event carries `key`, `traceId`, and `timestamp` for correlation.
|
|
19
|
-
* Event-specific fields are on the same object (no nesting) for flat
|
|
20
|
-
* destructuring in user code.
|
|
21
|
-
*
|
|
22
|
-
* # Ordering
|
|
23
|
-
*
|
|
24
|
-
* For a successful fresh call with retries:
|
|
25
|
-
* onAttempt (1) → onAttempt (2) → onRetry (1→2) → onAttempt (3) → onFinalSuccess
|
|
26
|
-
*
|
|
27
|
-
* For a cache hit:
|
|
28
|
-
* onCacheHit → onFinalSuccess
|
|
29
|
-
*
|
|
30
|
-
* For a dedupe joiner:
|
|
31
|
-
* onDedupeJoin → onFinalSuccess (or onFinalFailure)
|
|
32
|
-
*
|
|
33
|
-
* For a timeout:
|
|
34
|
-
* onAttempt → onTimeout → onFinalFailure
|
|
35
|
-
*/
|
|
36
|
-
/**
|
|
37
|
-
* Quick null-check helper. Policies call this once at decision points; if
|
|
38
|
-
* it returns false, no further observability work is done.
|
|
39
|
-
*/
|
|
40
1
|
export function hasObservers(ctx) {
|
|
41
2
|
return ctx.observability != null;
|
|
42
3
|
}
|
|
43
|
-
//# sourceMappingURL=observability.js.map
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.bulkheadPolicy = bulkheadPolicy;
|
|
4
4
|
const executor_js_1 = require("../core/executor.js");
|
|
5
5
|
const errors_js_1 = require("../errors.js");
|
|
6
|
+
const safeCall_js_1 = require("../utils/safeCall.js");
|
|
6
7
|
const NS = 'bulk:';
|
|
7
8
|
function getState(store, key) {
|
|
8
9
|
return store.get(NS + key) ?? { active: 0, queue: [] };
|
|
@@ -13,12 +14,12 @@ function setState(store, key, state) {
|
|
|
13
14
|
function bulkheadPolicy(opts) {
|
|
14
15
|
const maxConcurrent = Math.max(1, Math.floor(opts.maxConcurrent));
|
|
15
16
|
const queueTimeoutMs = opts.queueTimeoutMs ?? 0;
|
|
17
|
+
const maxQueueSize = opts.maxQueueSize ?? Number.POSITIVE_INFINITY;
|
|
16
18
|
const applier = (fn, ctx) => {
|
|
17
19
|
const syncCtx = ctx;
|
|
18
20
|
return async (signal) => {
|
|
19
21
|
const key = syncCtx.key;
|
|
20
22
|
const acquireSlot = () => {
|
|
21
|
-
// If signal already aborted, reject immediately
|
|
22
23
|
if (signal.aborted)
|
|
23
24
|
return Promise.reject(signal.reason);
|
|
24
25
|
const state = getState(syncCtx.store, key);
|
|
@@ -30,14 +31,19 @@ function bulkheadPolicy(opts) {
|
|
|
30
31
|
if (queueTimeoutMs === 0) {
|
|
31
32
|
throw new errors_js_1.BulkheadOverflowError(key, maxConcurrent);
|
|
32
33
|
}
|
|
34
|
+
const currentState = getState(syncCtx.store, key);
|
|
35
|
+
if (currentState.queue.length >= maxQueueSize) {
|
|
36
|
+
throw new errors_js_1.BulkheadOverflowError(key, maxConcurrent);
|
|
37
|
+
}
|
|
33
38
|
return new Promise((resolve, reject) => {
|
|
34
39
|
const state2 = getState(syncCtx.store, key);
|
|
35
40
|
const entry = {
|
|
36
41
|
resolve,
|
|
37
42
|
reject,
|
|
43
|
+
timer: undefined,
|
|
44
|
+
onAbort: undefined,
|
|
38
45
|
signal,
|
|
39
46
|
};
|
|
40
|
-
// Remove entry from queue on signal abort
|
|
41
47
|
entry.onAbort = () => {
|
|
42
48
|
const s = getState(syncCtx.store, key);
|
|
43
49
|
const idx = s.queue.indexOf(entry);
|
|
@@ -62,6 +68,30 @@ function bulkheadPolicy(opts) {
|
|
|
62
68
|
}
|
|
63
69
|
signal.addEventListener('abort', entry.onAbort, { once: true });
|
|
64
70
|
state2.queue.push(entry);
|
|
71
|
+
const obs = syncCtx.observability;
|
|
72
|
+
if (obs && maxQueueSize !== Number.POSITIVE_INFINITY) {
|
|
73
|
+
const utilization = state2.queue.length / maxQueueSize;
|
|
74
|
+
const wasOver80 = state2.backpressureEmitted === true;
|
|
75
|
+
if (utilization >= 0.8 && !wasOver80) {
|
|
76
|
+
;
|
|
77
|
+
state2.backpressureEmitted = true;
|
|
78
|
+
(0, safeCall_js_1.safeCall)(obs.hooks.onBackpressure, {
|
|
79
|
+
type: 'backpressure',
|
|
80
|
+
key: syncCtx.key,
|
|
81
|
+
traceId: obs.traceId,
|
|
82
|
+
timestamp: Date.now(),
|
|
83
|
+
source: 'bulkhead',
|
|
84
|
+
queueLength: state2.queue.length,
|
|
85
|
+
maxConcurrent,
|
|
86
|
+
maxQueueSize,
|
|
87
|
+
utilization,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
else if (utilization < 0.8 && wasOver80) {
|
|
91
|
+
;
|
|
92
|
+
state2.backpressureEmitted = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
65
95
|
setState(syncCtx.store, key, state2);
|
|
66
96
|
});
|
|
67
97
|
};
|
|
@@ -73,7 +103,6 @@ function bulkheadPolicy(opts) {
|
|
|
73
103
|
state.active++;
|
|
74
104
|
if (next.timer)
|
|
75
105
|
clearTimeout(next.timer);
|
|
76
|
-
// Remove abort listener from the QUEUED caller's signal (not releaser's)
|
|
77
106
|
if (next.onAbort && next.signal) {
|
|
78
107
|
next.signal.removeEventListener('abort', next.onAbort);
|
|
79
108
|
}
|
|
@@ -81,7 +110,18 @@ function bulkheadPolicy(opts) {
|
|
|
81
110
|
}
|
|
82
111
|
if (state.active < 0)
|
|
83
112
|
state.active = 0;
|
|
84
|
-
|
|
113
|
+
if (maxQueueSize !== Number.POSITIVE_INFINITY && state.backpressureEmitted === true) {
|
|
114
|
+
if (state.queue.length / maxQueueSize < 0.8) {
|
|
115
|
+
;
|
|
116
|
+
state.backpressureEmitted = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (state.active === 0 && state.queue.length === 0) {
|
|
120
|
+
syncCtx.store.delete(NS + key);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
setState(syncCtx.store, key, state);
|
|
124
|
+
}
|
|
85
125
|
};
|
|
86
126
|
await acquireSlot();
|
|
87
127
|
try {
|
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bulkhead.d.ts","sourceRoot":"","sources":["../../src/policies/bulkhead.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,eAAe,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"bulkhead.d.ts","sourceRoot":"","sources":["../../src/policies/bulkhead.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,eAAe,EAAE,MAAM,mBAAmB,CAAA;AA2B7F,wBAAgB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,eAAe,GAAG,aAAa,CAAC,CAAC,CAAC,CAkJzE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { REQUIRES_SYNC_STORE } from '../core/executor.js';
|
|
2
2
|
import { BulkheadOverflowError } from '../errors.js';
|
|
3
|
+
import { safeCall } from '../utils/safeCall.js';
|
|
3
4
|
const NS = 'bulk:';
|
|
4
5
|
function getState(store, key) {
|
|
5
6
|
return store.get(NS + key) ?? { active: 0, queue: [] };
|
|
@@ -10,12 +11,12 @@ function setState(store, key, state) {
|
|
|
10
11
|
export function bulkheadPolicy(opts) {
|
|
11
12
|
const maxConcurrent = Math.max(1, Math.floor(opts.maxConcurrent));
|
|
12
13
|
const queueTimeoutMs = opts.queueTimeoutMs ?? 0;
|
|
14
|
+
const maxQueueSize = opts.maxQueueSize ?? Number.POSITIVE_INFINITY;
|
|
13
15
|
const applier = (fn, ctx) => {
|
|
14
16
|
const syncCtx = ctx;
|
|
15
17
|
return async (signal) => {
|
|
16
18
|
const key = syncCtx.key;
|
|
17
19
|
const acquireSlot = () => {
|
|
18
|
-
// If signal already aborted, reject immediately
|
|
19
20
|
if (signal.aborted)
|
|
20
21
|
return Promise.reject(signal.reason);
|
|
21
22
|
const state = getState(syncCtx.store, key);
|
|
@@ -27,14 +28,19 @@ export function bulkheadPolicy(opts) {
|
|
|
27
28
|
if (queueTimeoutMs === 0) {
|
|
28
29
|
throw new BulkheadOverflowError(key, maxConcurrent);
|
|
29
30
|
}
|
|
31
|
+
const currentState = getState(syncCtx.store, key);
|
|
32
|
+
if (currentState.queue.length >= maxQueueSize) {
|
|
33
|
+
throw new BulkheadOverflowError(key, maxConcurrent);
|
|
34
|
+
}
|
|
30
35
|
return new Promise((resolve, reject) => {
|
|
31
36
|
const state2 = getState(syncCtx.store, key);
|
|
32
37
|
const entry = {
|
|
33
38
|
resolve,
|
|
34
39
|
reject,
|
|
40
|
+
timer: undefined,
|
|
41
|
+
onAbort: undefined,
|
|
35
42
|
signal,
|
|
36
43
|
};
|
|
37
|
-
// Remove entry from queue on signal abort
|
|
38
44
|
entry.onAbort = () => {
|
|
39
45
|
const s = getState(syncCtx.store, key);
|
|
40
46
|
const idx = s.queue.indexOf(entry);
|
|
@@ -59,6 +65,30 @@ export function bulkheadPolicy(opts) {
|
|
|
59
65
|
}
|
|
60
66
|
signal.addEventListener('abort', entry.onAbort, { once: true });
|
|
61
67
|
state2.queue.push(entry);
|
|
68
|
+
const obs = syncCtx.observability;
|
|
69
|
+
if (obs && maxQueueSize !== Number.POSITIVE_INFINITY) {
|
|
70
|
+
const utilization = state2.queue.length / maxQueueSize;
|
|
71
|
+
const wasOver80 = state2.backpressureEmitted === true;
|
|
72
|
+
if (utilization >= 0.8 && !wasOver80) {
|
|
73
|
+
;
|
|
74
|
+
state2.backpressureEmitted = true;
|
|
75
|
+
safeCall(obs.hooks.onBackpressure, {
|
|
76
|
+
type: 'backpressure',
|
|
77
|
+
key: syncCtx.key,
|
|
78
|
+
traceId: obs.traceId,
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
source: 'bulkhead',
|
|
81
|
+
queueLength: state2.queue.length,
|
|
82
|
+
maxConcurrent,
|
|
83
|
+
maxQueueSize,
|
|
84
|
+
utilization,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else if (utilization < 0.8 && wasOver80) {
|
|
88
|
+
;
|
|
89
|
+
state2.backpressureEmitted = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
62
92
|
setState(syncCtx.store, key, state2);
|
|
63
93
|
});
|
|
64
94
|
};
|
|
@@ -70,7 +100,6 @@ export function bulkheadPolicy(opts) {
|
|
|
70
100
|
state.active++;
|
|
71
101
|
if (next.timer)
|
|
72
102
|
clearTimeout(next.timer);
|
|
73
|
-
// Remove abort listener from the QUEUED caller's signal (not releaser's)
|
|
74
103
|
if (next.onAbort && next.signal) {
|
|
75
104
|
next.signal.removeEventListener('abort', next.onAbort);
|
|
76
105
|
}
|
|
@@ -78,7 +107,18 @@ export function bulkheadPolicy(opts) {
|
|
|
78
107
|
}
|
|
79
108
|
if (state.active < 0)
|
|
80
109
|
state.active = 0;
|
|
81
|
-
|
|
110
|
+
if (maxQueueSize !== Number.POSITIVE_INFINITY && state.backpressureEmitted === true) {
|
|
111
|
+
if (state.queue.length / maxQueueSize < 0.8) {
|
|
112
|
+
;
|
|
113
|
+
state.backpressureEmitted = false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (state.active === 0 && state.queue.length === 0) {
|
|
117
|
+
syncCtx.store.delete(NS + key);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
setState(syncCtx.store, key, state);
|
|
121
|
+
}
|
|
82
122
|
};
|
|
83
123
|
await acquireSlot();
|
|
84
124
|
try {
|
|
@@ -92,4 +132,3 @@ export function bulkheadPolicy(opts) {
|
|
|
92
132
|
applier[REQUIRES_SYNC_STORE] = true;
|
|
93
133
|
return applier;
|
|
94
134
|
}
|
|
95
|
-
//# sourceMappingURL=bulkhead.js.map
|