nexus-ai-pro 2.0.0 → 2.2.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/API_STABILITY.md +65 -0
- package/CHANGELOG.md +267 -1
- package/MIGRATING.md +1 -1
- package/README.md +17 -8
- package/dist/agent/create-agent.d.ts +9 -1
- package/dist/agent/create-agent.js +40 -1
- package/dist/cache/memory-cache.d.ts +8 -0
- package/dist/cache/memory-cache.js +9 -0
- package/dist/cache/semantic-cache.d.ts +6 -3
- package/dist/cache/semantic-cache.js +5 -2
- package/dist/cli/args.js +3 -0
- package/dist/cli/db.d.ts +16 -4
- package/dist/cli/db.js +169 -22
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +53 -0
- package/dist/cli/graph.d.ts +9 -0
- package/dist/cli/graph.js +47 -0
- package/dist/cli.js +21 -2
- package/dist/core/lifecycle.js +11 -6
- package/dist/core/nexus.d.ts +12 -2
- package/dist/core/nexus.js +106 -23
- package/dist/doctor/index.d.ts +90 -0
- package/dist/doctor/index.js +237 -0
- package/dist/evals/judge.d.ts +11 -0
- package/dist/evals/judge.js +33 -0
- package/dist/evaluate/index.d.ts +1 -0
- package/dist/evaluate/index.js +1 -0
- package/dist/evaluate/provenance.d.ts +22 -0
- package/dist/evaluate/provenance.js +10 -0
- package/dist/evaluate/run.js +20 -4
- package/dist/graph/durability.d.ts +27 -0
- package/dist/graph/durability.js +66 -0
- package/dist/graph/errors.d.ts +30 -4
- package/dist/graph/errors.js +18 -2
- package/dist/graph/event-stream.d.ts +75 -0
- package/dist/graph/event-stream.js +289 -0
- package/dist/graph/functional.d.ts +80 -7
- package/dist/graph/functional.js +138 -35
- package/dist/graph/graph.d.ts +56 -8
- package/dist/graph/graph.js +331 -49
- package/dist/graph/index.d.ts +4 -2
- package/dist/graph/index.js +3 -1
- package/dist/graph/lint.d.ts +57 -0
- package/dist/graph/lint.js +141 -0
- package/dist/graph/run-control.d.ts +28 -0
- package/dist/graph/run-control.js +26 -0
- package/dist/images/evals.js +4 -2
- package/dist/operations/adapters.d.ts +52 -4
- package/dist/operations/adapters.js +194 -7
- package/dist/operations/runner.js +14 -15
- package/dist/operations/stats.js +8 -1
- package/dist/operations/store.d.ts +5 -1
- package/dist/operations/store.js +7 -0
- package/dist/ops/circuit-breaker.d.ts +52 -0
- package/dist/ops/circuit-breaker.js +4 -0
- package/dist/ops/circuit-store.d.ts +13 -2
- package/dist/ops/circuit-store.js +68 -0
- package/dist/ops/circuit-sync.d.ts +17 -0
- package/dist/ops/circuit-sync.js +86 -0
- package/dist/ops/health.d.ts +24 -0
- package/dist/ops/health.js +38 -10
- package/dist/ops/index.d.ts +1 -1
- package/dist/ops/index.js +1 -1
- package/dist/ops/rate-limit-adapters.d.ts +23 -0
- package/dist/ops/rate-limit-adapters.js +34 -0
- package/dist/ops/rate-limiter.d.ts +14 -2
- package/dist/ops/rate-limiter.js +31 -0
- package/dist/postgres/circuits.d.ts +21 -4
- package/dist/postgres/circuits.js +65 -4
- package/dist/postgres/client.d.ts +0 -2
- package/dist/postgres/client.js +0 -4
- package/dist/postgres/evaluate.d.ts +16 -4
- package/dist/postgres/evaluate.js +17 -5
- package/dist/postgres/index.d.ts +32 -11
- package/dist/postgres/index.js +65 -31
- package/dist/postgres/migrations.d.ts +60 -0
- package/dist/postgres/migrations.js +121 -0
- package/dist/postgres/operations.d.ts +14 -3
- package/dist/postgres/operations.js +45 -15
- package/dist/postgres/prompts.d.ts +11 -2
- package/dist/postgres/prompts.js +14 -3
- package/dist/postgres/rollups.d.ts +35 -0
- package/dist/postgres/rollups.js +129 -0
- package/dist/postgres/store.d.ts +11 -2
- package/dist/postgres/store.js +14 -3
- package/dist/postgres/traces.d.ts +13 -3
- package/dist/postgres/traces.js +25 -3
- package/dist/postgres/vectors.d.ts +11 -2
- package/dist/postgres/vectors.js +14 -3
- package/dist/router/auto-router.d.ts +34 -7
- package/dist/router/auto-router.js +201 -68
- package/dist/server/assistant.d.ts +44 -0
- package/dist/server/assistant.js +39 -0
- package/dist/server/errors.d.ts +12 -0
- package/dist/server/errors.js +7 -0
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/runs.d.ts +28 -1
- package/dist/server/runs.js +110 -14
- package/dist/sqlite/index.d.ts +16 -3
- package/dist/sqlite/index.js +16 -3
- package/dist/sqlite/migrations.d.ts +26 -0
- package/dist/sqlite/migrations.js +63 -0
- package/dist/sqlite/operations.d.ts +14 -3
- package/dist/sqlite/operations.js +42 -15
- package/dist/sqlite/store.d.ts +9 -2
- package/dist/sqlite/store.js +13 -3
- package/dist/sqlite/vectors.d.ts +11 -2
- package/dist/sqlite/vectors.js +13 -2
- package/dist/store/tenant.d.ts +12 -0
- package/dist/store/tenant.js +29 -0
- package/dist/tenancy/index.d.ts +113 -0
- package/dist/tenancy/index.js +233 -0
- package/dist/testing/record.d.ts +42 -0
- package/dist/testing/record.js +57 -5
- package/dist/tracing/index.d.ts +3 -3
- package/dist/tracing/index.js +2 -2
- package/dist/tracing/instrument.js +30 -0
- package/dist/tracing/otlp.d.ts +91 -0
- package/dist/tracing/otlp.js +124 -0
- package/dist/tracing/rollups.d.ts +100 -0
- package/dist/tracing/rollups.js +139 -0
- package/dist/tracing/stores.d.ts +17 -0
- package/dist/tracing/stores.js +23 -0
- package/dist/tracing/tracer.d.ts +50 -4
- package/dist/tracing/tracer.js +85 -6
- package/dist/types/config.d.ts +33 -2
- package/dist/types/evaluate.d.ts +37 -0
- package/dist/types/graph.d.ts +354 -9
- package/dist/types/graph.js +3 -1
- package/dist/types/messages.d.ts +5 -0
- package/dist/types/operations.d.ts +22 -2
- package/dist/types/response.d.ts +20 -0
- package/dist/types/server.d.ts +18 -0
- package/dist/types/tracing.d.ts +11 -0
- package/dist/utils/schema-migrations.d.ts +97 -0
- package/dist/utils/schema-migrations.js +91 -0
- package/dist/utils/signals.d.ts +17 -0
- package/dist/utils/signals.js +26 -0
- package/dist/utils/tenant.d.ts +14 -0
- package/dist/utils/tenant.js +16 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.js +1 -0
- package/dist-cjs/agent/create-agent.d.ts +9 -1
- package/dist-cjs/agent/create-agent.js +40 -1
- package/dist-cjs/cache/memory-cache.d.ts +8 -0
- package/dist-cjs/cache/memory-cache.js +9 -0
- package/dist-cjs/cache/semantic-cache.d.ts +6 -3
- package/dist-cjs/cache/semantic-cache.js +5 -2
- package/dist-cjs/cli/args.js +3 -0
- package/dist-cjs/cli/db.d.ts +16 -4
- package/dist-cjs/cli/db.js +200 -20
- package/dist-cjs/cli/doctor.d.ts +17 -0
- package/dist-cjs/cli/doctor.js +56 -0
- package/dist-cjs/cli/graph.d.ts +9 -0
- package/dist-cjs/cli/graph.js +50 -0
- package/dist-cjs/core/lifecycle.js +11 -6
- package/dist-cjs/core/nexus.d.ts +12 -2
- package/dist-cjs/core/nexus.js +106 -23
- package/dist-cjs/doctor/index.d.ts +90 -0
- package/dist-cjs/doctor/index.js +276 -0
- package/dist-cjs/evals/judge.d.ts +11 -0
- package/dist-cjs/evals/judge.js +33 -0
- package/dist-cjs/evaluate/index.d.ts +1 -0
- package/dist-cjs/evaluate/index.js +3 -1
- package/dist-cjs/evaluate/provenance.d.ts +22 -0
- package/dist-cjs/evaluate/provenance.js +14 -0
- package/dist-cjs/evaluate/run.js +20 -4
- package/dist-cjs/graph/durability.d.ts +27 -0
- package/dist-cjs/graph/durability.js +70 -0
- package/dist-cjs/graph/errors.d.ts +30 -4
- package/dist-cjs/graph/errors.js +20 -3
- package/dist-cjs/graph/event-stream.d.ts +75 -0
- package/dist-cjs/graph/event-stream.js +295 -0
- package/dist-cjs/graph/functional.d.ts +80 -7
- package/dist-cjs/graph/functional.js +137 -34
- package/dist-cjs/graph/graph.d.ts +56 -8
- package/dist-cjs/graph/graph.js +330 -48
- package/dist-cjs/graph/index.d.ts +4 -2
- package/dist-cjs/graph/index.js +7 -1
- package/dist-cjs/graph/lint.d.ts +57 -0
- package/dist-cjs/graph/lint.js +144 -0
- package/dist-cjs/graph/run-control.d.ts +28 -0
- package/dist-cjs/graph/run-control.js +30 -0
- package/dist-cjs/images/evals.js +4 -2
- package/dist-cjs/operations/adapters.d.ts +52 -4
- package/dist-cjs/operations/adapters.js +193 -6
- package/dist-cjs/operations/runner.js +14 -15
- package/dist-cjs/operations/stats.js +8 -1
- package/dist-cjs/operations/store.d.ts +5 -1
- package/dist-cjs/operations/store.js +7 -0
- package/dist-cjs/ops/circuit-breaker.d.ts +52 -0
- package/dist-cjs/ops/circuit-breaker.js +4 -0
- package/dist-cjs/ops/circuit-store.d.ts +13 -2
- package/dist-cjs/ops/circuit-store.js +68 -0
- package/dist-cjs/ops/circuit-sync.d.ts +17 -0
- package/dist-cjs/ops/circuit-sync.js +87 -0
- package/dist-cjs/ops/health.d.ts +24 -0
- package/dist-cjs/ops/health.js +38 -10
- package/dist-cjs/ops/index.d.ts +1 -1
- package/dist-cjs/ops/rate-limit-adapters.d.ts +23 -0
- package/dist-cjs/ops/rate-limit-adapters.js +35 -1
- package/dist-cjs/ops/rate-limiter.d.ts +14 -2
- package/dist-cjs/ops/rate-limiter.js +32 -0
- package/dist-cjs/postgres/circuits.d.ts +21 -4
- package/dist-cjs/postgres/circuits.js +98 -3
- package/dist-cjs/postgres/client.d.ts +0 -2
- package/dist-cjs/postgres/client.js +0 -5
- package/dist-cjs/postgres/evaluate.d.ts +16 -4
- package/dist-cjs/postgres/evaluate.js +50 -4
- package/dist-cjs/postgres/index.d.ts +32 -11
- package/dist-cjs/postgres/index.js +69 -20
- package/dist-cjs/postgres/migrations.d.ts +60 -0
- package/dist-cjs/postgres/migrations.js +129 -0
- package/dist-cjs/postgres/operations.d.ts +14 -3
- package/dist-cjs/postgres/operations.js +78 -14
- package/dist-cjs/postgres/prompts.d.ts +11 -2
- package/dist-cjs/postgres/prompts.js +47 -2
- package/dist-cjs/postgres/rollups.d.ts +35 -0
- package/dist-cjs/postgres/rollups.js +168 -0
- package/dist-cjs/postgres/store.d.ts +11 -2
- package/dist-cjs/postgres/store.js +47 -2
- package/dist-cjs/postgres/traces.d.ts +13 -3
- package/dist-cjs/postgres/traces.js +58 -2
- package/dist-cjs/postgres/vectors.d.ts +11 -2
- package/dist-cjs/postgres/vectors.js +47 -2
- package/dist-cjs/router/auto-router.d.ts +34 -7
- package/dist-cjs/router/auto-router.js +201 -68
- package/dist-cjs/server/assistant.d.ts +44 -0
- package/dist-cjs/server/assistant.js +39 -0
- package/dist-cjs/server/errors.d.ts +12 -0
- package/dist-cjs/server/errors.js +9 -1
- package/dist-cjs/server/index.d.ts +1 -1
- package/dist-cjs/server/index.js +2 -1
- package/dist-cjs/server/runs.d.ts +28 -1
- package/dist-cjs/server/runs.js +109 -13
- package/dist-cjs/sqlite/index.d.ts +16 -3
- package/dist-cjs/sqlite/index.js +31 -10
- package/dist-cjs/sqlite/migrations.d.ts +26 -0
- package/dist-cjs/sqlite/migrations.js +71 -0
- package/dist-cjs/sqlite/operations.d.ts +14 -3
- package/dist-cjs/sqlite/operations.js +76 -15
- package/dist-cjs/sqlite/store.d.ts +9 -2
- package/dist-cjs/sqlite/store.js +47 -3
- package/dist-cjs/sqlite/vectors.d.ts +11 -2
- package/dist-cjs/sqlite/vectors.js +47 -2
- package/dist-cjs/store/tenant.d.ts +12 -0
- package/dist-cjs/store/tenant.js +33 -0
- package/dist-cjs/tenancy/index.d.ts +113 -0
- package/dist-cjs/tenancy/index.js +250 -0
- package/dist-cjs/testing/record.d.ts +42 -0
- package/dist-cjs/testing/record.js +57 -5
- package/dist-cjs/tracing/index.d.ts +3 -3
- package/dist-cjs/tracing/index.js +4 -1
- package/dist-cjs/tracing/instrument.js +30 -0
- package/dist-cjs/tracing/otlp.d.ts +91 -0
- package/dist-cjs/tracing/otlp.js +129 -0
- package/dist-cjs/tracing/rollups.d.ts +100 -0
- package/dist-cjs/tracing/rollups.js +147 -0
- package/dist-cjs/tracing/stores.d.ts +17 -0
- package/dist-cjs/tracing/stores.js +24 -0
- package/dist-cjs/tracing/tracer.d.ts +50 -4
- package/dist-cjs/tracing/tracer.js +87 -6
- package/dist-cjs/types/config.d.ts +33 -2
- package/dist-cjs/types/evaluate.d.ts +37 -0
- package/dist-cjs/types/graph.d.ts +354 -9
- package/dist-cjs/types/graph.js +3 -1
- package/dist-cjs/types/messages.d.ts +5 -0
- package/dist-cjs/types/operations.d.ts +22 -2
- package/dist-cjs/types/response.d.ts +20 -0
- package/dist-cjs/types/server.d.ts +18 -0
- package/dist-cjs/types/tracing.d.ts +11 -0
- package/dist-cjs/utils/schema-migrations.d.ts +97 -0
- package/dist-cjs/utils/schema-migrations.js +102 -0
- package/dist-cjs/utils/signals.d.ts +17 -0
- package/dist-cjs/utils/signals.js +29 -0
- package/dist-cjs/utils/tenant.d.ts +14 -0
- package/dist-cjs/utils/tenant.js +22 -0
- package/dist-cjs/version.d.ts +5 -0
- package/dist-cjs/version.js +4 -0
- package/llms.txt +4 -3
- package/package.json +110 -2
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.BullMQOperationDispatcher = exports.RedisOperationStore = void 0;
|
|
4
4
|
const operations_js_1 = require("../types/operations.js");
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
5
6
|
const serialization_js_1 = require("./serialization.js");
|
|
6
7
|
const stats_js_1 = require("./stats.js");
|
|
7
8
|
const CAS_SCRIPT = `
|
|
@@ -12,21 +13,102 @@ if tonumber(decoded.sequence) ~= tonumber(ARGV[2]) then return 0 end
|
|
|
12
13
|
redis.call('HSET', KEYS[1], ARGV[1], ARGV[3])
|
|
13
14
|
return 1
|
|
14
15
|
`;
|
|
16
|
+
const PLACE_LUA = `
|
|
17
|
+
local function place(key, score, id)
|
|
18
|
+
if score ~= '' then redis.call('ZADD', key, score, id) else redis.call('ZREM', key, id) end
|
|
19
|
+
end
|
|
20
|
+
`;
|
|
21
|
+
const UNIQUE_KEY_LUA = `
|
|
22
|
+
if ARGV[3] ~= '' then
|
|
23
|
+
local owner = redis.call('HGET', KEYS[2], ARGV[3])
|
|
24
|
+
if owner and owner ~= ARGV[1] and redis.call('HEXISTS', KEYS[1], owner) == 1 then return 0 end
|
|
25
|
+
end
|
|
26
|
+
`;
|
|
27
|
+
const CREATE_SCRIPT = `${UNIQUE_KEY_LUA}
|
|
28
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
29
|
+
if ARGV[3] ~= '' then redis.call('HSET', KEYS[2], ARGV[3], ARGV[1]) end
|
|
30
|
+
return 1
|
|
31
|
+
`;
|
|
32
|
+
const INDEXED_CREATE_SCRIPT = `${PLACE_LUA}${UNIQUE_KEY_LUA}
|
|
33
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
34
|
+
if ARGV[3] ~= '' then redis.call('HSET', KEYS[2], ARGV[3], ARGV[1]) end
|
|
35
|
+
place(KEYS[3], ARGV[4], ARGV[1])
|
|
36
|
+
place(KEYS[4], ARGV[5], ARGV[1])
|
|
37
|
+
return 1
|
|
38
|
+
`;
|
|
39
|
+
const INDEXED_CAS_SCRIPT = `${PLACE_LUA}
|
|
40
|
+
local current = redis.call('HGET', KEYS[1], ARGV[1])
|
|
41
|
+
if current == false then return 0 end
|
|
42
|
+
local decoded = cjson.decode(current)
|
|
43
|
+
if tonumber(decoded.sequence) ~= tonumber(ARGV[2]) then return 0 end
|
|
44
|
+
redis.call('HSET', KEYS[1], ARGV[1], ARGV[3])
|
|
45
|
+
place(KEYS[2], ARGV[4], ARGV[1])
|
|
46
|
+
place(KEYS[3], ARGV[5], ARGV[1])
|
|
47
|
+
return 1
|
|
48
|
+
`;
|
|
49
|
+
const PRUNE_SCRIPT = `
|
|
50
|
+
local terminal = { ${operations_js_1.TERMINAL_OPERATION_STATUSES.map((status) => `${status} = true`).join(', ')} }
|
|
51
|
+
local removed = 0
|
|
52
|
+
for i = 2, #ARGV do
|
|
53
|
+
local raw = redis.call('HGET', KEYS[1], ARGV[i])
|
|
54
|
+
local keep = false
|
|
55
|
+
if raw then
|
|
56
|
+
local record = cjson.decode(raw)
|
|
57
|
+
if ARGV[1] == 'queued' then
|
|
58
|
+
keep = record.status == 'queued'
|
|
59
|
+
else
|
|
60
|
+
keep = (not terminal[record.status]) and (record.lease ~= nil or record.status == 'running')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
if not keep then
|
|
64
|
+
redis.call('ZREM', KEYS[2], ARGV[i])
|
|
65
|
+
removed = removed + 1
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
return removed
|
|
69
|
+
`;
|
|
15
70
|
class RedisOperationStore {
|
|
16
71
|
client;
|
|
17
72
|
prefix;
|
|
18
73
|
useEval;
|
|
74
|
+
indexed;
|
|
19
75
|
constructor(client, options = {}) {
|
|
20
76
|
this.client = client;
|
|
21
77
|
this.prefix = options.prefix ?? 'nexus-ai-pro:operations:';
|
|
22
78
|
this.useEval = options.useEval !== false && typeof client.eval === 'function';
|
|
79
|
+
this.indexed = options.index === true;
|
|
80
|
+
if (this.indexed && !(client.zadd && client.zrem && client.zrangebyscore)) {
|
|
81
|
+
throw new TypeError('A Redis operation store with index: true needs zadd, zrem, and zrangebyscore');
|
|
82
|
+
}
|
|
23
83
|
}
|
|
24
84
|
async create(record) {
|
|
25
85
|
(0, serialization_js_1.assertSerializableRecord)(record);
|
|
26
|
-
|
|
86
|
+
const encoded = JSON.stringify(record);
|
|
87
|
+
const duplicate = () => new errors_js_1.OperationDuplicateError(record.idempotencyKey);
|
|
88
|
+
if (this.useEval && this.client.eval && !this.indexed) {
|
|
89
|
+
const created = await this.client.eval(CREATE_SCRIPT, 2, this.recordsKey(), this.idempotencyKey(), record.id, encoded, record.idempotencyKey ?? '');
|
|
90
|
+
if (Number(created) !== 1)
|
|
91
|
+
throw duplicate();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.indexed && this.useEval && this.client.eval) {
|
|
95
|
+
const { queued, lease } = indexScores(record);
|
|
96
|
+
const created = await this.client.eval(INDEXED_CREATE_SCRIPT, 4, this.recordsKey(), this.idempotencyKey(), this.queuedKey(), this.leasesKey(), record.id, encoded, record.idempotencyKey ?? '', queued, lease);
|
|
97
|
+
if (Number(created) !== 1)
|
|
98
|
+
throw duplicate();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (record.idempotencyKey) {
|
|
102
|
+
const owner = await this.client.hget(this.idempotencyKey(), record.idempotencyKey);
|
|
103
|
+
if (owner && owner !== record.id && (await this.client.hget(this.recordsKey(), owner)))
|
|
104
|
+
throw duplicate();
|
|
105
|
+
}
|
|
106
|
+
await this.client.hset(this.recordsKey(), record.id, encoded);
|
|
27
107
|
if (record.idempotencyKey) {
|
|
28
108
|
await this.client.hset(this.idempotencyKey(), record.idempotencyKey, record.id);
|
|
29
109
|
}
|
|
110
|
+
if (this.indexed)
|
|
111
|
+
await this.place(record);
|
|
30
112
|
}
|
|
31
113
|
async read(id) {
|
|
32
114
|
const raw = await this.client.hget(this.recordsKey(), id);
|
|
@@ -36,6 +118,11 @@ class RedisOperationStore {
|
|
|
36
118
|
(0, serialization_js_1.assertSerializableRecord)(record);
|
|
37
119
|
const encoded = JSON.stringify(record);
|
|
38
120
|
if (this.useEval && this.client.eval) {
|
|
121
|
+
if (this.indexed) {
|
|
122
|
+
const { queued, lease } = indexScores(record);
|
|
123
|
+
const result = await this.client.eval(INDEXED_CAS_SCRIPT, 3, this.recordsKey(), this.queuedKey(), this.leasesKey(), record.id, String(expectedSequence), encoded, queued, lease);
|
|
124
|
+
return Number(result) === 1;
|
|
125
|
+
}
|
|
39
126
|
const result = await this.client.eval(CAS_SCRIPT, 1, this.recordsKey(), record.id, String(expectedSequence), encoded);
|
|
40
127
|
return Number(result) === 1;
|
|
41
128
|
}
|
|
@@ -43,6 +130,8 @@ class RedisOperationStore {
|
|
|
43
130
|
if (!current || current.sequence !== expectedSequence)
|
|
44
131
|
return false;
|
|
45
132
|
await this.client.hset(this.recordsKey(), record.id, encoded);
|
|
133
|
+
if (this.indexed)
|
|
134
|
+
await this.place(record);
|
|
46
135
|
return true;
|
|
47
136
|
}
|
|
48
137
|
async delete(id) {
|
|
@@ -50,19 +139,22 @@ class RedisOperationStore {
|
|
|
50
139
|
if (record?.idempotencyKey)
|
|
51
140
|
await this.client.hdel(this.idempotencyKey(), record.idempotencyKey);
|
|
52
141
|
await this.client.hdel(this.recordsKey(), id);
|
|
142
|
+
if (this.indexed) {
|
|
143
|
+
await this.client.zrem?.(this.queuedKey(), id);
|
|
144
|
+
await this.client.zrem?.(this.leasesKey(), id);
|
|
145
|
+
}
|
|
53
146
|
return record !== undefined;
|
|
54
147
|
}
|
|
55
148
|
async claimExpired(now, limit) {
|
|
149
|
+
if (this.indexed) {
|
|
150
|
+
return this.scanIndex('leases', Date.parse(now), limit, (record) => isExpired(record, now), isLeased);
|
|
151
|
+
}
|
|
56
152
|
const records = await this.list();
|
|
57
153
|
const expired = [];
|
|
58
154
|
for (const record of records) {
|
|
59
155
|
if (expired.length >= limit)
|
|
60
156
|
break;
|
|
61
|
-
if ((
|
|
62
|
-
continue;
|
|
63
|
-
if (record.lease && record.lease.expiresAt > now)
|
|
64
|
-
continue;
|
|
65
|
-
if (record.status === 'running' || record.lease)
|
|
157
|
+
if (isExpired(record, now))
|
|
66
158
|
expired.push(record);
|
|
67
159
|
}
|
|
68
160
|
return expired;
|
|
@@ -77,6 +169,9 @@ class RedisOperationStore {
|
|
|
77
169
|
}
|
|
78
170
|
async listQueued(limit, filter) {
|
|
79
171
|
const now = new Date().toISOString();
|
|
172
|
+
if (this.indexed) {
|
|
173
|
+
return this.scanIndex('queued', Number.POSITIVE_INFINITY, limit, (record) => (0, stats_js_1.isUnheldQueued)(record, now) && (0, stats_js_1.matchesFilter)(record, filter), (record) => record.status === 'queued');
|
|
174
|
+
}
|
|
80
175
|
return (await this.list())
|
|
81
176
|
.filter((record) => (0, stats_js_1.isUnheldQueued)(record, now) && (0, stats_js_1.matchesFilter)(record, filter))
|
|
82
177
|
.sort((a, b) => (a.createdAt < b.createdAt ? -1 : a.createdAt > b.createdAt ? 1 : 0))
|
|
@@ -85,14 +180,106 @@ class RedisOperationStore {
|
|
|
85
180
|
async stats(now, filter) {
|
|
86
181
|
return (0, stats_js_1.countRecords)(await this.list(), now, filter);
|
|
87
182
|
}
|
|
183
|
+
async reindex() {
|
|
184
|
+
if (!this.indexed)
|
|
185
|
+
throw new TypeError('reindex() needs a store created with index: true');
|
|
186
|
+
const records = await this.list();
|
|
187
|
+
for (const record of records) {
|
|
188
|
+
const { queued, lease } = indexScores(record);
|
|
189
|
+
if (queued !== '')
|
|
190
|
+
await this.client.zadd?.(this.queuedKey(), Number(queued), record.id);
|
|
191
|
+
if (lease !== '')
|
|
192
|
+
await this.client.zadd?.(this.leasesKey(), Number(lease), record.id);
|
|
193
|
+
}
|
|
194
|
+
await this.prune('queued', records.filter((record) => record.status !== 'queued').map((record) => record.id));
|
|
195
|
+
await this.prune('leases', records.filter((record) => !isLeased(record)).map((record) => record.id));
|
|
196
|
+
return records.length;
|
|
197
|
+
}
|
|
198
|
+
async scanIndex(index, max, limit, keep, belongs) {
|
|
199
|
+
const found = [];
|
|
200
|
+
if (limit <= 0)
|
|
201
|
+
return found;
|
|
202
|
+
const stale = [];
|
|
203
|
+
const key = index === 'queued' ? this.queuedKey() : this.leasesKey();
|
|
204
|
+
const upper = max === Number.POSITIVE_INFINITY ? '+inf' : max;
|
|
205
|
+
const page = Math.max(16, limit * 4);
|
|
206
|
+
for (let offset = 0; found.length < limit; offset += page) {
|
|
207
|
+
const ids = (await this.client.zrangebyscore?.(key, '-inf', upper, 'LIMIT', offset, page)) ?? [];
|
|
208
|
+
const records = await this.readMany(ids);
|
|
209
|
+
for (const [position, id] of ids.entries()) {
|
|
210
|
+
const record = records[position];
|
|
211
|
+
if (!record || !belongs(record)) {
|
|
212
|
+
stale.push(id);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (keep(record))
|
|
216
|
+
found.push(record);
|
|
217
|
+
if (found.length >= limit)
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
if (ids.length < page)
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
await this.prune(index, stale);
|
|
224
|
+
return found;
|
|
225
|
+
}
|
|
226
|
+
async readMany(ids) {
|
|
227
|
+
if (ids.length === 0)
|
|
228
|
+
return [];
|
|
229
|
+
const raws = this.client.hmget
|
|
230
|
+
? await this.client.hmget(this.recordsKey(), ...ids)
|
|
231
|
+
: await Promise.all(ids.map((id) => this.client.hget(this.recordsKey(), id)));
|
|
232
|
+
return raws.map((raw) => (raw ? JSON.parse(raw) : undefined));
|
|
233
|
+
}
|
|
234
|
+
async prune(index, ids) {
|
|
235
|
+
if (ids.length === 0 || !this.useEval || !this.client.eval)
|
|
236
|
+
return;
|
|
237
|
+
const key = index === 'queued' ? this.queuedKey() : this.leasesKey();
|
|
238
|
+
for (let start = 0; start < ids.length; start += 500) {
|
|
239
|
+
await this.client.eval(PRUNE_SCRIPT, 2, this.recordsKey(), key, index, ...ids.slice(start, start + 500));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async place(record) {
|
|
243
|
+
const { queued, lease } = indexScores(record);
|
|
244
|
+
if (queued !== '')
|
|
245
|
+
await this.client.zadd?.(this.queuedKey(), Number(queued), record.id);
|
|
246
|
+
else
|
|
247
|
+
await this.client.zrem?.(this.queuedKey(), record.id);
|
|
248
|
+
if (lease !== '')
|
|
249
|
+
await this.client.zadd?.(this.leasesKey(), Number(lease), record.id);
|
|
250
|
+
else
|
|
251
|
+
await this.client.zrem?.(this.leasesKey(), record.id);
|
|
252
|
+
}
|
|
88
253
|
recordsKey() {
|
|
89
254
|
return `${this.prefix}records`;
|
|
90
255
|
}
|
|
91
256
|
idempotencyKey() {
|
|
92
257
|
return `${this.prefix}idempotency`;
|
|
93
258
|
}
|
|
259
|
+
queuedKey() {
|
|
260
|
+
return `${this.prefix}queued`;
|
|
261
|
+
}
|
|
262
|
+
leasesKey() {
|
|
263
|
+
return `${this.prefix}leases`;
|
|
264
|
+
}
|
|
94
265
|
}
|
|
95
266
|
exports.RedisOperationStore = RedisOperationStore;
|
|
267
|
+
function isExpired(record, now) {
|
|
268
|
+
if ((0, operations_js_1.isTerminalOperationStatus)(record.status))
|
|
269
|
+
return false;
|
|
270
|
+
if (record.lease && record.lease.expiresAt > now)
|
|
271
|
+
return false;
|
|
272
|
+
return record.status === 'running' || record.lease !== undefined;
|
|
273
|
+
}
|
|
274
|
+
function isLeased(record) {
|
|
275
|
+
return !(0, operations_js_1.isTerminalOperationStatus)(record.status) && (record.lease !== undefined || record.status === 'running');
|
|
276
|
+
}
|
|
277
|
+
function indexScores(record) {
|
|
278
|
+
return {
|
|
279
|
+
queued: record.status === 'queued' ? String(Date.parse(record.createdAt)) : '',
|
|
280
|
+
lease: isLeased(record) ? String(record.lease ? Date.parse(record.lease.expiresAt) : 0) : '',
|
|
281
|
+
};
|
|
282
|
+
}
|
|
96
283
|
class BullMQOperationDispatcher {
|
|
97
284
|
queue;
|
|
98
285
|
options;
|
|
@@ -96,6 +96,7 @@ class OperationRunner {
|
|
|
96
96
|
expiresAt: options.expiresAt,
|
|
97
97
|
idempotencyKey: options.idempotencyKey,
|
|
98
98
|
kind: options.kind,
|
|
99
|
+
...(options.tenantId ? { tenantId: options.tenantId } : {}),
|
|
99
100
|
traceContext: options.traceContext,
|
|
100
101
|
metadata: options.metadata,
|
|
101
102
|
};
|
|
@@ -217,7 +218,7 @@ class OperationRunner {
|
|
|
217
218
|
const heartbeat = this.startHeartbeat(record, handle);
|
|
218
219
|
let outcome;
|
|
219
220
|
try {
|
|
220
|
-
const value = await executor(this.buildContext(record, handle));
|
|
221
|
+
const value = await executor(this.buildContext(record, handle, heartbeat.beat));
|
|
221
222
|
outcome = { ok: true, value };
|
|
222
223
|
}
|
|
223
224
|
catch (error) {
|
|
@@ -293,7 +294,13 @@ class OperationRunner {
|
|
|
293
294
|
startHeartbeat(record, handle) {
|
|
294
295
|
let latest = record;
|
|
295
296
|
let stopped = false;
|
|
296
|
-
|
|
297
|
+
let inFlight = Promise.resolve();
|
|
298
|
+
const beat = (details) => {
|
|
299
|
+
const next = inFlight.then(() => renew(details));
|
|
300
|
+
inFlight = next.catch(() => undefined);
|
|
301
|
+
return next;
|
|
302
|
+
};
|
|
303
|
+
const renew = async (details) => {
|
|
297
304
|
if (stopped)
|
|
298
305
|
return;
|
|
299
306
|
const current = await this.store.read(latest.id);
|
|
@@ -318,6 +325,8 @@ class OperationRunner {
|
|
|
318
325
|
heartbeatAt: renewed.updatedAt,
|
|
319
326
|
};
|
|
320
327
|
renewed.progress = handle.progress() ?? current.progress;
|
|
328
|
+
if (details !== undefined)
|
|
329
|
+
renewed.heartbeatDetails = details;
|
|
321
330
|
if (await this.store.update(renewed, current.sequence))
|
|
322
331
|
latest = renewed;
|
|
323
332
|
};
|
|
@@ -338,24 +347,14 @@ class OperationRunner {
|
|
|
338
347
|
beat,
|
|
339
348
|
};
|
|
340
349
|
}
|
|
341
|
-
buildContext(record, handle) {
|
|
350
|
+
buildContext(record, handle, beat) {
|
|
342
351
|
return {
|
|
343
352
|
operationId: record.id,
|
|
344
353
|
attempt: record.attempt,
|
|
345
354
|
signal: handle.signal,
|
|
346
355
|
report: (progress) => handle.report(progress),
|
|
347
|
-
heartbeat:
|
|
348
|
-
|
|
349
|
-
if (!current || (0, operations_js_1.isTerminalOperationStatus)(current.status))
|
|
350
|
-
return;
|
|
351
|
-
const renewed = this.advance(current, 'running');
|
|
352
|
-
renewed.lease = {
|
|
353
|
-
owner: this.owner,
|
|
354
|
-
expiresAt: new Date(this.now().getTime() + this.leaseMs).toISOString(),
|
|
355
|
-
heartbeatAt: renewed.updatedAt,
|
|
356
|
-
};
|
|
357
|
-
await this.store.update(renewed, current.sequence);
|
|
358
|
-
},
|
|
356
|
+
heartbeat: (details) => beat(details),
|
|
357
|
+
...(record.heartbeatDetails === undefined ? {} : { previousHeartbeat: record.heartbeatDetails }),
|
|
359
358
|
traceContext: record.traceContext,
|
|
360
359
|
metadata: record.metadata,
|
|
361
360
|
};
|
|
@@ -9,6 +9,8 @@ function isUnheldQueued(record, now) {
|
|
|
9
9
|
return record.status === 'queued' && (!record.lease || record.lease.expiresAt <= now);
|
|
10
10
|
}
|
|
11
11
|
function matchesFilter(record, filter) {
|
|
12
|
+
if (filter?.tenantId !== undefined && record.tenantId !== filter.tenantId)
|
|
13
|
+
return false;
|
|
12
14
|
return !filter?.kindPrefix || (record.kind ?? '').startsWith(filter.kindPrefix);
|
|
13
15
|
}
|
|
14
16
|
function countRecords(records, now, filter) {
|
|
@@ -29,7 +31,12 @@ function countRecords(records, now, filter) {
|
|
|
29
31
|
}
|
|
30
32
|
async function operationStats(store, options = {}) {
|
|
31
33
|
const now = (options.now ?? new Date()).toISOString();
|
|
32
|
-
const filter = options.kindPrefix
|
|
34
|
+
const filter = options.kindPrefix || options.tenantId !== undefined
|
|
35
|
+
? {
|
|
36
|
+
...(options.kindPrefix ? { kindPrefix: options.kindPrefix } : {}),
|
|
37
|
+
...(options.tenantId !== undefined ? { tenantId: options.tenantId } : {}),
|
|
38
|
+
}
|
|
39
|
+
: undefined;
|
|
33
40
|
if (store.stats)
|
|
34
41
|
return store.stats(now, filter);
|
|
35
42
|
if (store.list)
|
|
@@ -19,7 +19,11 @@ export declare class MemoryOperationStore<TResult = unknown> implements Operatio
|
|
|
19
19
|
private readonly byIdempotencyKey;
|
|
20
20
|
private readonly maxRecords;
|
|
21
21
|
constructor(options?: MemoryOperationStoreOptions);
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Stores a new record. Throws `OperationDuplicateError` when another record already holds its
|
|
24
|
+
* idempotency key, as every shared store does, so two submissions racing under one key cannot both
|
|
25
|
+
* start. Refuses records carrying raw bytes.
|
|
26
|
+
*/
|
|
23
27
|
create(record: OperationRecord<TResult>): void;
|
|
24
28
|
/** Reads a record. */
|
|
25
29
|
read(id: string): OperationRecord<TResult> | undefined;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemoryOperationStore = exports.operationStats = exports.assertSerializableRecord = void 0;
|
|
4
4
|
const operations_js_1 = require("../types/operations.js");
|
|
5
|
+
const errors_js_1 = require("./errors.js");
|
|
5
6
|
const serialization_js_1 = require("./serialization.js");
|
|
6
7
|
Object.defineProperty(exports, "assertSerializableRecord", { enumerable: true, get: function () { return serialization_js_1.assertSerializableRecord; } });
|
|
7
8
|
const stats_js_1 = require("./stats.js");
|
|
@@ -16,6 +17,12 @@ class MemoryOperationStore {
|
|
|
16
17
|
}
|
|
17
18
|
create(record) {
|
|
18
19
|
(0, serialization_js_1.assertSerializableRecord)(record);
|
|
20
|
+
if (record.idempotencyKey) {
|
|
21
|
+
const owner = this.byIdempotencyKey.get(record.idempotencyKey);
|
|
22
|
+
if (owner !== undefined && owner !== record.id && this.records.has(owner)) {
|
|
23
|
+
throw new errors_js_1.OperationDuplicateError(record.idempotencyKey);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
19
26
|
this.records.set(record.id, clone(record));
|
|
20
27
|
if (record.idempotencyKey)
|
|
21
28
|
this.byIdempotencyKey.set(record.idempotencyKey, record.id);
|
|
@@ -41,6 +41,15 @@ export interface CircuitBreakerConfig {
|
|
|
41
41
|
* `nexus-ai-pro/ops/circuit-store` and `nexus-ai-pro/postgres/circuits`.
|
|
42
42
|
*/
|
|
43
43
|
store?: CircuitStateStore;
|
|
44
|
+
/**
|
|
45
|
+
* Counts every worker's calls together, so nine failures spread over three workers count as nine.
|
|
46
|
+
* Each worker adds the calls it saw to a rolling window in the `store` when it syncs, and opens the
|
|
47
|
+
* circuit everywhere once the shared count crosses `failureThreshold` (failures since any worker's
|
|
48
|
+
* last success) or `failureRateThreshold`. Checks stay synchronous: a worker learns the shared count
|
|
49
|
+
* on its next sync, at most `syncIntervalMs` late. Needs a store with `observe()`; every bundled one
|
|
50
|
+
* has it. Off by default, when each worker counts only its own calls.
|
|
51
|
+
*/
|
|
52
|
+
shareObservations?: boolean;
|
|
44
53
|
/** Names this worker in shared state. Defaults to a random id. */
|
|
45
54
|
workerId?: string;
|
|
46
55
|
/** Shared state is refreshed at most this often, on the next check that needs it. Defaults to 1 second. */
|
|
@@ -85,6 +94,41 @@ export interface CircuitSnapshot {
|
|
|
85
94
|
retryAt?: string;
|
|
86
95
|
/** The most recent error, as text. */
|
|
87
96
|
lastError?: string;
|
|
97
|
+
/** Every worker's calls together, as of this worker's last sync, with `shareObservations`. */
|
|
98
|
+
shared?: CircuitWindow;
|
|
99
|
+
}
|
|
100
|
+
/** Calls one worker saw since it last reported, which `CircuitStateStore.observe()` adds to the shared window. */
|
|
101
|
+
export interface CircuitObservation {
|
|
102
|
+
/** Calls and failures per window bucket, by the bucket's start in epoch milliseconds. */
|
|
103
|
+
buckets: Array<{
|
|
104
|
+
at: number;
|
|
105
|
+
calls: number;
|
|
106
|
+
failures: number;
|
|
107
|
+
}>;
|
|
108
|
+
/** Whether any of the calls succeeded, which ends the shared failure streak before `trailingFailures` is added. */
|
|
109
|
+
succeeded: boolean;
|
|
110
|
+
/** Failures after the last success, or every failure when none succeeded. */
|
|
111
|
+
trailingFailures: number;
|
|
112
|
+
/** The rolling window, in milliseconds. */
|
|
113
|
+
windowMs: number;
|
|
114
|
+
/** The width of one bucket. A bucket that ended before `now - windowMs` is dropped. */
|
|
115
|
+
bucketMs: number;
|
|
116
|
+
/** Epoch milliseconds now, by the breaker's clock. */
|
|
117
|
+
now: number;
|
|
118
|
+
/**
|
|
119
|
+
* Epoch milliseconds of the circuit's last close. Buckets that started before it are not counted, so
|
|
120
|
+
* failures that opened a circuit do not reopen it the moment a probe closes it.
|
|
121
|
+
*/
|
|
122
|
+
since?: number;
|
|
123
|
+
}
|
|
124
|
+
/** A provider's calls across every worker, inside the rolling window. */
|
|
125
|
+
export interface CircuitWindow {
|
|
126
|
+
/** Calls counted. */
|
|
127
|
+
calls: number;
|
|
128
|
+
/** Of those, failures. */
|
|
129
|
+
failures: number;
|
|
130
|
+
/** Failures since any worker last reported a success. */
|
|
131
|
+
consecutiveFailures: number;
|
|
88
132
|
}
|
|
89
133
|
/** A circuit decision as every worker sees it. */
|
|
90
134
|
export interface SharedCircuitState {
|
|
@@ -119,6 +163,12 @@ export interface CircuitStateStore {
|
|
|
119
163
|
* recovering provider is probed once rather than by every replica at the same moment.
|
|
120
164
|
*/
|
|
121
165
|
claimProbe(providerName: string, owner: string, ttlMs: number): Promise<boolean> | boolean;
|
|
166
|
+
/**
|
|
167
|
+
* Adds one worker's calls to a provider's shared window, and returns the window across every
|
|
168
|
+
* worker. Optional; `shareObservations` needs it. Each call must add atomically, so two workers
|
|
169
|
+
* reporting at once both count.
|
|
170
|
+
*/
|
|
171
|
+
observe?(providerName: string, observation: CircuitObservation): Promise<CircuitWindow> | CircuitWindow;
|
|
122
172
|
}
|
|
123
173
|
/** @internal A circuit's local state. Shared with the coordinator module, never exported publicly. */
|
|
124
174
|
export interface CircuitEntry {
|
|
@@ -139,6 +189,8 @@ export interface CircuitEntry {
|
|
|
139
189
|
probeClaimed: boolean;
|
|
140
190
|
/** Why the circuit last changed state. */
|
|
141
191
|
reason?: string;
|
|
192
|
+
/** The shared window from the last sync, with `shareObservations`. */
|
|
193
|
+
shared?: CircuitWindow;
|
|
142
194
|
}
|
|
143
195
|
/**
|
|
144
196
|
* Trips routing away from a provider that is failing.
|
|
@@ -105,6 +105,7 @@ class CircuitBreaker {
|
|
|
105
105
|
this.refreshState(providerName, entry);
|
|
106
106
|
this.pushWindow(entry, false);
|
|
107
107
|
entry.consecutiveFailures = 0;
|
|
108
|
+
this.coordinator?.observed(providerName, false);
|
|
108
109
|
if (entry.state === 'half-open') {
|
|
109
110
|
entry.halfOpenInFlight = Math.max(0, entry.halfOpenInFlight - 1);
|
|
110
111
|
entry.halfOpenSuccesses += 1;
|
|
@@ -126,6 +127,7 @@ class CircuitBreaker {
|
|
|
126
127
|
}
|
|
127
128
|
this.pushWindow(entry, true);
|
|
128
129
|
entry.consecutiveFailures += 1;
|
|
130
|
+
this.coordinator?.observed(providerName, true);
|
|
129
131
|
entry.lastError = error instanceof Error ? error.message : error === undefined ? undefined : String(error);
|
|
130
132
|
if (entry.state === 'half-open') {
|
|
131
133
|
entry.halfOpenInFlight = Math.max(0, entry.halfOpenInFlight - 1);
|
|
@@ -162,6 +164,7 @@ class CircuitBreaker {
|
|
|
162
164
|
? new Date(entry.openedAt + resetTimeoutMs).toISOString()
|
|
163
165
|
: undefined,
|
|
164
166
|
lastError: entry.lastError,
|
|
167
|
+
...(entry.shared ? { shared: { ...entry.shared } } : {}),
|
|
165
168
|
};
|
|
166
169
|
});
|
|
167
170
|
}
|
|
@@ -212,6 +215,7 @@ class CircuitBreaker {
|
|
|
212
215
|
entry.openedAt = undefined;
|
|
213
216
|
entry.consecutiveFailures = 0;
|
|
214
217
|
entry.window = [];
|
|
218
|
+
entry.shared = undefined;
|
|
215
219
|
entry.halfOpenInFlight = 0;
|
|
216
220
|
entry.halfOpenSuccesses = 0;
|
|
217
221
|
entry.probeClaimed = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { CircuitStateStore, SharedCircuitState } from './circuit-breaker.js';
|
|
2
|
-
export type { CircuitStateStore, SharedCircuitState } from './circuit-breaker.js';
|
|
1
|
+
import type { CircuitObservation, CircuitStateStore, CircuitWindow, SharedCircuitState } from './circuit-breaker.js';
|
|
2
|
+
export type { CircuitObservation, CircuitStateStore, CircuitWindow, SharedCircuitState, } from './circuit-breaker.js';
|
|
3
3
|
/**
|
|
4
4
|
* Shared circuit state in one process.
|
|
5
5
|
*
|
|
@@ -10,7 +10,10 @@ export declare class MemoryCircuitStateStore implements CircuitStateStore {
|
|
|
10
10
|
private readonly now;
|
|
11
11
|
private readonly states;
|
|
12
12
|
private readonly probes;
|
|
13
|
+
private readonly windows;
|
|
13
14
|
constructor(now?: () => number);
|
|
15
|
+
/** Adds one worker's calls to a provider's window and returns the window across every worker. */
|
|
16
|
+
observe(providerName: string, observation: CircuitObservation): CircuitWindow;
|
|
14
17
|
/** Every provider's shared circuit state. */
|
|
15
18
|
read(): SharedCircuitState[];
|
|
16
19
|
/**
|
|
@@ -66,7 +69,15 @@ export declare class RedisCircuitStateStore implements CircuitStateStore {
|
|
|
66
69
|
private readonly client;
|
|
67
70
|
private readonly prefix;
|
|
68
71
|
private readonly useEval;
|
|
72
|
+
/**
|
|
73
|
+
* Adds one worker's calls to a provider's shared window in one Lua call, and returns the window
|
|
74
|
+
* across every worker. Present only when the client has `eval`, since the add and the sum must
|
|
75
|
+
* happen together; without it, `shareObservations` has nothing to call and each worker counts
|
|
76
|
+
* its own calls.
|
|
77
|
+
*/
|
|
78
|
+
readonly observe?: (providerName: string, observation: CircuitObservation) => Promise<CircuitWindow>;
|
|
69
79
|
constructor(client: RedisCircuitLikeClient, options?: RedisCircuitStateStoreOptions);
|
|
80
|
+
private observeWithEval;
|
|
70
81
|
/** Every provider's shared circuit state. Unreadable entries are skipped. */
|
|
71
82
|
read(): Promise<SharedCircuitState[]>;
|
|
72
83
|
/**
|
|
@@ -5,9 +5,37 @@ class MemoryCircuitStateStore {
|
|
|
5
5
|
now;
|
|
6
6
|
states = new Map();
|
|
7
7
|
probes = new Map();
|
|
8
|
+
windows = new Map();
|
|
8
9
|
constructor(now = () => Date.now()) {
|
|
9
10
|
this.now = now;
|
|
10
11
|
}
|
|
12
|
+
observe(providerName, observation) {
|
|
13
|
+
let window = this.windows.get(providerName);
|
|
14
|
+
if (!window) {
|
|
15
|
+
window = { buckets: new Map(), streak: 0 };
|
|
16
|
+
this.windows.set(providerName, window);
|
|
17
|
+
}
|
|
18
|
+
for (const bucket of observation.buckets) {
|
|
19
|
+
const stored = window.buckets.get(bucket.at) ?? { calls: 0, failures: 0 };
|
|
20
|
+
stored.calls += bucket.calls;
|
|
21
|
+
stored.failures += bucket.failures;
|
|
22
|
+
window.buckets.set(bucket.at, stored);
|
|
23
|
+
}
|
|
24
|
+
window.streak = observation.succeeded ? observation.trailingFailures : window.streak + observation.trailingFailures;
|
|
25
|
+
const keepAfter = observation.now - observation.windowMs - observation.bucketMs;
|
|
26
|
+
const since = observation.since ?? Number.NEGATIVE_INFINITY;
|
|
27
|
+
let calls = 0;
|
|
28
|
+
let failures = 0;
|
|
29
|
+
for (const [at, bucket] of window.buckets) {
|
|
30
|
+
if (at <= keepAfter)
|
|
31
|
+
window.buckets.delete(at);
|
|
32
|
+
else if (at >= since) {
|
|
33
|
+
calls += bucket.calls;
|
|
34
|
+
failures += bucket.failures;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { calls, failures, consecutiveFailures: window.streak };
|
|
38
|
+
}
|
|
11
39
|
read() {
|
|
12
40
|
return [...this.states.values()].map((state) => ({ ...state }));
|
|
13
41
|
}
|
|
@@ -38,14 +66,54 @@ redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
|
|
|
38
66
|
redis.call('DEL', KEYS[2])
|
|
39
67
|
return 1
|
|
40
68
|
`;
|
|
69
|
+
const OBSERVE_SCRIPT = `
|
|
70
|
+
local key = KEYS[1]
|
|
71
|
+
for i = 6, #ARGV, 3 do
|
|
72
|
+
redis.call('HINCRBY', key, 'c:' .. ARGV[i], ARGV[i + 1])
|
|
73
|
+
redis.call('HINCRBY', key, 'f:' .. ARGV[i], ARGV[i + 2])
|
|
74
|
+
end
|
|
75
|
+
local streak
|
|
76
|
+
if ARGV[1] == '1' then
|
|
77
|
+
redis.call('HSET', key, 'streak', ARGV[2])
|
|
78
|
+
streak = tonumber(ARGV[2])
|
|
79
|
+
else
|
|
80
|
+
streak = tonumber(redis.call('HINCRBY', key, 'streak', ARGV[2]))
|
|
81
|
+
end
|
|
82
|
+
local keepAfter = tonumber(ARGV[3])
|
|
83
|
+
local since = tonumber(ARGV[4])
|
|
84
|
+
local calls, failures = 0, 0
|
|
85
|
+
local fields = redis.call('HGETALL', key)
|
|
86
|
+
for i = 1, #fields, 2 do
|
|
87
|
+
local name = fields[i]
|
|
88
|
+
local kind = string.sub(name, 1, 2)
|
|
89
|
+
if kind == 'c:' or kind == 'f:' then
|
|
90
|
+
local at = tonumber(string.sub(name, 3))
|
|
91
|
+
if at <= keepAfter then
|
|
92
|
+
redis.call('HDEL', key, name)
|
|
93
|
+
elseif at >= since then
|
|
94
|
+
if kind == 'c:' then calls = calls + tonumber(fields[i + 1]) else failures = failures + tonumber(fields[i + 1]) end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
redis.call('PEXPIRE', key, ARGV[5])
|
|
99
|
+
return { calls, failures, streak }
|
|
100
|
+
`;
|
|
41
101
|
class RedisCircuitStateStore {
|
|
42
102
|
client;
|
|
43
103
|
prefix;
|
|
44
104
|
useEval;
|
|
105
|
+
observe;
|
|
45
106
|
constructor(client, options = {}) {
|
|
46
107
|
this.client = client;
|
|
47
108
|
this.prefix = options.prefix ?? 'nexus-ai-pro:circuits:';
|
|
48
109
|
this.useEval = options.useEval !== false && typeof client.eval === 'function';
|
|
110
|
+
if (this.useEval)
|
|
111
|
+
this.observe = (providerName, observation) => this.observeWithEval(providerName, observation);
|
|
112
|
+
}
|
|
113
|
+
async observeWithEval(providerName, observation) {
|
|
114
|
+
const reply = (await this.client.eval(OBSERVE_SCRIPT, 1, `${this.prefix}window:${providerName}`, observation.succeeded ? '1' : '0', String(observation.trailingFailures), String(observation.now - observation.windowMs - observation.bucketMs), String(observation.since ?? -1), String(observation.windowMs * 2 + observation.bucketMs), ...observation.buckets.flatMap((bucket) => [String(bucket.at), String(bucket.calls), String(bucket.failures)])));
|
|
115
|
+
const [calls, failures, streak] = (reply ?? []).map(Number);
|
|
116
|
+
return { calls: calls ?? 0, failures: failures ?? 0, consecutiveFailures: streak ?? 0 };
|
|
49
117
|
}
|
|
50
118
|
async read() {
|
|
51
119
|
const all = (await this.client.hgetall(this.statesKey())) ?? {};
|