bulltrackers-module 1.0.821 → 1.0.823
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.
|
@@ -29,7 +29,7 @@ module.exports = {
|
|
|
29
29
|
queueName: 'computation-triggers-new',
|
|
30
30
|
dispatcherUrl: process.env.DISPATCHER_URL || 'https://europe-west1-stocks-12345.cloudfunctions.net/compute-dispatcher',
|
|
31
31
|
workerUrl: process.env.WORKER_URL || process.env.DISPATCHER_URL || 'https://europe-west1-stocks-12345.cloudfunctions.net/compute-dispatcher',
|
|
32
|
-
serviceAccountEmail: process.env.
|
|
32
|
+
serviceAccountEmail: process.env.CLOUD_TASKS_SA_EMAIL || '879684846540-compute@developer.gserviceaccount.com'
|
|
33
33
|
},
|
|
34
34
|
|
|
35
35
|
storage: {
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Cloud Tasks Adapter (
|
|
3
|
-
*
|
|
4
|
-
* - Logs full payload before creation.
|
|
5
|
-
* - Logs raw gRPC errors.
|
|
2
|
+
* @fileoverview Cloud Tasks Adapter (V3 - Production Complete)
|
|
3
|
+
* Encapsulates Google Cloud Tasks interactions with full V2 feature parity.
|
|
6
4
|
*/
|
|
7
5
|
|
|
6
|
+
|
|
8
7
|
class CloudTasksAdapter {
|
|
9
8
|
constructor(client, config, logger = console) {
|
|
10
9
|
this.client = client;
|
|
@@ -12,19 +11,22 @@ class CloudTasksAdapter {
|
|
|
12
11
|
this.logger = logger;
|
|
13
12
|
this.projectId = config.projectId;
|
|
14
13
|
this.location = config.location || 'europe-west1';
|
|
15
|
-
this.queueName = config.queueName || 'computation-
|
|
14
|
+
this.queueName = config.queueName || 'computation-tasks-new';
|
|
16
15
|
this.dispatcherUrl = config.dispatcherUrl;
|
|
17
|
-
this.workerUrl = config.workerUrl || config.dispatcherUrl;
|
|
18
|
-
this.serviceAccountEmail =
|
|
16
|
+
this.workerUrl = config.workerUrl || config.dispatcherUrl; // Fallback to dispatcher if not set
|
|
17
|
+
this.serviceAccountEmail = config.serviceAccountEmail;
|
|
19
18
|
|
|
19
|
+
// Internal concurrency limit for bulk operations
|
|
20
20
|
this.concurrency = 20;
|
|
21
|
-
this._queuePath = null;
|
|
22
21
|
|
|
23
|
-
//
|
|
24
|
-
this.
|
|
25
|
-
this.logger.log(`[CloudTasks] INIT: ServiceAccount=${this.serviceAccountEmail} (Target Audience: ${this.dispatcherUrl})\n`);
|
|
22
|
+
// Cache queue path
|
|
23
|
+
this._queuePath = null;
|
|
26
24
|
}
|
|
27
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Get the full queue path (cached).
|
|
28
|
+
* @returns {string} Full queue path
|
|
29
|
+
*/
|
|
28
30
|
getQueuePath() {
|
|
29
31
|
if (!this._queuePath) {
|
|
30
32
|
this._queuePath = this.client.queuePath(this.projectId, this.location, this.queueName);
|
|
@@ -32,61 +34,91 @@ class CloudTasksAdapter {
|
|
|
32
34
|
return this._queuePath;
|
|
33
35
|
}
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Create a task with OIDC authentication.
|
|
39
|
+
* @param {string} fullQueuePath - Full queue path from getQueuePath()
|
|
40
|
+
* @param {Object} payload - JSON payload for the task
|
|
41
|
+
* @param {Object} options - { scheduleTime: epochSeconds, name: string }
|
|
42
|
+
* @returns {Promise<Object>} Task response or { status: 'exists' }
|
|
43
|
+
*/
|
|
35
44
|
async createTask(fullQueuePath, payload, options = {}) {
|
|
36
|
-
const targetUrl = options.url || (payload.type === 'worker-batch' ? this.workerUrl : this.dispatcherUrl);
|
|
37
|
-
|
|
38
|
-
// --- TEST MODIFICATION: STRIP COMPLEXITY ---
|
|
39
45
|
const task = {
|
|
40
46
|
httpRequest: {
|
|
41
47
|
httpMethod: 'POST',
|
|
42
|
-
url:
|
|
48
|
+
url: options.url || (payload.type === 'worker-batch' ? this.workerUrl : this.dispatcherUrl),
|
|
43
49
|
headers: { 'Content-Type': 'application/json' },
|
|
44
50
|
body: Buffer.from(JSON.stringify(payload)).toString('base64'),
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// }
|
|
51
|
+
oidcToken: {
|
|
52
|
+
serviceAccountEmail: this.serviceAccountEmail,
|
|
53
|
+
audience: options.url || (payload.type === 'worker-batch' ? this.workerUrl : this.dispatcherUrl)
|
|
54
|
+
}
|
|
50
55
|
}
|
|
51
56
|
};
|
|
52
57
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
// Optional: Named task for deduplication
|
|
59
|
+
if (options.name) {
|
|
60
|
+
task.name = `${fullQueuePath}/tasks/${options.name}`;
|
|
61
|
+
}
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
// Optional: Scheduled execution
|
|
64
|
+
if (options.scheduleTime) {
|
|
65
|
+
task.scheduleTime = {
|
|
66
|
+
seconds: Math.floor(options.scheduleTime)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
58
69
|
|
|
59
70
|
try {
|
|
60
|
-
const [response] = await this.client.createTask({
|
|
61
|
-
|
|
71
|
+
const [response] = await this.client.createTask({
|
|
72
|
+
parent: fullQueuePath,
|
|
73
|
+
task
|
|
74
|
+
});
|
|
62
75
|
return response;
|
|
63
76
|
} catch (e) {
|
|
64
|
-
|
|
77
|
+
// ALREADY_EXISTS (gRPC code 6 or HTTP 409)
|
|
78
|
+
if (e.code === 6 || e.code === 409) {
|
|
79
|
+
return { status: 'exists' };
|
|
80
|
+
}
|
|
65
81
|
throw e;
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Delete a task by full name.
|
|
87
|
+
* @param {string} taskName - Full task name
|
|
88
|
+
* @returns {Promise<boolean>} true if deleted, false if not found
|
|
89
|
+
*/
|
|
69
90
|
async deleteTask(taskName) {
|
|
70
91
|
try {
|
|
71
92
|
await this.client.deleteTask({ name: taskName });
|
|
72
93
|
return true;
|
|
73
94
|
} catch (e) {
|
|
74
|
-
if (e.code === 5) return false;
|
|
95
|
+
if (e.code === 5) return false; // NOT_FOUND
|
|
75
96
|
throw e;
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
|
|
100
|
+
/**
|
|
101
|
+
* List all tasks in the queue (async iterator).
|
|
102
|
+
* @param {string} fullQueuePath - Queue path
|
|
103
|
+
* @yields {Object} Task object
|
|
104
|
+
*/
|
|
79
105
|
async *listTasks(fullQueuePath) {
|
|
80
106
|
const iterable = this.client.listTasksAsync({
|
|
81
107
|
parent: fullQueuePath,
|
|
82
108
|
responseView: 'BASIC',
|
|
83
109
|
pageSize: 1000
|
|
84
110
|
});
|
|
111
|
+
|
|
85
112
|
for await (const task of iterable) {
|
|
86
113
|
yield task;
|
|
87
114
|
}
|
|
88
115
|
}
|
|
89
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Bulk delete tasks with concurrency control.
|
|
119
|
+
* @param {string[]} taskNames - Array of full task names
|
|
120
|
+
* @returns {Promise<Object>} { success: number, failed: number }
|
|
121
|
+
*/
|
|
90
122
|
async bulkDelete(taskNames) {
|
|
91
123
|
const results = { success: 0, failed: 0 };
|
|
92
124
|
const chunkSize = this.concurrency;
|
|
@@ -107,11 +139,19 @@ class CloudTasksAdapter {
|
|
|
107
139
|
}
|
|
108
140
|
}));
|
|
109
141
|
}
|
|
142
|
+
|
|
110
143
|
return results;
|
|
111
144
|
}
|
|
112
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Schedule a root computation task (Planner-compatible).
|
|
148
|
+
* @param {Object} taskData - { computation, date, hash, source, force }
|
|
149
|
+
* @returns {Promise<Object>} { status: 'scheduled' | 'exists' | 'error', id }
|
|
150
|
+
*/
|
|
113
151
|
async scheduleTask(taskData) {
|
|
114
152
|
const { computation, targetDate, configHash, reason, runAtSeconds } = taskData;
|
|
153
|
+
|
|
154
|
+
// Generate deterministic task ID for deduplication
|
|
115
155
|
const normalizedName = computation.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
116
156
|
const hashSuffix = configHash ? configHash.substring(0, 8) : 'noHash';
|
|
117
157
|
const taskId = `root-${normalizedName}-${targetDate}-${hashSuffix}`;
|
|
@@ -128,45 +168,82 @@ class CloudTasksAdapter {
|
|
|
128
168
|
try {
|
|
129
169
|
const queuePath = this.getQueuePath();
|
|
130
170
|
const options = { name: taskId };
|
|
131
|
-
|
|
171
|
+
|
|
172
|
+
if (runAtSeconds) {
|
|
173
|
+
options.scheduleTime = runAtSeconds;
|
|
174
|
+
}
|
|
132
175
|
|
|
133
176
|
const result = await this.createTask(queuePath, payload, options);
|
|
134
|
-
|
|
177
|
+
|
|
178
|
+
if (result.status === 'exists') {
|
|
179
|
+
return { status: 'exists', id: taskId };
|
|
180
|
+
}
|
|
181
|
+
|
|
135
182
|
return { status: 'scheduled', id: taskId };
|
|
183
|
+
|
|
136
184
|
} catch (e) {
|
|
137
|
-
|
|
138
|
-
this.logger.error(`[CloudTasks] FAILED to schedule ${taskId}: ${e.message}`);
|
|
185
|
+
this.logger.error(`[CloudTasks] Schedule failed for ${computation}@${targetDate}: ${e.message}`);
|
|
139
186
|
return { status: 'error', error: e.message, id: taskId };
|
|
140
187
|
}
|
|
141
188
|
}
|
|
142
189
|
|
|
190
|
+
/**
|
|
191
|
+
* List and clean stale tasks (Scheduler GC support).
|
|
192
|
+
* Removes tasks with outdated config hashes.
|
|
193
|
+
* @param {Map<string, string>} activeHashes - Map<computationName, currentHash>
|
|
194
|
+
* @returns {Promise<Object>} { deleted: number, activeKeys: Set<string> }
|
|
195
|
+
*/
|
|
143
196
|
async listAndClean(activeHashes) {
|
|
144
197
|
const queuePath = this.getQueuePath();
|
|
145
198
|
const tasksToDelete = [];
|
|
146
199
|
const activeKeys = new Set();
|
|
200
|
+
|
|
147
201
|
try {
|
|
148
202
|
for await (const task of this.listTasks(queuePath)) {
|
|
203
|
+
// Parse task name to extract computation and hash
|
|
204
|
+
// Expected format: projects/.../tasks/root-{comp}-{date}-{hash}
|
|
149
205
|
const taskNameParts = task.name.split('/');
|
|
150
206
|
const taskId = taskNameParts[taskNameParts.length - 1];
|
|
207
|
+
|
|
208
|
+
// Extract hash from task ID
|
|
151
209
|
const match = taskId.match(/root-(.+?)-(\d{4}-\d{2}-\d{2})-([a-f0-9]+)/);
|
|
152
|
-
if (!match) {
|
|
210
|
+
if (!match) {
|
|
211
|
+
// Unknown format, skip
|
|
212
|
+
activeKeys.add(taskId);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
153
216
|
const [, compNormalized, date, hash] = match;
|
|
217
|
+
|
|
218
|
+
// Find matching computation in activeHashes
|
|
154
219
|
let isStale = true;
|
|
155
220
|
for (const [compName, currentHash] of activeHashes.entries()) {
|
|
156
221
|
const normalizedComp = compName.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
157
222
|
if (normalizedComp === compNormalized) {
|
|
158
|
-
if (currentHash.startsWith(hash)) {
|
|
223
|
+
if (currentHash.startsWith(hash)) {
|
|
224
|
+
// Task is current
|
|
225
|
+
isStale = false;
|
|
226
|
+
activeKeys.add(taskId);
|
|
227
|
+
}
|
|
159
228
|
break;
|
|
160
229
|
}
|
|
161
230
|
}
|
|
162
|
-
|
|
231
|
+
|
|
232
|
+
if (isStale) {
|
|
233
|
+
tasksToDelete.push(task.name);
|
|
234
|
+
}
|
|
163
235
|
}
|
|
236
|
+
|
|
237
|
+
// Bulk delete stale tasks
|
|
164
238
|
let deleted = 0;
|
|
165
239
|
if (tasksToDelete.length > 0) {
|
|
240
|
+
this.logger.log(`[CloudTasks] Cleaning ${tasksToDelete.length} stale tasks...`);
|
|
166
241
|
const result = await this.bulkDelete(tasksToDelete);
|
|
167
242
|
deleted = result.success;
|
|
168
243
|
}
|
|
244
|
+
|
|
169
245
|
return { deleted, activeKeys };
|
|
246
|
+
|
|
170
247
|
} catch (e) {
|
|
171
248
|
this.logger.error(`[CloudTasks] listAndClean failed: ${e.message}`);
|
|
172
249
|
return { deleted: 0, activeKeys: new Set() };
|