@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.
Files changed (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +75 -0
  3. package/package.json +51 -0
  4. package/src/host/host-bindings.js +138 -0
  5. package/src/index.js +99 -0
  6. package/src/orchestration/index.js +40 -0
  7. package/src/orchestration/orchestrator.js +630 -0
  8. package/src/orchestration/runtime-result-consumer.js +54 -0
  9. package/src/orchestration/scene-callbacks.js +542 -0
  10. package/src/orchestration/scene-orchestrator.js +601 -0
  11. package/src/orchestration/scene-restoration.js +129 -0
  12. package/src/orchestration/scene-utils.js +36 -0
  13. package/src/runtime/active-scenes-index.js +164 -0
  14. package/src/runtime/audio-fsm-port.js +187 -0
  15. package/src/runtime/circuit-breaker.js +588 -0
  16. package/src/runtime/counter-reconciliation.js +308 -0
  17. package/src/runtime/dispatch-engine.js +1768 -0
  18. package/src/runtime/failure-taxonomy.js +125 -0
  19. package/src/runtime/hub-cancel-port.js +122 -0
  20. package/src/runtime/layer-config-port.js +136 -0
  21. package/src/runtime/lease-manager.js +607 -0
  22. package/src/runtime/persistence-port.js +162 -0
  23. package/src/runtime/placeholder-audio-port.js +117 -0
  24. package/src/runtime/progress-events-port.js +121 -0
  25. package/src/runtime/reconciliation-engine.js +2405 -0
  26. package/src/runtime/retry-budget-manager.js +229 -0
  27. package/src/runtime/runtime-metrics.js +427 -0
  28. package/src/runtime/runtime-result-emitter.js +123 -0
  29. package/src/runtime/runtime-scheduler.js +629 -0
  30. package/src/runtime/scene-data-port.js +114 -0
  31. package/src/runtime/scene-window.js +827 -0
  32. package/src/runtime/video-fsm-port.js +189 -0
  33. package/src/runtime/worker-health.js +270 -0
  34. package/test/counter-reconciliation.test.js +164 -0
  35. package/test/mocks/redis-mock.js +221 -0
  36. 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
+ };