@tstdl/base 0.93.140 → 0.93.141
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/application/application.d.ts +1 -1
- package/application/application.js +1 -1
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/module/modules/web-server.module.js +2 -3
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-flow.test.js +2 -2
- package/orm/decorators.d.ts +5 -1
- package/orm/decorators.js +1 -1
- package/orm/server/drizzle/schema-converter.js +17 -30
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/index.d.ts +1 -6
- package/orm/server/index.js +1 -6
- package/orm/server/migration.d.ts +19 -0
- package/orm/server/migration.js +72 -0
- package/orm/server/repository.d.ts +1 -1
- package/orm/server/transaction.d.ts +5 -10
- package/orm/server/transaction.js +22 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-migration.test.d.ts +1 -0
- package/orm/tests/database-migration.test.js +82 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +4 -3
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/decorator-data.js +11 -12
- package/task-queue/README.md +2 -9
- package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +5 -1
- package/task-queue/postgres/schemas.d.ts +9 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +2 -12
- package/task-queue/postgres/task-queue.js +431 -354
- package/task-queue/postgres/task.model.d.ts +12 -5
- package/task-queue/postgres/task.model.js +51 -25
- package/task-queue/task-context.d.ts +2 -2
- package/task-queue/task-context.js +7 -7
- package/task-queue/task-queue.d.ts +36 -19
- package/task-queue/task-queue.js +18 -10
- package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
- package/task-queue/tests/cascading-cancellations.test.js +38 -0
- package/task-queue/tests/complex.test.js +44 -228
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +407 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +144 -0
- package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
- package/task-queue/tests/dag-dependencies.test.js +41 -0
- package/task-queue/tests/dependencies.test.js +26 -26
- package/task-queue/tests/extensive-dependencies.test.js +64 -139
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +53 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +61 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
- package/task-queue/tests/queue.test.js +33 -24
- package/task-queue/tests/worker.test.js +20 -5
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/test5.js +5 -5
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +54 -29
- package/text/localization.service.js +2 -2
- package/utils/file-reader.js +1 -2
|
@@ -56,7 +56,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
|
|
|
56
56
|
var e = new Error(message);
|
|
57
57
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
58
58
|
});
|
|
59
|
-
import { and, asc, count, eq, gt, gte, inArray, lt, lte,
|
|
59
|
+
import { aliasedTable, and, asc, count, eq, gt, gte, inArray, lt, lte, notExists, or, sql, count as sqlCount, isNull as sqlIsNull } from 'drizzle-orm';
|
|
60
60
|
import { merge, throttleTime } from 'rxjs';
|
|
61
61
|
import { CancellationSignal } from '../../cancellation/index.js';
|
|
62
62
|
import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
|
|
@@ -64,22 +64,20 @@ import { serializeError, TimeoutError } from '../../errors/index.js';
|
|
|
64
64
|
import { afterResolve, inject, provide, Singleton } from '../../injector/index.js';
|
|
65
65
|
import { Logger } from '../../logger/index.js';
|
|
66
66
|
import { MessageBus } from '../../message-bus/index.js';
|
|
67
|
-
import { arrayOverlaps, coalesce, interval, RANDOM_UUID_V4, TRANSACTION_TIMESTAMP
|
|
67
|
+
import { arrayOverlaps, coalesce, getEntityIds, interval, RANDOM_UUID_V4, TRANSACTION_TIMESTAMP } from '../../orm/index.js';
|
|
68
68
|
import { DatabaseConfig, injectRepository } from '../../orm/server/index.js';
|
|
69
69
|
import { RateLimiter } from '../../rate-limit/index.js';
|
|
70
|
-
import { createArray,
|
|
71
|
-
import { digest } from '../../utils/cryptography.js';
|
|
70
|
+
import { createArray, toArray } from '../../utils/array/array.js';
|
|
72
71
|
import { currentTimestamp } from '../../utils/date-time.js';
|
|
73
72
|
import { Timer } from '../../utils/timer.js';
|
|
74
73
|
import { cancelableTimeout } from '../../utils/timing.js';
|
|
75
|
-
import { isDefined, isNotNull, isNull, isString, isUndefined } from '../../utils/type-guards.js';
|
|
74
|
+
import { isDefined, isNotNull, isNotNullOrUndefined, isNull, isString, isUndefined } from '../../utils/type-guards.js';
|
|
76
75
|
import { millisecondsPerSecond } from '../../utils/units.js';
|
|
77
|
-
import { defaultQueueConfig,
|
|
76
|
+
import { defaultQueueConfig, TaskDependencyType, TaskQueue, TaskStatus } from '../task-queue.js';
|
|
78
77
|
import { PostgresTaskQueueModuleConfig } from './module.js';
|
|
79
|
-
import { taskArchive as taskArchiveTable, taskStatus, task as taskTable } from './schemas.js';
|
|
78
|
+
import { taskArchive as taskArchiveTable, taskDependency as taskDependencyTable, taskStatus, task as taskTable } from './schemas.js';
|
|
80
79
|
import { PostgresTask, PostgresTaskArchive } from './task.model.js';
|
|
81
80
|
let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
82
|
-
#internalThis = this;
|
|
83
81
|
#repository = injectRepository(PostgresTask);
|
|
84
82
|
#archiveRepository = injectRepository(PostgresTaskArchive);
|
|
85
83
|
#config = this.config;
|
|
@@ -113,20 +111,17 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
113
111
|
idempotencyWindow = this.#config.idempotencyWindow ?? defaultQueueConfig.idempotencyWindow;
|
|
114
112
|
globalConcurrency = this.#config.globalConcurrency ?? defaultQueueConfig.globalConcurrency;
|
|
115
113
|
#takeNewUpdate = {
|
|
116
|
-
id: RANDOM_UUID_V4,
|
|
117
114
|
namespace: this.#namespace,
|
|
118
115
|
type: sql `excluded.type`,
|
|
119
|
-
status: TaskStatus.Pending
|
|
116
|
+
status: sql `CASE WHEN excluded.unresolved_schedule_dependencies > 0 THEN ${TaskStatus.Waiting}::${taskStatus} ELSE ${TaskStatus.Pending}::${taskStatus} END`,
|
|
120
117
|
token: null,
|
|
121
118
|
priority: sql `excluded.priority`,
|
|
122
119
|
idempotencyKey: sql `excluded.idempotency_key`,
|
|
123
120
|
traceId: sql `excluded.trace_id`,
|
|
124
121
|
tags: sql `excluded.tags`,
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
unresolvedScheduleDependencies: sql `excluded.unresolved_schedule_dependencies`,
|
|
123
|
+
unresolvedCompleteDependencies: sql `excluded.unresolved_complete_dependencies`,
|
|
127
124
|
failFast: sql `excluded.fail_fast`,
|
|
128
|
-
dependencyJoinMode: sql `excluded.dependency_join_mode`,
|
|
129
|
-
dependencyTriggerStatuses: sql `excluded.dependency_trigger_statuses`,
|
|
130
125
|
tries: 0,
|
|
131
126
|
creationTimestamp: TRANSACTION_TIMESTAMP,
|
|
132
127
|
priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
|
|
@@ -143,7 +138,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
143
138
|
};
|
|
144
139
|
[afterResolve]() {
|
|
145
140
|
if (!this.isInTransaction) {
|
|
146
|
-
this.startSystemWorker();
|
|
147
141
|
void this.maintenanceLoop();
|
|
148
142
|
}
|
|
149
143
|
}
|
|
@@ -158,21 +152,21 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
158
152
|
return (options?.returnTasks == true) ? [] : undefined;
|
|
159
153
|
}
|
|
160
154
|
const newEntities = items.map((item) => {
|
|
161
|
-
const
|
|
155
|
+
const scheduleAfterCount = item.scheduleAfter?.length ?? 0;
|
|
156
|
+
const completeAfterCount = item.completeAfter?.length ?? 0;
|
|
157
|
+
const isWaiting = scheduleAfterCount > 0;
|
|
162
158
|
return {
|
|
163
159
|
namespace: this.#namespace,
|
|
164
160
|
type: item.type,
|
|
165
|
-
status:
|
|
161
|
+
status: isWaiting ? TaskStatus.Waiting : TaskStatus.Pending,
|
|
166
162
|
token: null,
|
|
167
163
|
priority: item.priority ?? 1000,
|
|
168
164
|
idempotencyKey: item.idempotencyKey ?? null,
|
|
169
165
|
traceId: null,
|
|
170
166
|
tags: item.tags ?? [],
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
unresolvedScheduleDependencies: scheduleAfterCount,
|
|
168
|
+
unresolvedCompleteDependencies: completeAfterCount,
|
|
173
169
|
failFast: item.failFast ?? false,
|
|
174
|
-
dependencyJoinMode: item.dependencyJoinMode ?? DependencyJoinMode.And,
|
|
175
|
-
dependencyTriggerStatuses: item.dependencyTriggerStatuses ?? [TaskStatus.Completed],
|
|
176
170
|
parentId: item.parentId ?? null,
|
|
177
171
|
tries: 0,
|
|
178
172
|
progress: 0,
|
|
@@ -191,42 +185,117 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
191
185
|
});
|
|
192
186
|
const itemsWithIdempotency = newEntities.filter((e) => isNotNull(e.idempotencyKey));
|
|
193
187
|
const itemsWithoutIdempotency = newEntities.filter((e) => isNull(e.idempotencyKey));
|
|
194
|
-
const hasDependencies = items.some((item) => ((item.
|
|
188
|
+
const hasDependencies = items.some((item) => ((item.scheduleAfter?.length ?? 0) > 0) || ((item.completeAfter?.length ?? 0) > 0) || (isDefined(item.parentId) && (item.waitForCompletion ?? true)));
|
|
195
189
|
const mustUseTransaction = ((itemsWithoutIdempotency.length + itemsWithIdempotency.length) > 1) || hasDependencies;
|
|
196
190
|
const newTransaction = __addDisposableResource(env_1, (mustUseTransaction && isUndefined(options?.transaction)) ? await this.#repository.startTransaction() : undefined, true);
|
|
197
191
|
const transaction = newTransaction ?? options?.transaction;
|
|
192
|
+
const session = transaction?.pgTransaction ?? this.#repository.session;
|
|
198
193
|
const tasks = [];
|
|
199
194
|
if (itemsWithoutIdempotency.length > 0) {
|
|
200
|
-
const
|
|
201
|
-
tasks.push(...
|
|
195
|
+
const insertedRows = await session.insert(taskTable).values(itemsWithoutIdempotency).returning();
|
|
196
|
+
tasks.push(...insertedRows);
|
|
202
197
|
}
|
|
203
198
|
if (itemsWithIdempotency.length > 0) {
|
|
204
|
-
const repository = this.#repository.withOptionalTransaction(transaction);
|
|
205
199
|
const windowCutoff = sql `${TRANSACTION_TIMESTAMP} - ${interval(this.idempotencyWindow, 'milliseconds')}`;
|
|
206
200
|
const setWhere = (options?.replace == true) ? undefined : sql `${taskTable.creationTimestamp} < ${windowCutoff}`;
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
201
|
+
const upsertedRows = await session
|
|
202
|
+
.insert(taskTable)
|
|
203
|
+
.values(itemsWithIdempotency)
|
|
204
|
+
.onConflictDoUpdate({
|
|
205
|
+
target: [taskTable.namespace, taskTable.idempotencyKey],
|
|
206
|
+
set: this.#takeNewUpdate,
|
|
207
|
+
setWhere,
|
|
208
|
+
})
|
|
209
|
+
.returning();
|
|
210
|
+
tasks.push(...upsertedRows);
|
|
211
|
+
if (options?.replace == true) {
|
|
212
|
+
const upsertedIds = upsertedRows.map((t) => t.id);
|
|
213
|
+
if (upsertedIds.length > 0) {
|
|
214
|
+
await session.delete(taskDependencyTable).where(and(eq(taskDependencyTable.namespace, this.#namespace), inArray(taskDependencyTable.taskId, upsertedIds)));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (((options?.returnTasks == true) || hasDependencies) && (upsertedRows.length < itemsWithIdempotency.length)) {
|
|
218
|
+
const upsertedKeys = new Set(upsertedRows.map((t) => t.idempotencyKey));
|
|
211
219
|
const missingKeys = itemsWithIdempotency.map((i) => i.idempotencyKey).filter((k) => !upsertedKeys.has(k));
|
|
212
220
|
if (missingKeys.length > 0) {
|
|
213
|
-
const
|
|
221
|
+
const existingRows = await session
|
|
222
|
+
.select()
|
|
223
|
+
.from(taskTable)
|
|
224
|
+
.where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.idempotencyKey, missingKeys)));
|
|
225
|
+
tasks.push(...existingRows);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (hasDependencies) {
|
|
230
|
+
const dependencies = [];
|
|
231
|
+
const parentIncrements = new Map();
|
|
232
|
+
for (const [index, item] of items.entries()) {
|
|
233
|
+
const task = tasks[index];
|
|
234
|
+
if (isDefined(item.parentId) && (item.waitForCompletion ?? true)) {
|
|
235
|
+
dependencies.push({
|
|
214
236
|
namespace: this.#namespace,
|
|
215
|
-
|
|
237
|
+
taskId: item.parentId,
|
|
238
|
+
dependencyTaskId: task.id,
|
|
239
|
+
type: TaskDependencyType.Complete,
|
|
240
|
+
requiredStatuses: [TaskStatus.Completed],
|
|
216
241
|
});
|
|
217
|
-
|
|
242
|
+
parentIncrements.set(item.parentId, (parentIncrements.get(item.parentId) ?? 0) + 1);
|
|
243
|
+
}
|
|
244
|
+
if (isDefined(item.scheduleAfter)) {
|
|
245
|
+
for (const dependency of item.scheduleAfter) {
|
|
246
|
+
const dependencyTaskId = isString(dependency) ? dependency : dependency.id;
|
|
247
|
+
const requiredStatuses = (isString(dependency) ? [TaskStatus.Completed] : (dependency.requiredStatuses ?? [TaskStatus.Completed]));
|
|
248
|
+
dependencies.push({
|
|
249
|
+
namespace: this.#namespace,
|
|
250
|
+
taskId: task.id,
|
|
251
|
+
dependencyTaskId,
|
|
252
|
+
type: TaskDependencyType.Schedule,
|
|
253
|
+
requiredStatuses,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (isDefined(item.completeAfter)) {
|
|
258
|
+
for (const dependency of item.completeAfter) {
|
|
259
|
+
const dependencyTaskId = isString(dependency) ? dependency : dependency.id;
|
|
260
|
+
const requiredStatuses = (isString(dependency) ? [TaskStatus.Completed] : (dependency.requiredStatuses ?? [TaskStatus.Completed]));
|
|
261
|
+
dependencies.push({
|
|
262
|
+
namespace: this.#namespace,
|
|
263
|
+
taskId: task.id,
|
|
264
|
+
dependencyTaskId,
|
|
265
|
+
type: TaskDependencyType.Complete,
|
|
266
|
+
requiredStatuses,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (dependencies.length > 0) {
|
|
272
|
+
const inserted = await session
|
|
273
|
+
.insert(taskDependencyTable)
|
|
274
|
+
.values(dependencies)
|
|
275
|
+
.onConflictDoNothing()
|
|
276
|
+
.returning();
|
|
277
|
+
if (parentIncrements.size > 0) {
|
|
278
|
+
for (const [parentId, increment] of parentIncrements) {
|
|
279
|
+
// We only increment for those that were actually inserted.
|
|
280
|
+
// Since we use onConflict: 'do-nothing', inserted array only contains actually inserted ones.
|
|
281
|
+
const actualIncrement = inserted.filter((d) => d.taskId == parentId && d.type == TaskDependencyType.Complete).length;
|
|
282
|
+
if (actualIncrement > 0) {
|
|
283
|
+
await session
|
|
284
|
+
.update(taskTable)
|
|
285
|
+
.set({
|
|
286
|
+
unresolvedCompleteDependencies: sql `${taskTable.unresolvedCompleteDependencies} + ${actualIncrement}`,
|
|
287
|
+
})
|
|
288
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, parentId)));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
218
291
|
}
|
|
219
292
|
}
|
|
220
293
|
}
|
|
221
294
|
await newTransaction?.commit();
|
|
222
295
|
this.#messageBus.publishAndForget();
|
|
223
|
-
const anyWaiting = tasks.some((t) => t.status == TaskStatus.Waiting);
|
|
224
|
-
if (anyWaiting) {
|
|
225
|
-
const tagsToTrigger = distinct(tasks.filter((t) => t.status == TaskStatus.Waiting).flatMap((t) => [...t.completeAfterTags, ...t.scheduleAfterTags]));
|
|
226
|
-
await this.triggerTagFanIn(tagsToTrigger, { transaction });
|
|
227
|
-
}
|
|
228
296
|
if (options?.returnTasks == true) {
|
|
229
|
-
|
|
297
|
+
const mapped = await this.#repository.mapManyToEntity(tasks);
|
|
298
|
+
return mapped;
|
|
230
299
|
}
|
|
231
300
|
return undefined;
|
|
232
301
|
}
|
|
@@ -241,38 +310,55 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
241
310
|
}
|
|
242
311
|
}
|
|
243
312
|
async has(id, options) {
|
|
244
|
-
|
|
313
|
+
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
314
|
+
const [result] = await session
|
|
315
|
+
.select({ id: taskTable.id })
|
|
316
|
+
.from(taskTable)
|
|
317
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)))
|
|
318
|
+
.limit(1);
|
|
319
|
+
return isDefined(result);
|
|
245
320
|
}
|
|
246
321
|
async countByTags(tags, options) {
|
|
247
322
|
const tagArray = toArray(tags);
|
|
248
323
|
if (tagArray.length == 0) {
|
|
249
324
|
return 0;
|
|
250
325
|
}
|
|
251
|
-
const
|
|
326
|
+
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
327
|
+
const [result] = await session
|
|
252
328
|
.select({ count: count() })
|
|
253
329
|
.from(taskTable)
|
|
254
|
-
.where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)))
|
|
255
|
-
.execute();
|
|
330
|
+
.where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)));
|
|
256
331
|
return result?.count ?? 0;
|
|
257
332
|
}
|
|
258
333
|
async getTask(id, options) {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
334
|
+
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
335
|
+
const [activeRow] = await session
|
|
336
|
+
.select()
|
|
337
|
+
.from(taskTable)
|
|
338
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)));
|
|
339
|
+
if (isDefined(activeRow)) {
|
|
340
|
+
return await this.#repository.mapToEntity(activeRow);
|
|
262
341
|
}
|
|
263
|
-
|
|
342
|
+
const [archiveRow] = await session
|
|
343
|
+
.select()
|
|
344
|
+
.from(taskArchiveTable)
|
|
345
|
+
.where(and(eq(taskArchiveTable.namespace, this.#namespace), eq(taskArchiveTable.id, id)));
|
|
346
|
+
if (isDefined(archiveRow)) {
|
|
347
|
+
return await this.#archiveRepository.mapToEntity(archiveRow);
|
|
348
|
+
}
|
|
349
|
+
return undefined;
|
|
264
350
|
}
|
|
265
351
|
async getManyByTags(tags, options) {
|
|
266
352
|
const tagArray = toArray(tags);
|
|
267
353
|
if (tagArray.length == 0) {
|
|
268
354
|
return [];
|
|
269
355
|
}
|
|
270
|
-
const
|
|
356
|
+
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
357
|
+
const rows = await session
|
|
271
358
|
.select()
|
|
272
359
|
.from(taskTable)
|
|
273
|
-
.where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)))
|
|
274
|
-
|
|
275
|
-
return await this.#repository.withOptionalTransaction(options?.transaction).mapManyToEntity(rows);
|
|
360
|
+
.where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, tagArray)));
|
|
361
|
+
return await this.#repository.mapManyToEntity(rows);
|
|
276
362
|
}
|
|
277
363
|
async getTree(rootId, options) {
|
|
278
364
|
const ids = toArray(rootId);
|
|
@@ -280,13 +366,16 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
280
366
|
}
|
|
281
367
|
async getTreeByQuery(query, options) {
|
|
282
368
|
return await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
283
|
-
const
|
|
284
|
-
const roots = await
|
|
369
|
+
const querySql = this.#repository.convertQuery(query);
|
|
370
|
+
const roots = await tx.pgTransaction
|
|
371
|
+
.select({ id: taskTable.id })
|
|
372
|
+
.from(taskTable)
|
|
373
|
+
.where(querySql);
|
|
285
374
|
if (roots.length == 0) {
|
|
286
375
|
return [];
|
|
287
376
|
}
|
|
288
377
|
const rootIds = roots.map((t) => t.id);
|
|
289
|
-
const
|
|
378
|
+
const { rows } = await tx.pgTransaction.execute(sql `
|
|
290
379
|
WITH RECURSIVE task_tree AS (
|
|
291
380
|
SELECT *, 0 as depth
|
|
292
381
|
FROM ${taskTable}
|
|
@@ -301,8 +390,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
301
390
|
)
|
|
302
391
|
SELECT * FROM task_tree
|
|
303
392
|
`);
|
|
304
|
-
|
|
305
|
-
return await repositoryWithTransaction.mapManyToEntity(rows);
|
|
393
|
+
return await this.#repository.mapManyToEntity(rows);
|
|
306
394
|
});
|
|
307
395
|
}
|
|
308
396
|
async waitForTasks(ids, options) {
|
|
@@ -312,7 +400,8 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
312
400
|
const timeout = options?.timeout ?? Infinity;
|
|
313
401
|
const interval = options?.interval ?? 1000;
|
|
314
402
|
const cancellationSignal = this.#cancellationSignal.optionallyInherit(options?.cancellationSignal);
|
|
315
|
-
const
|
|
403
|
+
const messageBus$ = this.#messageBus.allMessages$.pipe(throttleTime(500, undefined, { leading: true, trailing: true }));
|
|
404
|
+
const continue$ = merge(messageBus$, cancellationSignal);
|
|
316
405
|
const timer = Timer.startNew();
|
|
317
406
|
const finalizedStatuses = [TaskStatus.Completed, TaskStatus.Cancelled, TaskStatus.Dead];
|
|
318
407
|
while (true) {
|
|
@@ -344,13 +433,17 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
344
433
|
if (treeIds.length == 0) {
|
|
345
434
|
return;
|
|
346
435
|
}
|
|
347
|
-
await
|
|
436
|
+
await tx.pgTransaction
|
|
437
|
+
.update(taskTable)
|
|
438
|
+
.set({
|
|
348
439
|
status: TaskStatus.Cancelled,
|
|
349
440
|
token: null,
|
|
350
441
|
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
|
|
442
|
+
})
|
|
443
|
+
.where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.id, treeIds)));
|
|
444
|
+
for (const task of tree) {
|
|
445
|
+
await this.resolveDependencies(task.id, TaskStatus.Cancelled, { transaction: tx });
|
|
446
|
+
}
|
|
354
447
|
});
|
|
355
448
|
}
|
|
356
449
|
async cancelManyByTags(tags, options) {
|
|
@@ -362,17 +455,9 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
362
455
|
}
|
|
363
456
|
async clear(options) {
|
|
364
457
|
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
.select({ id: taskTable.id })
|
|
368
|
-
.from(taskTable)
|
|
458
|
+
await tx.pgTransaction
|
|
459
|
+
.delete(taskTable)
|
|
369
460
|
.where(eq(taskTable.namespace, this.#namespace));
|
|
370
|
-
await repository.session
|
|
371
|
-
.update(taskTable)
|
|
372
|
-
.set({ parentId: null })
|
|
373
|
-
.where(inArray(taskTable.parentId, parentIds))
|
|
374
|
-
.execute();
|
|
375
|
-
await repository.hardDeleteManyByQuery({ namespace: this.#namespace });
|
|
376
461
|
});
|
|
377
462
|
}
|
|
378
463
|
async dequeue(options) {
|
|
@@ -386,23 +471,31 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
386
471
|
// 1. Flow Control
|
|
387
472
|
const forceDequeue = options?.forceDequeue ?? false;
|
|
388
473
|
let circuitBreakerResult;
|
|
474
|
+
let rateLimitAcquired = 0;
|
|
389
475
|
if (!forceDequeue) {
|
|
390
476
|
circuitBreakerResult = await this.#circuitBreaker.check();
|
|
391
477
|
if (!circuitBreakerResult.allowed) {
|
|
392
478
|
return [];
|
|
393
479
|
}
|
|
394
|
-
|
|
395
|
-
|
|
480
|
+
for (let i = count; i > 0; i--) {
|
|
481
|
+
if (await this.#rateLimiter.tryAcquire(this.#namespace, i)) {
|
|
482
|
+
rateLimitAcquired = i;
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (rateLimitAcquired == 0) {
|
|
396
487
|
return [];
|
|
397
488
|
}
|
|
398
489
|
}
|
|
399
490
|
const tasks = await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
400
|
-
|
|
401
|
-
let effectiveCount = count;
|
|
491
|
+
let effectiveCount = forceDequeue ? count : rateLimitAcquired;
|
|
402
492
|
if (!forceDequeue && circuitBreakerResult?.state == CircuitBreakerState.HalfOpen) {
|
|
403
493
|
if (circuitBreakerResult.isProbe != true) {
|
|
404
|
-
const
|
|
405
|
-
|
|
494
|
+
const [runningResult] = await tx.pgTransaction
|
|
495
|
+
.select({ count: sqlCount() })
|
|
496
|
+
.from(taskTable)
|
|
497
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running)));
|
|
498
|
+
if ((runningResult?.count ?? 0) > 0) {
|
|
406
499
|
return [];
|
|
407
500
|
}
|
|
408
501
|
}
|
|
@@ -412,7 +505,11 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
412
505
|
if (!forceDequeue && isNotNull(this.globalConcurrency)) {
|
|
413
506
|
// WARN: This is a check-then-act race condition.
|
|
414
507
|
// A distributed lock or an atomic update strategy is needed for strict enforcement.
|
|
415
|
-
const
|
|
508
|
+
const [runningResult] = await tx.pgTransaction
|
|
509
|
+
.select({ count: sqlCount() })
|
|
510
|
+
.from(taskTable)
|
|
511
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running)));
|
|
512
|
+
const runningCount = runningResult?.count ?? 0;
|
|
416
513
|
if (runningCount >= this.globalConcurrency) {
|
|
417
514
|
return [];
|
|
418
515
|
}
|
|
@@ -423,7 +520,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
423
520
|
* https://stackoverflow.com/questions/73966670/select-for-update-subquery-not-respecting-limit-clause-under-load
|
|
424
521
|
* https://dba.stackexchange.com/questions/69471/postgres-update-limit-1
|
|
425
522
|
*/
|
|
426
|
-
const selection =
|
|
523
|
+
const selection = tx.pgTransaction.$with('selection').as((qb) => qb
|
|
427
524
|
.select({ id: taskTable.id })
|
|
428
525
|
.from(taskTable)
|
|
429
526
|
.where(and(eq(taskTable.namespace, this.#namespace), lte(taskTable.scheduleTimestamp, TRANSACTION_TIMESTAMP), eq(taskTable.status, TaskStatus.Pending), or(sqlIsNull(taskTable.timeToLive), lt(TRANSACTION_TIMESTAMP, taskTable.timeToLive)), isDefined(options?.types) ? inArray(taskTable.type, options.types) : undefined, sql `pg_sleep(0) IS NOT NULL` // Materialization hack until drizzle implements https://github.com/drizzle-team/drizzle-orm/issues/2318
|
|
@@ -431,7 +528,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
431
528
|
.orderBy(asc(taskTable.priority), asc(taskTable.scheduleTimestamp))
|
|
432
529
|
.limit(effectiveCount)
|
|
433
530
|
.for('update', { skipLocked: true }));
|
|
434
|
-
const rows = await
|
|
531
|
+
const rows = await tx.pgTransaction
|
|
435
532
|
.with(selection)
|
|
436
533
|
.update(taskTable)
|
|
437
534
|
.set({
|
|
@@ -443,13 +540,13 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
443
540
|
// If it was RUNNING (Zombie), previous try failed -> increment tries.
|
|
444
541
|
tries: sql `${taskTable.tries} + 1`,
|
|
445
542
|
})
|
|
446
|
-
.where(inArray(taskTable.id,
|
|
543
|
+
.where(inArray(taskTable.id, tx.pgTransaction.select().from(selection)))
|
|
447
544
|
.returning();
|
|
448
545
|
return await this.#repository.mapManyToEntity(rows);
|
|
449
546
|
});
|
|
450
547
|
// 3. Compensation
|
|
451
|
-
if (!forceDequeue && (tasks.length <
|
|
452
|
-
const shortfall =
|
|
548
|
+
if (!forceDequeue && (tasks.length < rateLimitAcquired)) {
|
|
549
|
+
const shortfall = rateLimitAcquired - tasks.length;
|
|
453
550
|
await this.#rateLimiter.refund(this.#namespace, shortfall);
|
|
454
551
|
}
|
|
455
552
|
return tasks;
|
|
@@ -458,19 +555,26 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
458
555
|
await this.rescheduleMany([id], timestamp, options);
|
|
459
556
|
}
|
|
460
557
|
async rescheduleMany(ids, timestamp, options) {
|
|
461
|
-
|
|
558
|
+
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
559
|
+
await session
|
|
560
|
+
.update(taskTable)
|
|
561
|
+
.set({
|
|
462
562
|
status: TaskStatus.Pending,
|
|
463
563
|
token: null,
|
|
464
564
|
scheduleTimestamp: timestamp,
|
|
465
565
|
visibilityDeadline: null,
|
|
466
566
|
tries: sql `CASE
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
})
|
|
567
|
+
WHEN ${taskTable.status} = ${TaskStatus.Running} THEN GREATEST(0, ${taskTable.tries} - 1)
|
|
568
|
+
ELSE ${taskTable.tries}
|
|
569
|
+
END`,
|
|
570
|
+
})
|
|
571
|
+
.where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.id, ids)));
|
|
471
572
|
}
|
|
472
573
|
async rescheduleManyByTags(tags, timestamp, options) {
|
|
473
|
-
|
|
574
|
+
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
575
|
+
await session
|
|
576
|
+
.update(taskTable)
|
|
577
|
+
.set({
|
|
474
578
|
status: TaskStatus.Pending,
|
|
475
579
|
token: null,
|
|
476
580
|
scheduleTimestamp: timestamp,
|
|
@@ -479,14 +583,14 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
479
583
|
WHEN ${taskTable.status} = ${TaskStatus.Running} THEN GREATEST(0, ${taskTable.tries} - 1)
|
|
480
584
|
ELSE ${taskTable.tries}
|
|
481
585
|
END`,
|
|
482
|
-
})
|
|
586
|
+
})
|
|
587
|
+
.where(and(eq(taskTable.namespace, this.#namespace), arrayOverlaps(taskTable.tags, toArray(tags))));
|
|
483
588
|
}
|
|
484
589
|
async touch(task, options) {
|
|
485
590
|
if (isNull(task.token)) {
|
|
486
591
|
return undefined;
|
|
487
592
|
}
|
|
488
593
|
return await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
489
|
-
const repository = this.#repository.withTransaction(tx);
|
|
490
594
|
const update = {
|
|
491
595
|
visibilityDeadline: sql `${TRANSACTION_TIMESTAMP} + ${interval(this.visibilityTimeout, 'milliseconds')}`,
|
|
492
596
|
};
|
|
@@ -496,34 +600,35 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
496
600
|
if (isDefined(options?.state)) {
|
|
497
601
|
update.state = options.state;
|
|
498
602
|
}
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
status
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// TODO: reduce required DB roundtrips
|
|
507
|
-
if (isUndefined(result)) {
|
|
508
|
-
const existing = await repository.tryLoadByQuery({ id: task.id, namespace: this.#namespace, status: TaskStatus.Running, token: task.token });
|
|
509
|
-
if (isDefined(existing) && isNotNull(existing.startTimestamp) && (currentTimestamp() - existing.startTimestamp) > this.maxExecutionTime) {
|
|
510
|
-
await this.fail(task, { message: 'Hard Execution Timeout' }, { fatal: true, transaction: tx });
|
|
511
|
-
}
|
|
603
|
+
const [updatedRow] = await tx.pgTransaction
|
|
604
|
+
.update(taskTable)
|
|
605
|
+
.set(update)
|
|
606
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), eq(taskTable.status, TaskStatus.Running), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token), gt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`)))
|
|
607
|
+
.returning();
|
|
608
|
+
if (isDefined(updatedRow)) {
|
|
609
|
+
return await this.#repository.mapToEntity(updatedRow);
|
|
512
610
|
}
|
|
513
|
-
|
|
611
|
+
const [existingRow] = await tx.pgTransaction
|
|
612
|
+
.select({ startTimestamp: taskTable.startTimestamp })
|
|
613
|
+
.from(taskTable)
|
|
614
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), eq(taskTable.status, TaskStatus.Running), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token)));
|
|
615
|
+
if (isDefined(existingRow) && isNotNull(existingRow.startTimestamp) && (currentTimestamp() - existingRow.startTimestamp) > this.maxExecutionTime) {
|
|
616
|
+
await this.fail(task, { message: 'Hard Execution Timeout' }, { fatal: true, transaction: tx });
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
514
619
|
});
|
|
515
620
|
}
|
|
516
621
|
async touchMany(tasks, options) {
|
|
517
622
|
if (tasks.length == 0) {
|
|
518
623
|
return [];
|
|
519
624
|
}
|
|
520
|
-
const
|
|
625
|
+
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
521
626
|
const rows = tasks.map((t, i) => {
|
|
522
627
|
const progress = options?.progresses?.[i] ?? null;
|
|
523
628
|
const state = options?.states?.[i] ?? null;
|
|
524
629
|
return sql `(${t.id}::uuid, ${t.token}::uuid, ${progress}::numeric, ${state}::jsonb)`;
|
|
525
630
|
});
|
|
526
|
-
const updates =
|
|
631
|
+
const updates = session.$with('updates').as((qb) => qb
|
|
527
632
|
.select({
|
|
528
633
|
updateId: sql `(id)::uuid`.as('update_id'),
|
|
529
634
|
updateToken: sql `(token)::uuid`.as('update_token'),
|
|
@@ -531,7 +636,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
531
636
|
updateState: sql `(state)::jsonb`.as('update_state'),
|
|
532
637
|
})
|
|
533
638
|
.from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, progress, state)`));
|
|
534
|
-
const updated =
|
|
639
|
+
const updated = session.$with('updated').as(() => session
|
|
535
640
|
.update(taskTable)
|
|
536
641
|
.set({
|
|
537
642
|
visibilityDeadline: sql `${TRANSACTION_TIMESTAMP} + ${interval(this.visibilityTimeout, 'milliseconds')}`,
|
|
@@ -541,32 +646,42 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
541
646
|
.from(updates)
|
|
542
647
|
.where(and(eq(taskTable.id, updates.updateId), eq(taskTable.namespace, this.#namespace), eq(taskTable.token, updates.updateToken), eq(taskTable.status, TaskStatus.Running), gt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`)))
|
|
543
648
|
.returning({ id: taskTable.id }));
|
|
544
|
-
const result = await
|
|
649
|
+
const result = await session
|
|
545
650
|
.with(updates, updated)
|
|
546
651
|
.select({ id: updated.id })
|
|
547
|
-
.from(updated)
|
|
548
|
-
.execute();
|
|
652
|
+
.from(updated);
|
|
549
653
|
return result.map((r) => r.id);
|
|
550
654
|
}
|
|
551
655
|
async complete(task, options) {
|
|
552
656
|
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
657
|
+
const [freshTask] = await tx.pgTransaction
|
|
658
|
+
.select({ unresolvedCompleteDependencies: taskTable.unresolvedCompleteDependencies })
|
|
659
|
+
.from(taskTable)
|
|
660
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id)))
|
|
661
|
+
.for('update');
|
|
662
|
+
if (isUndefined(freshTask)) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const hasActiveChildren = freshTask.unresolvedCompleteDependencies > 0;
|
|
666
|
+
const nextStatus = hasActiveChildren ? TaskStatus.WaitingChildren : TaskStatus.Completed;
|
|
667
|
+
const [updatedTask] = await tx.pgTransaction.update(taskTable)
|
|
668
|
+
.set({
|
|
669
|
+
status: nextStatus,
|
|
559
670
|
token: null,
|
|
560
671
|
result: options?.result,
|
|
561
|
-
progress: 1,
|
|
562
|
-
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
672
|
+
progress: hasActiveChildren ? task.progress : 1,
|
|
673
|
+
completeTimestamp: (nextStatus == TaskStatus.Completed) ? TRANSACTION_TIMESTAMP : null,
|
|
563
674
|
visibilityDeadline: null,
|
|
564
|
-
})
|
|
675
|
+
})
|
|
676
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token)))
|
|
677
|
+
.returning({ id: taskTable.id });
|
|
565
678
|
if (isUndefined(updatedTask)) {
|
|
566
679
|
return;
|
|
567
680
|
}
|
|
568
|
-
|
|
569
|
-
|
|
681
|
+
if (nextStatus == TaskStatus.Completed) {
|
|
682
|
+
await this.#circuitBreaker.recordSuccess();
|
|
683
|
+
}
|
|
684
|
+
await this.resolveDependencies(task.id, nextStatus, { transaction: tx });
|
|
570
685
|
});
|
|
571
686
|
}
|
|
572
687
|
async completeMany(tasks, options) {
|
|
@@ -574,40 +689,55 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
574
689
|
return;
|
|
575
690
|
}
|
|
576
691
|
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
577
|
-
const
|
|
692
|
+
const taskIds = getEntityIds(tasks);
|
|
693
|
+
const freshTasks = await tx.pgTransaction
|
|
694
|
+
.select({ id: taskTable.id, unresolvedCompleteDependencies: taskTable.unresolvedCompleteDependencies })
|
|
695
|
+
.from(taskTable)
|
|
696
|
+
.where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.id, taskIds)))
|
|
697
|
+
.for('update');
|
|
698
|
+
const unresolvedDependencyCountMap = new Map(freshTasks.map((task) => [task.id, task.unresolvedCompleteDependencies]));
|
|
578
699
|
const rows = tasks.map((t, i) => {
|
|
579
700
|
const result = options?.results?.[i] ?? null;
|
|
580
|
-
|
|
701
|
+
const unresolvedCount = unresolvedDependencyCountMap.get(t.id) ?? 0;
|
|
702
|
+
const nextStatus = (unresolvedCount > 0) ? TaskStatus.WaitingChildren : TaskStatus.Completed;
|
|
703
|
+
const progress = (nextStatus == TaskStatus.Completed) ? 1 : t.progress;
|
|
704
|
+
const completeTimestamp = (nextStatus == TaskStatus.Completed) ? TRANSACTION_TIMESTAMP : null;
|
|
705
|
+
return sql `(${t.id}::uuid, ${t.token}::uuid, ${nextStatus}::text, ${result}::jsonb, ${progress}::numeric, ${completeTimestamp}::timestamptz)`;
|
|
581
706
|
});
|
|
582
|
-
const updates =
|
|
707
|
+
const updates = tx.pgTransaction.$with('updates').as((qb) => qb
|
|
583
708
|
.select({
|
|
584
709
|
updateId: sql `(id)::uuid`.as('update_id'),
|
|
585
710
|
updateToken: sql `(token)::uuid`.as('update_token'),
|
|
711
|
+
updateStatus: sql `(status)::text`.as('update_status'),
|
|
586
712
|
updateResult: sql `(result)::jsonb`.as('update_result'),
|
|
713
|
+
updateProgress: sql `(progress)::numeric`.as('update_progress'),
|
|
714
|
+
updateComplete: sql `(complete_timestamp)::timestamptz`.as('update_complete'),
|
|
587
715
|
})
|
|
588
|
-
.from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, result)`));
|
|
589
|
-
const updated =
|
|
716
|
+
.from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, status, result, progress, complete_timestamp)`));
|
|
717
|
+
const updated = tx.pgTransaction.$with('updated').as(() => tx.pgTransaction
|
|
590
718
|
.update(taskTable)
|
|
591
719
|
.set({
|
|
592
|
-
status:
|
|
720
|
+
status: sql `${updates.updateStatus}::${taskStatus}`,
|
|
593
721
|
token: null,
|
|
594
722
|
result: updates.updateResult,
|
|
595
|
-
progress:
|
|
596
|
-
completeTimestamp:
|
|
723
|
+
progress: sql `${updates.updateProgress}`,
|
|
724
|
+
completeTimestamp: sql `${updates.updateComplete}`,
|
|
597
725
|
visibilityDeadline: null,
|
|
598
726
|
})
|
|
599
727
|
.from(updates)
|
|
600
728
|
.where(and(eq(taskTable.id, updates.updateId), eq(taskTable.token, updates.updateToken), eq(taskTable.namespace, this.#namespace)))
|
|
601
|
-
.returning({ id: taskTable.id,
|
|
602
|
-
const updatedRows = await
|
|
729
|
+
.returning({ id: taskTable.id, status: taskTable.status }));
|
|
730
|
+
const updatedRows = await tx.pgTransaction
|
|
603
731
|
.with(updates, updated)
|
|
604
|
-
.select({ id: updated.id,
|
|
605
|
-
.from(updated)
|
|
606
|
-
.execute();
|
|
732
|
+
.select({ id: updated.id, status: updated.status })
|
|
733
|
+
.from(updated);
|
|
607
734
|
if (updatedRows.length > 0) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
735
|
+
if (updatedRows.some((r) => r.status == TaskStatus.Completed)) {
|
|
736
|
+
await this.#circuitBreaker.recordSuccess();
|
|
737
|
+
}
|
|
738
|
+
for (const row of updatedRows) {
|
|
739
|
+
await this.resolveDependencies(row.id, row.status, { transaction: tx });
|
|
740
|
+
}
|
|
611
741
|
}
|
|
612
742
|
});
|
|
613
743
|
}
|
|
@@ -619,12 +749,9 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
619
749
|
: 0;
|
|
620
750
|
const nextSchedule = currentTimestamp() + delay;
|
|
621
751
|
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
token: task.token,
|
|
626
|
-
tries: task.tries,
|
|
627
|
-
}, {
|
|
752
|
+
const [updatedRow] = await tx.pgTransaction
|
|
753
|
+
.update(taskTable)
|
|
754
|
+
.set({
|
|
628
755
|
status: nextStatus,
|
|
629
756
|
token: null,
|
|
630
757
|
error: serializeError(error),
|
|
@@ -632,28 +759,14 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
632
759
|
scheduleTimestamp: nextSchedule,
|
|
633
760
|
startTimestamp: null,
|
|
634
761
|
completeTimestamp: (nextStatus == TaskStatus.Dead) ? TRANSACTION_TIMESTAMP : null,
|
|
635
|
-
})
|
|
636
|
-
|
|
762
|
+
})
|
|
763
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, task.id), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token), eq(taskTable.tries, task.tries)))
|
|
764
|
+
.returning();
|
|
765
|
+
if (isUndefined(updatedRow)) {
|
|
637
766
|
return;
|
|
638
767
|
}
|
|
639
768
|
await this.#circuitBreaker.recordFailure();
|
|
640
|
-
|
|
641
|
-
await this.triggerTagFanIn(task.tags, { transaction: tx });
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
async triggerTagFanIn(tags, options) {
|
|
646
|
-
if (tags.length == 0) {
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
649
|
-
const distinctSortedTags = distinct(tags).toSorted();
|
|
650
|
-
const hash = await digest('SHA-256', distinctSortedTags.join(',')).toHex();
|
|
651
|
-
const idempotencyKey = `Sys:FanIn:${hash}`;
|
|
652
|
-
await this.#internalThis.enqueue('[SystemWorker]:FanIn', { targetTags: distinctSortedTags }, {
|
|
653
|
-
priority: 0,
|
|
654
|
-
idempotencyKey,
|
|
655
|
-
replace: true,
|
|
656
|
-
transaction: options?.transaction,
|
|
769
|
+
await this.resolveDependencies(task.id, nextStatus, { transaction: tx });
|
|
657
770
|
});
|
|
658
771
|
}
|
|
659
772
|
async failMany(tasks, errors, options) {
|
|
@@ -661,7 +774,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
661
774
|
return;
|
|
662
775
|
}
|
|
663
776
|
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
664
|
-
const repository = this.#repository.withTransaction(tx);
|
|
665
777
|
const rows = tasks.map((task, index) => {
|
|
666
778
|
const error = errors[index];
|
|
667
779
|
const isRetryable = (task.tries < this.maxTries);
|
|
@@ -673,7 +785,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
673
785
|
const completeTimestamp = (nextStatus == TaskStatus.Dead) ? new Date() : null;
|
|
674
786
|
return sql `(${task.id}::uuid, ${task.token}::uuid, ${task.tries}::int, ${nextStatus}::text, ${serializeError(error)}::jsonb, ${nextSchedule}::timestamptz, ${completeTimestamp}::timestamptz)`;
|
|
675
787
|
});
|
|
676
|
-
const updates =
|
|
788
|
+
const updates = tx.pgTransaction.$with('updates').as((qb) => qb
|
|
677
789
|
.select({
|
|
678
790
|
updateId: sql `(id)::uuid`.as('update_id'),
|
|
679
791
|
updateToken: sql `(token)::uuid`.as('update_token'),
|
|
@@ -684,7 +796,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
684
796
|
updateComplete: sql `(complete_timestamp)::timestamptz`.as('update_complete'),
|
|
685
797
|
})
|
|
686
798
|
.from(sql `(VALUES ${sql.join(rows, sql `, `)}) AS t(id, token, tries, status, error, schedule_timestamp, complete_timestamp)`));
|
|
687
|
-
const updated =
|
|
799
|
+
const updated = tx.pgTransaction.$with('updated').as(() => tx.pgTransaction
|
|
688
800
|
.update(taskTable)
|
|
689
801
|
.set({
|
|
690
802
|
status: sql `${updates.updateStatus}::${taskStatus}`,
|
|
@@ -697,136 +809,130 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
697
809
|
})
|
|
698
810
|
.from(updates)
|
|
699
811
|
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, updates.updateId), eq(taskTable.token, updates.updateToken), eq(taskTable.tries, updates.updateTries)))
|
|
700
|
-
.returning({ id: taskTable.id, status: taskTable.status
|
|
701
|
-
const result = await
|
|
812
|
+
.returning({ id: taskTable.id, status: taskTable.status }));
|
|
813
|
+
const result = await tx.pgTransaction
|
|
702
814
|
.with(updates, updated)
|
|
703
|
-
.select()
|
|
704
|
-
.from(updated)
|
|
705
|
-
.execute();
|
|
815
|
+
.select({ id: updated.id, status: updated.status })
|
|
816
|
+
.from(updated);
|
|
706
817
|
if (result.length > 0) {
|
|
707
818
|
await Promise.all(createArray(result.length, async () => await this.#circuitBreaker.recordFailure()));
|
|
708
|
-
const
|
|
709
|
-
.
|
|
710
|
-
|
|
711
|
-
await this.triggerTagFanIn(deadTags, { transaction: tx });
|
|
819
|
+
for (const row of result) {
|
|
820
|
+
await this.resolveDependencies(row.id, row.status, { transaction: tx });
|
|
821
|
+
}
|
|
712
822
|
}
|
|
713
823
|
});
|
|
714
824
|
}
|
|
715
|
-
async resolveDependencies(
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
//
|
|
719
|
-
const
|
|
720
|
-
.select()
|
|
721
|
-
.from(taskTable)
|
|
722
|
-
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Waiting), or(arrayOverlaps(taskTable.completeAfterTags, targetTags), arrayOverlaps(taskTable.scheduleAfterTags, targetTags))))
|
|
723
|
-
.for('update', { skipLocked: true })
|
|
724
|
-
.execute();
|
|
725
|
-
const entities = await repository.mapManyToEntity(waiters);
|
|
726
|
-
if (entities.length == 0) {
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
const allRequiredTags = distinct(entities.flatMap((t) => [...t.completeAfterTags, ...t.scheduleAfterTags]));
|
|
730
|
-
const statsRows = await repository.session
|
|
825
|
+
async resolveDependencies(id, status, options) {
|
|
826
|
+
this.#logger.debug(`Resolving dependencies for task ${id} with status ${status}`);
|
|
827
|
+
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
828
|
+
// Find all dependents of the task that just finished
|
|
829
|
+
const dependents = await tx.pgTransaction
|
|
731
830
|
.select({
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
831
|
+
taskId: taskDependencyTable.taskId,
|
|
832
|
+
type: taskDependencyTable.type,
|
|
833
|
+
requiredStatuses: taskDependencyTable.requiredStatuses,
|
|
735
834
|
})
|
|
736
|
-
.from(
|
|
737
|
-
.where(and(eq(
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
for (const row of statsRows) {
|
|
741
|
-
if (!tagStats.has(row.tag)) {
|
|
742
|
-
tagStats.set(row.tag, new Map());
|
|
743
|
-
}
|
|
744
|
-
tagStats.get(row.tag).set(row.status, row.count);
|
|
835
|
+
.from(taskDependencyTable)
|
|
836
|
+
.where(and(eq(taskDependencyTable.namespace, this.#namespace), eq(taskDependencyTable.dependencyTaskId, id)));
|
|
837
|
+
if (dependents.length == 0) {
|
|
838
|
+
return;
|
|
745
839
|
}
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
840
|
+
for (const dep of dependents) {
|
|
841
|
+
const isMatched = dep.requiredStatuses.includes(status);
|
|
842
|
+
const isTerminal = (status == TaskStatus.Completed || status == TaskStatus.Cancelled || status == TaskStatus.Dead);
|
|
843
|
+
if (isMatched || isTerminal) {
|
|
844
|
+
// Dependency resolved (either matched or finished in a different terminal status)
|
|
845
|
+
const deletedEdge = await tx.pgTransaction
|
|
846
|
+
.delete(taskDependencyTable)
|
|
847
|
+
.where(and(eq(taskDependencyTable.namespace, this.#namespace), eq(taskDependencyTable.taskId, dep.taskId), eq(taskDependencyTable.dependencyTaskId, id), eq(taskDependencyTable.type, dep.type)))
|
|
848
|
+
.returning();
|
|
849
|
+
if (deletedEdge.length > 0) {
|
|
850
|
+
const [dependentTask] = await tx.pgTransaction
|
|
851
|
+
.select({ failFast: taskTable.failFast, status: taskTable.status })
|
|
852
|
+
.from(taskTable)
|
|
853
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, dep.taskId)));
|
|
854
|
+
if (isDefined(dependentTask)) {
|
|
855
|
+
if (!isMatched && dependentTask.failFast && dependentTask.status != TaskStatus.Dead && dependentTask.status != TaskStatus.Cancelled) {
|
|
856
|
+
// Dependency failed and failFast is enabled: fail the dependent task immediately
|
|
857
|
+
await this.transitionTask(dep.taskId, TaskStatus.Dead, { error: { code: 'DependencyFailed', message: `Dependency ${id} failed (status: ${status}). Task failed fast because it required one of: ${dep.requiredStatuses.join(', ')}` }, transaction: tx });
|
|
858
|
+
}
|
|
859
|
+
else {
|
|
860
|
+
// Dependency resolved (matched, or failed but no failFast): decrement counter
|
|
861
|
+
const [updatedTask] = await tx.pgTransaction
|
|
862
|
+
.update(taskTable)
|
|
863
|
+
.set((dep.type == TaskDependencyType.Schedule)
|
|
864
|
+
? { unresolvedScheduleDependencies: sql `${taskTable.unresolvedScheduleDependencies} - 1` }
|
|
865
|
+
: { unresolvedCompleteDependencies: sql `${taskTable.unresolvedCompleteDependencies} - 1` })
|
|
866
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, dep.taskId), gt((dep.type == TaskDependencyType.Schedule) ? taskTable.unresolvedScheduleDependencies : taskTable.unresolvedCompleteDependencies, 0)))
|
|
867
|
+
.returning();
|
|
868
|
+
if (isDefined(updatedTask)) {
|
|
869
|
+
// Check for transitions
|
|
870
|
+
if (updatedTask.unresolvedScheduleDependencies == 0 || updatedTask.unresolvedCompleteDependencies == 0) {
|
|
871
|
+
await this.evaluateTaskStatus(updatedTask.id, { transaction: tx });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
753
875
|
}
|
|
754
876
|
}
|
|
755
877
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
isReady = requiredTags.every((tag) => {
|
|
781
|
-
const stats = tagStats.get(tag);
|
|
782
|
-
if (isUndefined(stats)) {
|
|
783
|
-
return false;
|
|
784
|
-
}
|
|
785
|
-
const activeCount = (stats.get(TaskStatus.Pending) ?? 0)
|
|
786
|
-
+ (stats.get(TaskStatus.Running) ?? 0)
|
|
787
|
-
+ (stats.get(TaskStatus.Waiting) ?? 0);
|
|
788
|
-
return activeCount == 0;
|
|
789
|
-
});
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
async evaluateTaskStatus(id, options) {
|
|
882
|
+
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
883
|
+
const [rawTask] = await tx.pgTransaction
|
|
884
|
+
.select()
|
|
885
|
+
.from(taskTable)
|
|
886
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)))
|
|
887
|
+
.for('update');
|
|
888
|
+
if (isUndefined(rawTask)) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const task = await this.#repository.mapToEntity(rawTask);
|
|
892
|
+
if (task.status != TaskStatus.Waiting && task.status != TaskStatus.WaitingChildren && task.status != TaskStatus.Pending) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (task.unresolvedScheduleDependencies == 0) {
|
|
896
|
+
if (task.unresolvedCompleteDependencies == 0) {
|
|
897
|
+
// If it hasn't started yet (startTimestamp is null), it should be Pending.
|
|
898
|
+
// If it has already run (startTimestamp is set), it should be Completed.
|
|
899
|
+
const nextStatus = isNull(task.startTimestamp) ? TaskStatus.Pending : TaskStatus.Completed;
|
|
900
|
+
if (task.status != nextStatus) {
|
|
901
|
+
await this.transitionTask(id, nextStatus, { transaction: tx });
|
|
790
902
|
}
|
|
791
903
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
idsToFail.push(waiter.id);
|
|
904
|
+
else if (task.status == TaskStatus.Waiting && isNull(task.startTimestamp)) {
|
|
905
|
+
await this.transitionTask(id, TaskStatus.Pending, { transaction: tx });
|
|
795
906
|
}
|
|
796
|
-
else if (
|
|
797
|
-
if
|
|
798
|
-
|
|
799
|
-
fanInTags.push(...waiter.tags);
|
|
800
|
-
}
|
|
801
|
-
else if (waiter.completeAfterTags.length > 0) {
|
|
802
|
-
idsToComplete.push(waiter.id);
|
|
803
|
-
fanInTags.push(...waiter.tags);
|
|
804
|
-
}
|
|
907
|
+
else if (task.status == TaskStatus.Pending && isNotNull(task.startTimestamp)) {
|
|
908
|
+
// This case happens if a task was somehow Pending but already has a startTimestamp and now its completion dependencies are resolved
|
|
909
|
+
await this.transitionTask(id, TaskStatus.WaitingChildren, { transaction: tx });
|
|
805
910
|
}
|
|
806
911
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
async transitionTask(id, status, options) {
|
|
915
|
+
this.#logger.debug(`Transitioning task ${id} to ${status}`);
|
|
916
|
+
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
917
|
+
const update = {
|
|
918
|
+
status,
|
|
919
|
+
completeTimestamp: ((status == TaskStatus.Completed) || (status == TaskStatus.Cancelled) || (status == TaskStatus.Dead)) ? TRANSACTION_TIMESTAMP : null,
|
|
920
|
+
};
|
|
921
|
+
if (isNotNullOrUndefined(options?.error)) {
|
|
922
|
+
update.error = options.error;
|
|
815
923
|
}
|
|
816
|
-
if (
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
scheduleTimestamp: TRANSACTION_TIMESTAMP,
|
|
820
|
-
});
|
|
924
|
+
if (status == TaskStatus.Completed) {
|
|
925
|
+
update.progress = 1;
|
|
926
|
+
update.token = null;
|
|
821
927
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
});
|
|
829
|
-
|
|
928
|
+
const [updatedRow] = await tx.pgTransaction
|
|
929
|
+
.update(taskTable)
|
|
930
|
+
.set(update)
|
|
931
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id)))
|
|
932
|
+
.returning();
|
|
933
|
+
if (isDefined(updatedRow)) {
|
|
934
|
+
await this.resolveDependencies(id, status, { transaction: tx });
|
|
935
|
+
this.#messageBus.publishAndForget();
|
|
830
936
|
}
|
|
831
937
|
});
|
|
832
938
|
}
|
|
@@ -836,26 +942,22 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
836
942
|
// 1. Archival: Move old terminal tasks to archive
|
|
837
943
|
while (true) {
|
|
838
944
|
const archivedCount = await repository.useTransaction(options?.transaction, async (tx) => {
|
|
839
|
-
const
|
|
840
|
-
const
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
}, { limit: 1000, for: { mode: 'update', skipLocked: true } });
|
|
854
|
-
if (tasksToArchive.length > 0) {
|
|
855
|
-
await archiveRepositoryWithTx.insertMany(tasksToArchive);
|
|
856
|
-
await repositoryWithTx.hardDeleteMany(tasksToArchive.map((t) => t.id));
|
|
945
|
+
const childTaskTable = aliasedTable(taskTable, 'childTask');
|
|
946
|
+
const rowsToArchive = await tx.pgTransaction
|
|
947
|
+
.select()
|
|
948
|
+
.from(taskTable)
|
|
949
|
+
.where(and(eq(taskTable.namespace, this.#namespace), inArray(taskTable.status, [TaskStatus.Completed, TaskStatus.Dead, TaskStatus.Cancelled]), lte(taskTable.completeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.retention, 'milliseconds')}`), notExists(tx.pgTransaction
|
|
950
|
+
.select({ id: childTaskTable.id })
|
|
951
|
+
.from(childTaskTable)
|
|
952
|
+
.where(and(eq(childTaskTable.namespace, this.#namespace), eq(childTaskTable.parentId, taskTable.id))))))
|
|
953
|
+
.limit(1000)
|
|
954
|
+
.for('update', { skipLocked: true });
|
|
955
|
+
if (rowsToArchive.length > 0) {
|
|
956
|
+
const rowsToArchiveIds = getEntityIds(rowsToArchive);
|
|
957
|
+
await tx.pgTransaction.insert(taskArchiveTable).values(rowsToArchive);
|
|
958
|
+
await tx.pgTransaction.delete(taskTable).where(inArray(taskTable.id, rowsToArchiveIds));
|
|
857
959
|
}
|
|
858
|
-
return
|
|
960
|
+
return rowsToArchive.length;
|
|
859
961
|
});
|
|
860
962
|
if (archivedCount < 1000) {
|
|
861
963
|
break;
|
|
@@ -864,16 +966,15 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
864
966
|
// 2. Purge Archive: Remove very old archived tasks
|
|
865
967
|
while (true) {
|
|
866
968
|
const deletedArchiveCount = await archiveRepository.useTransaction(options?.transaction, async (tx) => {
|
|
867
|
-
const
|
|
868
|
-
const selection = repositoryWithTx.session.$with('archive_purge_selection').as((qb) => qb
|
|
969
|
+
const selection = tx.pgTransaction.$with('archive_purge_selection').as((qb) => qb
|
|
869
970
|
.select({ id: taskArchiveTable.id })
|
|
870
971
|
.from(taskArchiveTable)
|
|
871
|
-
.where(and(eq(taskArchiveTable.namespace, this.#namespace),
|
|
972
|
+
.where(and(eq(taskArchiveTable.namespace, this.#namespace), lte(taskArchiveTable.completeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.archiveRetention, 'milliseconds')}`)))
|
|
872
973
|
.limit(1000));
|
|
873
|
-
const result = await
|
|
974
|
+
const result = await tx.pgTransaction
|
|
874
975
|
.with(selection)
|
|
875
976
|
.delete(taskArchiveTable)
|
|
876
|
-
.where(inArray(taskArchiveTable.id,
|
|
977
|
+
.where(inArray(taskArchiveTable.id, tx.pgTransaction.select().from(selection)))
|
|
877
978
|
.returning({ id: taskArchiveTable.id });
|
|
878
979
|
return result.length;
|
|
879
980
|
});
|
|
@@ -884,16 +985,15 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
884
985
|
// 3. Maintenance Loop
|
|
885
986
|
while (true) {
|
|
886
987
|
const maintenanceCount = await repository.useTransaction(options?.transaction, async (tx) => {
|
|
887
|
-
const repositoryWithTx = repository.withTransaction(tx);
|
|
888
988
|
let totalUpdated = 0;
|
|
889
989
|
// 3.1 Handle Pending Expiration
|
|
890
|
-
const expiredSelection =
|
|
990
|
+
const expiredSelection = tx.pgTransaction.$with('expired_selection').as((qb) => qb
|
|
891
991
|
.select({ id: taskTable.id })
|
|
892
992
|
.from(taskTable)
|
|
893
993
|
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Pending), lt(taskTable.timeToLive, TRANSACTION_TIMESTAMP)))
|
|
894
994
|
.limit(1000)
|
|
895
995
|
.for('update', { skipLocked: true }));
|
|
896
|
-
const expiredRows = await
|
|
996
|
+
const expiredRows = await tx.pgTransaction
|
|
897
997
|
.with(expiredSelection)
|
|
898
998
|
.update(taskTable)
|
|
899
999
|
.set({
|
|
@@ -902,40 +1002,43 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
902
1002
|
error: { code: 'Expired', message: 'Task expired before processing' },
|
|
903
1003
|
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
904
1004
|
})
|
|
905
|
-
.where(inArray(taskTable.id,
|
|
906
|
-
.returning({
|
|
1005
|
+
.where(inArray(taskTable.id, tx.pgTransaction.select().from(expiredSelection)))
|
|
1006
|
+
.returning({ id: taskTable.id });
|
|
907
1007
|
totalUpdated += expiredRows.length;
|
|
908
1008
|
if (expiredRows.length > 0) {
|
|
909
|
-
|
|
1009
|
+
for (const row of expiredRows) {
|
|
1010
|
+
await this.resolveDependencies(row.id, TaskStatus.Dead, { transaction: tx });
|
|
1011
|
+
}
|
|
910
1012
|
}
|
|
911
1013
|
// 3.2 Handle Zombie Tasks (Retry)
|
|
912
|
-
const zombieRetrySelection =
|
|
1014
|
+
const zombieRetrySelection = tx.pgTransaction.$with('zombie_retry_selection').as((qb) => qb
|
|
913
1015
|
.select({ id: taskTable.id })
|
|
914
1016
|
.from(taskTable)
|
|
915
1017
|
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), lt(taskTable.tries, this.maxTries)))
|
|
916
1018
|
.limit(1000)
|
|
917
1019
|
.for('update', { skipLocked: true }));
|
|
918
|
-
const zombieRetryRows = await
|
|
1020
|
+
const zombieRetryRows = await tx.pgTransaction
|
|
919
1021
|
.with(zombieRetrySelection)
|
|
920
1022
|
.update(taskTable)
|
|
921
1023
|
.set({
|
|
922
1024
|
status: TaskStatus.Pending,
|
|
923
1025
|
token: null,
|
|
924
1026
|
visibilityDeadline: null,
|
|
1027
|
+
startTimestamp: null,
|
|
925
1028
|
scheduleTimestamp: sql `${TRANSACTION_TIMESTAMP} + ${interval(this.retryDelayMinimum, 'milliseconds')}`, // Simple backoff for zombies
|
|
926
1029
|
error: sql `jsonb_build_object('code', 'VisibilityTimeout', 'message', 'Worker Lost', 'last_error', ${taskTable.error})`,
|
|
927
1030
|
})
|
|
928
|
-
.where(inArray(taskTable.id,
|
|
1031
|
+
.where(inArray(taskTable.id, tx.pgTransaction.select().from(zombieRetrySelection)))
|
|
929
1032
|
.returning({ id: taskTable.id });
|
|
930
1033
|
totalUpdated += zombieRetryRows.length;
|
|
931
1034
|
// 3.3 Handle Zombie Tasks (Exhaustion)
|
|
932
|
-
const zombieExhaustionSelection =
|
|
1035
|
+
const zombieExhaustionSelection = tx.pgTransaction.$with('zombie_exhaustion_selection').as((qb) => qb
|
|
933
1036
|
.select({ id: taskTable.id })
|
|
934
1037
|
.from(taskTable)
|
|
935
1038
|
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), gte(taskTable.tries, this.maxTries)))
|
|
936
1039
|
.limit(1000)
|
|
937
1040
|
.for('update', { skipLocked: true }));
|
|
938
|
-
const exhaustionRows = await
|
|
1041
|
+
const exhaustionRows = await tx.pgTransaction
|
|
939
1042
|
.with(zombieExhaustionSelection)
|
|
940
1043
|
.update(taskTable)
|
|
941
1044
|
.set({
|
|
@@ -945,20 +1048,22 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
945
1048
|
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
946
1049
|
error: sql `jsonb_build_object('code', 'ZombieExhausted', 'message', 'Exceeded max retries after repeated crashes', 'last_error', ${taskTable.error})`,
|
|
947
1050
|
})
|
|
948
|
-
.where(inArray(taskTable.id,
|
|
949
|
-
.returning({
|
|
1051
|
+
.where(inArray(taskTable.id, tx.pgTransaction.select().from(zombieExhaustionSelection)))
|
|
1052
|
+
.returning({ id: taskTable.id });
|
|
950
1053
|
totalUpdated += exhaustionRows.length;
|
|
951
1054
|
if (exhaustionRows.length > 0) {
|
|
952
|
-
|
|
1055
|
+
for (const row of exhaustionRows) {
|
|
1056
|
+
await this.resolveDependencies(row.id, TaskStatus.Dead, { transaction: tx });
|
|
1057
|
+
}
|
|
953
1058
|
}
|
|
954
1059
|
// 3.4 Handle Hard Timeout
|
|
955
|
-
const timeoutSelection =
|
|
1060
|
+
const timeoutSelection = tx.pgTransaction.$with('timeout_selection').as((qb) => qb
|
|
956
1061
|
.select({ id: taskTable.id })
|
|
957
1062
|
.from(taskTable)
|
|
958
1063
|
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`)))
|
|
959
1064
|
.limit(1000)
|
|
960
1065
|
.for('update', { skipLocked: true }));
|
|
961
|
-
const timeoutRows = await
|
|
1066
|
+
const timeoutRows = await tx.pgTransaction
|
|
962
1067
|
.with(timeoutSelection)
|
|
963
1068
|
.update(taskTable)
|
|
964
1069
|
.set({
|
|
@@ -968,27 +1073,29 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
968
1073
|
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
969
1074
|
error: sql `jsonb_build_object('code', 'MaxTimeExceeded', 'message', 'Hard Execution Timeout: Task ran longer than ' || ${this.maxExecutionTime} || 'ms', 'last_error', ${taskTable.error})`,
|
|
970
1075
|
})
|
|
971
|
-
.where(inArray(taskTable.id,
|
|
972
|
-
.returning({
|
|
1076
|
+
.where(inArray(taskTable.id, tx.pgTransaction.select().from(timeoutSelection)))
|
|
1077
|
+
.returning({ id: taskTable.id });
|
|
973
1078
|
totalUpdated += timeoutRows.length;
|
|
974
1079
|
if (timeoutRows.length > 0) {
|
|
975
|
-
|
|
1080
|
+
for (const row of timeoutRows) {
|
|
1081
|
+
await this.resolveDependencies(row.id, TaskStatus.Dead, { transaction: tx });
|
|
1082
|
+
}
|
|
976
1083
|
}
|
|
977
1084
|
// 3.5 Promote Priority (Aging)
|
|
978
|
-
const agingSelection =
|
|
1085
|
+
const agingSelection = tx.pgTransaction.$with('aging_selection').as((qb) => qb
|
|
979
1086
|
.select({ id: taskTable.id })
|
|
980
1087
|
.from(taskTable)
|
|
981
1088
|
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Pending), lt(taskTable.priorityAgeTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.priorityAgingInterval, 'milliseconds')}`)))
|
|
982
1089
|
.limit(1000)
|
|
983
1090
|
.for('update', { skipLocked: true }));
|
|
984
|
-
const agingRows = await
|
|
1091
|
+
const agingRows = await tx.pgTransaction
|
|
985
1092
|
.with(agingSelection)
|
|
986
1093
|
.update(taskTable)
|
|
987
1094
|
.set({
|
|
988
1095
|
priority: sql `${taskTable.priority} - ${this.priorityAgingStep}`,
|
|
989
1096
|
priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
|
|
990
1097
|
})
|
|
991
|
-
.where(inArray(taskTable.id,
|
|
1098
|
+
.where(inArray(taskTable.id, tx.pgTransaction.select().from(agingSelection)))
|
|
992
1099
|
.returning({ id: taskTable.id });
|
|
993
1100
|
totalUpdated += agingRows.length;
|
|
994
1101
|
return totalUpdated;
|
|
@@ -1009,13 +1116,13 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1009
1116
|
result: null,
|
|
1010
1117
|
scheduleTimestamp: TRANSACTION_TIMESTAMP,
|
|
1011
1118
|
completeTimestamp: null,
|
|
1119
|
+
startTimestamp: null,
|
|
1012
1120
|
tries: 0,
|
|
1013
1121
|
progress: 0,
|
|
1014
1122
|
priorityAgeTimestamp: TRANSACTION_TIMESTAMP,
|
|
1015
1123
|
state: (options?.resetState == true) ? null : undefined,
|
|
1016
1124
|
})
|
|
1017
|
-
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id), or(eq(taskTable.status, TaskStatus.Pending), eq(taskTable.status, TaskStatus.Completed), eq(taskTable.status, TaskStatus.Cancelled), eq(taskTable.status, TaskStatus.Dead), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP))))
|
|
1018
|
-
.execute();
|
|
1125
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.id, id), or(eq(taskTable.status, TaskStatus.Pending), eq(taskTable.status, TaskStatus.Completed), eq(taskTable.status, TaskStatus.Cancelled), eq(taskTable.status, TaskStatus.Dead), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP))));
|
|
1019
1126
|
}
|
|
1020
1127
|
notify() {
|
|
1021
1128
|
this.#messageBus.publishAndForget();
|
|
@@ -1044,36 +1151,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1044
1151
|
}
|
|
1045
1152
|
}
|
|
1046
1153
|
}
|
|
1047
|
-
/**
|
|
1048
|
-
* @internal for internal unit test use only
|
|
1049
|
-
* @param options
|
|
1050
|
-
* @returns
|
|
1051
|
-
*/
|
|
1052
|
-
async processPendingFanIn(options) {
|
|
1053
|
-
let processedCount = 0;
|
|
1054
|
-
while (true) {
|
|
1055
|
-
const task = await this.#internalThis.dequeue({ types: ['[SystemWorker]:FanIn'], forceDequeue: true, transaction: options?.transaction });
|
|
1056
|
-
if (isUndefined(task)) {
|
|
1057
|
-
break;
|
|
1058
|
-
}
|
|
1059
|
-
try {
|
|
1060
|
-
await this.resolveDependencies(task.data.targetTags);
|
|
1061
|
-
await this.#internalThis.complete(task, { transaction: options?.transaction });
|
|
1062
|
-
processedCount++;
|
|
1063
|
-
}
|
|
1064
|
-
catch (error) {
|
|
1065
|
-
await this.#internalThis.fail(task, error, { transaction: options?.transaction });
|
|
1066
|
-
throw error;
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
return processedCount;
|
|
1070
|
-
}
|
|
1071
|
-
startSystemWorker() {
|
|
1072
|
-
this.#internalThis.process({ concurrency: 1, cancellationSignal: this.#cancellationSignal, forceDequeue: true, types: ['[SystemWorker]:FanIn'] }, async (context) => {
|
|
1073
|
-
await this.resolveDependencies(context.data.targetTags);
|
|
1074
|
-
return TaskProcessResult.Complete();
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
1154
|
async *getBatchConsumer(size, cancellationSignal, options) {
|
|
1078
1155
|
const continue$ = merge(this.#messageBus.allMessages$, cancellationSignal);
|
|
1079
1156
|
while (cancellationSignal.isUnset) {
|