@tstdl/base 0.93.157 → 0.93.159
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/package.json
CHANGED
package/polyfills.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import 'disposablestack/auto';
|
|
2
|
-
globalThis.Symbol.observable ??=
|
|
2
|
+
globalThis.Symbol.observable ??= '@@observable';
|
|
@@ -110,6 +110,7 @@ export declare class PostgresTaskQueue<Definitions extends TaskDefinitionMap = T
|
|
|
110
110
|
failMany(tasks: Task<Definitions>[], errors: unknown[], options?: {
|
|
111
111
|
transaction?: Transaction;
|
|
112
112
|
}): Promise<void>;
|
|
113
|
+
private getFailureUpdate;
|
|
113
114
|
private resolveDependencies;
|
|
114
115
|
private resolveDependenciesMany;
|
|
115
116
|
maintenance(options?: {
|
|
@@ -118,9 +119,7 @@ export declare class PostgresTaskQueue<Definitions extends TaskDefinitionMap = T
|
|
|
118
119
|
private performArchival;
|
|
119
120
|
private performArchivePurge;
|
|
120
121
|
private processExpirations;
|
|
121
|
-
private
|
|
122
|
-
private processZombieExhaustions;
|
|
123
|
-
private processHardTimeouts;
|
|
122
|
+
private recoverStaleTasks;
|
|
124
123
|
private processPriorityAging;
|
|
125
124
|
restart(id: string, options?: {
|
|
126
125
|
resetState?: boolean;
|
|
@@ -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 { aliasedTable, and, asc, count, eq, gt, gte, inArray, lt, lte, notExists, notInArray, or, sql, count as sqlCount, isNotNull as sqlIsNotNull, isNull as sqlIsNull } from 'drizzle-orm';
|
|
59
|
+
import { aliasedTable, and, asc, count, eq, gt, gte, inArray, lt, lte, not, notExists, notInArray, or, sql, count as sqlCount, isNotNull as sqlIsNotNull, isNull as sqlIsNull } from 'drizzle-orm';
|
|
60
60
|
import { filter, merge, throttleTime } from 'rxjs';
|
|
61
61
|
import { CancellationSignal } from '../../cancellation/index.js';
|
|
62
62
|
import { CircuitBreaker, CircuitBreakerState } from '../../circuit-breaker/index.js';
|
|
@@ -837,23 +837,17 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
837
837
|
}
|
|
838
838
|
async fail(task, caughtError, options) {
|
|
839
839
|
const error = ensureTaskError(caughtError);
|
|
840
|
-
const
|
|
841
|
-
const nextStatus = isRetryable ? TaskStatus.Retrying : TaskStatus.Dead;
|
|
842
|
-
const delay = isRetryable
|
|
843
|
-
? Math.min(this.retryDelayMaximum, this.retryDelayMinimum * (this.retryDelayGrowth ** (task.tries - 1)))
|
|
844
|
-
: 0;
|
|
845
|
-
const nextSchedule = sql `${TRANSACTION_TIMESTAMP} + ${interval(delay, 'milliseconds')}`;
|
|
840
|
+
const update = this.getFailureUpdate(task.tries, error, options);
|
|
846
841
|
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
847
842
|
const [updatedRow] = await tx.pgTransaction
|
|
848
843
|
.update(taskTable)
|
|
849
844
|
.set({
|
|
850
|
-
status:
|
|
845
|
+
status: update.status,
|
|
851
846
|
token: null,
|
|
852
|
-
error:
|
|
847
|
+
error: update.error,
|
|
853
848
|
visibilityDeadline: null,
|
|
854
|
-
scheduleTimestamp:
|
|
855
|
-
|
|
856
|
-
completeTimestamp: (nextStatus == TaskStatus.Dead) ? TRANSACTION_TIMESTAMP : null,
|
|
849
|
+
scheduleTimestamp: (update.status == TaskStatus.Retrying) ? update.scheduleTimestamp : taskTable.scheduleTimestamp,
|
|
850
|
+
completeTimestamp: update.completeTimestamp,
|
|
857
851
|
})
|
|
858
852
|
.where(and(eq(taskTable.id, task.id), isNull(task.token) ? sqlIsNull(taskTable.token) : eq(taskTable.token, task.token), eq(taskTable.tries, task.tries)))
|
|
859
853
|
.returning();
|
|
@@ -861,7 +855,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
861
855
|
return;
|
|
862
856
|
}
|
|
863
857
|
await this.#circuitBreaker.recordFailure();
|
|
864
|
-
await this.resolveDependencies(task.id,
|
|
858
|
+
await this.resolveDependencies(task.id, update.status, { namespace: task.namespace, transaction: tx });
|
|
865
859
|
});
|
|
866
860
|
}
|
|
867
861
|
async failMany(tasks, errors, options) {
|
|
@@ -871,14 +865,8 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
871
865
|
await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
872
866
|
const rows = tasks.map((task, index) => {
|
|
873
867
|
const error = ensureTaskError(errors[index]);
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
const delay = isRetryable
|
|
877
|
-
? Math.min(this.retryDelayMaximum, this.retryDelayMinimum * (this.retryDelayGrowth ** (task.tries - 1)))
|
|
878
|
-
: 0;
|
|
879
|
-
const nextSchedule = sql `(${TRANSACTION_TIMESTAMP} + ${interval(delay, 'milliseconds')})`;
|
|
880
|
-
const completeTimestamp = (nextStatus == TaskStatus.Dead) ? TRANSACTION_TIMESTAMP : null;
|
|
881
|
-
return sql `(${task.id}::uuid, ${task.token}::uuid, ${task.tries}::int, ${nextStatus}::${taskStatus}, ${serializeError(error)}::jsonb, ${nextSchedule}::timestamptz, ${completeTimestamp}::timestamptz)`;
|
|
868
|
+
const update = this.getFailureUpdate(task.tries, error);
|
|
869
|
+
return sql `(${task.id}::uuid, ${task.token}::uuid, ${task.tries}::int, ${update.status}::${taskStatus}, ${update.error}::jsonb, ${update.scheduleTimestamp}::timestamptz, ${update.completeTimestamp}::timestamptz)`;
|
|
882
870
|
});
|
|
883
871
|
const updates = tx.pgTransaction.$with('updates').as((qb) => qb
|
|
884
872
|
.select({
|
|
@@ -899,7 +887,6 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
899
887
|
error: sql `${updates.updateError}`,
|
|
900
888
|
visibilityDeadline: null,
|
|
901
889
|
scheduleTimestamp: caseWhen(eq(updates.updateStatus, TaskStatus.Retrying), updates.updateSchedule).else(taskTable.scheduleTimestamp),
|
|
902
|
-
startTimestamp: null,
|
|
903
890
|
completeTimestamp: sql `${updates.updateComplete}`,
|
|
904
891
|
})
|
|
905
892
|
.from(updates)
|
|
@@ -915,6 +902,16 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
915
902
|
}
|
|
916
903
|
});
|
|
917
904
|
}
|
|
905
|
+
getFailureUpdate(tries, error, options) {
|
|
906
|
+
const isRetryable = (options?.fatal != true) && (tries < this.maxTries);
|
|
907
|
+
const status = isRetryable ? TaskStatus.Retrying : TaskStatus.Dead;
|
|
908
|
+
const delay = isRetryable
|
|
909
|
+
? Math.min(this.retryDelayMaximum, this.retryDelayMinimum * (this.retryDelayGrowth ** (tries - 1)))
|
|
910
|
+
: 0;
|
|
911
|
+
const scheduleTimestamp = sql `(${TRANSACTION_TIMESTAMP} + ${interval(delay, 'milliseconds')})`;
|
|
912
|
+
const completeTimestamp = (status == TaskStatus.Dead) ? TRANSACTION_TIMESTAMP : null;
|
|
913
|
+
return { status, error: serializeError(error), scheduleTimestamp, completeTimestamp };
|
|
914
|
+
}
|
|
918
915
|
async resolveDependencies(id, status, options) {
|
|
919
916
|
await this.resolveDependenciesMany([{ id, status, namespace: options?.namespace }], options);
|
|
920
917
|
}
|
|
@@ -1063,10 +1060,8 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1063
1060
|
}
|
|
1064
1061
|
}
|
|
1065
1062
|
async maintenance(options) {
|
|
1063
|
+
await this.recoverStaleTasks(options);
|
|
1066
1064
|
await this.processExpirations(options);
|
|
1067
|
-
await this.processZombieRetries(options);
|
|
1068
|
-
await this.processZombieExhaustions(options);
|
|
1069
|
-
await this.processHardTimeouts(options);
|
|
1070
1065
|
await this.processPriorityAging(options);
|
|
1071
1066
|
await this.performArchival(options);
|
|
1072
1067
|
await this.performArchivePurge(options);
|
|
@@ -1152,91 +1147,35 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1152
1147
|
}
|
|
1153
1148
|
}
|
|
1154
1149
|
}
|
|
1155
|
-
async
|
|
1150
|
+
async recoverStaleTasks(options) {
|
|
1156
1151
|
const session = options?.transaction?.pgTransaction ?? this.#repository.session;
|
|
1157
|
-
const
|
|
1158
|
-
.select({ id: taskTable.id })
|
|
1159
|
-
.from(taskTable)
|
|
1160
|
-
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), lt(taskTable.tries, this.maxTries)))
|
|
1161
|
-
.limit(1000)
|
|
1162
|
-
.for('update', { skipLocked: true }));
|
|
1163
|
-
while (true) {
|
|
1164
|
-
const zombieRetryRows = await session
|
|
1165
|
-
.with(zombieRetrySelection)
|
|
1166
|
-
.update(taskTable)
|
|
1167
|
-
.set({
|
|
1168
|
-
status: TaskStatus.Retrying,
|
|
1169
|
-
token: null,
|
|
1170
|
-
visibilityDeadline: null,
|
|
1171
|
-
startTimestamp: null,
|
|
1172
|
-
scheduleTimestamp: sql `${TRANSACTION_TIMESTAMP} + ${interval(least(this.retryDelayMaximum, sql `${this.retryDelayMinimum} * ${power(this.retryDelayGrowth, sql `${taskTable.tries} - 1`)}`), 'milliseconds')}`,
|
|
1173
|
-
error: jsonbBuildObject({ code: 'VisibilityTimeout', message: 'Worker Lost', lastError: taskTable.error }),
|
|
1174
|
-
})
|
|
1175
|
-
.where(inArray(taskTable.id, session.select().from(zombieRetrySelection)))
|
|
1176
|
-
.returning({ id: taskTable.id });
|
|
1177
|
-
if (zombieRetryRows.length < 1000) {
|
|
1178
|
-
break;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
async processZombieExhaustions(options) {
|
|
1183
|
-
const zombieExhaustionSelection = this.#repository.session.$with('selection').as((qb) => qb
|
|
1184
|
-
.select({ id: taskTable.id })
|
|
1185
|
-
.from(taskTable)
|
|
1186
|
-
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), gte(taskTable.tries, this.maxTries)))
|
|
1187
|
-
.limit(1000)
|
|
1188
|
-
.for('update', { skipLocked: true }));
|
|
1189
|
-
while (true) {
|
|
1190
|
-
const updatedCount = await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
1191
|
-
const exhaustionRows = await tx.pgTransaction
|
|
1192
|
-
.with(zombieExhaustionSelection)
|
|
1193
|
-
.update(taskTable)
|
|
1194
|
-
.set({
|
|
1195
|
-
status: TaskStatus.Orphaned,
|
|
1196
|
-
token: null,
|
|
1197
|
-
visibilityDeadline: null,
|
|
1198
|
-
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
1199
|
-
error: jsonbBuildObject({ code: 'ZombieExhausted', message: 'Exceeded max retries after repeated crashes', lastError: taskTable.error }),
|
|
1200
|
-
})
|
|
1201
|
-
.where(inArray(taskTable.id, tx.pgTransaction.select().from(zombieExhaustionSelection)))
|
|
1202
|
-
.returning({ id: taskTable.id });
|
|
1203
|
-
if (exhaustionRows.length > 0) {
|
|
1204
|
-
await this.resolveDependenciesMany(exhaustionRows.map((r) => ({ id: r.id, status: TaskStatus.Orphaned, namespace: this.#namespace })), { transaction: tx });
|
|
1205
|
-
}
|
|
1206
|
-
return exhaustionRows.length;
|
|
1207
|
-
});
|
|
1208
|
-
if (updatedCount < 1000) {
|
|
1209
|
-
break;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
async processHardTimeouts(options) {
|
|
1214
|
-
const timeoutSelection = this.#repository.session.$with('selection').as((qb) => qb
|
|
1152
|
+
const selection = session.$with('selection').as((qb) => qb
|
|
1215
1153
|
.select({ id: taskTable.id })
|
|
1216
1154
|
.from(taskTable)
|
|
1217
|
-
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`)))
|
|
1155
|
+
.where(and(eq(taskTable.namespace, this.#namespace), eq(taskTable.status, TaskStatus.Running), or(lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`))))
|
|
1218
1156
|
.limit(1000)
|
|
1219
1157
|
.for('update', { skipLocked: true }));
|
|
1220
1158
|
while (true) {
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
.with(
|
|
1159
|
+
const rows = await this.#repository.useTransaction(options?.transaction, async (tx) => {
|
|
1160
|
+
const updatedRows = await tx.pgTransaction
|
|
1161
|
+
.with(selection)
|
|
1224
1162
|
.update(taskTable)
|
|
1225
1163
|
.set({
|
|
1226
|
-
status: TaskStatus.TimedOut,
|
|
1164
|
+
status: caseWhen(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`), enumValue(TaskStatus, taskStatus, TaskStatus.TimedOut)).else(caseWhen(lt(taskTable.tries, this.maxTries), enumValue(TaskStatus, taskStatus, TaskStatus.Retrying)).else(enumValue(TaskStatus, taskStatus, TaskStatus.Orphaned))),
|
|
1227
1165
|
token: null,
|
|
1228
1166
|
visibilityDeadline: null,
|
|
1229
|
-
completeTimestamp: TRANSACTION_TIMESTAMP,
|
|
1230
|
-
|
|
1167
|
+
completeTimestamp: caseWhen(or(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`), gte(taskTable.tries, this.maxTries)), TRANSACTION_TIMESTAMP).else(null),
|
|
1168
|
+
scheduleTimestamp: caseWhen(and(lt(taskTable.visibilityDeadline, TRANSACTION_TIMESTAMP), lt(taskTable.tries, this.maxTries), not(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`))), sql `${TRANSACTION_TIMESTAMP} + ${interval(least(this.retryDelayMaximum, sql `${this.retryDelayMinimum} * ${power(this.retryDelayGrowth, sql `${taskTable.tries} - 1`)}`), 'milliseconds')}`).else(taskTable.scheduleTimestamp),
|
|
1169
|
+
error: caseWhen(lt(taskTable.startTimestamp, sql `${TRANSACTION_TIMESTAMP} - ${interval(this.maxExecutionTime, 'milliseconds')}`), jsonbBuildObject({ code: 'MaxTimeExceeded', message: sql `'Hard Execution Timeout: Task ran longer than ' || ${this.maxExecutionTime} || 'ms'`, lastError: taskTable.error })).else(caseWhen(lt(taskTable.tries, this.maxTries), jsonbBuildObject({ code: 'VisibilityTimeout', message: 'Worker Lost', lastError: taskTable.error })).else(jsonbBuildObject({ code: 'ZombieExhausted', message: 'Exceeded max retries after repeated crashes', lastError: taskTable.error }))),
|
|
1231
1170
|
})
|
|
1232
|
-
.where(inArray(taskTable.id, tx.pgTransaction.select().from(
|
|
1233
|
-
.returning({ id: taskTable.id });
|
|
1234
|
-
if (
|
|
1235
|
-
await this.resolveDependenciesMany(
|
|
1171
|
+
.where(inArray(taskTable.id, tx.pgTransaction.select().from(selection)))
|
|
1172
|
+
.returning({ id: taskTable.id, status: taskTable.status });
|
|
1173
|
+
if (updatedRows.length > 0) {
|
|
1174
|
+
await this.resolveDependenciesMany(updatedRows.map((r) => ({ id: r.id, status: r.status, namespace: this.#namespace })), { transaction: tx });
|
|
1236
1175
|
}
|
|
1237
|
-
return
|
|
1176
|
+
return updatedRows;
|
|
1238
1177
|
});
|
|
1239
|
-
if (
|
|
1178
|
+
if (rows.length < 1000) {
|
|
1240
1179
|
break;
|
|
1241
1180
|
}
|
|
1242
1181
|
}
|
|
@@ -1345,12 +1284,7 @@ let PostgresTaskQueue = class PostgresTaskQueue extends TaskQueue {
|
|
|
1345
1284
|
async highFrequencyMaintenanceLoop() {
|
|
1346
1285
|
while (this.#cancellationSignal.isUnset) {
|
|
1347
1286
|
try {
|
|
1348
|
-
|
|
1349
|
-
this.processZombieRetries(),
|
|
1350
|
-
this.processZombieExhaustions(),
|
|
1351
|
-
this.processHardTimeouts(),
|
|
1352
|
-
]);
|
|
1353
|
-
this.logPromiseAllSettledErrors(results, 'high frequency maintenance loop');
|
|
1287
|
+
await this.recoverStaleTasks();
|
|
1354
1288
|
}
|
|
1355
1289
|
catch (error) {
|
|
1356
1290
|
this.#logger.error('Error during high frequency maintenance loop', error);
|
|
@@ -117,7 +117,7 @@ describe('Task Queue Optimization Edge Cases', () => {
|
|
|
117
117
|
tries: 2,
|
|
118
118
|
})
|
|
119
119
|
.where(and(eq(taskTable.id, task.id), eq(taskTable.namespace, namespace)));
|
|
120
|
-
await queue.
|
|
120
|
+
await queue.recoverStaleTasks();
|
|
121
121
|
const updated = await queue.getTask(task.id);
|
|
122
122
|
expect(updated.status).toBe(TaskStatus.Retrying);
|
|
123
123
|
const delay = updated.scheduleTimestamp - Date.now();
|
|
@@ -185,6 +185,42 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
185
185
|
expect(updated?.tries).toBe(1);
|
|
186
186
|
expect(updated?.error).toBeDefined();
|
|
187
187
|
});
|
|
188
|
+
it('should NOT clear startTimestamp when transitioning to terminal or retry states', async () => {
|
|
189
|
+
// Re-create queue with high circuit breaker threshold for this test
|
|
190
|
+
const q = injector.resolve(TaskQueueProvider).get(`start-timestamp-${crypto.randomUUID()}`, {
|
|
191
|
+
circuitBreakerThreshold: 10,
|
|
192
|
+
});
|
|
193
|
+
const task = await q.enqueue('foo', { foo: 'bar' });
|
|
194
|
+
// 1. Dequeue to start it (sets startTimestamp)
|
|
195
|
+
const dequeued = await q.dequeue();
|
|
196
|
+
expect(dequeued?.startTimestamp).not.toBeNull();
|
|
197
|
+
const startTimestamp = dequeued.startTimestamp;
|
|
198
|
+
// 2. Fail it (transitions to Retrying)
|
|
199
|
+
await q.fail(dequeued, new Error('temp failure'));
|
|
200
|
+
const retryingTask = await q.getTask(task.id);
|
|
201
|
+
expect(retryingTask?.status).toBe(TaskStatus.Retrying);
|
|
202
|
+
expect(retryingTask?.startTimestamp).toBe(startTimestamp);
|
|
203
|
+
// 3. Dequeue again (updates startTimestamp)
|
|
204
|
+
await q.reschedule(task.id, currentTimestamp());
|
|
205
|
+
const dequeued2 = await q.dequeue();
|
|
206
|
+
expect(dequeued2?.startTimestamp).not.toBe(startTimestamp);
|
|
207
|
+
const startTimestamp2 = dequeued2.startTimestamp;
|
|
208
|
+
// 4. Fail fatally (transitions to Dead)
|
|
209
|
+
await q.fail(dequeued2, new Error('fatal failure'), { fatal: true });
|
|
210
|
+
const deadTask = await q.getTask(task.id);
|
|
211
|
+
expect(deadTask?.status).toBe(TaskStatus.Dead);
|
|
212
|
+
expect(deadTask?.startTimestamp).toBe(startTimestamp2);
|
|
213
|
+
// 5. Success (transitions to Completed)
|
|
214
|
+
const task2 = await q.enqueue('foo', { foo: 'bar2' });
|
|
215
|
+
const dequeued3 = await q.dequeue();
|
|
216
|
+
expect(dequeued3).toBeDefined();
|
|
217
|
+
const startTimestamp3 = dequeued3.startTimestamp;
|
|
218
|
+
await q.complete(dequeued3);
|
|
219
|
+
const completedTask = await q.getTask(task2.id);
|
|
220
|
+
expect(completedTask?.status).toBe(TaskStatus.Completed);
|
|
221
|
+
expect(completedTask?.startTimestamp).toBe(startTimestamp3);
|
|
222
|
+
await q.clear();
|
|
223
|
+
});
|
|
188
224
|
});
|
|
189
225
|
describe('Hierarchy and Cross-Namespace', () => {
|
|
190
226
|
it('should correctly increment parent unresolved dependencies when a child is spawned in a different namespace', async () => {
|
|
@@ -336,9 +372,11 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
336
372
|
});
|
|
337
373
|
});
|
|
338
374
|
describe('Timeouts and Maintenance (Pruning)', () => {
|
|
339
|
-
it('should recover "Zombie" tasks (crashed workers)', async () => {
|
|
375
|
+
it('should recover "Zombie" tasks (crashed workers) and preserve startTimestamp', async () => {
|
|
340
376
|
const task = await queue.enqueue('foo', { foo: 'zombie' });
|
|
341
|
-
await queue.dequeue(); // Task is now Running with a token
|
|
377
|
+
const dequeued = await queue.dequeue(); // Task is now Running with a token
|
|
378
|
+
expect(dequeued?.startTimestamp).not.toBeNull();
|
|
379
|
+
const startTimestamp = dequeued.startTimestamp;
|
|
342
380
|
// processTimeout is 50ms. Wait for it to expire.
|
|
343
381
|
await timeout(100);
|
|
344
382
|
await queue.maintenance();
|
|
@@ -346,6 +384,7 @@ describe('PostgresQueue (Distributed Task Orchestration)', () => {
|
|
|
346
384
|
expect(recovered?.status).toBe(TaskStatus.Retrying);
|
|
347
385
|
expect(recovered?.tries).toBe(1);
|
|
348
386
|
expect(recovered?.token).toBeNull();
|
|
387
|
+
expect(recovered?.startTimestamp).toBe(startTimestamp);
|
|
349
388
|
});
|
|
350
389
|
it('should fail tasks that exceed Hard Execution Timeout via prune', async () => {
|
|
351
390
|
// Re-configure queue with very short execution timeout
|