@trigger.dev/redis-worker 4.0.0-v4-beta.21 → 4.0.0-v4-beta.23

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/dist/index.cjs CHANGED
@@ -8,6 +8,8 @@ var crypto = require('crypto');
8
8
  require('@trigger.dev/core/v3/utils/flattenAttributes');
9
9
  var v3 = require('@trigger.dev/core/v3');
10
10
  var serverOnly = require('@trigger.dev/core/v3/serverOnly');
11
+ var zod = require('zod');
12
+ var cronParser = require('cron-parser');
11
13
 
12
14
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
15
 
@@ -8742,7 +8744,7 @@ var require_Redis = __commonJS({
8742
8744
  var lodash_1 = require_lodash3();
8743
8745
  var Deque = require_denque();
8744
8746
  var debug = (0, utils_1.Debug)("redis");
8745
- var Redis3 = class _Redis extends Commander_1.default {
8747
+ var Redis4 = class _Redis extends Commander_1.default {
8746
8748
  constructor(arg1, arg2, arg3) {
8747
8749
  super();
8748
8750
  this.status = "wait";
@@ -9305,12 +9307,12 @@ var require_Redis = __commonJS({
9305
9307
  }).catch(lodash_1.noop);
9306
9308
  }
9307
9309
  };
9308
- Redis3.Cluster = cluster_1.default;
9309
- Redis3.Command = Command_1.default;
9310
- Redis3.defaultOptions = RedisOptions_1.DEFAULT_REDIS_OPTIONS;
9311
- (0, applyMixin_1.default)(Redis3, events_1.EventEmitter);
9312
- (0, transaction_1.addTransactionSupport)(Redis3.prototype);
9313
- exports.default = Redis3;
9310
+ Redis4.Cluster = cluster_1.default;
9311
+ Redis4.Command = Command_1.default;
9312
+ Redis4.defaultOptions = RedisOptions_1.DEFAULT_REDIS_OPTIONS;
9313
+ (0, applyMixin_1.default)(Redis4, events_1.EventEmitter);
9314
+ (0, transaction_1.addTransactionSupport)(Redis4.prototype);
9315
+ exports.default = Redis4;
9314
9316
  }
9315
9317
  });
9316
9318
 
@@ -9563,7 +9565,8 @@ var SimpleQueue = class {
9563
9565
  id,
9564
9566
  item: parsedItem,
9565
9567
  job: parsedItem.job,
9566
- timestamp
9568
+ timestamp,
9569
+ availableJobs: Object.keys(this.schema)
9567
9570
  });
9568
9571
  continue;
9569
9572
  }
@@ -9644,8 +9647,30 @@ var SimpleQueue = class {
9644
9647
  throw e;
9645
9648
  }
9646
9649
  }
9650
+ async getJob(id) {
9651
+ const result = await this.redis.getJob(`queue`, `items`, id);
9652
+ if (!result) {
9653
+ return null;
9654
+ }
9655
+ const [_, score, serializedItem] = result;
9656
+ const item = JSON.parse(serializedItem);
9657
+ return {
9658
+ id,
9659
+ job: item.job,
9660
+ item: item.item,
9661
+ visibilityTimeoutMs: item.visibilityTimeoutMs,
9662
+ attempt: item.attempt ?? 0,
9663
+ timestamp: new Date(Number(score)),
9664
+ deduplicationKey: item.deduplicationKey ?? void 0
9665
+ };
9666
+ }
9647
9667
  async moveToDeadLetterQueue(id, errorMessage) {
9648
9668
  try {
9669
+ this.logger.debug(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): moving item to DLQ`, {
9670
+ queue: this.name,
9671
+ id,
9672
+ errorMessage
9673
+ });
9649
9674
  const result = await this.redis.moveToDeadLetterQueue(
9650
9675
  `queue`,
9651
9676
  `items`,
@@ -9766,6 +9791,25 @@ var SimpleQueue = class {
9766
9791
  return dequeued
9767
9792
  `
9768
9793
  });
