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