tina4-nodejs 3.13.104 → 3.13.105
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/CLAUDE.md +16 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +157 -38
- package/packages/core/dist/index.js +157 -38
- package/packages/core/src/queue.ts +75 -18
- package/packages/core/src/queueBackends/liteBackend.ts +17 -1
- package/packages/core/src/queueBackends/mongoBackend.ts +80 -14
- package/packages/orm/dist/index.js +157 -38
- package/packages/orm/src/baseModel.ts +18 -5
- package/packages/orm/src/migration.ts +14 -5
- package/types/core/src/queue.d.ts +41 -4
- package/types/core/src/queueBackends/liteBackend.d.ts +7 -1
- package/types/core/src/queueBackends/mongoBackend.d.ts +7 -2
- package/types/orm/src/baseModel.d.ts +10 -5
|
@@ -144,7 +144,13 @@ export interface QueueBackendInterface {
|
|
|
144
144
|
// persistent-connection rewrite lands.
|
|
145
145
|
complete?(queue: string, id: string): void;
|
|
146
146
|
fail?(queue: string, id: string, error: string, maxRetries: number, retryBackoff: number): void;
|
|
147
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Explicit manual re-queue: returns true if the id was found and revived,
|
|
149
|
+
* false otherwise (parity with Python's backend.retry_job()). A backend may
|
|
150
|
+
* legacy-return void; callers coerce a void return to true so nothing that
|
|
151
|
+
* used to be reported as success silently flips to failure.
|
|
152
|
+
*/
|
|
153
|
+
retry?(queue: string, id: string, delaySeconds?: number): boolean | void;
|
|
148
154
|
deadLetters?(queue: string, maxRetries?: number): QueueJob[];
|
|
149
155
|
failed?(queue: string, maxRetries?: number): QueueJob[];
|
|
150
156
|
retryFailed?(queue: string, maxRetries?: number): number;
|
|
@@ -400,7 +406,19 @@ export class Queue {
|
|
|
400
406
|
}
|
|
401
407
|
|
|
402
408
|
/**
|
|
403
|
-
* Count jobs
|
|
409
|
+
* Count jobs by status. Defaults to "pending".
|
|
410
|
+
*
|
|
411
|
+
* ``"pending"`` counts jobs waiting to be popped -- INCLUDES retryable-
|
|
412
|
+
* but-attempted ones, because they live in the pending queue under the
|
|
413
|
+
* auto-retry lifecycle (see failed()).
|
|
414
|
+
* ``"reserved"`` counts jobs a consumer has popped but not yet
|
|
415
|
+
* completed/failed (in-flight against the visibility timeout).
|
|
416
|
+
* ``"completed"`` counts jobs the consumer has finished successfully.
|
|
417
|
+
* ``"failed"``, ``"dead"``, ``"dead_letter"`` are ALIASES that all count
|
|
418
|
+
* the dead-letter store -- jobs whose attempts >= maxRetries and that
|
|
419
|
+
* have given up. Use deadLetters() to list them. Retryable-but-attempted
|
|
420
|
+
* jobs are NOT counted by size("failed"); use failed() to list them or
|
|
421
|
+
* size("pending") to include them in a total.
|
|
404
422
|
*/
|
|
405
423
|
size(status: string = "pending"): number {
|
|
406
424
|
const q = this.topic;
|
|
@@ -455,13 +473,21 @@ export class Queue {
|
|
|
455
473
|
/**
|
|
456
474
|
* Get jobs that failed at least once but are still being retried
|
|
457
475
|
* (0 < attempts < maxRetries). These live in the pending queue under the
|
|
458
|
-
* auto-retry lifecycle
|
|
476
|
+
* auto-retry lifecycle (fail() re-queues them with an incremented attempts
|
|
477
|
+
* count and a retryBackoff delay) so pop() picks them up again. They are
|
|
478
|
+
* NOT counted by size("failed") -- that alias counts the dead-letter store,
|
|
479
|
+
* matching deadLetters(). To include retryable-failed jobs in a total, use
|
|
480
|
+
* size("pending"). Terminal failures are returned by deadLetters().
|
|
459
481
|
*/
|
|
460
482
|
failed(): QueueJob[] {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
483
|
+
const raw = this.externalBackend?.failed
|
|
484
|
+
? this.externalBackend.failed(this.topic, this._maxRetries)
|
|
485
|
+
: this.liteBackend.failed(this.topic, this._maxRetries);
|
|
486
|
+
// Wrap so callers get the full Job lifecycle (parity with deadLetters()
|
|
487
|
+
// and Python's failed()).
|
|
488
|
+
return raw.map((data) =>
|
|
489
|
+
createJob({ ...(data as JobData), topic: (data as JobData).topic ?? this.topic }, this),
|
|
490
|
+
);
|
|
465
491
|
}
|
|
466
492
|
|
|
467
493
|
/**
|
|
@@ -473,21 +499,31 @@ export class Queue {
|
|
|
473
499
|
*/
|
|
474
500
|
retry(jobId?: string, delaySeconds?: number): boolean {
|
|
475
501
|
if (jobId) {
|
|
476
|
-
// Retry a specific job by ID
|
|
502
|
+
// Retry a specific job by ID. Honour whatever the external backend
|
|
503
|
+
// returns (a boolean) so an unknown id reports false; only coerce a
|
|
504
|
+
// legacy void return to true to preserve the pre-3.13.105 contract on
|
|
505
|
+
// a backend that hasn't been updated (LiteBackend already returns bool).
|
|
477
506
|
if (this.externalBackend?.retry) {
|
|
478
|
-
this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
479
|
-
return true;
|
|
507
|
+
const result = this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
508
|
+
return result === undefined ? true : Boolean(result);
|
|
480
509
|
}
|
|
481
510
|
return this.liteBackend.retry(this.topic, jobId, delaySeconds);
|
|
482
511
|
}
|
|
483
|
-
// Retry
|
|
512
|
+
// Retry ALL dead-letter jobs -- an explicit for...of iterates every
|
|
513
|
+
// entry rather than a reducer like .some() that would short-circuit on
|
|
514
|
+
// the first truthy result (PY-12-04 parity: Python's generator-inside-
|
|
515
|
+
// any() had exactly that bug pre-3.13.105 and left the remaining
|
|
516
|
+
// dead letters silently in the store).
|
|
484
517
|
const deadJobs = this.deadLetters();
|
|
485
518
|
if (deadJobs.length === 0) return false;
|
|
486
519
|
let retried = false;
|
|
487
520
|
for (const job of deadJobs) {
|
|
488
521
|
if (this.externalBackend?.retry) {
|
|
489
|
-
this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
490
|
-
|
|
522
|
+
const result = this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
523
|
+
// A modern backend returns bool; a legacy backend returns void which
|
|
524
|
+
// we optimistically treat as revived (parity with the pre-3.13.105
|
|
525
|
+
// pathway that never had a way to know otherwise).
|
|
526
|
+
if (result === undefined || Boolean(result)) retried = true;
|
|
491
527
|
} else if (this.liteBackend.retry(this.topic, job.id, delaySeconds)) {
|
|
492
528
|
retried = true;
|
|
493
529
|
}
|
|
@@ -496,13 +532,34 @@ export class Queue {
|
|
|
496
532
|
}
|
|
497
533
|
|
|
498
534
|
/**
|
|
499
|
-
* Get
|
|
535
|
+
* Get jobs that exceeded max_retries -- terminal failures.
|
|
536
|
+
*
|
|
537
|
+
* Same set counted by size("failed") / size("dead") / size("dead_letter")
|
|
538
|
+
* (three aliases for the dead-letter store). To LIST retryable-but-
|
|
539
|
+
* attempted jobs (attempts > 0 AND attempts < maxRetries) that are still
|
|
540
|
+
* being auto-retried, use failed() -- those live in the pending queue and
|
|
541
|
+
* are NOT dead letters.
|
|
542
|
+
*
|
|
543
|
+
* Returns Job objects with the failure reason on ``.error`` (not raw dicts)
|
|
544
|
+
* so callers can iterate uniformly with the rest of the queue API and, in
|
|
545
|
+
* particular, call ``.retry()`` on each to manually revive it:
|
|
546
|
+
*
|
|
547
|
+
* for (const job of queue.deadLetters()) {
|
|
548
|
+
* Log.warn(`revived ${job.id}: ${job.error}`);
|
|
549
|
+
* job.retry();
|
|
550
|
+
* }
|
|
500
551
|
*/
|
|
501
552
|
deadLetters(maxRetries?: number): QueueJob[] {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
553
|
+
const raw = this.externalBackend?.deadLetters
|
|
554
|
+
? this.externalBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries)
|
|
555
|
+
: this.liteBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries);
|
|
556
|
+
// Wrap so ``job.retry()`` / ``job.fail()`` / ``job.complete()`` work
|
|
557
|
+
// uniformly (parity with pop() and the Python master). Preserves the
|
|
558
|
+
// job's own topic so the lifecycle methods route back to THIS queue's
|
|
559
|
+
// backend even on a job that dead-lettered on a different topic.
|
|
560
|
+
return raw.map((data) =>
|
|
561
|
+
createJob({ ...(data as JobData), topic: (data as JobData).topic ?? this.topic }, this),
|
|
562
|
+
);
|
|
506
563
|
}
|
|
507
564
|
|
|
508
565
|
/**
|
|
@@ -677,11 +677,27 @@ export class LiteBackend {
|
|
|
677
677
|
* Explicit re-queue requested by the caller (job.retry()).
|
|
678
678
|
*
|
|
679
679
|
* Always re-enqueues regardless of the retry limit — manual override,
|
|
680
|
-
* distinct from the automatic failJob() path.
|
|
680
|
+
* distinct from the automatic failJob() path. Cleans up BOTH the
|
|
681
|
+
* reservation record AND any dead-letter file for this id, so a caller
|
|
682
|
+
* that iterates deadLetters() and calls .retry() on each doesn't leave
|
|
683
|
+
* the failed/ directory carrying duplicates (PY-12-05, 3.13.105).
|
|
684
|
+
* Aligns with retry(queue, jobId) which had always unlinked the
|
|
685
|
+
* dead-letter file -- two spellings of the same intent that previously
|
|
686
|
+
* diverged.
|
|
681
687
|
*/
|
|
682
688
|
retryJob(queue: string, job: QueueJob, delaySeconds?: number): void {
|
|
683
689
|
// Clear the reservation — the consumer acknowledged (with an explicit retry).
|
|
684
690
|
this.clearReservation(queue, job.id);
|
|
691
|
+
// Drop any dead-letter file for this id BEFORE the re-queue -- if this
|
|
692
|
+
// job came from deadLetters() it lives in failed/ and would otherwise
|
|
693
|
+
// stay on disk while a fresh pending file appears in the queue dir, so
|
|
694
|
+
// the next deadLetters() call reports the job again and a consumer
|
|
695
|
+
// processes it twice.
|
|
696
|
+
try {
|
|
697
|
+
unlinkSync(join(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
698
|
+
} catch {
|
|
699
|
+
// ENOENT is fine (the job never dead-lettered or was already cleared).
|
|
700
|
+
}
|
|
685
701
|
job.attempts = (job.attempts || 0) + 1;
|
|
686
702
|
job.error = undefined;
|
|
687
703
|
this.requeue(queue, job, delaySeconds ?? 0, undefined);
|
|
@@ -343,17 +343,67 @@ export class MongoBackend implements QueueBackend {
|
|
|
343
343
|
process.stdout.write("__OK__");
|
|
344
344
|
}
|
|
345
345
|
else if (operation === "retry") {
|
|
346
|
-
// Explicit manual re-queue (
|
|
347
|
-
//
|
|
346
|
+
// Explicit manual re-queue. Serves BOTH Queue.retry(id) (revive
|
|
347
|
+
// a dead-letter job) AND job.retry() (manual re-queue of a live
|
|
348
|
+
// reserved/pending job) so the Mongo backend matches
|
|
349
|
+
// LiteBackend's dual behaviour.
|
|
350
|
+
//
|
|
351
|
+
// 1) DL revival (Queue.retry(id) after fail exhausted retries).
|
|
352
|
+
// Pre-3.13.105 this branch was BROKEN: the search filter was
|
|
353
|
+
// { queue: queueName, id, status: "failed" } -- three separate
|
|
354
|
+
// reasons it could never match. dead_letter() inserts under
|
|
355
|
+
// queueName + ".dead_letter" (not queueName), carries
|
|
356
|
+
// status "dead" (not "failed"), and the original under
|
|
357
|
+
// queueName was already acked to "completed" by the time the
|
|
358
|
+
// DL was written. Now we look up in the DL namespace by id,
|
|
359
|
+
// delete the DL doc first (so an interrupted retry never
|
|
360
|
+
// leaves both a DL and a fresh pending doc), and upsert the
|
|
361
|
+
// original back to pending -- re-hydrating if the original
|
|
362
|
+
// was purged (housekeeping) so a retry always works.
|
|
363
|
+
// 2) Live-doc manual re-queue (job.retry() on a job the caller
|
|
364
|
+
// just popped and wants back in pending). The live-doc path
|
|
365
|
+
// is preserved from before 3.13.105.
|
|
366
|
+
//
|
|
367
|
+
// Returns __OK__ when either path acted; __NOT_FOUND__ when
|
|
368
|
+
// neither the DL nor the live doc existed, so Queue.retry(id)
|
|
369
|
+
// can now report the pre-3.13.105 blanket-true as false for
|
|
370
|
+
// unknown ids. data = JSON { id, delaySeconds }.
|
|
348
371
|
const info = JSON.parse(data);
|
|
372
|
+
const dlTopic = queueName + ".dead_letter";
|
|
373
|
+
const now = new Date().toISOString();
|
|
349
374
|
const avail = info.delaySeconds > 0
|
|
350
375
|
? new Date(Date.now() + info.delaySeconds * 1000).toISOString()
|
|
351
|
-
:
|
|
352
|
-
await col.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
376
|
+
: now;
|
|
377
|
+
const dlDoc = await col.findOne({ queue: dlTopic, id: info.id });
|
|
378
|
+
if (dlDoc !== null) {
|
|
379
|
+
await col.deleteOne({ _id: dlDoc._id });
|
|
380
|
+
const payload = dlDoc.payload ?? {};
|
|
381
|
+
const priority = dlDoc.priority ?? 0;
|
|
382
|
+
await col.updateOne(
|
|
383
|
+
{ queue: queueName, id: info.id },
|
|
384
|
+
{
|
|
385
|
+
$set: {
|
|
386
|
+
status: "pending",
|
|
387
|
+
availableAt: avail,
|
|
388
|
+
reservedAt: null,
|
|
389
|
+
error: null,
|
|
390
|
+
payload,
|
|
391
|
+
priority,
|
|
392
|
+
id: info.id,
|
|
393
|
+
createdAt: dlDoc.createdAt ?? now,
|
|
394
|
+
},
|
|
395
|
+
$inc: { attempts: 1 },
|
|
396
|
+
},
|
|
397
|
+
{ upsert: true },
|
|
398
|
+
);
|
|
399
|
+
process.stdout.write("__OK__");
|
|
400
|
+
} else {
|
|
401
|
+
const result = await col.updateOne(
|
|
402
|
+
{ queue: queueName, id: info.id },
|
|
403
|
+
{ $set: { status: "pending", availableAt: avail, reservedAt: null }, $inc: { attempts: 1 } },
|
|
404
|
+
);
|
|
405
|
+
process.stdout.write(result.matchedCount > 0 ? "__OK__" : "__NOT_FOUND__");
|
|
406
|
+
}
|
|
357
407
|
}
|
|
358
408
|
else if (operation === "deadLetters") {
|
|
359
409
|
const docs = await col.find({ queue: queueName + ".dead_letter" }).toArray();
|
|
@@ -398,10 +448,20 @@ export class MongoBackend implements QueueBackend {
|
|
|
398
448
|
process.stdout.write(String(revived));
|
|
399
449
|
}
|
|
400
450
|
else if (operation === "purge") {
|
|
401
|
-
// Delete docs by status (default:
|
|
451
|
+
// Delete docs by status (default: every doc for the topic).
|
|
452
|
+
// Pre-3.13.105 this filtered by { queue: queueName, status } for
|
|
453
|
+
// EVERY status -- correct for pending/reserved/completed, wrong
|
|
454
|
+
// for the dead-letter states (dead/failed/dead_letter) which
|
|
455
|
+
// live under queueName + ".dead_letter" and carry status "dead".
|
|
456
|
+
// A purge("dead") therefore deleted nothing and returned 0.
|
|
457
|
+
// data = JSON { status }.
|
|
402
458
|
const info = data ? JSON.parse(data) : {};
|
|
403
|
-
const
|
|
404
|
-
|
|
459
|
+
const isDead = info.status && ["dead", "failed", "dead_letter"].includes(info.status);
|
|
460
|
+
const filter = isDead
|
|
461
|
+
? { queue: queueName + ".dead_letter" }
|
|
462
|
+
: (info.status
|
|
463
|
+
? { queue: queueName, status: info.status }
|
|
464
|
+
: { queue: queueName });
|
|
405
465
|
const res = await col.deleteMany(filter);
|
|
406
466
|
process.stdout.write(String(res.deletedCount || 0));
|
|
407
467
|
}
|
|
@@ -513,9 +573,15 @@ export class MongoBackend implements QueueBackend {
|
|
|
513
573
|
this.execSync("fail", queue, JSON.stringify({ id, error, maxRetries, retryBackoff }));
|
|
514
574
|
}
|
|
515
575
|
|
|
516
|
-
/**
|
|
517
|
-
|
|
518
|
-
|
|
576
|
+
/**
|
|
577
|
+
* Revive a specific dead-letter job by id. Returns true if the DL was found
|
|
578
|
+
* and revived, false otherwise (parity with LiteBackend.retry(queue, id)
|
|
579
|
+
* and Python's mongo_backend.retry_job()). Pre-3.13.105 this returned void
|
|
580
|
+
* and Queue.retry(id) reported success for every call, even for unknown ids.
|
|
581
|
+
*/
|
|
582
|
+
retry(queue: string, id: string, delaySeconds: number = 0): boolean {
|
|
583
|
+
const out = this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
584
|
+
return out.includes("__OK__");
|
|
519
585
|
}
|
|
520
586
|
|
|
521
587
|
/** Jobs that exceeded max retries (the `<queue>.dead_letter` collection topic). */
|
|
@@ -17249,17 +17249,67 @@ var init_mongoBackend = __esm({
|
|
|
17249
17249
|
process.stdout.write("__OK__");
|
|
17250
17250
|
}
|
|
17251
17251
|
else if (operation === "retry") {
|
|
17252
|
-
// Explicit manual re-queue (
|
|
17253
|
-
//
|
|
17252
|
+
// Explicit manual re-queue. Serves BOTH Queue.retry(id) (revive
|
|
17253
|
+
// a dead-letter job) AND job.retry() (manual re-queue of a live
|
|
17254
|
+
// reserved/pending job) so the Mongo backend matches
|
|
17255
|
+
// LiteBackend's dual behaviour.
|
|
17256
|
+
//
|
|
17257
|
+
// 1) DL revival (Queue.retry(id) after fail exhausted retries).
|
|
17258
|
+
// Pre-3.13.105 this branch was BROKEN: the search filter was
|
|
17259
|
+
// { queue: queueName, id, status: "failed" } -- three separate
|
|
17260
|
+
// reasons it could never match. dead_letter() inserts under
|
|
17261
|
+
// queueName + ".dead_letter" (not queueName), carries
|
|
17262
|
+
// status "dead" (not "failed"), and the original under
|
|
17263
|
+
// queueName was already acked to "completed" by the time the
|
|
17264
|
+
// DL was written. Now we look up in the DL namespace by id,
|
|
17265
|
+
// delete the DL doc first (so an interrupted retry never
|
|
17266
|
+
// leaves both a DL and a fresh pending doc), and upsert the
|
|
17267
|
+
// original back to pending -- re-hydrating if the original
|
|
17268
|
+
// was purged (housekeeping) so a retry always works.
|
|
17269
|
+
// 2) Live-doc manual re-queue (job.retry() on a job the caller
|
|
17270
|
+
// just popped and wants back in pending). The live-doc path
|
|
17271
|
+
// is preserved from before 3.13.105.
|
|
17272
|
+
//
|
|
17273
|
+
// Returns __OK__ when either path acted; __NOT_FOUND__ when
|
|
17274
|
+
// neither the DL nor the live doc existed, so Queue.retry(id)
|
|
17275
|
+
// can now report the pre-3.13.105 blanket-true as false for
|
|
17276
|
+
// unknown ids. data = JSON { id, delaySeconds }.
|
|
17254
17277
|
const info = JSON.parse(data);
|
|
17278
|
+
const dlTopic = queueName + ".dead_letter";
|
|
17279
|
+
const now = new Date().toISOString();
|
|
17255
17280
|
const avail = info.delaySeconds > 0
|
|
17256
17281
|
? new Date(Date.now() + info.delaySeconds * 1000).toISOString()
|
|
17257
|
-
:
|
|
17258
|
-
await col.
|
|
17259
|
-
|
|
17260
|
-
|
|
17261
|
-
|
|
17262
|
-
|
|
17282
|
+
: now;
|
|
17283
|
+
const dlDoc = await col.findOne({ queue: dlTopic, id: info.id });
|
|
17284
|
+
if (dlDoc !== null) {
|
|
17285
|
+
await col.deleteOne({ _id: dlDoc._id });
|
|
17286
|
+
const payload = dlDoc.payload ?? {};
|
|
17287
|
+
const priority = dlDoc.priority ?? 0;
|
|
17288
|
+
await col.updateOne(
|
|
17289
|
+
{ queue: queueName, id: info.id },
|
|
17290
|
+
{
|
|
17291
|
+
$set: {
|
|
17292
|
+
status: "pending",
|
|
17293
|
+
availableAt: avail,
|
|
17294
|
+
reservedAt: null,
|
|
17295
|
+
error: null,
|
|
17296
|
+
payload,
|
|
17297
|
+
priority,
|
|
17298
|
+
id: info.id,
|
|
17299
|
+
createdAt: dlDoc.createdAt ?? now,
|
|
17300
|
+
},
|
|
17301
|
+
$inc: { attempts: 1 },
|
|
17302
|
+
},
|
|
17303
|
+
{ upsert: true },
|
|
17304
|
+
);
|
|
17305
|
+
process.stdout.write("__OK__");
|
|
17306
|
+
} else {
|
|
17307
|
+
const result = await col.updateOne(
|
|
17308
|
+
{ queue: queueName, id: info.id },
|
|
17309
|
+
{ $set: { status: "pending", availableAt: avail, reservedAt: null }, $inc: { attempts: 1 } },
|
|
17310
|
+
);
|
|
17311
|
+
process.stdout.write(result.matchedCount > 0 ? "__OK__" : "__NOT_FOUND__");
|
|
17312
|
+
}
|
|
17263
17313
|
}
|
|
17264
17314
|
else if (operation === "deadLetters") {
|
|
17265
17315
|
const docs = await col.find({ queue: queueName + ".dead_letter" }).toArray();
|
|
@@ -17304,10 +17354,20 @@ var init_mongoBackend = __esm({
|
|
|
17304
17354
|
process.stdout.write(String(revived));
|
|
17305
17355
|
}
|
|
17306
17356
|
else if (operation === "purge") {
|
|
17307
|
-
// Delete docs by status (default:
|
|
17357
|
+
// Delete docs by status (default: every doc for the topic).
|
|
17358
|
+
// Pre-3.13.105 this filtered by { queue: queueName, status } for
|
|
17359
|
+
// EVERY status -- correct for pending/reserved/completed, wrong
|
|
17360
|
+
// for the dead-letter states (dead/failed/dead_letter) which
|
|
17361
|
+
// live under queueName + ".dead_letter" and carry status "dead".
|
|
17362
|
+
// A purge("dead") therefore deleted nothing and returned 0.
|
|
17363
|
+
// data = JSON { status }.
|
|
17308
17364
|
const info = data ? JSON.parse(data) : {};
|
|
17309
|
-
const
|
|
17310
|
-
|
|
17365
|
+
const isDead = info.status && ["dead", "failed", "dead_letter"].includes(info.status);
|
|
17366
|
+
const filter = isDead
|
|
17367
|
+
? { queue: queueName + ".dead_letter" }
|
|
17368
|
+
: (info.status
|
|
17369
|
+
? { queue: queueName, status: info.status }
|
|
17370
|
+
: { queue: queueName });
|
|
17311
17371
|
const res = await col.deleteMany(filter);
|
|
17312
17372
|
process.stdout.write(String(res.deletedCount || 0));
|
|
17313
17373
|
}
|
|
@@ -17404,9 +17464,15 @@ var init_mongoBackend = __esm({
|
|
|
17404
17464
|
fail(queue, id, error, maxRetries, retryBackoff = 0) {
|
|
17405
17465
|
this.execSync("fail", queue, JSON.stringify({ id, error, maxRetries, retryBackoff }));
|
|
17406
17466
|
}
|
|
17407
|
-
/**
|
|
17467
|
+
/**
|
|
17468
|
+
* Revive a specific dead-letter job by id. Returns true if the DL was found
|
|
17469
|
+
* and revived, false otherwise (parity with LiteBackend.retry(queue, id)
|
|
17470
|
+
* and Python's mongo_backend.retry_job()). Pre-3.13.105 this returned void
|
|
17471
|
+
* and Queue.retry(id) reported success for every call, even for unknown ids.
|
|
17472
|
+
*/
|
|
17408
17473
|
retry(queue, id, delaySeconds = 0) {
|
|
17409
|
-
this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
17474
|
+
const out = this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
17475
|
+
return out.includes("__OK__");
|
|
17410
17476
|
}
|
|
17411
17477
|
/** Jobs that exceeded max retries (the `<queue>.dead_letter` collection topic). */
|
|
17412
17478
|
deadLetters(queue, maxRetries) {
|
|
@@ -18075,10 +18141,20 @@ var init_liteBackend = __esm({
|
|
|
18075
18141
|
* Explicit re-queue requested by the caller (job.retry()).
|
|
18076
18142
|
*
|
|
18077
18143
|
* Always re-enqueues regardless of the retry limit — manual override,
|
|
18078
|
-
* distinct from the automatic failJob() path.
|
|
18144
|
+
* distinct from the automatic failJob() path. Cleans up BOTH the
|
|
18145
|
+
* reservation record AND any dead-letter file for this id, so a caller
|
|
18146
|
+
* that iterates deadLetters() and calls .retry() on each doesn't leave
|
|
18147
|
+
* the failed/ directory carrying duplicates (PY-12-05, 3.13.105).
|
|
18148
|
+
* Aligns with retry(queue, jobId) which had always unlinked the
|
|
18149
|
+
* dead-letter file -- two spellings of the same intent that previously
|
|
18150
|
+
* diverged.
|
|
18079
18151
|
*/
|
|
18080
18152
|
retryJob(queue, job, delaySeconds) {
|
|
18081
18153
|
this.clearReservation(queue, job.id);
|
|
18154
|
+
try {
|
|
18155
|
+
unlinkSync6(join17(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
18156
|
+
} catch {
|
|
18157
|
+
}
|
|
18082
18158
|
job.attempts = (job.attempts || 0) + 1;
|
|
18083
18159
|
job.error = void 0;
|
|
18084
18160
|
this.requeue(queue, job, delaySeconds ?? 0, void 0);
|
|
@@ -18277,7 +18353,19 @@ var init_queue = __esm({
|
|
|
18277
18353
|
}
|
|
18278
18354
|
}
|
|
18279
18355
|
/**
|
|
18280
|
-
* Count jobs
|
|
18356
|
+
* Count jobs by status. Defaults to "pending".
|
|
18357
|
+
*
|
|
18358
|
+
* ``"pending"`` counts jobs waiting to be popped -- INCLUDES retryable-
|
|
18359
|
+
* but-attempted ones, because they live in the pending queue under the
|
|
18360
|
+
* auto-retry lifecycle (see failed()).
|
|
18361
|
+
* ``"reserved"`` counts jobs a consumer has popped but not yet
|
|
18362
|
+
* completed/failed (in-flight against the visibility timeout).
|
|
18363
|
+
* ``"completed"`` counts jobs the consumer has finished successfully.
|
|
18364
|
+
* ``"failed"``, ``"dead"``, ``"dead_letter"`` are ALIASES that all count
|
|
18365
|
+
* the dead-letter store -- jobs whose attempts >= maxRetries and that
|
|
18366
|
+
* have given up. Use deadLetters() to list them. Retryable-but-attempted
|
|
18367
|
+
* jobs are NOT counted by size("failed"); use failed() to list them or
|
|
18368
|
+
* size("pending") to include them in a total.
|
|
18281
18369
|
*/
|
|
18282
18370
|
size(status2 = "pending") {
|
|
18283
18371
|
const q = this.topic;
|
|
@@ -18327,13 +18415,17 @@ var init_queue = __esm({
|
|
|
18327
18415
|
/**
|
|
18328
18416
|
* Get jobs that failed at least once but are still being retried
|
|
18329
18417
|
* (0 < attempts < maxRetries). These live in the pending queue under the
|
|
18330
|
-
* auto-retry lifecycle
|
|
18418
|
+
* auto-retry lifecycle (fail() re-queues them with an incremented attempts
|
|
18419
|
+
* count and a retryBackoff delay) so pop() picks them up again. They are
|
|
18420
|
+
* NOT counted by size("failed") -- that alias counts the dead-letter store,
|
|
18421
|
+
* matching deadLetters(). To include retryable-failed jobs in a total, use
|
|
18422
|
+
* size("pending"). Terminal failures are returned by deadLetters().
|
|
18331
18423
|
*/
|
|
18332
18424
|
failed() {
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18336
|
-
|
|
18425
|
+
const raw = this.externalBackend?.failed ? this.externalBackend.failed(this.topic, this._maxRetries) : this.liteBackend.failed(this.topic, this._maxRetries);
|
|
18426
|
+
return raw.map(
|
|
18427
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
18428
|
+
);
|
|
18337
18429
|
}
|
|
18338
18430
|
/**
|
|
18339
18431
|
* Retry all dead letter jobs for this queue's topic.
|
|
@@ -18345,8 +18437,8 @@ var init_queue = __esm({
|
|
|
18345
18437
|
retry(jobId, delaySeconds) {
|
|
18346
18438
|
if (jobId) {
|
|
18347
18439
|
if (this.externalBackend?.retry) {
|
|
18348
|
-
this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
18349
|
-
return true;
|
|
18440
|
+
const result = this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
18441
|
+
return result === void 0 ? true : Boolean(result);
|
|
18350
18442
|
}
|
|
18351
18443
|
return this.liteBackend.retry(this.topic, jobId, delaySeconds);
|
|
18352
18444
|
}
|
|
@@ -18355,8 +18447,8 @@ var init_queue = __esm({
|
|
|
18355
18447
|
let retried = false;
|
|
18356
18448
|
for (const job of deadJobs) {
|
|
18357
18449
|
if (this.externalBackend?.retry) {
|
|
18358
|
-
this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
18359
|
-
retried = true;
|
|
18450
|
+
const result = this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
18451
|
+
if (result === void 0 || Boolean(result)) retried = true;
|
|
18360
18452
|
} else if (this.liteBackend.retry(this.topic, job.id, delaySeconds)) {
|
|
18361
18453
|
retried = true;
|
|
18362
18454
|
}
|
|
@@ -18364,13 +18456,28 @@ var init_queue = __esm({
|
|
|
18364
18456
|
return retried;
|
|
18365
18457
|
}
|
|
18366
18458
|
/**
|
|
18367
|
-
* Get
|
|
18459
|
+
* Get jobs that exceeded max_retries -- terminal failures.
|
|
18460
|
+
*
|
|
18461
|
+
* Same set counted by size("failed") / size("dead") / size("dead_letter")
|
|
18462
|
+
* (three aliases for the dead-letter store). To LIST retryable-but-
|
|
18463
|
+
* attempted jobs (attempts > 0 AND attempts < maxRetries) that are still
|
|
18464
|
+
* being auto-retried, use failed() -- those live in the pending queue and
|
|
18465
|
+
* are NOT dead letters.
|
|
18466
|
+
*
|
|
18467
|
+
* Returns Job objects with the failure reason on ``.error`` (not raw dicts)
|
|
18468
|
+
* so callers can iterate uniformly with the rest of the queue API and, in
|
|
18469
|
+
* particular, call ``.retry()`` on each to manually revive it:
|
|
18470
|
+
*
|
|
18471
|
+
* for (const job of queue.deadLetters()) {
|
|
18472
|
+
* Log.warn(`revived ${job.id}: ${job.error}`);
|
|
18473
|
+
* job.retry();
|
|
18474
|
+
* }
|
|
18368
18475
|
*/
|
|
18369
18476
|
deadLetters(maxRetries) {
|
|
18370
|
-
|
|
18371
|
-
|
|
18372
|
-
|
|
18373
|
-
|
|
18477
|
+
const raw = this.externalBackend?.deadLetters ? this.externalBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries) : this.liteBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries);
|
|
18478
|
+
return raw.map(
|
|
18479
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
18480
|
+
);
|
|
18374
18481
|
}
|
|
18375
18482
|
/**
|
|
18376
18483
|
* Delete messages by status (e.g. "completed", "failed", "dead").
|
|
@@ -38322,7 +38429,9 @@ function buildAddColumnSql(adapter, table2, colName, def) {
|
|
|
38322
38429
|
return sql;
|
|
38323
38430
|
}
|
|
38324
38431
|
function mt(db) {
|
|
38325
|
-
|
|
38432
|
+
const engine = engineOf(db);
|
|
38433
|
+
if (engine === "firebird") return MIGRATION_TABLE;
|
|
38434
|
+
return engine === "mysql" ? `\`${MIGRATION_TABLE}\`` : `"${MIGRATION_TABLE}"`;
|
|
38326
38435
|
}
|
|
38327
38436
|
function deriveDescription(name) {
|
|
38328
38437
|
return name.replace(/^\d+_/, "").replace(/_/g, " ");
|
|
@@ -38345,9 +38454,9 @@ async function ensureMigrationTableOn(db) {
|
|
|
38345
38454
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
38346
38455
|
migration_name VARCHAR(500) NOT NULL UNIQUE,
|
|
38347
38456
|
description VARCHAR(500),
|
|
38348
|
-
batch INTEGER NOT NULL
|
|
38457
|
+
batch INTEGER DEFAULT 1 NOT NULL,
|
|
38349
38458
|
executed_at VARCHAR(50) NOT NULL,
|
|
38350
|
-
passed INTEGER NOT NULL
|
|
38459
|
+
passed INTEGER DEFAULT 1 NOT NULL
|
|
38351
38460
|
)`);
|
|
38352
38461
|
} else {
|
|
38353
38462
|
const idCol = migrationIdColumn(db);
|
|
@@ -38444,7 +38553,7 @@ async function recordApplied(db, name, batch, passed = 1) {
|
|
|
38444
38553
|
"SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE"
|
|
38445
38554
|
);
|
|
38446
38555
|
insertCols.unshift("id");
|
|
38447
|
-
values.unshift(rows[0]?.
|
|
38556
|
+
values.unshift(rows[0]?.next_id ?? 1);
|
|
38448
38557
|
}
|
|
38449
38558
|
const placeholders = insertCols.map(() => "?").join(", ");
|
|
38450
38559
|
await adapterExecute(
|
|
@@ -40858,15 +40967,25 @@ var init_baseModel = __esm({
|
|
|
40858
40967
|
/**
|
|
40859
40968
|
* Invalidate every cached query that touches this model's table.
|
|
40860
40969
|
*
|
|
40861
|
-
* Tag-scoped
|
|
40862
|
-
* this table is busted too
|
|
40863
|
-
* never touches this table is left intact
|
|
40864
|
-
*
|
|
40865
|
-
*
|
|
40970
|
+
* Tag-scoped in the ORM layer (a cached JOIN on another model that reads
|
|
40971
|
+
* this table is busted too because it carries this table's tag; a query
|
|
40972
|
+
* that never touches this table is left intact), then cascaded to the
|
|
40973
|
+
* DB layer on this model's bound connection so an out-of-band write /
|
|
40974
|
+
* deliberate refresh / race-with-another-process cannot leave stale rows
|
|
40975
|
+
* in db.fetch()'s persistent cache. Called after every ORM write
|
|
40976
|
+
* (save/delete/forceDelete/restore) so a read-after-write never serves
|
|
40977
|
+
* a stale/deleted row (CACHE-DEC-01). PY-06-22 (3.13.105) added the
|
|
40978
|
+
* DB-layer cascade -- previously the two cache layers disagreed under
|
|
40979
|
+
* TINA4_AUTO_CACHING=true + TINA4_DB_CACHE=true.
|
|
40866
40980
|
*/
|
|
40867
40981
|
static clearCache() {
|
|
40868
40982
|
const ModelClass = this;
|
|
40869
40983
|
modelQueryCache.clearTag((ModelClass.tableName ?? "").toLowerCase());
|
|
40984
|
+
try {
|
|
40985
|
+
const db = ModelClass.getDb();
|
|
40986
|
+
if (typeof db?.cacheClear === "function") db.cacheClear();
|
|
40987
|
+
} catch {
|
|
40988
|
+
}
|
|
40870
40989
|
}
|
|
40871
40990
|
/**
|
|
40872
40991
|
* Execute a raw SQL SELECT and return results as model instances.
|
|
@@ -1385,15 +1385,28 @@ export class BaseModel {
|
|
|
1385
1385
|
/**
|
|
1386
1386
|
* Invalidate every cached query that touches this model's table.
|
|
1387
1387
|
*
|
|
1388
|
-
* Tag-scoped
|
|
1389
|
-
* this table is busted too
|
|
1390
|
-
* never touches this table is left intact
|
|
1391
|
-
*
|
|
1392
|
-
*
|
|
1388
|
+
* Tag-scoped in the ORM layer (a cached JOIN on another model that reads
|
|
1389
|
+
* this table is busted too because it carries this table's tag; a query
|
|
1390
|
+
* that never touches this table is left intact), then cascaded to the
|
|
1391
|
+
* DB layer on this model's bound connection so an out-of-band write /
|
|
1392
|
+
* deliberate refresh / race-with-another-process cannot leave stale rows
|
|
1393
|
+
* in db.fetch()'s persistent cache. Called after every ORM write
|
|
1394
|
+
* (save/delete/forceDelete/restore) so a read-after-write never serves
|
|
1395
|
+
* a stale/deleted row (CACHE-DEC-01). PY-06-22 (3.13.105) added the
|
|
1396
|
+
* DB-layer cascade -- previously the two cache layers disagreed under
|
|
1397
|
+
* TINA4_AUTO_CACHING=true + TINA4_DB_CACHE=true.
|
|
1393
1398
|
*/
|
|
1394
1399
|
static clearCache(): void {
|
|
1395
1400
|
const ModelClass = this as unknown as typeof BaseModel;
|
|
1396
1401
|
modelQueryCache.clearTag((ModelClass.tableName ?? "").toLowerCase());
|
|
1402
|
+
try {
|
|
1403
|
+
const db: any = ModelClass.getDb();
|
|
1404
|
+
if (typeof db?.cacheClear === "function") db.cacheClear();
|
|
1405
|
+
} catch {
|
|
1406
|
+
// A resolvable DB is not guaranteed at every clearCache() call site
|
|
1407
|
+
// (module-import time in odd bootstraps, tests that mutate bindings);
|
|
1408
|
+
// never let a cache-clear crash a save/delete.
|
|
1409
|
+
}
|
|
1397
1410
|
}
|
|
1398
1411
|
|
|
1399
1412
|
/**
|