@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,308 @@
|
|
|
1
|
+
// ======================================================
|
|
2
|
+
// COUNTER RECONCILIATION - CORRECT LEAKED COUNTERS
|
|
3
|
+
// ======================================================
|
|
4
|
+
// Compares active leases (source of truth) with counters.
|
|
5
|
+
// Automatically corrects counter drift.
|
|
6
|
+
//
|
|
7
|
+
// LEASES = source of truth
|
|
8
|
+
// COUNTERS = derived runtime optimization
|
|
9
|
+
|
|
10
|
+
const logPrefix = '[DRIFT]';
|
|
11
|
+
|
|
12
|
+
// S-2: registered stage list resolved from media registry
|
|
13
|
+
const mediaRegistry = require('@animastor/generation').mediaRegistry;
|
|
14
|
+
|
|
15
|
+
function log(msg) {
|
|
16
|
+
console.log(`${logPrefix} ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function warn(msg) {
|
|
20
|
+
console.warn(`${logPrefix} ⚠️ ${msg}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function error(msg) {
|
|
24
|
+
console.error(`${logPrefix} ❌ ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ======================================================
|
|
28
|
+
// CONFIGURATION
|
|
29
|
+
// ======================================================
|
|
30
|
+
|
|
31
|
+
const COUNTER_PREFIX = 'animastor:runtime:active';
|
|
32
|
+
const LEASE_PREFIX = 'animastor:dispatch-lease';
|
|
33
|
+
|
|
34
|
+
// ======================================================
|
|
35
|
+
// KEY GENERATORS
|
|
36
|
+
// ======================================================
|
|
37
|
+
|
|
38
|
+
function getCounterKey(stage) {
|
|
39
|
+
return `${COUNTER_PREFIX}-${stage}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getLeaseKeyPattern(stage) {
|
|
43
|
+
return `${LEASE_PREFIX}:*:${stage}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ======================================================
|
|
47
|
+
// COUNTER RECONCILIATION
|
|
48
|
+
// ======================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Count active leases for a stage.
|
|
52
|
+
*/
|
|
53
|
+
async function countActiveLeasesByStage(redis, stage) {
|
|
54
|
+
const pattern = getLeaseKeyPattern(stage);
|
|
55
|
+
let cursor = 0;
|
|
56
|
+
let count = 0;
|
|
57
|
+
|
|
58
|
+
do {
|
|
59
|
+
const result = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 500);
|
|
60
|
+
cursor = parseInt(result[0], 10);
|
|
61
|
+
count += result[1].length;
|
|
62
|
+
} while (cursor !== 0 && count < 10000);
|
|
63
|
+
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get current counter value.
|
|
69
|
+
*/
|
|
70
|
+
async function getCurrentCounter(redis, stage) {
|
|
71
|
+
const key = getCounterKey(stage);
|
|
72
|
+
const val = await redis.get(key);
|
|
73
|
+
return parseInt(val || '0', 10);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Set counter value.
|
|
78
|
+
*/
|
|
79
|
+
async function setCounter(redis, stage, value) {
|
|
80
|
+
const key = getCounterKey(stage);
|
|
81
|
+
await redis.set(key, String(value));
|
|
82
|
+
return { set: true, key, value };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Atomically correct counter using Lua script.
|
|
87
|
+
*/
|
|
88
|
+
async function correctCounterWithLua(redis, stage, targetValue) {
|
|
89
|
+
const key = getCounterKey(stage);
|
|
90
|
+
const target = String(targetValue);
|
|
91
|
+
|
|
92
|
+
// NOTE: no "expected" guard here — ioredis serializes null args to ""
|
|
93
|
+
// which is truthy in Lua, so a guard would always fire and skip the SET.
|
|
94
|
+
const luaScript = `
|
|
95
|
+
local key = KEYS[1]
|
|
96
|
+
local newval = ARGV[1]
|
|
97
|
+
|
|
98
|
+
local current = redis.call('GET', key)
|
|
99
|
+
|
|
100
|
+
redis.call('SET', key, newval)
|
|
101
|
+
|
|
102
|
+
return {tostring(true), 'corrected', current, newval}
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const result = await redis.eval(luaScript, 1, key, target);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
success: result[0] === 'true',
|
|
109
|
+
action: result[1],
|
|
110
|
+
old: result[2],
|
|
111
|
+
new: result[3],
|
|
112
|
+
key
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get current counter with drift check.
|
|
118
|
+
*/
|
|
119
|
+
async function getCounterWithDriftCheck(redis, stage) {
|
|
120
|
+
const leaseCount = await countActiveLeasesByStage(redis, stage);
|
|
121
|
+
const counterValue = await getCurrentCounter(redis, stage);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
stage,
|
|
125
|
+
leaseCount,
|
|
126
|
+
counterValue,
|
|
127
|
+
drift: counterValue - leaseCount,
|
|
128
|
+
correct: counterValue === leaseCount
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Reconcile all counters.
|
|
134
|
+
* Returns reconciliation report.
|
|
135
|
+
*/
|
|
136
|
+
async function reconcileCounters(redis) {
|
|
137
|
+
const stages = mediaRegistry.listMediaTypes();
|
|
138
|
+
const report = {
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
stages: {},
|
|
141
|
+
summary: {
|
|
142
|
+
totalDrift: 0,
|
|
143
|
+
correctedCount: 0,
|
|
144
|
+
errors: []
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
for (const stage of stages) {
|
|
149
|
+
try {
|
|
150
|
+
const data = await getCounterWithDriftCheck(redis, stage);
|
|
151
|
+
|
|
152
|
+
if (!data.correct) {
|
|
153
|
+
warn(`COUNTER_DRIFT: ${stage}: counter=${data.counterValue}, leases=${data.leaseCount}, drift=${data.drift}`);
|
|
154
|
+
|
|
155
|
+
// Correct the counter
|
|
156
|
+
const correction = await correctCounterWithLua(redis, stage, data.leaseCount);
|
|
157
|
+
|
|
158
|
+
if (correction.success && correction.action === 'corrected') {
|
|
159
|
+
log(`COUNTER_RECONCILED: ${stage}: ${correction.old} -> ${correction.new}`);
|
|
160
|
+
report.stages[stage] = {
|
|
161
|
+
leaseCount: data.leaseCount,
|
|
162
|
+
counterValue: data.counterValue,
|
|
163
|
+
drift: data.drift,
|
|
164
|
+
corrected: true,
|
|
165
|
+
correction: correction
|
|
166
|
+
};
|
|
167
|
+
report.summary.correctedCount++;
|
|
168
|
+
report.summary.totalDrift += Math.abs(data.drift);
|
|
169
|
+
} else {
|
|
170
|
+
error(`COUNTER_CORRECTION_FAILED: ${stage}: ${correction ? correction.key : 'unknown'}`);
|
|
171
|
+
report.stages[stage] = {
|
|
172
|
+
leaseCount: data.leaseCount,
|
|
173
|
+
counterValue: data.counterValue,
|
|
174
|
+
drift: data.drift,
|
|
175
|
+
corrected: false,
|
|
176
|
+
correction
|
|
177
|
+
};
|
|
178
|
+
report.summary.errors.push(`Failed to correct ${stage}: ${correction?.key}`);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
report.stages[stage] = {
|
|
182
|
+
leaseCount: data.leaseCount,
|
|
183
|
+
counterValue: data.counterValue,
|
|
184
|
+
drift: 0,
|
|
185
|
+
corrected: false
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
} catch (err) {
|
|
189
|
+
error(`Counter reconciliation error for ${stage}: ${err.message}`);
|
|
190
|
+
report.stages[stage] = {
|
|
191
|
+
error: err.message,
|
|
192
|
+
corrected: false
|
|
193
|
+
};
|
|
194
|
+
report.summary.errors.push(`Error reconciling ${stage}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
log(`COUNTER_RECONCILIATION_COMPLETE: ${report.summary.correctedCount} corrected, ${report.summary.totalDrift} total drift`);
|
|
199
|
+
|
|
200
|
+
return report;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get current counter status.
|
|
205
|
+
*/
|
|
206
|
+
async function getCountersStatus(redis) {
|
|
207
|
+
const stages = mediaRegistry.listMediaTypes();
|
|
208
|
+
const status = {};
|
|
209
|
+
|
|
210
|
+
for (const stage of stages) {
|
|
211
|
+
status[stage] = await getCounterWithDriftCheck(redis, stage);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return status;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get reconciliation metrics.
|
|
219
|
+
*/
|
|
220
|
+
async function getReconciliationMetrics(redis) {
|
|
221
|
+
const status = await getCountersStatus(redis);
|
|
222
|
+
const totalLeases = status.audio.leaseCount + status.image.leaseCount + status.video.leaseCount;
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
timestamp: new Date().toISOString(),
|
|
226
|
+
counters: status,
|
|
227
|
+
totalActiveLeases: totalLeases,
|
|
228
|
+
driftStatus: Object.fromEntries(
|
|
229
|
+
mediaRegistry.listMediaTypes().map(t => [t, status[t] && status[t].drift !== 0])
|
|
230
|
+
)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Quick check for problematic drift.
|
|
236
|
+
* Returns { hasDrift: boolean, issues: [] }
|
|
237
|
+
*/
|
|
238
|
+
async function checkForDrift(redis) {
|
|
239
|
+
const status = await getCountersStatus(redis);
|
|
240
|
+
const issues = [];
|
|
241
|
+
|
|
242
|
+
for (const stage of Object.keys(status)) {
|
|
243
|
+
if (status[stage].drift !== 0) {
|
|
244
|
+
issues.push({
|
|
245
|
+
stage,
|
|
246
|
+
counter: status[stage].counterValue,
|
|
247
|
+
leases: status[stage].leaseCount,
|
|
248
|
+
drift: status[stage].drift
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
hasDrift: issues.length > 0,
|
|
255
|
+
issues
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Manual counter correction.
|
|
261
|
+
* Use only when you know the target value.
|
|
262
|
+
*/
|
|
263
|
+
async function manualCounterCorrection(redis, stage, value) {
|
|
264
|
+
const result = await correctCounterWithLua(redis, stage, String(value));
|
|
265
|
+
return {
|
|
266
|
+
stage,
|
|
267
|
+
value,
|
|
268
|
+
...result
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Increment counter atomically.
|
|
274
|
+
*/
|
|
275
|
+
async function incrementCounter(redis, stage) {
|
|
276
|
+
const key = getCounterKey(stage);
|
|
277
|
+
const newVal = await redis.incr(key);
|
|
278
|
+
return { stage, key, value: newVal };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ======================================================
|
|
282
|
+
// EXPORTS
|
|
283
|
+
// ======================================================
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
// Configuration
|
|
287
|
+
COUNTER_PREFIX,
|
|
288
|
+
LEASE_PREFIX,
|
|
289
|
+
|
|
290
|
+
// Key generators
|
|
291
|
+
getCounterKey,
|
|
292
|
+
getLeaseKeyPattern,
|
|
293
|
+
|
|
294
|
+
// Core reconciliation
|
|
295
|
+
countActiveLeasesByStage,
|
|
296
|
+
getCurrentCounter,
|
|
297
|
+
setCounter,
|
|
298
|
+
correctCounterWithLua,
|
|
299
|
+
getCounterWithDriftCheck,
|
|
300
|
+
reconcileCounters,
|
|
301
|
+
getCountersStatus,
|
|
302
|
+
getReconciliationMetrics,
|
|
303
|
+
checkForDrift,
|
|
304
|
+
manualCounterCorrection,
|
|
305
|
+
|
|
306
|
+
// Counter manipulation
|
|
307
|
+
incrementCounter
|
|
308
|
+
};
|