@animastor/orchestration 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +75 -0
- package/package.json +51 -0
- package/src/host/host-bindings.js +138 -0
- package/src/index.js +99 -0
- package/src/orchestration/index.js +40 -0
- package/src/orchestration/orchestrator.js +630 -0
- package/src/orchestration/runtime-result-consumer.js +54 -0
- package/src/orchestration/scene-callbacks.js +542 -0
- package/src/orchestration/scene-orchestrator.js +601 -0
- package/src/orchestration/scene-restoration.js +129 -0
- package/src/orchestration/scene-utils.js +36 -0
- package/src/runtime/active-scenes-index.js +164 -0
- package/src/runtime/audio-fsm-port.js +187 -0
- package/src/runtime/circuit-breaker.js +588 -0
- package/src/runtime/counter-reconciliation.js +308 -0
- package/src/runtime/dispatch-engine.js +1768 -0
- package/src/runtime/failure-taxonomy.js +125 -0
- package/src/runtime/hub-cancel-port.js +122 -0
- package/src/runtime/layer-config-port.js +136 -0
- package/src/runtime/lease-manager.js +607 -0
- package/src/runtime/persistence-port.js +162 -0
- package/src/runtime/placeholder-audio-port.js +117 -0
- package/src/runtime/progress-events-port.js +121 -0
- package/src/runtime/reconciliation-engine.js +2405 -0
- package/src/runtime/retry-budget-manager.js +229 -0
- package/src/runtime/runtime-metrics.js +427 -0
- package/src/runtime/runtime-result-emitter.js +123 -0
- package/src/runtime/runtime-scheduler.js +629 -0
- package/src/runtime/scene-data-port.js +114 -0
- package/src/runtime/scene-window.js +827 -0
- package/src/runtime/video-fsm-port.js +189 -0
- package/src/runtime/worker-health.js +270 -0
- package/test/counter-reconciliation.test.js +164 -0
- package/test/mocks/redis-mock.js +221 -0
- package/test/test-bindings.js +60 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// In-memory Redis mock for tests.
|
|
2
|
+
// Operations are internally consistent: hset/hget/hgetall use flat `key:field` storage,
|
|
3
|
+
// scan filters the store by glob pattern, lrange handles stop=-1.
|
|
4
|
+
|
|
5
|
+
function globToRegex(pattern) {
|
|
6
|
+
let regexStr = '^';
|
|
7
|
+
for (const ch of pattern) {
|
|
8
|
+
if (ch === '*') regexStr += '.*';
|
|
9
|
+
else if (ch === '?') regexStr += '.';
|
|
10
|
+
else if ('[](){}+.^$|\\'.includes(ch)) regexStr += '\\' + ch;
|
|
11
|
+
else regexStr += ch;
|
|
12
|
+
}
|
|
13
|
+
regexStr += '$';
|
|
14
|
+
return new RegExp(regexStr);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createMockRedis() {
|
|
18
|
+
const store = new Map();
|
|
19
|
+
// Recorded TTLs (seconds) so ttl() can report them. Keys are NOT
|
|
20
|
+
// auto-expired — existing tests rely on keys persisting. Tests simulate
|
|
21
|
+
// renewal / decay by calling set(...,'EX',n) or expire(key, n).
|
|
22
|
+
const expiries = new Map();
|
|
23
|
+
const recordExpiry = (key, args) => {
|
|
24
|
+
const i = args.indexOf('EX');
|
|
25
|
+
if (i !== -1 && args[i + 1] !== undefined) expiries.set(key, Number(args[i + 1]));
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
hset: async (key, field, value) => {
|
|
29
|
+
if (field && typeof field === 'object' && !Array.isArray(field)) {
|
|
30
|
+
for (const [name, fieldValue] of Object.entries(field)) {
|
|
31
|
+
store.set(`${key}:${name}`, fieldValue);
|
|
32
|
+
}
|
|
33
|
+
return Object.keys(field).length;
|
|
34
|
+
}
|
|
35
|
+
store.set(`${key}:${field}`, value);
|
|
36
|
+
return 1;
|
|
37
|
+
},
|
|
38
|
+
hget: async (key, field) => store.get(`${key}:${field}`) || null,
|
|
39
|
+
hgetall: async (key) => {
|
|
40
|
+
const result = {};
|
|
41
|
+
const prefix = key + ':';
|
|
42
|
+
for (const [k, v] of store) {
|
|
43
|
+
if (k.startsWith(prefix)) {
|
|
44
|
+
result[k.slice(prefix.length)] = v;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
48
|
+
},
|
|
49
|
+
hdel: async (key, ...fields) => {
|
|
50
|
+
let n = 0;
|
|
51
|
+
for (const field of fields) {
|
|
52
|
+
if (store.delete(`${key}:${field}`)) n++;
|
|
53
|
+
}
|
|
54
|
+
return n;
|
|
55
|
+
},
|
|
56
|
+
hlen: async (key) => {
|
|
57
|
+
const prefix = key + ':';
|
|
58
|
+
let n = 0;
|
|
59
|
+
for (const k of store.keys()) if (k.startsWith(prefix)) n++;
|
|
60
|
+
return n;
|
|
61
|
+
},
|
|
62
|
+
hexists: async (key, field) => store.has(`${key}:${field}`) ? 1 : 0,
|
|
63
|
+
get: async (key) => store.get(key) || null,
|
|
64
|
+
set: async (key, value, ...args) => {
|
|
65
|
+
const nx = args.includes('NX');
|
|
66
|
+
if (nx && store.has(key)) return null;
|
|
67
|
+
store.set(key, value);
|
|
68
|
+
recordExpiry(key, args);
|
|
69
|
+
return 'OK';
|
|
70
|
+
},
|
|
71
|
+
del: async (...keys) => { let n = 0; for (const k of keys) { expiries.delete(k); if (store.delete(k)) n++; } return n; },
|
|
72
|
+
expire: async (key, ttl) => {
|
|
73
|
+
if (!store.has(key)) return 0;
|
|
74
|
+
expiries.set(key, Number(ttl));
|
|
75
|
+
return 1;
|
|
76
|
+
},
|
|
77
|
+
// Mirrors ioredis: -2 if key missing, -1 if no expiry, else seconds.
|
|
78
|
+
ttl: async (key) => {
|
|
79
|
+
if (!store.has(key)) return -2;
|
|
80
|
+
return expiries.has(key) ? expiries.get(key) : -1;
|
|
81
|
+
},
|
|
82
|
+
incr: async (key) => { const v = (parseInt(store.get(key) || '0', 10)) + 1; store.set(key, String(v)); return v; },
|
|
83
|
+
incrby: async (key, increment) => { const v = (parseInt(store.get(key) || '0', 10)) + Number(increment || 0); store.set(key, String(v)); return v; },
|
|
84
|
+
decr: async (key) => { const v = Math.max(0, (parseInt(store.get(key) || '0', 10)) - 1); store.set(key, String(v)); return v; },
|
|
85
|
+
exists: async (key) => store.has(key) ? 1 : 0,
|
|
86
|
+
scan: async (cursor, ...args) => {
|
|
87
|
+
let pattern = null;
|
|
88
|
+
for (let i = 0; i < args.length; i++) {
|
|
89
|
+
if (args[i] === 'MATCH') pattern = args[i + 1];
|
|
90
|
+
}
|
|
91
|
+
if (!pattern) return ['0', []];
|
|
92
|
+
const regex = globToRegex(pattern);
|
|
93
|
+
const matched = [...store.keys()].filter(k => regex.test(k));
|
|
94
|
+
return ['0', matched];
|
|
95
|
+
},
|
|
96
|
+
hscan: async (key, cursor, ...args) => {
|
|
97
|
+
const prefix = key + ':';
|
|
98
|
+
const entries = [];
|
|
99
|
+
for (const [k, v] of store) {
|
|
100
|
+
if (k.startsWith(prefix)) {
|
|
101
|
+
entries.push(k.slice(prefix.length), v);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return ['0', entries];
|
|
105
|
+
},
|
|
106
|
+
eval: async (script, keysCount, ...args) => {
|
|
107
|
+
if (script.includes('local metadata') && script.includes("ARGV[5] == '1'")) {
|
|
108
|
+
const [metadataKey, completedKey, leaseKey, quotaKey] = args.slice(0, keysCount);
|
|
109
|
+
const [
|
|
110
|
+
expectedMetadata,
|
|
111
|
+
markerValue,
|
|
112
|
+
markerTtl,
|
|
113
|
+
expectedLease,
|
|
114
|
+
quotaOwned,
|
|
115
|
+
] = args.slice(keysCount);
|
|
116
|
+
const metadata = store.get(metadataKey);
|
|
117
|
+
if (metadata === undefined) return store.has(completedKey) ? 2 : 0;
|
|
118
|
+
if (metadata !== expectedMetadata) return -1;
|
|
119
|
+
const lease = store.get(leaseKey);
|
|
120
|
+
if (lease !== undefined && (!expectedLease || lease !== expectedLease)) return -2;
|
|
121
|
+
if (store.has(completedKey)) return 2;
|
|
122
|
+
|
|
123
|
+
store.set(completedKey, markerValue);
|
|
124
|
+
store.delete(metadataKey);
|
|
125
|
+
expiries.delete(metadataKey);
|
|
126
|
+
if (lease !== undefined && expectedLease) {
|
|
127
|
+
store.delete(leaseKey);
|
|
128
|
+
expiries.delete(leaseKey);
|
|
129
|
+
}
|
|
130
|
+
if (quotaOwned === '1') {
|
|
131
|
+
const current = Number(store.get(quotaKey) || 0);
|
|
132
|
+
if (current > 0) store.set(quotaKey, String(current - 1));
|
|
133
|
+
}
|
|
134
|
+
void markerTtl;
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
// Lease renewal (lease-manager.renewLeaseIfOwner): compare-and-EXPIRE.
|
|
138
|
+
// KEYS=[leaseKey, metadataKey], ARGV=[expectedToken, ttl, now].
|
|
139
|
+
if (script.includes('ttl_add') && script.includes('EXPIRE')) {
|
|
140
|
+
const leaseKey = args[0];
|
|
141
|
+
const metadataKey = args[1];
|
|
142
|
+
const expected = args[keysCount];
|
|
143
|
+
const ttlAdd = Number(args[keysCount + 1]);
|
|
144
|
+
const now = args[keysCount + 2];
|
|
145
|
+
const current = store.get(leaseKey);
|
|
146
|
+
if (current === undefined) return ['false', 'no_lease'];
|
|
147
|
+
if (current !== expected) return ['false', 'token_mismatch', current];
|
|
148
|
+
expiries.set(leaseKey, ttlAdd);
|
|
149
|
+
if (store.has(metadataKey)) expiries.set(metadataKey, ttlAdd);
|
|
150
|
+
return ['true', 'renewed', String(now)];
|
|
151
|
+
}
|
|
152
|
+
if (script.includes("current ~= ARGV[1]") && script.includes("redis.call('DEL', KEYS[1])")) {
|
|
153
|
+
const key = args[0];
|
|
154
|
+
const expected = args[keysCount];
|
|
155
|
+
const current = store.get(key);
|
|
156
|
+
if (current === undefined) return 0;
|
|
157
|
+
if (current !== expected) return -1;
|
|
158
|
+
store.delete(key);
|
|
159
|
+
expiries.delete(key);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
if (script.includes("return redis.call('INCR', key)")) {
|
|
163
|
+
const key = args[0];
|
|
164
|
+
const max = Number(args[keysCount]);
|
|
165
|
+
const current = Number(store.get(key) || 0);
|
|
166
|
+
if (current >= max) return 0;
|
|
167
|
+
const next = current + 1;
|
|
168
|
+
store.set(key, String(next));
|
|
169
|
+
return next;
|
|
170
|
+
}
|
|
171
|
+
// Return array mimicking Lua return: {true, 'corrected', old, new}
|
|
172
|
+
// Callers (e.g. correctCounterWithLua) destructure result[0]..result[3]
|
|
173
|
+
const key = args[0];
|
|
174
|
+
const target = args[keysCount] || args[1];
|
|
175
|
+
let current = store.get(key);
|
|
176
|
+
if (current === undefined) current = null;
|
|
177
|
+
store.set(key, target);
|
|
178
|
+
return ['true', 'corrected', current, target];
|
|
179
|
+
},
|
|
180
|
+
rpush: async (key, ...values) => { const arr = store.get(key) || []; arr.push(...values); store.set(key, arr); return arr.length; },
|
|
181
|
+
lpush: async (key, ...values) => { const arr = store.get(key) || []; arr.unshift(...values); store.set(key, arr); return arr.length; },
|
|
182
|
+
rpoplpush: async (source, destination) => {
|
|
183
|
+
const arr = store.get(source);
|
|
184
|
+
if (!arr || arr.length === 0) return null;
|
|
185
|
+
const value = arr.pop();
|
|
186
|
+
if (arr.length === 0) store.delete(source);
|
|
187
|
+
const dest = store.get(destination) || [];
|
|
188
|
+
dest.unshift(value);
|
|
189
|
+
store.set(destination, dest);
|
|
190
|
+
return value;
|
|
191
|
+
},
|
|
192
|
+
llen: async (key) => { const arr = store.get(key); return arr ? arr.length : 0; },
|
|
193
|
+
lrange: async (key, start, stop) => {
|
|
194
|
+
const arr = store.get(key) || [];
|
|
195
|
+
if (stop === -1 || stop === undefined) return arr.slice(start);
|
|
196
|
+
return arr.slice(start, stop + 1);
|
|
197
|
+
},
|
|
198
|
+
lrem: async (key, count, value) => {
|
|
199
|
+
const arr = store.get(key) || [];
|
|
200
|
+
const index = arr.indexOf(value);
|
|
201
|
+
if (index === -1) return 0;
|
|
202
|
+
arr.splice(index, 1);
|
|
203
|
+
return 1;
|
|
204
|
+
},
|
|
205
|
+
sadd: async (key, member) => { const s = store.get(key) || new Set(); s.add(member); store.set(key, s); return 1; },
|
|
206
|
+
srem: async (key, member) => { const s = store.get(key); if (s) { s.delete(member); return 1; } return 0; },
|
|
207
|
+
smembers: async (key) => { const s = store.get(key); return s ? [...s] : []; },
|
|
208
|
+
scard: async (key) => { const s = store.get(key); return s ? s.size : 0; },
|
|
209
|
+
zrem: async (key, member) => {
|
|
210
|
+
const s = store.get(key);
|
|
211
|
+
if (s && s instanceof Set) { return s.delete(member) ? 1 : 0; }
|
|
212
|
+
return 0;
|
|
213
|
+
},
|
|
214
|
+
multi: () => ({ exec: async () => [] }),
|
|
215
|
+
quit: async () => 'OK',
|
|
216
|
+
disconnect: () => {},
|
|
217
|
+
duplicate: () => createMockRedis(),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { createMockRedis };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ======================================================
|
|
2
|
+
// ORCHESTRATION PACKAGE TEST BINDINGS
|
|
3
|
+
// ======================================================
|
|
4
|
+
// The package self-containedness fixture: wires ONLY what package-local
|
|
5
|
+
// (pure-policy) tests need and what the package itself cannot own:
|
|
6
|
+
//
|
|
7
|
+
// 1. @animastor/generation GenerationConfig port — the media-registry
|
|
8
|
+
// self-bootstrap (default-registrations) reads the canonical config
|
|
9
|
+
// slices through this port. In the host it is bound by the host adapter
|
|
10
|
+
// (backend/src/config/generation-config-adapter.js) from runtime-config.
|
|
11
|
+
// Package tests have no host, so this fixture binds the SAME canonical
|
|
12
|
+
// slice VALUES frozen here (the host S2-G drift guards still compare
|
|
13
|
+
// the registry against runtime-config directly, so any divergence of
|
|
14
|
+
// these literals fails there first).
|
|
15
|
+
//
|
|
16
|
+
// 2. @animastor/orchestration host-bindings — package files fail fast at
|
|
17
|
+
// call time unless the composition root wires the host surfaces. Tests
|
|
18
|
+
// that exercise only pure-policy modules (no host touch) need nothing;
|
|
19
|
+
// tests that stub host modules install their stubs directly here via
|
|
20
|
+
// `bindHostModules` (same discipline as the host composition root).
|
|
21
|
+
//
|
|
22
|
+
// Loaded for EVERY package mocha run via `npm test` (see package.json).
|
|
23
|
+
//
|
|
24
|
+
// Docs: docs/architecture/generation-module-extraction-reconnaissance.md §32.30
|
|
25
|
+
// ======================================================
|
|
26
|
+
|
|
27
|
+
// 1) GenerationConfig port (canonical host slice values, frozen)
|
|
28
|
+
const { setGenerationConfig } = require('@animastor/generation').ports.generationConfig;
|
|
29
|
+
|
|
30
|
+
setGenerationConfig({
|
|
31
|
+
leaseTtlS: {
|
|
32
|
+
AUDIO: 1860, // = ceil(STALL_FAILSAFE_MS/1000) + 60 (GPU_TIMEOUT_MS=600_000)
|
|
33
|
+
IMAGE: 1200, // = 20 * 60
|
|
34
|
+
VIDEO: 1800, // = 30 * 60
|
|
35
|
+
},
|
|
36
|
+
quotas: {
|
|
37
|
+
MAX_ACTIVE_AUDIO: 8,
|
|
38
|
+
MAX_ACTIVE_IMAGE: 4,
|
|
39
|
+
MAX_ACTIVE_VIDEO: 2,
|
|
40
|
+
},
|
|
41
|
+
stuckThresholds: {
|
|
42
|
+
AUDIO_GENERATING: 15,
|
|
43
|
+
AUDIO_PENDING: 15,
|
|
44
|
+
IMAGE_GENERATING: 30,
|
|
45
|
+
IMAGE_PENDING: 30,
|
|
46
|
+
VIDEO_GENERATING: 60,
|
|
47
|
+
VIDEO_PENDING: 60,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 2) host-bindings default: a fail-fast stub set — any host touch that a
|
|
52
|
+
// package test does not explicitly re-wire must throw, never silently no-op
|
|
53
|
+
// (matches the production unwired-seam semantics).
|
|
54
|
+
const { bindHostModules } = require('../src/host/host-bindings');
|
|
55
|
+
|
|
56
|
+
bindHostModules({
|
|
57
|
+
journal: () => {
|
|
58
|
+
throw new Error('[ORCH-TEST-BINDINGS] host journal not wired — wire it in the test that asserts journal behavior');
|
|
59
|
+
},
|
|
60
|
+
});
|