bulltrackers-module 1.0.105 → 1.0.106
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/README.MD +222 -222
- package/functions/appscript-api/helpers/errors.js +19 -19
- package/functions/appscript-api/index.js +58 -58
- package/functions/computation-system/helpers/orchestration_helpers.js +647 -113
- package/functions/computation-system/utils/data_loader.js +191 -191
- package/functions/computation-system/utils/utils.js +149 -254
- package/functions/core/utils/firestore_utils.js +433 -433
- package/functions/core/utils/pubsub_utils.js +53 -53
- package/functions/dispatcher/helpers/dispatch_helpers.js +47 -47
- package/functions/dispatcher/index.js +52 -52
- package/functions/etoro-price-fetcher/helpers/handler_helpers.js +124 -124
- package/functions/fetch-insights/helpers/handler_helpers.js +91 -91
- package/functions/generic-api/helpers/api_helpers.js +379 -379
- package/functions/generic-api/index.js +150 -150
- package/functions/invalid-speculator-handler/helpers/handler_helpers.js +75 -75
- package/functions/orchestrator/helpers/discovery_helpers.js +226 -226
- package/functions/orchestrator/helpers/update_helpers.js +92 -92
- package/functions/orchestrator/index.js +147 -147
- package/functions/price-backfill/helpers/handler_helpers.js +116 -123
- package/functions/social-orchestrator/helpers/orchestrator_helpers.js +61 -61
- package/functions/social-task-handler/helpers/handler_helpers.js +288 -288
- package/functions/task-engine/handler_creator.js +78 -78
- package/functions/task-engine/helpers/discover_helpers.js +125 -125
- package/functions/task-engine/helpers/update_helpers.js +118 -118
- package/functions/task-engine/helpers/verify_helpers.js +162 -162
- package/functions/task-engine/utils/firestore_batch_manager.js +258 -258
- package/index.js +105 -113
- package/package.json +45 -45
- package/functions/computation-system/computation_dependencies.json +0 -120
- package/functions/computation-system/helpers/worker_helpers.js +0 -340
- package/functions/computation-system/utils/computation_state_manager.js +0 -178
- package/functions/computation-system/utils/dependency_graph.js +0 -191
- package/functions/speculator-cleanup-orchestrator/helpers/cleanup_helpers.js +0 -160
|
@@ -1,340 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Main pipe: pipe.computationSystem.handleNodeTask
|
|
3
|
-
*
|
|
4
|
-
* This function is the "Worker" for the event-driven DAG.
|
|
5
|
-
* It is triggered by a Pub/Sub message for a *single node* (computation).
|
|
6
|
-
*
|
|
7
|
-
* Its responsibilities are to:
|
|
8
|
-
* 1. Receive a task: { date, nodeName }.
|
|
9
|
-
* 2. Execute that single computation.
|
|
10
|
-
* 3. On success:
|
|
11
|
-
* a. Mark the node as 'success' in the StateManager.
|
|
12
|
-
* b. Ask the StateManager for children that are now 'ready'.
|
|
13
|
-
* c. Publish new tasks for those ready children.
|
|
14
|
-
* 4. On failure:
|
|
15
|
-
* a. Log the error.
|
|
16
|
-
* b. Acknowledge the message (do not re-throw).
|
|
17
|
-
* c. The system will "heal" when the missing dependency eventually runs
|
|
18
|
-
* and re-triggers this failed node.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const { FieldPath } = require('@google-cloud/firestore');
|
|
22
|
-
// Import data loaders
|
|
23
|
-
const {
|
|
24
|
-
getPortfolioPartRefs,
|
|
25
|
-
loadFullDayMap,
|
|
26
|
-
loadDataByRefs,
|
|
27
|
-
loadDailyInsights,
|
|
28
|
-
loadDailySocialPostInsights
|
|
29
|
-
} = require('../utils/data_loader.js');
|
|
30
|
-
// Import utils
|
|
31
|
-
const { commitBatchInChunks, categorizeCalculations } = require('../utils/utils.js');
|
|
32
|
-
const stateManager = require('../utils/computation_state_manager.js');
|
|
33
|
-
|
|
34
|
-
// --- Internal Helper: findCalcClass (no changes) ---
|
|
35
|
-
function findCalcClass(calculations, category, name) {
|
|
36
|
-
if (calculations[category]) {
|
|
37
|
-
if (typeof calculations[category][name] === 'function') {
|
|
38
|
-
return calculations[category][name];
|
|
39
|
-
}
|
|
40
|
-
if (calculations[category].historical && typeof calculations[category].historical[name] === 'function') {
|
|
41
|
-
return calculations[category].historical[name];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// --- NEW: Internal helper for previous date string ---
|
|
48
|
-
function getPrevStr(dateStr) {
|
|
49
|
-
const prev = new Date(dateStr + 'T00:00:00Z');
|
|
50
|
-
prev.setUTCDate(prev.getUTCDate() - 1);
|
|
51
|
-
return prev.toISOString().slice(0, 10);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Streams data and runs a *single* Batch 1-style computation.
|
|
56
|
-
* --- THIS IS THE FULLY UPDATED FUNCTION ---
|
|
57
|
-
*/
|
|
58
|
-
async function runStreamingComputation(dateStr, node, config, dependencies, calculations) {
|
|
59
|
-
const { db, logger } = dependencies;
|
|
60
|
-
|
|
61
|
-
// --- MODIFIED: Destructure 'requires' from the node ---
|
|
62
|
-
const { category, name: calcName, calcClass: CalculationClass, requires } = node;
|
|
63
|
-
const passName = `[Worker ${dateStr}/${calcName}]`;
|
|
64
|
-
|
|
65
|
-
logger.log('INFO', `${passName} Starting streaming computation... (Requires: ${requires.join(', ') || 'none'})`);
|
|
66
|
-
|
|
67
|
-
// --- Dynamic Data Loading ---
|
|
68
|
-
let todayInsightsData = null;
|
|
69
|
-
let yesterdayInsightsData = null;
|
|
70
|
-
let todaySocialPostInsightsData = null;
|
|
71
|
-
let yesterdaySocialPostInsightsData = null;
|
|
72
|
-
let todayRefs = [];
|
|
73
|
-
let yesterdayPortfolios = {};
|
|
74
|
-
const prevStr = getPrevStr(dateStr); // Get yesterday's date string
|
|
75
|
-
|
|
76
|
-
if (requires.includes('today_insights')) {
|
|
77
|
-
todayInsightsData = await loadDailyInsights(config, dependencies, dateStr);
|
|
78
|
-
}
|
|
79
|
-
if (requires.includes('yesterday_insights')) {
|
|
80
|
-
yesterdayInsightsData = await loadDailyInsights(config, dependencies, prevStr);
|
|
81
|
-
}
|
|
82
|
-
if (requires.includes('today_social')) {
|
|
83
|
-
todaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, dateStr);
|
|
84
|
-
}
|
|
85
|
-
if (requires.includes('yesterday_social')) {
|
|
86
|
-
yesterdaySocialPostInsightsData = await loadDailySocialPostInsights(config, dependencies, prevStr);
|
|
87
|
-
}
|
|
88
|
-
if (requires.includes('today_portfolio')) {
|
|
89
|
-
todayRefs = await getPortfolioPartRefs(config, dependencies, dateStr);
|
|
90
|
-
}
|
|
91
|
-
if (requires.includes('yesterday_portfolio')) {
|
|
92
|
-
const yesterdayRefs = await getPortfolioPartRefs(config, dependencies, prevStr);
|
|
93
|
-
if (yesterdayRefs.length > 0) {
|
|
94
|
-
yesterdayPortfolios = await loadFullDayMap(config, dependencies, yesterdayRefs);
|
|
95
|
-
} else if (requires.includes('today_portfolio')) {
|
|
96
|
-
// Only warn if today_portfolio was also expected, otherwise this might be normal
|
|
97
|
-
logger.log('WARN', `${passName} Requires 'yesterday_portfolio' but no data was found for ${prevStr}.`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// --- End Dynamic Data Loading ---
|
|
101
|
-
|
|
102
|
-
// Check if any primary data source was loaded. If not, we can't run.
|
|
103
|
-
if (todayRefs.length === 0 && !todayInsightsData && !todaySocialPostInsightsData) {
|
|
104
|
-
logger.log('WARN', `${passName} No primary data sources (portfolio, insights, social) found for ${dateStr}. Skipping.`);
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// --- Context Object (no change) ---
|
|
109
|
-
const { instrumentToTicker, instrumentToSector } = await dependencies.calculationUtils.loadInstrumentMappings();
|
|
110
|
-
const context = {
|
|
111
|
-
instrumentMappings: instrumentToTicker,
|
|
112
|
-
sectorMapping: instrumentToSector,
|
|
113
|
-
todayDateStr: dateStr,
|
|
114
|
-
yesterdayDateStr: prevStr,
|
|
115
|
-
dependencies: dependencies,
|
|
116
|
-
config: config
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const calc = new CalculationClass();
|
|
120
|
-
const batchSize = config.partRefBatchSize || 10;
|
|
121
|
-
|
|
122
|
-
// --- MODIFIED: Stream and Process ---
|
|
123
|
-
let processedOnce = false; // Flag for insights/social calcs
|
|
124
|
-
const allContextArgs = [context, todayInsightsData, yesterdayInsightsData, todaySocialPostInsightsData, yesterdaySocialPostInsightsData];
|
|
125
|
-
|
|
126
|
-
// Only stream portfolios if they are required and were found
|
|
127
|
-
if (requires.includes('today_portfolio') && todayRefs.length > 0) {
|
|
128
|
-
for (let i = 0; i < todayRefs.length; i += batchSize) {
|
|
129
|
-
const batchRefs = todayRefs.slice(i, i + batchSize);
|
|
130
|
-
const todayPortfoliosChunk = await loadDataByRefs(config, dependencies, batchRefs);
|
|
131
|
-
|
|
132
|
-
for (const uid in todayPortfoliosChunk) {
|
|
133
|
-
const p = todayPortfoliosChunk[uid];
|
|
134
|
-
if (!p) continue;
|
|
135
|
-
|
|
136
|
-
// User-type filtering
|
|
137
|
-
const userType = p.PublicPositions ? 'speculator' : 'normal';
|
|
138
|
-
if ((userType === 'normal' && category === 'speculators') || (userType === 'speculator' && !['speculators', 'sanity'].includes(category))) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let processArgs;
|
|
143
|
-
if (requires.includes('yesterday_portfolio')) {
|
|
144
|
-
const pYesterday = yesterdayPortfolios[uid];
|
|
145
|
-
if (!pYesterday) continue; // Skip if user is missing yesterday's data
|
|
146
|
-
processArgs = [p, pYesterday, uid, ...allContextArgs];
|
|
147
|
-
} else {
|
|
148
|
-
processArgs = [p, null, uid, ...allContextArgs];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
await Promise.resolve(calc.process(...processArgs));
|
|
152
|
-
processedOnce = true;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// If this is an insights/social calc and no portfolios ran (or were needed), run it once.
|
|
158
|
-
if (!processedOnce && (requires.includes('today_insights') || requires.includes('today_social'))) {
|
|
159
|
-
logger.log('INFO', `${passName} Running once for insights/social data.`);
|
|
160
|
-
await Promise.resolve(calc.process(null, null, null, ...allContextArgs));
|
|
161
|
-
}
|
|
162
|
-
// --- End Stream ---
|
|
163
|
-
|
|
164
|
-
// --- Get Result and Commit (no change) ---
|
|
165
|
-
const result = await Promise.resolve(calc.getResult());
|
|
166
|
-
|
|
167
|
-
if (result === null) {
|
|
168
|
-
throw new Error(`Calculation ${calcName} returned null. Dependency likely missing.`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
|
|
172
|
-
const pendingWrites = [];
|
|
173
|
-
const summaryData = {};
|
|
174
|
-
|
|
175
|
-
if (result && Object.keys(result).length > 0) {
|
|
176
|
-
let isSharded = false;
|
|
177
|
-
const shardedCollections = {
|
|
178
|
-
'sharded_user_profile': config.shardedUserProfileCollection,
|
|
179
|
-
'sharded_user_profitability': config.shardedProfitabilityCollection
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
for (const resultKey in shardedCollections) {
|
|
183
|
-
if (result[resultKey]) {
|
|
184
|
-
isSharded = true;
|
|
185
|
-
const shardCollectionName = shardedCollections[resultKey];
|
|
186
|
-
const shardedData = result[resultKey];
|
|
187
|
-
for (const shardId in shardedData) {
|
|
188
|
-
const shardDocData = shardedData[shardId];
|
|
189
|
-
if (shardDocData && Object.keys(shardDocData).length > 0) {
|
|
190
|
-
const shardRef = db.collection(shardCollectionName).doc(shardId);
|
|
191
|
-
pendingWrites.push({ ref: shardRef, data: shardDocData });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const { [resultKey]: _, ...otherResults } = result;
|
|
195
|
-
if (Object.keys(otherResults).length > 0) {
|
|
196
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
197
|
-
.collection(config.computationsSubcollection)
|
|
198
|
-
.doc(calcName);
|
|
199
|
-
pendingWrites.push({ ref: computationDocRef, data: otherResults });
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (!isSharded) {
|
|
205
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
206
|
-
.collection(config.computationsSubcollection)
|
|
207
|
-
.doc(calcName);
|
|
208
|
-
pendingWrites.push({ ref: computationDocRef, data: result });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
summaryData[`${category}.${calcName}`] = true; // Use dot notation for summary
|
|
212
|
-
const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
|
|
213
|
-
pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
|
|
214
|
-
|
|
215
|
-
await commitBatchInChunks(config, dependencies, pendingWrites, `${passName} Commit`);
|
|
216
|
-
} else {
|
|
217
|
-
logger.log('WARN', `${passName} Calculation produced no results. Skipping write.`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Runs a *single* meta/backtest computation.
|
|
223
|
-
* (This function remains unchanged as 'meta' calcs handle their own data loading)
|
|
224
|
-
*/
|
|
225
|
-
async function runMetaComputation(dateStr, node, config, dependencies) {
|
|
226
|
-
const { db, logger } = dependencies;
|
|
227
|
-
const { category, name: calcName, calcClass: CalculationClass } = node;
|
|
228
|
-
const passName = `[Worker ${dateStr}/${calcName}]`;
|
|
229
|
-
|
|
230
|
-
logger.log('INFO', `${passName} Starting meta computation...`);
|
|
231
|
-
|
|
232
|
-
const calc = new CalculationClass();
|
|
233
|
-
const result = await Promise.resolve(calc.process(dateStr, dependencies, config));
|
|
234
|
-
|
|
235
|
-
// Handle null result as a dependency failure
|
|
236
|
-
if (result === null) {
|
|
237
|
-
throw new Error(`Meta-calculation ${calcName} returned null. Dependency likely missing.`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const resultsCollectionRef = db.collection(config.resultsCollection).doc(dateStr).collection(config.resultsSubcollection);
|
|
241
|
-
const pendingWrites = [];
|
|
242
|
-
const summaryData = {};
|
|
243
|
-
|
|
244
|
-
if (result && Object.keys(result).length > 0) {
|
|
245
|
-
const computationDocRef = resultsCollectionRef.doc(category)
|
|
246
|
-
.collection(config.computationsSubcollection)
|
|
247
|
-
.doc(calcName);
|
|
248
|
-
pendingWrites.push({ ref: computationDocRef, data: result });
|
|
249
|
-
|
|
250
|
-
summaryData[`${category}.${calcName}`] = true; // Use dot notation for summary
|
|
251
|
-
const topLevelDocRef = db.collection(config.resultsCollection).doc(dateStr);
|
|
252
|
-
pendingWrites.push({ ref: topLevelDocRef, data: summaryData });
|
|
253
|
-
|
|
254
|
-
await commitBatchInChunks(config, dependencies, pendingWrites, `${passName} Commit`);
|
|
255
|
-
} else {
|
|
256
|
-
logger.log('WARN', `${passName} Meta-calculation produced no results. Skipping write.`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// --- Main Exported Function ---
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Main pipe: pipe.computationSystem.handleNodeTask
|
|
265
|
-
* (This function remains unchanged)
|
|
266
|
-
*/
|
|
267
|
-
async function handleNodeTask(message, context, config, dependencies, calculations) {
|
|
268
|
-
const { logger, pubsubUtils } = dependencies;
|
|
269
|
-
const graph = dependencies.computationGraph; // Get the graph from dependencies
|
|
270
|
-
|
|
271
|
-
let task;
|
|
272
|
-
try {
|
|
273
|
-
task = JSON.parse(Buffer.from(message.data, 'base64').toString('utf-8'));
|
|
274
|
-
} catch (e) {
|
|
275
|
-
logger.log('ERROR', '[Compute Worker] Failed to parse Pub/Sub message data.', { error: e.message, data: message.data });
|
|
276
|
-
return; // Acknowledge the message
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const { date, nodeName } = task;
|
|
280
|
-
const taskId = `[Worker ${date}/${nodeName}]`;
|
|
281
|
-
|
|
282
|
-
if (!graph || !graph.isBuilt) {
|
|
283
|
-
logger.log('ERROR', `${taskId} CRITICAL: ComputationGraph not available. Cannot proceed.`);
|
|
284
|
-
return; // Acknowledge
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const node = graph.getNode(nodeName);
|
|
288
|
-
if (!node) {
|
|
289
|
-
logger.log('ERROR', `${taskId} CRITICAL: Node name not found in graph. Task is invalid.`);
|
|
290
|
-
return; // Acknowledge
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
logger.log('INFO', `🚀 ${taskId} Received task. Category: ${node.category}`);
|
|
294
|
-
|
|
295
|
-
try {
|
|
296
|
-
// 1. Route to the correct execution helper
|
|
297
|
-
// --- MODIFIED: Check for 'meta' requirement key ---
|
|
298
|
-
if (node.requires.includes('meta')) {
|
|
299
|
-
await runMetaComputation(date, node, config, dependencies);
|
|
300
|
-
} else {
|
|
301
|
-
// All other calcs (pnl, behavioural, etc.) use the streaming helper
|
|
302
|
-
await runStreamingComputation(date, node, config, dependencies, calculations);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// 2. On Success: Mark success and trigger children
|
|
306
|
-
const readyChildren = await stateManager.markNodeSuccessAndGetReadyChildren(dependencies, date, nodeName);
|
|
307
|
-
|
|
308
|
-
if (readyChildren.length > 0) {
|
|
309
|
-
const nextTasks = readyChildren.map(childName => ({
|
|
310
|
-
date: date,
|
|
311
|
-
nodeName: childName
|
|
312
|
-
}));
|
|
313
|
-
|
|
314
|
-
const topicName = config.computeNodeTopicName;
|
|
315
|
-
await pubsubUtils.batchPublishTasks(dependencies, {
|
|
316
|
-
topicName: topicName,
|
|
317
|
-
tasks: nextTasks,
|
|
318
|
-
taskType: `Compute Node Trigger`
|
|
319
|
-
});
|
|
320
|
-
logger.log('SUCCESS', `${taskId} Complete. Triggering ${readyChildren.length} child node(s): ${readyChildren.join(', ')}`);
|
|
321
|
-
|
|
322
|
-
} else {
|
|
323
|
-
logger.log('SUCCESS', `${taskId} Complete. No new children are ready.`);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
} catch (error) {
|
|
327
|
-
logger.log('WARN', `${taskId} FAILED (Will retry when dependencies are met).`, {
|
|
328
|
-
err: error.message,
|
|
329
|
-
stack: error.stack
|
|
330
|
-
});
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
module.exports = {
|
|
336
|
-
handleNodeTask,
|
|
337
|
-
// Exporting these for testing
|
|
338
|
-
runStreamingComputation,
|
|
339
|
-
runMetaComputation
|
|
340
|
-
};
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Manages the state of a computation DAG run in Firestore.
|
|
3
|
-
*
|
|
4
|
-
* This allows the system to be event-driven, resilient, and self-healing.
|
|
5
|
-
* It tracks which nodes have completed and which are ready to run.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const { FieldValue, FieldPath } = require('@google-cloud/firestore');
|
|
9
|
-
|
|
10
|
-
const STATE_COLLECTION = 'computation_state'; // Stores state docs, e.g., /computation_state/2025-10-31
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Initializes the state document for a new DAG run.
|
|
14
|
-
* @param {object} dependencies - Contains db, logger.
|
|
15
|
-
* @param {string} dateStr - The date (YYYY-MM-DD) to run for.
|
|
16
|
-
* @param {Array<object>} allNodes - Array of all node objects from the graph.
|
|
17
|
-
*/
|
|
18
|
-
async function initializeState(dependencies, dateStr, allNodes) {
|
|
19
|
-
const { db, logger } = dependencies;
|
|
20
|
-
const stateDocRef = db.collection(STATE_COLLECTION).doc(dateStr);
|
|
21
|
-
|
|
22
|
-
logger.log('INFO', `[StateManager] Initializing state for ${dateStr}...`);
|
|
23
|
-
|
|
24
|
-
const initialState = {
|
|
25
|
-
status: 'processing',
|
|
26
|
-
lastUpdated: FieldValue.serverTimestamp(),
|
|
27
|
-
nodes: {}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
allNodes.forEach(node => {
|
|
31
|
-
initialState.nodes[node.name] = {
|
|
32
|
-
status: 'pending',
|
|
33
|
-
category: node.category,
|
|
34
|
-
parentCount: node.parents.size,
|
|
35
|
-
parents: Array.from(node.parents),
|
|
36
|
-
children: Array.from(node.children)
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await stateDocRef.set(initialState);
|
|
41
|
-
logger.log('INFO', `[StateManager] State for ${dateStr} initialized with ${allNodes.length} nodes.`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Marks a node as 'success' and returns all children that are now ready to run.
|
|
46
|
-
* @param {object} dependencies - Contains db, logger.
|
|
47
|
-
* @param {string} dateStr - The date (YYYY-MM-DD) of the run.
|
|
48
|
-
* @param {string} nodeName - The name of the node that just completed.
|
|
49
|
-
* @returns {Promise<string[]>} A list of child node names that are ready to run.
|
|
50
|
-
*/
|
|
51
|
-
async function markNodeSuccessAndGetReadyChildren(dependencies, dateStr, nodeName) {
|
|
52
|
-
const { db, logger } = dependencies;
|
|
53
|
-
const stateDocRef = db.collection(STATE_COLLECTION).doc(dateStr);
|
|
54
|
-
|
|
55
|
-
logger.log('INFO', `[StateManager] Marking node '${nodeName}' as success for ${dateStr}.`);
|
|
56
|
-
|
|
57
|
-
const readyChildren = [];
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
await db.runTransaction(async (transaction) => {
|
|
61
|
-
const stateDoc = await transaction.get(stateDocRef);
|
|
62
|
-
if (!stateDoc.exists) {
|
|
63
|
-
logger.log('ERROR', `[StateManager] State doc for ${dateStr} does not exist. Cannot mark success.`);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const state = stateDoc.data();
|
|
68
|
-
const nodes = state.nodes;
|
|
69
|
-
|
|
70
|
-
// 1. Mark this node as success
|
|
71
|
-
const update = {};
|
|
72
|
-
update[`nodes.${nodeName}.status`] = 'success';
|
|
73
|
-
update['lastUpdated'] = FieldValue.serverTimestamp();
|
|
74
|
-
|
|
75
|
-
// 2. Check all children of this node
|
|
76
|
-
const children = nodes[nodeName]?.children || [];
|
|
77
|
-
for (const childName of children) {
|
|
78
|
-
const childNode = nodes[childName];
|
|
79
|
-
if (!childNode) {
|
|
80
|
-
logger.log('WARN', `[StateManager] Child node ${childName} not found in state doc.`);
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// If child is already done or failed, skip it
|
|
85
|
-
if (childNode.status !== 'pending') {
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Check if all parents of this child are now 'success'
|
|
90
|
-
let allParentsSuccess = true;
|
|
91
|
-
for (const parentName of childNode.parents) {
|
|
92
|
-
const parentStatus = (parentName === nodeName) ? 'success' : nodes[parentName]?.status;
|
|
93
|
-
if (parentStatus !== 'success') {
|
|
94
|
-
allParentsSuccess = false;
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (allParentsSuccess) {
|
|
100
|
-
logger.log('INFO', `[StateManager] All dependencies for '${childName}' are met. Marking as 'ready'.`);
|
|
101
|
-
update[`nodes.${childName}.status`] = 'ready'; // Mark as 'ready' to avoid double-runs
|
|
102
|
-
readyChildren.push(childName);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
transaction.update(stateDocRef, update);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
return readyChildren;
|
|
110
|
-
|
|
111
|
-
} catch (error) {
|
|
112
|
-
logger.log('ERROR', `[StateManager] Transaction failed for ${nodeName} on ${dateStr}.`, { err: error.message });
|
|
113
|
-
return []; // Return empty on error
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Finds all dates that are incomplete (status 'processing') or missing.
|
|
119
|
-
* @param {object} dependencies - Contains db, logger.
|
|
120
|
-
* @param {string[]} allExpectedDates - All dates that *should* exist.
|
|
121
|
-
* @returns {Promise<string[]>} A list of date strings (YYYY-MM-DD) to process.
|
|
122
|
-
*/
|
|
123
|
-
async function getIncompleteDates(dependencies, allExpectedDates) {
|
|
124
|
-
const { db, logger } = dependencies;
|
|
125
|
-
if (allExpectedDates.length === 0) return [];
|
|
126
|
-
|
|
127
|
-
logger.log('INFO', `[StateManager] Checking for incomplete dates...`);
|
|
128
|
-
|
|
129
|
-
const datesToProcess = new Set();
|
|
130
|
-
const refs = allExpectedDates.map(dateStr => db.collection(STATE_COLLECTION).doc(dateStr));
|
|
131
|
-
const snapshots = await db.getAll(...refs);
|
|
132
|
-
|
|
133
|
-
for (let i = 0; i < snapshots.length; i++) {
|
|
134
|
-
const dateStr = allExpectedDates[i];
|
|
135
|
-
const doc = snapshots[i];
|
|
136
|
-
|
|
137
|
-
if (!doc.exists) {
|
|
138
|
-
// Date is completely missing
|
|
139
|
-
datesToProcess.add(dateStr);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const data = doc.data();
|
|
144
|
-
if (data.status === 'processing') {
|
|
145
|
-
// Date was started but not finished (e.g., a node failed)
|
|
146
|
-
datesToProcess.add(dateStr);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// If status is 'complete', we're good.
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
logger.log('INFO', `[StateManager] Found ${datesToProcess.size} dates to process.`);
|
|
154
|
-
return Array.from(datesToProcess);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Marks an entire DAG run as 'complete'.
|
|
159
|
-
* @param {object} dependencies - Contains db, logger.
|
|
160
|
-
* @param {string} dateStr - The date (YYYY-MM-DD) to mark as complete.
|
|
161
|
-
*/
|
|
162
|
-
async function markDagComplete(dependencies, dateStr) {
|
|
163
|
-
const { db, logger } = dependencies;
|
|
164
|
-
const stateDocRef = db.collection(STATE_COLLECTION).doc(dateStr);
|
|
165
|
-
|
|
166
|
-
logger.log('SUCCESS', `[StateManager] Marking DAG for ${dateStr} as complete.`);
|
|
167
|
-
await stateDocRef.update({
|
|
168
|
-
status: 'complete',
|
|
169
|
-
lastUpdated: FieldValue.serverTimestamp()
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
module.exports = {
|
|
174
|
-
initializeState,
|
|
175
|
-
markNodeSuccessAndGetReadyChildren,
|
|
176
|
-
getIncompleteDates,
|
|
177
|
-
markDagComplete
|
|
178
|
-
};
|