9794
+ this.redis.defineCommand("getJob", {
9795
+ numberOfKeys: 2,
9796
+ lua: `
9797
+ local queue = KEYS[1]
9798
+ local items = KEYS[2]
9799
+ local jobId = ARGV[1]
9800
+
9801
+ local serializedItem = redis.call('HGET', items, jobId)
9802
+
9803
+ if serializedItem == false then
9804
+ return nil
9805
+ end
9806
+
9807
+ -- get the score from the queue sorted set
9808
+ local score = redis.call('ZSCORE', queue, jobId)
9809
+
9810
+ return { jobId, score, serializedItem }
9811
+ `
9812
+ });
9769
9813
  this.redis.defineCommand("ackItem", {
9770
9814
  numberOfKeys: 2,
9771
9815
  lua: `
@@ -11007,6 +11051,11 @@ function validateConcurrency(concurrency) {
11007
11051
  throw new TypeError("Expected `concurrency` to be a number from 1 and up");
11008
11052
  }
11009
11053
  }
11054
+ var CronSchema = zod.z.object({
11055
+ cron: zod.z.string(),
11056
+ lastTimestamp: zod.z.number().optional(),
11057
+ timestamp: zod.z.number()
11058
+ });
11010
11059
  var defaultRetrySettings = {
11011
11060
  maxAttempts: 12,
11012
11061
  factor: 2,
@@ -11035,7 +11084,6 @@ var Worker = class _Worker {
11035
11084
  this.jobs = options.jobs;
11036
11085
  const { workers = 1, tasksPerWorker = 1, limit = 10 } = options.concurrency ?? {};
11037
11086
  this.concurrency = { workers, tasksPerWorker, limit };
11038
- this.limiter = pLimit(this.concurrency.limit);
11039
11087
  const masterQueueObservableGauge = this.meter.createObservableGauge("redis_worker.queue.size", {
11040
11088
  description: "The number of items in the queue",
11041
11089
  unit: "items",
@@ -11086,7 +11134,7 @@ var Worker = class _Worker {
11086
11134
  concurrency;
11087
11135
  shutdownTimeoutMs;
11088
11136
  // The p-limit limiter to control overall concurrency.
11089
- limiter;
11137
+ limiters = {};
11090
11138
  async #updateQueueSizeMetric(observableResult) {
11091
11139
  const queueSize = await this.queue.size();
11092
11140
  observableResult.observe(queueSize, {
@@ -11100,19 +11148,30 @@ var Worker = class _Worker {
11100
11148
  });
11101
11149
  }
11102
11150
  async #updateConcurrencyLimitActiveMetric(observableResult) {
11103
- observableResult.observe(this.limiter.activeCount, {
11104
- worker_name: this.options.name
11105
- });
11151
+ for (const [workerId, limiter] of Object.entries(this.limiters)) {
11152
+ observableResult.observe(limiter.activeCount, {
11153
+ worker_name: this.options.name,
11154
+ worker_id: workerId
11155
+ });
11156
+ }
11106
11157
  }
11107
11158
  async #updateConcurrencyLimitPendingMetric(observableResult) {
11108
- observableResult.observe(this.limiter.pendingCount, {
11109
- worker_name: this.options.name
11110
- });
11159
+ for (const [workerId, limiter] of Object.entries(this.limiters)) {
11160
+ observableResult.observe(limiter.pendingCount, {
11161
+ worker_name: this.options.name,
11162
+ worker_id: workerId
11163
+ });
11164
+ }
11111
11165
  }
11112
11166
  start() {
11113
11167
  const { workers, tasksPerWorker } = this.concurrency;
11168
+ this.logger.info("Starting worker", {
11169
+ workers,
11170
+ tasksPerWorker,
11171
+ concurrency: this.concurrency
11172
+ });
11114
11173
  for (let i = 0; i < workers; i++) {
11115
- this.workerLoops.push(this.runWorkerLoop(`worker-${nanoid(12)}`, tasksPerWorker));
11174
+ this.workerLoops.push(this.runWorkerLoop(`worker-${nanoid(12)}`, tasksPerWorker, i, workers));
11116
11175
  }
11117
11176
  this.setupShutdownHandlers();
11118
11177
  this.subscriber = createRedisClient(this.options.redisOptions, {
@@ -11124,6 +11183,7 @@ var Worker = class _Worker {
11124
11183
  }
11125
11184
  });
11126
11185
  this.setupSubscriber();
11186
+ this.setupCron();
11127
11187
  return this;
11128
11188
  }
11129
11189
  /**
@@ -11262,37 +11322,78 @@ var Worker = class _Worker {
11262
11322
  }
11263
11323
  );
11264
11324
  }
11325
+ async getJob(id) {
11326
+ return this.queue.getJob(id);
11327
+ }
11265
11328
  /**
11266
11329
  * The main loop that each worker runs. It repeatedly polls for items,
11267
11330
  * processes them, and then waits before the next iteration.
11268
11331
  */
11269
- async runWorkerLoop(workerId, taskCount) {
11332
+ async runWorkerLoop(workerId, taskCount, workerIndex, totalWorkers) {
11333
+ const limiter = pLimit(this.concurrency.limit);
11334
+ this.limiters[workerId] = limiter;
11270
11335
  const pollIntervalMs = this.options.pollIntervalMs ?? 1e3;
11271
11336
  const immediatePollIntervalMs = this.options.immediatePollIntervalMs ?? 100;
11337
+ const delayBetweenWorkers = this.options.pollIntervalMs ?? 1e3;
11338
+ const delay = delayBetweenWorkers * (totalWorkers - workerIndex);
11339
+ await _Worker.delay(delay);
11340
+ this.logger.info("Starting worker loop", {
11341
+ workerIndex,
11342
+ totalWorkers,
11343
+ delay,
11344
+ workerId,
11345
+ taskCount,
11346
+ pollIntervalMs,
11347
+ immediatePollIntervalMs,
11348
+ concurrencyOptions: this.concurrency
11349
+ });
11272
11350
  while (!this.isShuttingDown) {
11273
- if (this.limiter.activeCount + this.limiter.pendingCount >= this.concurrency.limit) {
11351
+ if (limiter.activeCount + limiter.pendingCount >= this.concurrency.limit) {
11352
+ this.logger.debug("Worker at capacity, waiting", {
11353
+ workerId,
11354
+ concurrencyOptions: this.concurrency,
11355
+ activeCount: limiter.activeCount,
11356
+ pendingCount: limiter.pendingCount
11357
+ });
11274
11358
  await _Worker.delay(pollIntervalMs);
11275
11359
  continue;
11276
11360
  }
11361
+ const $taskCount = Math.min(
11362
+ taskCount,
11363
+ this.concurrency.limit - limiter.activeCount - limiter.pendingCount
11364
+ );
11277
11365
  try {
11278
11366
  const items = await this.withHistogram(
11279
11367
  this.metrics.dequeueDuration,
11280
- this.queue.dequeue(taskCount),
11368
+ this.queue.dequeue($taskCount),
11281
11369
  {
11282
11370
  worker_id: workerId,
11283
- task_count: taskCount
11371
+ task_count: $taskCount
11284
11372
  }
11285
11373
  );
11286
11374
  if (items.length === 0) {
11375
+ this.logger.debug("No items to dequeue", {
11376
+ workerId,
11377
+ concurrencyOptions: this.concurrency,
11378
+ activeCount: limiter.activeCount,
11379
+ pendingCount: limiter.pendingCount
11380
+ });
11287
11381
  await _Worker.delay(pollIntervalMs);
11288
11382
  continue;
11289
11383
  }
11384
+ this.logger.debug("Dequeued items", {
11385
+ workerId,
11386
+ itemCount: items.length,
11387
+ concurrencyOptions: this.concurrency,
11388
+ activeCount: limiter.activeCount,
11389
+ pendingCount: limiter.pendingCount
11390
+ });
11290
11391
  for (const item of items) {
11291
- this.limiter(() => this.processItem(item, items.length, workerId)).catch(
11292
- (err) => {
11293
- this.logger.error("Unhandled error in processItem:", { error: err, workerId, item });
11294
- }
11295
- );
11392
+ limiter(
11393
+ () => this.processItem(item, items.length, workerId, limiter)
11394
+ ).catch((err) => {
11395
+ this.logger.error("Unhandled error in processItem:", { error: err, workerId, item });
11396
+ });
11296
11397
  }
11297
11398
  } catch (error) {
11298
11399
  this.logger.error("Error dequeuing items:", { name: this.options.name, error });
@@ -11301,17 +11402,22 @@ var Worker = class _Worker {
11301
11402
  }
11302
11403
  await _Worker.delay(immediatePollIntervalMs);
11303
11404
  }
11405
+ this.logger.info("Worker loop finished", { workerId });
11304
11406
  }
11305
11407
  /**
11306
11408
  * Processes a single item.
11307
11409
  */
11308
- async processItem({ id, job, item, visibilityTimeoutMs, attempt, timestamp, deduplicationKey }, batchSize, workerId) {
11410
+ async processItem({ id, job, item, visibilityTimeoutMs, attempt, timestamp, deduplicationKey }, batchSize, workerId, limiter) {
11309
11411
  const catalogItem = this.options.catalog[job];
11310
11412
  const handler = this.jobs[job];
11311
11413
  if (!handler) {
11312
11414
  this.logger.error(`No handler found for job type: ${job}`);
11313
11415
  return;
11314
11416
  }
11417
+ if (!catalogItem) {
11418
+ this.logger.error(`No catalog item found for job type: ${job}`);
11419
+ return;
11420
+ }
11315
11421
  await startSpan(
11316
11422
  this.tracer,
11317
11423
  "processItem",
@@ -11327,6 +11433,9 @@ var Worker = class _Worker {
11327
11433
  }
11328
11434
  );
11329
11435
  await this.queue.ack(id, deduplicationKey);
11436
+ if (catalogItem.cron) {
11437
+ await this.rescheduleCronJob(job, catalogItem, item);
11438
+ }
11330
11439
  },
11331
11440
  {
11332
11441
  kind: SpanKind.CONSUMER,
@@ -11337,9 +11446,9 @@ var Worker = class _Worker {
11337
11446
  job_timestamp: timestamp.getTime(),
11338
11447
  job_age_in_ms: Date.now() - timestamp.getTime(),
11339
11448
  worker_id: workerId,
11340
- worker_limit_concurrency: this.limiter.concurrency,
11341
- worker_limit_active: this.limiter.activeCount,
11342
- worker_limit_pending: this.limiter.pendingCount,
11449
+ worker_limit_concurrency: limiter.concurrency,
11450
+ worker_limit_active: limiter.activeCount,
11451
+ worker_limit_pending: limiter.pendingCount,
11343
11452
  worker_name: this.options.name,
11344
11453
  batch_size: batchSize
11345
11454
  }
@@ -11373,6 +11482,9 @@ var Worker = class _Worker {
11373
11482
  errorMessage
11374
11483
  });
11375
11484
  await this.queue.moveToDeadLetterQueue(id, errorMessage);
11485
+ if (catalogItem.cron) {
11486
+ await this.rescheduleCronJob(job, catalogItem, item);
11487
+ }
11376
11488
  return;
11377
11489
  }
11378
11490
  const retryDate = new Date(Date.now() + retryDelay);
@@ -11425,6 +11537,93 @@ var Worker = class _Worker {
11425
11537
  static delay(ms) {
11426
11538
  return new Promise((resolve) => setTimeout(resolve, ms));
11427
11539
  }
11540
+ setupCron() {
11541
+ const cronJobs = Object.entries(this.options.catalog).filter(([_, value]) => value.cron);
11542
+ if (cronJobs.length === 0) {
11543
+ return;
11544
+ }
11545
+ this.logger.info("Setting up cron jobs", {
11546
+ cronJobs: cronJobs.map(([job, value]) => ({
11547
+ job,
11548
+ cron: value.cron,
11549
+ jitterInMs: value.jitterInMs
11550
+ }))
11551
+ });
11552
+ const enqueuePromises = cronJobs.map(
11553
+ ([job, value]) => this.enqueueCronJob(value.cron, job, value.jitterInMs)
11554
+ );
11555
+ Promise.allSettled(enqueuePromises).then((results) => {
11556
+ results.forEach((result) => {
11557
+ if (result.status === "fulfilled") {
11558
+ this.logger.info("Enqueued cron job", { result: result.value });
11559
+ } else {
11560
+ this.logger.error("Failed to enqueue cron job", { reason: result.reason });
11561
+ }
11562
+ });
11563
+ });
11564
+ }
11565
+ async enqueueCronJob(cron, job, jitter, lastTimestamp) {
11566
+ const scheduledAt = this.calculateNextScheduledAt(cron, lastTimestamp);
11567
+ const identifier = [job, this.timestampIdentifier(scheduledAt)].join(":");
11568
+ const appliedJitter = typeof jitter === "number" ? Math.random() * jitter - jitter / 2 : 0;
11569
+ const availableAt = new Date(scheduledAt.getTime() + appliedJitter);
11570
+ const enqueued = await this.enqueueOnce({
11571
+ id: identifier,
11572
+ job,
11573
+ payload: {
11574
+ timestamp: scheduledAt.getTime(),
11575
+ lastTimestamp: lastTimestamp?.getTime(),
11576
+ cron
11577
+ },
11578
+ availableAt
11579
+ });
11580
+ this.logger.info("Enqueued cron job", {
11581
+ identifier,
11582
+ cron,
11583
+ job,
11584
+ scheduledAt,
11585
+ enqueued,
11586
+ availableAt,
11587
+ appliedJitter,
11588
+ jitter
11589
+ });
11590
+ return {
11591
+ identifier,
11592
+ cron,
11593
+ job,
11594
+ scheduledAt,
11595
+ enqueued
11596
+ };
11597
+ }
11598
+ async rescheduleCronJob(job, catalogItem, item) {
11599
+ if (!catalogItem.cron) {
11600
+ return;
11601
+ }
11602
+ return this.enqueueCronJob(
11603
+ catalogItem.cron,
11604
+ job,
11605
+ catalogItem.jitterInMs,
11606
+ new Date(item.timestamp)
11607
+ );
11608
+ }
11609
+ calculateNextScheduledAt(cron, lastTimestamp) {
11610
+ const scheduledAt = cronParser.parseExpression(cron, {
11611
+ currentDate: lastTimestamp
11612
+ }).next().toDate();
11613
+ if (scheduledAt < /* @__PURE__ */ new Date()) {
11614
+ return this.calculateNextScheduledAt(cron);
11615
+ }
11616
+ return scheduledAt;
11617
+ }
11618
+ timestampIdentifier(timestamp) {
11619
+ const year = timestamp.getUTCFullYear();
11620
+ const month = timestamp.getUTCMonth();
11621
+ const day = timestamp.getUTCDate();
11622
+ const hour = timestamp.getUTCHours();
11623
+ const minute = timestamp.getUTCMinutes();
11624
+ const second = timestamp.getUTCSeconds();
11625
+ return `${year}-${month}-${day}-${hour}-${minute}-${second}`;
11626
+ }
11428
11627
  setupSubscriber() {
11429
11628
  const channel = `${this.options.name}:redrive`;
11430
11629
  this.subscriber?.subscribe(channel, (err) => {
@@ -11481,6 +11680,7 @@ var Worker = class _Worker {
11481
11680
  }
11482
11681
  };
11483
11682
 
11683
+ exports.CronSchema = CronSchema;
11484
11684
  exports.SimpleQueue = SimpleQueue;
11485
11685
  exports.Worker = Worker;
11486
11686
  //# sourceMappingURL=index.cjs.map