@blokjs/runner 0.2.2 → 0.6.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/dist/Blok.js +32 -3
- package/dist/Blok.js.map +1 -1
- package/dist/Configuration.d.ts +59 -5
- package/dist/Configuration.js +366 -96
- package/dist/Configuration.js.map +1 -1
- package/dist/ForEachNode.d.ts +59 -0
- package/dist/ForEachNode.js +522 -0
- package/dist/ForEachNode.js.map +1 -0
- package/dist/LoopMaxIterationsError.d.ts +11 -0
- package/dist/LoopMaxIterationsError.js +18 -0
- package/dist/LoopMaxIterationsError.js.map +1 -0
- package/dist/LoopNode.d.ts +36 -0
- package/dist/LoopNode.js +182 -0
- package/dist/LoopNode.js.map +1 -0
- package/dist/PayloadTooLargeError.d.ts +19 -0
- package/dist/PayloadTooLargeError.js +29 -0
- package/dist/PayloadTooLargeError.js.map +1 -0
- package/dist/RunCancelledError.d.ts +17 -0
- package/dist/RunCancelledError.js +25 -0
- package/dist/RunCancelledError.js.map +1 -0
- package/dist/Runner.d.ts +11 -1
- package/dist/Runner.js +9 -2
- package/dist/Runner.js.map +1 -1
- package/dist/RunnerSteps.js +648 -44
- package/dist/RunnerSteps.js.map +1 -1
- package/dist/RuntimeAdapterNode.d.ts +2 -1
- package/dist/RuntimeAdapterNode.js +2 -2
- package/dist/RuntimeAdapterNode.js.map +1 -1
- package/dist/RuntimeRegistry.d.ts +23 -2
- package/dist/RuntimeRegistry.js +31 -2
- package/dist/RuntimeRegistry.js.map +1 -1
- package/dist/SubworkflowNode.d.ts +181 -0
- package/dist/SubworkflowNode.js +479 -0
- package/dist/SubworkflowNode.js.map +1 -0
- package/dist/SwitchNode.d.ts +37 -0
- package/dist/SwitchNode.js +153 -0
- package/dist/SwitchNode.js.map +1 -0
- package/dist/TriggerBase.d.ts +178 -0
- package/dist/TriggerBase.js +1032 -5
- package/dist/TriggerBase.js.map +1 -1
- package/dist/TryCatchNode.d.ts +32 -0
- package/dist/TryCatchNode.js +207 -0
- package/dist/TryCatchNode.js.map +1 -0
- package/dist/WaitDispatchRequest.d.ts +38 -0
- package/dist/WaitDispatchRequest.js +13 -0
- package/dist/WaitDispatchRequest.js.map +1 -0
- package/dist/WaitNode.d.ts +23 -0
- package/dist/WaitNode.js +26 -0
- package/dist/WaitNode.js.map +1 -0
- package/dist/adapters/grpc/GrpcCodec.js +2 -2
- package/dist/adapters/grpc/GrpcRuntimeAdapter.d.ts +6 -4
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js +6 -4
- package/dist/adapters/grpc/GrpcRuntimeAdapter.js.map +1 -1
- package/dist/adapters/grpc/types.d.ts +7 -5
- package/dist/adapters/grpc/types.js.map +1 -1
- package/dist/adapters/transport.d.ts +12 -41
- package/dist/adapters/transport.js +21 -70
- package/dist/adapters/transport.js.map +1 -1
- package/dist/cache/NodeResultCache.js +7 -0
- package/dist/cache/NodeResultCache.js.map +1 -1
- package/dist/concurrency/ConcurrencyBackend.d.ts +61 -0
- package/dist/concurrency/ConcurrencyBackend.js +20 -0
- package/dist/concurrency/ConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/ConcurrencyLimitError.d.ts +37 -0
- package/dist/concurrency/ConcurrencyLimitError.js +16 -0
- package/dist/concurrency/ConcurrencyLimitError.js.map +1 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.d.ts +64 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js +310 -0
- package/dist/concurrency/NatsKvConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/QueueExpiredError.d.ts +40 -0
- package/dist/concurrency/QueueExpiredError.js +15 -0
- package/dist/concurrency/QueueExpiredError.js.map +1 -0
- package/dist/concurrency/RedisConcurrencyBackend.d.ts +64 -0
- package/dist/concurrency/RedisConcurrencyBackend.js +374 -0
- package/dist/concurrency/RedisConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/createConcurrencyBackend.d.ts +24 -0
- package/dist/concurrency/createConcurrencyBackend.js +38 -0
- package/dist/concurrency/createConcurrencyBackend.js.map +1 -0
- package/dist/concurrency/readConcurrencyConfig.d.ts +60 -0
- package/dist/concurrency/readConcurrencyConfig.js +60 -0
- package/dist/concurrency/readConcurrencyConfig.js.map +1 -0
- package/dist/defineNode.d.ts +8 -0
- package/dist/defineNode.js +25 -5
- package/dist/defineNode.js.map +1 -1
- package/dist/graphql/GraphQLSchemaGenerator.js +1 -1
- package/dist/graphql/GraphQLSchemaGenerator.js.map +1 -1
- package/dist/idempotency/resolveIdempotencyKey.d.ts +20 -0
- package/dist/idempotency/resolveIdempotencyKey.js +37 -0
- package/dist/idempotency/resolveIdempotencyKey.js.map +1 -0
- package/dist/index.d.ts +30 -6
- package/dist/index.js +55 -6
- package/dist/index.js.map +1 -1
- package/dist/marketplace/RuntimeCatalog.d.ts +6 -0
- package/dist/marketplace/RuntimeCatalog.js.map +1 -1
- package/dist/marketplace/RuntimeDiscovery.d.ts +2 -2
- package/dist/marketplace/RuntimeDiscovery.js +18 -6
- package/dist/marketplace/RuntimeDiscovery.js.map +1 -1
- package/dist/monitoring/ConcurrencyMetrics.d.ts +82 -0
- package/dist/monitoring/ConcurrencyMetrics.js +139 -0
- package/dist/monitoring/ConcurrencyMetrics.js.map +1 -0
- package/dist/monitoring/ForEachWaitMetrics.d.ts +22 -0
- package/dist/monitoring/ForEachWaitMetrics.js +36 -0
- package/dist/monitoring/ForEachWaitMetrics.js.map +1 -0
- package/dist/monitoring/JanitorMetrics.d.ts +27 -0
- package/dist/monitoring/JanitorMetrics.js +48 -0
- package/dist/monitoring/JanitorMetrics.js.map +1 -0
- package/dist/openapi/OpenAPIGenerator.js +7 -2
- package/dist/openapi/OpenAPIGenerator.js.map +1 -1
- package/dist/runtime/PrimitiveStack.d.ts +64 -0
- package/dist/runtime/PrimitiveStack.js +92 -0
- package/dist/runtime/PrimitiveStack.js.map +1 -0
- package/dist/scheduling/DebounceBackend.d.ts +108 -0
- package/dist/scheduling/DebounceBackend.js +23 -0
- package/dist/scheduling/DebounceBackend.js.map +1 -0
- package/dist/scheduling/DebounceCoordinator.d.ts +141 -0
- package/dist/scheduling/DebounceCoordinator.js +362 -0
- package/dist/scheduling/DebounceCoordinator.js.map +1 -0
- package/dist/scheduling/DeferredDispatchSignal.d.ts +50 -0
- package/dist/scheduling/DeferredDispatchSignal.js +14 -0
- package/dist/scheduling/DeferredDispatchSignal.js.map +1 -0
- package/dist/scheduling/DeferredRunScheduler.d.ts +96 -0
- package/dist/scheduling/DeferredRunScheduler.js +256 -0
- package/dist/scheduling/DeferredRunScheduler.js.map +1 -0
- package/dist/scheduling/NatsKvDebounceBackend.d.ts +53 -0
- package/dist/scheduling/NatsKvDebounceBackend.js +334 -0
- package/dist/scheduling/NatsKvDebounceBackend.js.map +1 -0
- package/dist/scheduling/RedisDebounceBackend.d.ts +49 -0
- package/dist/scheduling/RedisDebounceBackend.js +356 -0
- package/dist/scheduling/RedisDebounceBackend.js.map +1 -0
- package/dist/scheduling/createDebounceBackend.d.ts +25 -0
- package/dist/scheduling/createDebounceBackend.js +39 -0
- package/dist/scheduling/createDebounceBackend.js.map +1 -0
- package/dist/scheduling/readSchedulingConfig.d.ts +24 -0
- package/dist/scheduling/readSchedulingConfig.js +52 -0
- package/dist/scheduling/readSchedulingConfig.js.map +1 -0
- package/dist/security/AuditLogger.js +1 -1
- package/dist/security/AuditLogger.js.map +1 -1
- package/dist/security/AuthMiddleware.d.ts +19 -20
- package/dist/security/AuthMiddleware.js +35 -20
- package/dist/security/AuthMiddleware.js.map +1 -1
- package/dist/security/OAuthProvider.js +2 -2
- package/dist/security/OAuthProvider.js.map +1 -1
- package/dist/security/SecretManager.js +14 -13
- package/dist/security/SecretManager.js.map +1 -1
- package/dist/security/index.d.ts +3 -1
- package/dist/security/index.js +3 -1
- package/dist/security/index.js.map +1 -1
- package/dist/testing/TestHarness.d.ts +27 -12
- package/dist/testing/TestHarness.js +19 -3
- package/dist/testing/TestHarness.js.map +1 -1
- package/dist/testing/WorkflowTestRunner.js +0 -7
- package/dist/testing/WorkflowTestRunner.js.map +1 -1
- package/dist/timeouts/StepTimeoutError.d.ts +22 -0
- package/dist/timeouts/StepTimeoutError.js +31 -0
- package/dist/timeouts/StepTimeoutError.js.map +1 -0
- package/dist/tracing/InMemoryRunStore.d.ts +41 -1
- package/dist/tracing/InMemoryRunStore.js +239 -0
- package/dist/tracing/InMemoryRunStore.js.map +1 -1
- package/dist/tracing/Janitor.d.ts +70 -0
- package/dist/tracing/Janitor.js +150 -0
- package/dist/tracing/Janitor.js.map +1 -0
- package/dist/tracing/PostgresRunStore.d.ts +57 -1
- package/dist/tracing/PostgresRunStore.js +711 -6
- package/dist/tracing/PostgresRunStore.js.map +1 -1
- package/dist/tracing/RoutingDiagnostics.d.ts +55 -0
- package/dist/tracing/RoutingDiagnostics.js +50 -0
- package/dist/tracing/RoutingDiagnostics.js.map +1 -0
- package/dist/tracing/RunStore.d.ts +181 -1
- package/dist/tracing/RunTracker.d.ts +244 -9
- package/dist/tracing/RunTracker.js +594 -1
- package/dist/tracing/RunTracker.js.map +1 -1
- package/dist/tracing/SqliteRunStore.d.ts +79 -2
- package/dist/tracing/SqliteRunStore.js +775 -16
- package/dist/tracing/SqliteRunStore.js.map +1 -1
- package/dist/tracing/TraceRouter.d.ts +20 -2
- package/dist/tracing/TraceRouter.js +612 -6
- package/dist/tracing/TraceRouter.js.map +1 -1
- package/dist/tracing/createStore.js +14 -3
- package/dist/tracing/createStore.js.map +1 -1
- package/dist/tracing/metadataFilter.d.ts +63 -0
- package/dist/tracing/metadataFilter.js +224 -0
- package/dist/tracing/metadataFilter.js.map +1 -0
- package/dist/tracing/sanitize.d.ts +11 -0
- package/dist/tracing/sanitize.js +29 -0
- package/dist/tracing/sanitize.js.map +1 -1
- package/dist/tracing/types.d.ts +672 -2
- package/dist/utils/createChildContext.d.ts +32 -0
- package/dist/utils/createChildContext.js +113 -0
- package/dist/utils/createChildContext.js.map +1 -0
- package/dist/utils/envAllowlist.d.ts +35 -0
- package/dist/utils/envAllowlist.js +113 -0
- package/dist/utils/envAllowlist.js.map +1 -0
- package/dist/version/RuntimeVersionValidator.d.ts +38 -0
- package/dist/version/RuntimeVersionValidator.js +121 -0
- package/dist/version/RuntimeVersionValidator.js.map +1 -0
- package/dist/visualization/WorkflowVisualizer.js +4 -4
- package/dist/visualization/WorkflowVisualizer.js.map +1 -1
- package/dist/workflow/PersistenceHelper.d.ts +18 -10
- package/dist/workflow/PersistenceHelper.js +35 -9
- package/dist/workflow/PersistenceHelper.js.map +1 -1
- package/dist/workflow/WorkflowNormalizer.d.ts +48 -42
- package/dist/workflow/WorkflowNormalizer.js +650 -18
- package/dist/workflow/WorkflowNormalizer.js.map +1 -1
- package/dist/workflow/WorkflowRegistry.d.ts +186 -0
- package/dist/workflow/WorkflowRegistry.js +202 -0
- package/dist/workflow/WorkflowRegistry.js.map +1 -0
- package/dist/workflow/sampleBody.d.ts +54 -0
- package/dist/workflow/sampleBody.js +320 -0
- package/dist/workflow/sampleBody.js.map +1 -0
- package/package.json +3 -8
- package/dist/adapters/HttpRuntimeAdapter.d.ts +0 -79
- package/dist/adapters/HttpRuntimeAdapter.js +0 -233
- package/dist/adapters/HttpRuntimeAdapter.js.map +0 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 2 #6 follow-up · NATS KV-backed concurrency backend.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates per-(workflow, concurrencyKey) lease state across processes
|
|
5
|
+
* via a single NATS JetStream KV value per bucket using revision-based
|
|
6
|
+
* compare-and-swap (OCC).
|
|
7
|
+
*
|
|
8
|
+
* Storage model: one KV key per `(workflowName, concurrencyKey)` pair.
|
|
9
|
+
* Value is a JSON `{leases: [{runId, expiresAt}]}` document. Bounded
|
|
10
|
+
* cardinality assumption — typical concurrency keys hold 1-50 active
|
|
11
|
+
* leases (per-tenant rate limits). For higher cardinality, a per-lease
|
|
12
|
+
* key model would scale better; revisit when needed.
|
|
13
|
+
*
|
|
14
|
+
* Atomicity: NATS KV's only guarantee is `kv.create(key, value)` (fails
|
|
15
|
+
* on conflict) and `kv.update(key, value, expectedRevision)` (fails on
|
|
16
|
+
* concurrent modification). The acquire loop reads → filters → checks
|
|
17
|
+
* limit → CAS update. On CAS failure, retry up to 10 times then
|
|
18
|
+
* fail-closed (deny the slot).
|
|
19
|
+
*
|
|
20
|
+
* Lease leak: each lease carries an `expiresAt`. Expired leases are
|
|
21
|
+
* lazy-purged inside the same `acquireSlot` call that observes them;
|
|
22
|
+
* an explicit `purgeExpired` sweep is also exposed for janitor use.
|
|
23
|
+
*/
|
|
24
|
+
import { ConcurrencyMetrics } from "../monitoring/ConcurrencyMetrics";
|
|
25
|
+
const DEFAULT_BUCKET_NAME = "blok-concurrency";
|
|
26
|
+
const MAX_CAS_RETRIES = 10;
|
|
27
|
+
/**
|
|
28
|
+
* Read configuration from environment variables. Used by
|
|
29
|
+
* {@link createConcurrencyBackend} when the user opts into NATS KV.
|
|
30
|
+
*/
|
|
31
|
+
export function readNatsKvConfigFromEnv() {
|
|
32
|
+
const serversRaw = process.env.BLOK_CONCURRENCY_NATS_SERVERS ?? "nats://localhost:4222";
|
|
33
|
+
const servers = serversRaw
|
|
34
|
+
.split(",")
|
|
35
|
+
.map((s) => s.trim())
|
|
36
|
+
.filter(Boolean);
|
|
37
|
+
return {
|
|
38
|
+
servers,
|
|
39
|
+
token: process.env.BLOK_CONCURRENCY_NATS_TOKEN,
|
|
40
|
+
user: process.env.BLOK_CONCURRENCY_NATS_USER,
|
|
41
|
+
pass: process.env.BLOK_CONCURRENCY_NATS_PASS,
|
|
42
|
+
bucketName: process.env.BLOK_CONCURRENCY_NATS_KV_BUCKET ?? DEFAULT_BUCKET_NAME,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export class NatsKvConcurrencyBackend {
|
|
46
|
+
name = "nats-kv";
|
|
47
|
+
nc = null;
|
|
48
|
+
kv = null;
|
|
49
|
+
config;
|
|
50
|
+
connected = false;
|
|
51
|
+
constructor(config) {
|
|
52
|
+
const env = readNatsKvConfigFromEnv();
|
|
53
|
+
this.config = {
|
|
54
|
+
servers: config?.servers ?? env.servers,
|
|
55
|
+
token: config?.token ?? env.token,
|
|
56
|
+
user: config?.user ?? env.user,
|
|
57
|
+
pass: config?.pass ?? env.pass,
|
|
58
|
+
bucketName: config?.bucketName ?? env.bucketName,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async connect() {
|
|
62
|
+
if (this.connected)
|
|
63
|
+
return;
|
|
64
|
+
// Security review FW-5 — refuse to start in production with the
|
|
65
|
+
// default bucket name. Two deployments sharing a NATS server with
|
|
66
|
+
// the default would contend on the same `(workflow, key)` buckets,
|
|
67
|
+
// silently corrupting concurrency state across tenants. The fix
|
|
68
|
+
// is operator-mandatory: set BLOK_CONCURRENCY_NATS_KV_BUCKET
|
|
69
|
+
// per-deployment.
|
|
70
|
+
const blokEnv = process.env.BLOK_ENV;
|
|
71
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
72
|
+
const isProd = blokEnv === "production" || nodeEnv === "production";
|
|
73
|
+
if (isProd && this.config.bucketName === DEFAULT_BUCKET_NAME) {
|
|
74
|
+
throw new Error(`[blok] NATS KV concurrency backend refuses to start in production with the default bucket name ('${DEFAULT_BUCKET_NAME}'). Set BLOK_CONCURRENCY_NATS_KV_BUCKET to a deployment-unique value (e.g. 'blok-concurrency-acme-prod') to prevent cross-deployment collision on a shared NATS server.`);
|
|
75
|
+
}
|
|
76
|
+
let natsModule;
|
|
77
|
+
try {
|
|
78
|
+
natsModule = (await import("nats"));
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
throw new Error(`NatsKvConcurrencyBackend requires the 'nats' package. Install it: \`bun add nats\` or \`npm install nats\`. Underlying error: ${err instanceof Error ? err.message : String(err)}`);
|
|
82
|
+
}
|
|
83
|
+
const connectOpts = { servers: this.config.servers };
|
|
84
|
+
if (this.config.token)
|
|
85
|
+
connectOpts.token = this.config.token;
|
|
86
|
+
if (this.config.user)
|
|
87
|
+
connectOpts.user = this.config.user;
|
|
88
|
+
if (this.config.pass)
|
|
89
|
+
connectOpts.pass = this.config.pass;
|
|
90
|
+
this.nc = await natsModule.connect(connectOpts);
|
|
91
|
+
// nats.js v2.x — KV lives at `nc.jetstream().views.kv(name)`. The
|
|
92
|
+
// returned `KV` auto-creates the bucket on first use given the
|
|
93
|
+
// connection has KV bucket-create permission. (Earlier versions
|
|
94
|
+
// exposed `nc.kv(name)` directly; that API was removed.)
|
|
95
|
+
const js = this.nc.jetstream();
|
|
96
|
+
this.kv = await js.views.kv(this.config.bucketName);
|
|
97
|
+
this.connected = true;
|
|
98
|
+
}
|
|
99
|
+
async disconnect() {
|
|
100
|
+
if (!this.connected)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
await this.nc?.drain();
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
this.nc = null;
|
|
107
|
+
this.kv = null;
|
|
108
|
+
this.connected = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
bucketKey(workflowName, concurrencyKey) {
|
|
112
|
+
// Use `__` (double underscore) — KV keys cannot contain `.` or
|
|
113
|
+
// `>` per NATS subject grammar; `__` is unambiguous and allows
|
|
114
|
+
// arbitrary workflow / key strings.
|
|
115
|
+
return `${this.encodeSegment(workflowName)}__${this.encodeSegment(concurrencyKey)}`;
|
|
116
|
+
}
|
|
117
|
+
encodeSegment(s) {
|
|
118
|
+
// NATS KV keys must match `[-/_=\.a-zA-Z0-9]+`. Replace anything
|
|
119
|
+
// outside the safe set with hex escape `_HHHH_` to keep the
|
|
120
|
+
// roundtrip lossless.
|
|
121
|
+
return s.replace(/[^-_=.a-zA-Z0-9]/g, (ch) => `_${ch.codePointAt(0)?.toString(16)}_`);
|
|
122
|
+
}
|
|
123
|
+
requireKv() {
|
|
124
|
+
if (!this.kv) {
|
|
125
|
+
throw new Error("NatsKvConcurrencyBackend not connected — call connect() first.");
|
|
126
|
+
}
|
|
127
|
+
return this.kv;
|
|
128
|
+
}
|
|
129
|
+
async acquireSlot(workflowName, concurrencyKey, concurrencyLimit, runId, leaseExpiresAt) {
|
|
130
|
+
const kv = this.requireKv();
|
|
131
|
+
const bucketKey = this.bucketKey(workflowName, concurrencyKey);
|
|
132
|
+
// PR 3 D2 — record OCC retry depth + outcome on every exit path.
|
|
133
|
+
const metricAttrs = { workflow_name: workflowName, concurrency_key: concurrencyKey };
|
|
134
|
+
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
135
|
+
const entry = await this.safeGet(kv, bucketKey);
|
|
136
|
+
// PR 2 A6 — fetch failure (broker unreachable / non-NotFound
|
|
137
|
+
// error). Spinning 10× CAS retries on a connection problem just
|
|
138
|
+
// burns latency. Fail-fast so the trigger sees the issue and
|
|
139
|
+
// can fall back / alert. Existing run continues with no slot;
|
|
140
|
+
// the gate is conservative.
|
|
141
|
+
if (entry === "fetch-failed") {
|
|
142
|
+
console.warn(`[blok][concurrency][nats-kv] acquireSlot fetch-failed for ${workflowName}:${concurrencyKey} (attempt ${attempt + 1}); failing closed`);
|
|
143
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "fail-closed" }, attempt);
|
|
144
|
+
return { acquired: false, currentInFlight: -1 };
|
|
145
|
+
}
|
|
146
|
+
if (!entry) {
|
|
147
|
+
// Bucket doesn't exist — create with first lease.
|
|
148
|
+
const initial = { leases: [{ runId, expiresAt: leaseExpiresAt }] };
|
|
149
|
+
try {
|
|
150
|
+
await kv.create(bucketKey, JSON.stringify(initial));
|
|
151
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
|
|
152
|
+
return { acquired: true, currentInFlight: 1 };
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Race — another process created. Retry.
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Read current state, lazy-purge expired.
|
|
160
|
+
const current = this.parseBucket(entry);
|
|
161
|
+
const now = Date.now();
|
|
162
|
+
const active = current.leases.filter((l) => l.expiresAt > now);
|
|
163
|
+
// Idempotent re-acquire: refresh lease, don't grow count.
|
|
164
|
+
const existingIdx = active.findIndex((l) => l.runId === runId);
|
|
165
|
+
if (existingIdx >= 0) {
|
|
166
|
+
active[existingIdx] = { runId, expiresAt: leaseExpiresAt };
|
|
167
|
+
try {
|
|
168
|
+
await kv.update(bucketKey, JSON.stringify({ leases: active }), entry.revision);
|
|
169
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
|
|
170
|
+
return { acquired: true, currentInFlight: active.length };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Limit check.
|
|
177
|
+
if (active.length >= concurrencyLimit) {
|
|
178
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "denied" }, attempt);
|
|
179
|
+
return { acquired: false, currentInFlight: active.length };
|
|
180
|
+
}
|
|
181
|
+
// Insert + CAS.
|
|
182
|
+
const updated = { leases: [...active, { runId, expiresAt: leaseExpiresAt }] };
|
|
183
|
+
try {
|
|
184
|
+
await kv.update(bucketKey, JSON.stringify(updated), entry.revision);
|
|
185
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "success" }, attempt);
|
|
186
|
+
return { acquired: true, currentInFlight: updated.leases.length };
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
}
|
|
190
|
+
// Retry exhausted — fail-closed.
|
|
191
|
+
console.warn(`[blok][concurrency][nats-kv] acquireSlot exhausted ${MAX_CAS_RETRIES} CAS retries for ${workflowName}:${concurrencyKey}; denying slot to runId=${runId}`);
|
|
192
|
+
ConcurrencyMetrics.getInstance().recordOccRetries({ ...metricAttrs, outcome: "fail-closed" }, MAX_CAS_RETRIES);
|
|
193
|
+
return { acquired: false, currentInFlight: -1 };
|
|
194
|
+
}
|
|
195
|
+
async releaseSlot(workflowName, concurrencyKey, runId) {
|
|
196
|
+
const kv = this.requireKv();
|
|
197
|
+
const bucketKey = this.bucketKey(workflowName, concurrencyKey);
|
|
198
|
+
for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt++) {
|
|
199
|
+
const entry = await this.safeGet(kv, bucketKey);
|
|
200
|
+
// PR 2 A6 — fetch failure on release. Lease will expire via
|
|
201
|
+
// TTL; safe to fail-fast.
|
|
202
|
+
if (entry === "fetch-failed") {
|
|
203
|
+
console.warn(`[blok][concurrency][nats-kv] releaseSlot fetch-failed for ${workflowName}:${concurrencyKey} (attempt ${attempt + 1}); lease for runId=${runId} will expire via TTL`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!entry)
|
|
207
|
+
return; // Idempotent — bucket already gone.
|
|
208
|
+
const current = this.parseBucket(entry);
|
|
209
|
+
const next = current.leases.filter((l) => l.runId !== runId);
|
|
210
|
+
// No-op when the runId wasn't holding a slot.
|
|
211
|
+
if (next.length === current.leases.length)
|
|
212
|
+
return;
|
|
213
|
+
if (next.length === 0) {
|
|
214
|
+
try {
|
|
215
|
+
await kv.delete(bucketKey);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Another process beat us to delete — fine.
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await kv.update(bucketKey, JSON.stringify({ leases: next }), entry.revision);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
catch { }
|
|
228
|
+
}
|
|
229
|
+
console.warn(`[blok][concurrency][nats-kv] releaseSlot exhausted ${MAX_CAS_RETRIES} CAS retries for ${workflowName}:${concurrencyKey}; lease for runId=${runId} will expire via TTL`);
|
|
230
|
+
}
|
|
231
|
+
async purgeExpired(now) {
|
|
232
|
+
const kv = this.requireKv();
|
|
233
|
+
let purged = 0;
|
|
234
|
+
// **DRAIN THE ITERATOR FIRST.** nats.js v2.x `kv.keys()` returns a
|
|
235
|
+
// `QueuedIterator` backed by a JetStream watch consumer. Calling
|
|
236
|
+
// `kv.get()` mid-iteration interferes with the iterator's internal
|
|
237
|
+
// state — observed in practice that subsequent yields silently
|
|
238
|
+
// drop. Collect every key into an array before doing per-key
|
|
239
|
+
// reads, then operate on the array.
|
|
240
|
+
const allKeys = [];
|
|
241
|
+
for await (const key of await kv.keys()) {
|
|
242
|
+
allKeys.push(key);
|
|
243
|
+
}
|
|
244
|
+
for (const key of allKeys) {
|
|
245
|
+
const entry = await this.safeGet(kv, key);
|
|
246
|
+
// Treat both legitimate misses and fetch failures as "skip
|
|
247
|
+
// this bucket" — purge is a best-effort sweep.
|
|
248
|
+
if (!entry || entry === "fetch-failed")
|
|
249
|
+
continue;
|
|
250
|
+
const current = this.parseBucket(entry);
|
|
251
|
+
const active = current.leases.filter((l) => l.expiresAt > now);
|
|
252
|
+
const expired = current.leases.length - active.length;
|
|
253
|
+
if (expired === 0)
|
|
254
|
+
continue;
|
|
255
|
+
if (active.length === 0) {
|
|
256
|
+
try {
|
|
257
|
+
await kv.delete(key);
|
|
258
|
+
purged += expired;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// best-effort
|
|
262
|
+
}
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
await kv.update(key, JSON.stringify({ leases: active }), entry.revision);
|
|
267
|
+
purged += expired;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// CAS conflict — leave for next sweep.
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return purged;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* PR 2 A6 — distinguishes legitimate "key not found" from "broker
|
|
277
|
+
* unreachable / non-NotFound error". Returns:
|
|
278
|
+
* - `NatsKvEntry` on a successful fetch.
|
|
279
|
+
* - `null` when the key doesn't exist (NotFound code or null entry).
|
|
280
|
+
* - `"fetch-failed"` for any other error (transient broker outage,
|
|
281
|
+
* auth failure, network blip, etc.) so the OCC loop can fail-fast
|
|
282
|
+
* instead of spinning 10× before fail-closing.
|
|
283
|
+
*/
|
|
284
|
+
async safeGet(kv, key) {
|
|
285
|
+
try {
|
|
286
|
+
const e = await kv.get(key);
|
|
287
|
+
return e ?? null;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
// NATS surfaces "not found" via a code. Different `nats`
|
|
291
|
+
// package versions use different shapes; cover the common ones.
|
|
292
|
+
const code = err.code;
|
|
293
|
+
if (code === "NotFound" || code === "404")
|
|
294
|
+
return null;
|
|
295
|
+
return "fetch-failed";
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
parseBucket(entry) {
|
|
299
|
+
try {
|
|
300
|
+
const parsed = JSON.parse(entry.string());
|
|
301
|
+
if (!parsed || !Array.isArray(parsed.leases))
|
|
302
|
+
return { leases: [] };
|
|
303
|
+
return parsed;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return { leases: [] };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
//# sourceMappingURL=NatsKvConcurrencyBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NatsKvConcurrencyBackend.js","sourceRoot":"","sources":["../../src/concurrency/NatsKvConcurrencyBackend.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PR 1-5 polish · thrown by `TriggerBase.run()` when a queued run's
|
|
3
|
+
* `concurrencyQueueTimeoutMs` (PR 5 B2) has elapsed before the gate granted
|
|
4
|
+
* a slot.
|
|
5
|
+
*
|
|
6
|
+
* Distinct from `ConcurrencyLimitError`: a TTL-expired queued run will NEVER
|
|
7
|
+
* succeed (the timer won't re-fire). Conflating it with a transient denial
|
|
8
|
+
* misleads HTTP clients into retrying — the queued run is permanently dead,
|
|
9
|
+
* so the only correct response is `410 Gone`.
|
|
10
|
+
*
|
|
11
|
+
* Triggers catch this and translate:
|
|
12
|
+
* - HTTP trigger → `410 Gone` with structured body (no `Retry-After` — it
|
|
13
|
+
* would contradict the 410 contract).
|
|
14
|
+
* - Worker trigger → ACK without retry (the in-process scheduler owns the
|
|
15
|
+
* eventual dispatch and won't reschedule an expired run).
|
|
16
|
+
*
|
|
17
|
+
* The run record itself is already flipped to `expired` by
|
|
18
|
+
* `tracker.markRunExpired` before this error is thrown, so observability
|
|
19
|
+
* surfaces (Studio status badge, `RUN_EXPIRED` event) are independent of
|
|
20
|
+
* the transport-level response.
|
|
21
|
+
*/
|
|
22
|
+
export interface QueueExpiredInfo {
|
|
23
|
+
/** Workflow name whose queue TTL elapsed. */
|
|
24
|
+
workflowName: string;
|
|
25
|
+
/** Resolved key value (after evaluating the `concurrencyKey` expression). */
|
|
26
|
+
concurrencyKey: string;
|
|
27
|
+
/**
|
|
28
|
+
* The deadline that was breached (ms since epoch). The run was queued
|
|
29
|
+
* with `expiresAt = scheduledAt + concurrencyQueueTimeoutMs` and the
|
|
30
|
+
* dispatcher observed `now > expiresAt`.
|
|
31
|
+
*/
|
|
32
|
+
queueExpiredAt: number;
|
|
33
|
+
/** Run id allocated by the tracer; flipped to `expired` before throw. */
|
|
34
|
+
runId: string;
|
|
35
|
+
}
|
|
36
|
+
export declare class QueueExpiredError extends Error {
|
|
37
|
+
readonly info: QueueExpiredInfo;
|
|
38
|
+
constructor(info: QueueExpiredInfo);
|
|
39
|
+
}
|
|
40
|
+
export declare function isQueueExpiredError(err: unknown): err is QueueExpiredError;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export class QueueExpiredError extends Error {
|
|
2
|
+
info;
|
|
3
|
+
constructor(info) {
|
|
4
|
+
super(`Queued run expired for workflow '${info.workflowName}' (key='${info.concurrencyKey}', ` +
|
|
5
|
+
`queueExpiredAt=${info.queueExpiredAt}). Run will not be retried.`);
|
|
6
|
+
this.name = "QueueExpiredError";
|
|
7
|
+
this.info = info;
|
|
8
|
+
// Restore prototype chain when extending Error in transpiled code.
|
|
9
|
+
Object.setPrototypeOf(this, QueueExpiredError.prototype);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isQueueExpiredError(err) {
|
|
13
|
+
return err instanceof QueueExpiredError;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=QueueExpiredError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueueExpiredError.js","sourceRoot":"","sources":["../../src/concurrency/QueueExpiredError.ts"],"names":[],"mappings":"AAoCA,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC3B,IAAI,CAAmB;IAEvC,YAAY,IAAsB;QACjC,KAAK,CACJ,oCAAoC,IAAI,CAAC,YAAY,WAAW,IAAI,CAAC,cAAc,KAAK;YACvF,kBAAkB,IAAI,CAAC,cAAc,6BAA6B,CACnE,CAAC;QACF,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,mEAAmE;QACnE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1D,CAAC;CACD;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC/C,OAAO,GAAG,YAAY,iBAAiB,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier C #4 follow-up · Redis-backed concurrency backend.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates per-(workflow, concurrencyKey) lease state across processes
|
|
5
|
+
* via a single Redis key per bucket. Atomicity comes from server-side Lua
|
|
6
|
+
* scripts — `EVAL` runs single-threaded against the keyspace, so the
|
|
7
|
+
* read → filter → check-limit → write sequence is a single round-trip
|
|
8
|
+
* with no OCC retry loop (the headline win over the NATS KV backend's
|
|
9
|
+
* `WATCH`/`MULTI`/`EXEC`-style optimistic concurrency).
|
|
10
|
+
*
|
|
11
|
+
* Storage model: one Redis string key per `(workflowName, concurrencyKey)`
|
|
12
|
+
* bucket. Value is a JSON-encoded `{leases: [{runId, expiresAt}]}`
|
|
13
|
+
* document. Bounded-cardinality assumption identical to NATS KV — typical
|
|
14
|
+
* concurrency keys hold 1-50 active leases.
|
|
15
|
+
*
|
|
16
|
+
* Lease leak: each lease carries an `expiresAt`. Expired leases are
|
|
17
|
+
* lazy-purged inside the Lua script that observes them; an explicit
|
|
18
|
+
* `purgeExpired` SCAN sweep is also exposed for janitor use.
|
|
19
|
+
*
|
|
20
|
+
* Connection: ioredis is loaded via dynamic `import("ioredis")` so the
|
|
21
|
+
* dependency stays optional. Matches the existing pattern used by
|
|
22
|
+
* `triggers/worker`'s `RedisStreamsAdapter` and
|
|
23
|
+
* `triggers/pubsub`'s `RedisStreamsPubSubAdapter`.
|
|
24
|
+
*/
|
|
25
|
+
import type { ConcurrencySlotResult } from "../tracing/types";
|
|
26
|
+
import type { ConcurrencyBackend } from "./ConcurrencyBackend";
|
|
27
|
+
export interface RedisConcurrencyConfig {
|
|
28
|
+
/** Full Redis connection URL (e.g. `redis://[user:pass@]host:port[/db]`). Takes precedence over host/port. */
|
|
29
|
+
url?: string;
|
|
30
|
+
host?: string;
|
|
31
|
+
port?: number;
|
|
32
|
+
password?: string;
|
|
33
|
+
username?: string;
|
|
34
|
+
db?: number;
|
|
35
|
+
tls?: boolean;
|
|
36
|
+
/** Namespace prefix for every Redis key the backend touches. */
|
|
37
|
+
keyPrefix: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read configuration from environment variables. Used by
|
|
41
|
+
* {@link createConcurrencyBackend} when the operator opts into Redis.
|
|
42
|
+
*/
|
|
43
|
+
export declare function readRedisConfigFromEnv(): RedisConcurrencyConfig;
|
|
44
|
+
export declare class RedisConcurrencyBackend implements ConcurrencyBackend {
|
|
45
|
+
readonly name = "redis";
|
|
46
|
+
private client;
|
|
47
|
+
private readonly config;
|
|
48
|
+
private connected;
|
|
49
|
+
constructor(config?: Partial<RedisConcurrencyConfig>);
|
|
50
|
+
connect(): Promise<void>;
|
|
51
|
+
disconnect(): Promise<void>;
|
|
52
|
+
private bucketKey;
|
|
53
|
+
private encodeSegment;
|
|
54
|
+
private requireClient;
|
|
55
|
+
acquireSlot(workflowName: string, concurrencyKey: string, concurrencyLimit: number, runId: string, leaseExpiresAt: number): Promise<ConcurrencySlotResult>;
|
|
56
|
+
releaseSlot(workflowName: string, concurrencyKey: string, runId: string): Promise<void>;
|
|
57
|
+
purgeExpired(now: number): Promise<number>;
|
|
58
|
+
/**
|
|
59
|
+
* Decode the `{acquired, currentInFlight}` pair from a Lua eval result.
|
|
60
|
+
* ioredis returns Redis arrays as plain JS arrays of (string | number)
|
|
61
|
+
* — the script returns integers, so both elements should be numbers.
|
|
62
|
+
*/
|
|
63
|
+
private parsePair;
|
|
64
|
+
}
|