@trigger.dev/redis-worker 4.0.0-v4-beta.20 → 4.0.0-v4-beta.22

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.js CHANGED
@@ -7,7 +7,8 @@ import { webcrypto } from 'node:crypto';
7
7
  import '@trigger.dev/core/v3/utils/flattenAttributes';
8
8
  import { calculateNextRetryDelay } from '@trigger.dev/core/v3';
9
9
  import { shutdownManager } from '@trigger.dev/core/v3/serverOnly';
10
- import { Histogram } from 'prom-client';
10
+ import { z } from 'zod';
11
+ import { parseExpression } from 'cron-parser';
11
12
 
12
13
  const require = createRequire(import.meta.url || process.cwd() + '/index.js');
13
14
  var __create = Object.create;
@@ -8737,7 +8738,7 @@ var require_Redis = __commonJS({
8737
8738
  var lodash_1 = require_lodash3();
8738
8739
  var Deque = require_denque();
8739
8740
  var debug = (0, utils_1.Debug)("redis");
8740
- var Redis3 = class _Redis extends Commander_1.default {
8741
+ var Redis4 = class _Redis extends Commander_1.default {
8741
8742
  constructor(arg1, arg2, arg3) {
8742
8743
  super();
8743
8744
  this.status = "wait";
@@ -9300,12 +9301,12 @@ var require_Redis = __commonJS({
9300
9301
  }).catch(lodash_1.noop);
9301
9302
  }
9302
9303
  };
9303
- Redis3.Cluster = cluster_1.default;
9304
- Redis3.Command = Command_1.default;
9305
- Redis3.defaultOptions = RedisOptions_1.DEFAULT_REDIS_OPTIONS;
9306
- (0, applyMixin_1.default)(Redis3, events_1.EventEmitter);
9307
- (0, transaction_1.addTransactionSupport)(Redis3.prototype);
9308
- exports.default = Redis3;
9304
+ Redis4.Cluster = cluster_1.default;
9305
+ Redis4.Command = Command_1.default;
9306
+ Redis4.defaultOptions = RedisOptions_1.DEFAULT_REDIS_OPTIONS;
9307
+ (0, applyMixin_1.default)(Redis4, events_1.EventEmitter);
9308
+ (0, transaction_1.addTransactionSupport)(Redis4.prototype);
9309
+ exports.default = Redis4;
9309
9310
  }
9310
9311
  });
9311
9312
 
@@ -9503,6 +9504,39 @@ var SimpleQueue = class {
9503
9504
  throw e;
9504
9505
  }
9505
9506
  }
9507
+ async enqueueOnce({
9508
+ id,
9509
+ job,
9510
+ item,
9511
+ attempt,
9512
+ availableAt,
9513
+ visibilityTimeoutMs
9514
+ }) {
9515
+ if (!id) {
9516
+ throw new Error("enqueueOnce requires an id");
9517
+ }
9518
+ try {
9519
+ const score = availableAt ? availableAt.getTime() : Date.now();
9520
+ const deduplicationKey = nanoid();
9521
+ const serializedItem = JSON.stringify({
9522
+ job,
9523
+ item,
9524
+ visibilityTimeoutMs,
9525
+ attempt,
9526
+ deduplicationKey
9527
+ });
9528
+ const result = await this.redis.enqueueItemOnce(`queue`, `items`, id, score, serializedItem);
9529
+ return result === 1;
9530
+ } catch (e) {
9531
+ this.logger.error(`SimpleQueue ${this.name}.enqueueOnce(): error enqueuing`, {
9532
+ queue: this.name,
9533
+ error: e,
9534
+ id,
9535
+ item
9536
+ });
9537
+ throw e;
9538
+ }
9539
+ }
9506
9540
  async dequeue(count = 1) {
9507
9541
  const now = Date.now();
9508
9542
  try {
@@ -9525,7 +9559,8 @@ var SimpleQueue = class {
9525
9559
  id,
9526
9560
  item: parsedItem,
9527
9561
  job: parsedItem.job,
9528
- timestamp
9562
+ timestamp,
9563
+ availableJobs: Object.keys(this.schema)
9529
9564
  });
9530
9565
  continue;
9531
9566
  }
@@ -9606,8 +9641,30 @@ var SimpleQueue = class {
9606
9641
  throw e;
9607
9642
  }
9608
9643
  }
9644
+ async getJob(id) {
9645
+ const result = await this.redis.getJob(`queue`, `items`, id);
9646
+ if (!result) {
9647
+ return null;
9648
+ }
9649
+ const [_, score, serializedItem] = result;
9650
+ const item = JSON.parse(serializedItem);
9651
+ return {
9652
+ id,
9653
+ job: item.job,
9654
+ item: item.item,
9655
+ visibilityTimeoutMs: item.visibilityTimeoutMs,
9656
+ attempt: item.attempt ?? 0,
9657
+ timestamp: new Date(Number(score)),
9658
+ deduplicationKey: item.deduplicationKey ?? void 0
9659
+ };
9660
+ }
9609
9661
  async moveToDeadLetterQueue(id, errorMessage) {
9610
9662
  try {
9663
+ this.logger.debug(`SimpleQueue ${this.name}.moveToDeadLetterQueue(): moving item to DLQ`, {
9664
+ queue: this.name,
9665
+ id,
9666
+ errorMessage
9667
+ });
9611
9668
  const result = await this.redis.moveToDeadLetterQueue(
9612
9669
  `queue`,
9613
9670
  `items`,
@@ -9728,6 +9785,25 @@ var SimpleQueue = class {
9728
9785
  return dequeued
9729
9786
  `
9730
9787
  });
9788
+ this.redis.defineCommand("getJob", {
9789
+ numberOfKeys: 2,
9790
+ lua: `
9791
+ local queue = KEYS[1]
9792
+ local items = KEYS[2]
9793
+ local jobId = ARGV[1]
9794
+
9795
+ local serializedItem = redis.call('HGET', items, jobId)
9796
+
9797
+ if serializedItem == false then
9798
+ return nil
9799
+ end
9800
+
9801
+ -- get the score from the queue sorted set
9802
+ local score = redis.call('ZSCORE', queue, jobId)
9803
+
9804
+ return { jobId, score, serializedItem }
9805
+ `
9806
+ });
9731
9807
  this.redis.defineCommand("ackItem", {
9732
9808
  numberOfKeys: 2,
9733
9809
  lua: `
@@ -9817,6 +9893,25 @@ var SimpleQueue = class {
9817
9893
  return 1
9818
9894
  `
9819
9895
  });
9896
+ this.redis.defineCommand("enqueueItemOnce", {
9897
+ numberOfKeys: 2,
9898
+ lua: `
9899
+ local queue = KEYS[1]
9900
+ local items = KEYS[2]
9901
+ local id = ARGV[1]
9902
+ local score = ARGV[2]
9903
+ local serializedItem = ARGV[3]
9904
+
9905
+ -- Only add if not exists
9906
+ local added = redis.call('HSETNX', items, id, serializedItem)
9907
+ if added == 1 then
9908
+ redis.call('ZADD', queue, 'NX', score, id)
9909
+ return 1
9910
+ else
9911
+ return 0
9912
+ end
9913
+ `
9914
+ });
9820
9915
  }
9821
9916
  };
9822
9917
 
@@ -10173,6 +10268,173 @@ var BaseContext = (
10173
10268
  );
10174
10269
  var ROOT_CONTEXT = new BaseContext();
10175
10270
 
10271
+ // ../../node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/metrics/NoopMeter.js
10272
+ var __extends = /* @__PURE__ */ function() {
10273
+ var extendStatics = function(d, b) {
10274
+ extendStatics = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(d2, b2) {
10275
+ d2.__proto__ = b2;
10276
+ } || function(d2, b2) {
10277
+ for (var p in b2) if (Object.prototype.hasOwnProperty.call(b2, p)) d2[p] = b2[p];
10278
+ };
10279
+ return extendStatics(d, b);
10280
+ };
10281
+ return function(d, b) {
10282
+ if (typeof b !== "function" && b !== null)
10283
+ throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
10284
+ extendStatics(d, b);
10285
+ function __() {
10286
+ this.constructor = d;
10287
+ }
10288
+ d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
10289
+ };
10290
+ }();
10291
+ var NoopMeter = (
10292
+ /** @class */
10293
+ function() {
10294
+ function NoopMeter2() {
10295
+ }
10296
+ NoopMeter2.prototype.createGauge = function(_name, _options) {
10297
+ return NOOP_GAUGE_METRIC;
10298
+ };
10299
+ NoopMeter2.prototype.createHistogram = function(_name, _options) {
10300
+ return NOOP_HISTOGRAM_METRIC;
10301
+ };
10302
+ NoopMeter2.prototype.createCounter = function(_name, _options) {
10303
+ return NOOP_COUNTER_METRIC;
10304
+ };
10305
+ NoopMeter2.prototype.createUpDownCounter = function(_name, _options) {
10306
+ return NOOP_UP_DOWN_COUNTER_METRIC;
10307
+ };
10308
+ NoopMeter2.prototype.createObservableGauge = function(_name, _options) {
10309
+ return NOOP_OBSERVABLE_GAUGE_METRIC;
10310
+ };
10311
+ NoopMeter2.prototype.createObservableCounter = function(_name, _options) {
10312
+ return NOOP_OBSERVABLE_COUNTER_METRIC;
10313
+ };
10314
+ NoopMeter2.prototype.createObservableUpDownCounter = function(_name, _options) {
10315
+ return NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC;
10316
+ };
10317
+ NoopMeter2.prototype.addBatchObservableCallback = function(_callback, _observables) {
10318
+ };
10319
+ NoopMeter2.prototype.removeBatchObservableCallback = function(_callback) {
10320
+ };
10321
+ return NoopMeter2;
10322
+ }()
10323
+ );
10324
+ var NoopMetric = (
10325
+ /** @class */
10326
+ /* @__PURE__ */ function() {
10327
+ function NoopMetric2() {
10328
+ }
10329
+ return NoopMetric2;
10330
+ }()
10331
+ );
10332
+ var NoopCounterMetric = (
10333
+ /** @class */
10334
+ function(_super) {
10335
+ __extends(NoopCounterMetric2, _super);
10336
+ function NoopCounterMetric2() {
10337
+ return _super !== null && _super.apply(this, arguments) || this;
10338
+ }
10339
+ NoopCounterMetric2.prototype.add = function(_value, _attributes) {
10340
+ };
10341
+ return NoopCounterMetric2;
10342
+ }(NoopMetric)
10343
+ );
10344
+ var NoopUpDownCounterMetric = (
10345
+ /** @class */
10346
+ function(_super) {
10347
+ __extends(NoopUpDownCounterMetric2, _super);
10348
+ function NoopUpDownCounterMetric2() {
10349
+ return _super !== null && _super.apply(this, arguments) || this;
10350
+ }
10351
+ NoopUpDownCounterMetric2.prototype.add = function(_value, _attributes) {
10352
+ };
10353
+ return NoopUpDownCounterMetric2;
10354
+ }(NoopMetric)
10355
+ );
10356
+ var NoopGaugeMetric = (
10357
+ /** @class */
10358
+ function(_super) {
10359
+ __extends(NoopGaugeMetric2, _super);
10360
+ function NoopGaugeMetric2() {
10361
+ return _super !== null && _super.apply(this, arguments) || this;
10362
+ }
10363
+ NoopGaugeMetric2.prototype.record = function(_value, _attributes) {
10364
+ };
10365
+ return NoopGaugeMetric2;
10366
+ }(NoopMetric)
10367
+ );
10368
+ var NoopHistogramMetric = (
10369
+ /** @class */
10370
+ function(_super) {
10371
+ __extends(NoopHistogramMetric2, _super);
10372
+ function NoopHistogramMetric2() {
10373
+ return _super !== null && _super.apply(this, arguments) || this;
10374
+ }
10375
+ NoopHistogramMetric2.prototype.record = function(_value, _attributes) {
10376
+ };
10377
+ return NoopHistogramMetric2;
10378
+ }(NoopMetric)
10379
+ );
10380
+ var NoopObservableMetric = (
10381
+ /** @class */
10382
+ function() {
10383
+ function NoopObservableMetric2() {
10384
+ }
10385
+ NoopObservableMetric2.prototype.addCallback = function(_callback) {
10386
+ };
10387
+ NoopObservableMetric2.prototype.removeCallback = function(_callback) {
10388
+ };
10389
+ return NoopObservableMetric2;
10390
+ }()
10391
+ );
10392
+ var NoopObservableCounterMetric = (
10393
+ /** @class */
10394
+ function(_super) {
10395
+ __extends(NoopObservableCounterMetric2, _super);
10396
+ function NoopObservableCounterMetric2() {
10397
+ return _super !== null && _super.apply(this, arguments) || this;
10398
+ }
10399
+ return NoopObservableCounterMetric2;
10400
+ }(NoopObservableMetric)
10401
+ );
10402
+ var NoopObservableGaugeMetric = (
10403
+ /** @class */
10404
+ function(_super) {
10405
+ __extends(NoopObservableGaugeMetric2, _super);
10406
+ function NoopObservableGaugeMetric2() {
10407
+ return _super !== null && _super.apply(this, arguments) || this;
10408
+ }
10409
+ return NoopObservableGaugeMetric2;
10410
+ }(NoopObservableMetric)
10411
+ );
10412
+ var NoopObservableUpDownCounterMetric = (
10413
+ /** @class */
10414
+ function(_super) {
10415
+ __extends(NoopObservableUpDownCounterMetric2, _super);
10416
+ function NoopObservableUpDownCounterMetric2() {
10417
+ return _super !== null && _super.apply(this, arguments) || this;
10418
+ }
10419
+ return NoopObservableUpDownCounterMetric2;
10420
+ }(NoopObservableMetric)
10421
+ );
10422
+ var NOOP_METER = new NoopMeter();
10423
+ var NOOP_COUNTER_METRIC = new NoopCounterMetric();
10424
+ var NOOP_GAUGE_METRIC = new NoopGaugeMetric();
10425
+ var NOOP_HISTOGRAM_METRIC = new NoopHistogramMetric();
10426
+ var NOOP_UP_DOWN_COUNTER_METRIC = new NoopUpDownCounterMetric();
10427
+ var NOOP_OBSERVABLE_COUNTER_METRIC = new NoopObservableCounterMetric();
10428
+ var NOOP_OBSERVABLE_GAUGE_METRIC = new NoopObservableGaugeMetric();
10429
+ var NOOP_OBSERVABLE_UP_DOWN_COUNTER_METRIC = new NoopObservableUpDownCounterMetric();
10430
+
10431
+ // ../../node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/metrics/Metric.js
10432
+ var ValueType;
10433
+ (function(ValueType2) {
10434
+ ValueType2[ValueType2["INT"] = 0] = "INT";
10435
+ ValueType2[ValueType2["DOUBLE"] = 1] = "DOUBLE";
10436
+ })(ValueType || (ValueType = {}));
10437
+
10176
10438
  // ../../node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/context/NoopContextManager.js
10177
10439
  var __read3 = function(o, n) {
10178
10440
  var m = typeof Symbol === "function" && o[Symbol.iterator];
@@ -10535,8 +10797,54 @@ var SpanStatusCode;
10535
10797
  SpanStatusCode2[SpanStatusCode2["ERROR"] = 2] = "ERROR";
10536
10798
  })(SpanStatusCode || (SpanStatusCode = {}));
10537
10799
 
10800
+ // ../../node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/metrics/NoopMeterProvider.js
10801
+ var NoopMeterProvider = (
10802
+ /** @class */
10803
+ function() {
10804
+ function NoopMeterProvider2() {
10805
+ }
10806
+ NoopMeterProvider2.prototype.getMeter = function(_name, _version, _options) {
10807
+ return NOOP_METER;
10808
+ };
10809
+ return NoopMeterProvider2;
10810
+ }()
10811
+ );
10812
+ var NOOP_METER_PROVIDER = new NoopMeterProvider();
10813
+
10814
+ // ../../node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/api/metrics.js
10815
+ var API_NAME3 = "metrics";
10816
+ var MetricsAPI = (
10817
+ /** @class */
10818
+ function() {
10819
+ function MetricsAPI2() {
10820
+ }
10821
+ MetricsAPI2.getInstance = function() {
10822
+ if (!this._instance) {
10823
+ this._instance = new MetricsAPI2();
10824
+ }
10825
+ return this._instance;
10826
+ };
10827
+ MetricsAPI2.prototype.setGlobalMeterProvider = function(provider) {
10828
+ return registerGlobal(API_NAME3, provider, DiagAPI.instance());
10829
+ };
10830
+ MetricsAPI2.prototype.getMeterProvider = function() {
10831
+ return getGlobal(API_NAME3) || NOOP_METER_PROVIDER;
10832
+ };
10833
+ MetricsAPI2.prototype.getMeter = function(name, version, options) {
10834
+ return this.getMeterProvider().getMeter(name, version, options);
10835
+ };
10836
+ MetricsAPI2.prototype.disable = function() {
10837
+ unregisterGlobal(API_NAME3, DiagAPI.instance());
10838
+ };
10839
+ return MetricsAPI2;
10840
+ }()
10841
+ );
10842
+
10843
+ // ../../node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/metrics-api.js
10844
+ var metrics = MetricsAPI.getInstance();
10845
+
10538
10846
  // ../../node_modules/.pnpm/@opentelemetry+api@1.9.0/node_modules/@opentelemetry/api/build/esm/api/trace.js
10539
- var API_NAME3 = "trace";
10847
+ var API_NAME4 = "trace";
10540
10848
  var TraceAPI = (
10541
10849
  /** @class */
10542
10850
  function() {
@@ -10558,20 +10866,20 @@ var TraceAPI = (
10558
10866
  return this._instance;
10559
10867
  };
10560
10868
  TraceAPI2.prototype.setGlobalTracerProvider = function(provider) {
10561
- var success = registerGlobal(API_NAME3, this._proxyTracerProvider, DiagAPI.instance());
10869
+ var success = registerGlobal(API_NAME4, this._proxyTracerProvider, DiagAPI.instance());
10562
10870
  if (success) {
10563
10871
  this._proxyTracerProvider.setDelegate(provider);
10564
10872
  }
10565
10873
  return success;
10566
10874
  };
10567
10875
  TraceAPI2.prototype.getTracerProvider = function() {
10568
- return getGlobal(API_NAME3) || this._proxyTracerProvider;
10876
+ return getGlobal(API_NAME4) || this._proxyTracerProvider;
10569
10877
  };
10570
10878
  TraceAPI2.prototype.getTracer = function(name, version) {
10571
10879
  return this.getTracerProvider().getTracer(name, version);
10572
10880
  };
10573
10881
  TraceAPI2.prototype.disable = function() {
10574
- unregisterGlobal(API_NAME3, DiagAPI.instance());
10882
+ unregisterGlobal(API_NAME4, DiagAPI.instance());
10575
10883
  this._proxyTracerProvider = new ProxyTracerProvider();
10576
10884
  };
10577
10885
  return TraceAPI2;
@@ -10737,6 +11045,11 @@ function validateConcurrency(concurrency) {
10737
11045
  throw new TypeError("Expected `concurrency` to be a number from 1 and up");
10738
11046
  }
10739
11047
  }
11048
+ var CronSchema = z.object({
11049
+ cron: z.string(),
11050
+ lastTimestamp: z.number().optional(),
11051
+ timestamp: z.number()
11052
+ });
10740
11053
  var defaultRetrySettings = {
10741
11054
  maxAttempts: 12,
10742
11055
  factor: 2,
@@ -10751,6 +11064,7 @@ var Worker = class _Worker {
10751
11064
  this.options = options;
10752
11065
  this.logger = options.logger ?? new Logger("Worker", "debug");
10753
11066
  this.tracer = options.tracer ?? trace.getTracer(options.name);
11067
+ this.meter = options.meter ?? metrics.getMeter(options.name);
10754
11068
  this.shutdownTimeoutMs = options.shutdownTimeoutMs ?? 6e4;
10755
11069
  const schema = Object.fromEntries(
10756
11070
  Object.entries(this.options.catalog).map(([key, value]) => [key, value.schema])
@@ -10764,57 +11078,47 @@ var Worker = class _Worker {
10764
11078
  this.jobs = options.jobs;
10765
11079
  const { workers = 1, tasksPerWorker = 1, limit = 10 } = options.concurrency ?? {};
10766
11080
  this.concurrency = { workers, tasksPerWorker, limit };
10767
- this.limiter = pLimit(this.concurrency.limit);
10768
- this.metrics.register = options.metrics?.register;
10769
- if (!this.metrics.register) {
10770
- return;
10771
- }
10772
- this.metrics.enqueueDuration = new Histogram({
10773
- name: "redis_worker_enqueue_duration_seconds",
10774
- help: "The duration of enqueue operations",
10775
- labelNames: ["worker_name", "job_type", "has_available_at"],
10776
- buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
10777
- registers: [this.metrics.register]
10778
- });
10779
- this.metrics.dequeueDuration = new Histogram({
10780
- name: "redis_worker_dequeue_duration_seconds",
10781
- help: "The duration of dequeue operations",
10782
- labelNames: ["worker_name", "worker_id", "task_count"],
10783
- buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
10784
- registers: [this.metrics.register]
10785
- });
10786
- this.metrics.jobDuration = new Histogram({
10787
- name: "redis_worker_job_duration_seconds",
10788
- help: "The duration of job operations",
10789
- labelNames: ["worker_name", "worker_id", "batch_size", "job_type", "attempt"],
10790
- // use different buckets here as jobs can take a while to run
10791
- buckets: [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 20, 30, 45, 60],
10792
- registers: [this.metrics.register]
10793
- });
10794
- this.metrics.ackDuration = new Histogram({
10795
- name: "redis_worker_ack_duration_seconds",
10796
- help: "The duration of ack operations",
10797
- labelNames: ["worker_name"],
10798
- buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
10799
- registers: [this.metrics.register]
10800
- });
10801
- this.metrics.redriveDuration = new Histogram({
10802
- name: "redis_worker_redrive_duration_seconds",
10803
- help: "The duration of redrive operations",
10804
- labelNames: ["worker_name"],
10805
- buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
10806
- registers: [this.metrics.register]
10807
- });
10808
- this.metrics.rescheduleDuration = new Histogram({
10809
- name: "redis_worker_reschedule_duration_seconds",
10810
- help: "The duration of reschedule operations",
10811
- labelNames: ["worker_name"],
10812
- buckets: [1e-3, 5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
10813
- registers: [this.metrics.register]
11081
+ const masterQueueObservableGauge = this.meter.createObservableGauge("redis_worker.queue.size", {
11082
+ description: "The number of items in the queue",
11083
+ unit: "items",
11084
+ valueType: ValueType.INT
10814
11085
  });
11086
+ masterQueueObservableGauge.addCallback(this.#updateQueueSizeMetric.bind(this));
11087
+ const deadLetterQueueObservableGauge = this.meter.createObservableGauge(
11088
+ "redis_worker.queue.dead_letter_size",
11089
+ {
11090
+ description: "The number of items in the dead letter queue",
11091
+ unit: "items",
11092
+ valueType: ValueType.INT
11093
+ }
11094
+ );
11095
+ deadLetterQueueObservableGauge.addCallback(this.#updateDeadLetterQueueSizeMetric.bind(this));
11096
+ const concurrencyLimitActiveObservableGauge = this.meter.createObservableGauge(
11097
+ "redis_worker.concurrency.active",
11098
+ {
11099
+ description: "The number of active workers",
11100
+ unit: "workers",
11101
+ valueType: ValueType.INT
11102
+ }
11103
+ );
11104
+ concurrencyLimitActiveObservableGauge.addCallback(
11105
+ this.#updateConcurrencyLimitActiveMetric.bind(this)
11106
+ );
11107
+ const concurrencyLimitPendingObservableGauge = this.meter.createObservableGauge(
11108
+ "redis_worker.concurrency.pending",
11109
+ {
11110
+ description: "The number of pending workers",
11111
+ unit: "workers",
11112
+ valueType: ValueType.INT
11113
+ }
11114
+ );
11115
+ concurrencyLimitPendingObservableGauge.addCallback(
11116
+ this.#updateConcurrencyLimitPendingMetric.bind(this)
11117
+ );
10815
11118
  }
10816
11119
  subscriber;
10817
11120
  tracer;
11121
+ meter;
10818
11122
  metrics = {};
10819
11123
  queue;
10820
11124
  jobs;
@@ -10824,11 +11128,44 @@ var Worker = class _Worker {
10824
11128
  concurrency;
10825
11129
  shutdownTimeoutMs;
10826
11130
  // The p-limit limiter to control overall concurrency.
10827
- limiter;
11131
+ limiters = {};
11132
+ async #updateQueueSizeMetric(observableResult) {
11133
+ const queueSize = await this.queue.size();
11134
+ observableResult.observe(queueSize, {
11135
+ worker_name: this.options.name
11136
+ });
11137
+ }
11138
+ async #updateDeadLetterQueueSizeMetric(observableResult) {
11139
+ const deadLetterQueueSize = await this.queue.sizeOfDeadLetterQueue();
11140
+ observableResult.observe(deadLetterQueueSize, {
11141
+ worker_name: this.options.name
11142
+ });
11143
+ }
11144
+ async #updateConcurrencyLimitActiveMetric(observableResult) {
11145
+ for (const [workerId, limiter] of Object.entries(this.limiters)) {
11146
+ observableResult.observe(limiter.activeCount, {
11147
+ worker_name: this.options.name,
11148
+ worker_id: workerId
11149
+ });
11150
+ }
11151
+ }
11152
+ async #updateConcurrencyLimitPendingMetric(observableResult) {
11153
+ for (const [workerId, limiter] of Object.entries(this.limiters)) {
11154
+ observableResult.observe(limiter.pendingCount, {
11155
+ worker_name: this.options.name,
11156
+ worker_id: workerId
11157
+ });
11158
+ }
11159
+ }
10828
11160
  start() {
10829
11161
  const { workers, tasksPerWorker } = this.concurrency;
11162
+ this.logger.info("Starting worker", {
11163
+ workers,
11164
+ tasksPerWorker,
11165
+ concurrency: this.concurrency
11166
+ });
10830
11167
  for (let i = 0; i < workers; i++) {
10831
- this.workerLoops.push(this.runWorkerLoop(`worker-${nanoid(12)}`, tasksPerWorker));
11168
+ this.workerLoops.push(this.runWorkerLoop(`worker-${nanoid(12)}`, tasksPerWorker, i, workers));
10832
11169
  }
10833
11170
  this.setupShutdownHandlers();
10834
11171
  this.subscriber = createRedisClient(this.options.redisOptions, {
@@ -10840,6 +11177,7 @@ var Worker = class _Worker {
10840
11177
  }
10841
11178
  });
10842
11179
  this.setupSubscriber();
11180
+ this.setupCron();
10843
11181
  return this;
10844
11182
  }
10845
11183
  /**
@@ -10892,6 +11230,56 @@ var Worker = class _Worker {
10892
11230
  }
10893
11231
  );
10894
11232
  }
11233
+ /**
11234
+ * Enqueues a job for processing once. If the job is already in the queue, it will be ignored.
11235
+ * @param options - The enqueue options.
11236
+ * @param options.id - Required unique identifier for the job.
11237
+ * @param options.job - The job type from the worker catalog.
11238
+ * @param options.payload - The job payload that matches the schema defined in the catalog.
11239
+ * @param options.visibilityTimeoutMs - Optional visibility timeout in milliseconds. Defaults to value from catalog.
11240
+ * @param options.availableAt - Optional date when the job should become available for processing. Defaults to now.
11241
+ * @returns A promise that resolves when the job is enqueued.
11242
+ */
11243
+ enqueueOnce({
11244
+ id,
11245
+ job,
11246
+ payload,
11247
+ visibilityTimeoutMs,
11248
+ availableAt
11249
+ }) {
11250
+ return startSpan(
11251
+ this.tracer,
11252
+ "enqueueOnce",
11253
+ async (span) => {
11254
+ const timeout = visibilityTimeoutMs ?? this.options.catalog[job]?.visibilityTimeoutMs;
11255
+ if (!timeout) {
11256
+ throw new Error(`No visibility timeout found for job ${String(job)} with id ${id}`);
11257
+ }
11258
+ span.setAttribute("job_visibility_timeout_ms", timeout);
11259
+ return this.withHistogram(
11260
+ this.metrics.enqueueDuration,
11261
+ this.queue.enqueueOnce({
11262
+ id,
11263
+ job,
11264
+ item: payload,
11265
+ visibilityTimeoutMs: timeout,
11266
+ availableAt
11267
+ }),
11268
+ {
11269
+ job_type: String(job),
11270
+ has_available_at: availableAt ? "true" : "false"
11271
+ }
11272
+ );
11273
+ },
11274
+ {
11275
+ kind: SpanKind.PRODUCER,
11276
+ attributes: {
11277
+ job_type: String(job),
11278
+ job_id: id
11279
+ }
11280
+ }
11281
+ );
11282
+ }
10895
11283
  /**
10896
11284
  * Reschedules an existing job to a new available date.
10897
11285
  * If the job isn't in the queue, it will be ignored.
@@ -10928,37 +11316,78 @@ var Worker = class _Worker {
10928
11316
  }
10929
11317
  );
10930
11318
  }
11319
+ async getJob(id) {
11320
+ return this.queue.getJob(id);
11321
+ }
10931
11322
  /**
10932
11323
  * The main loop that each worker runs. It repeatedly polls for items,
10933
11324
  * processes them, and then waits before the next iteration.
10934
11325
  */
10935
- async runWorkerLoop(workerId, taskCount) {
11326
+ async runWorkerLoop(workerId, taskCount, workerIndex, totalWorkers) {
11327
+ const limiter = pLimit(this.concurrency.limit);
11328
+ this.limiters[workerId] = limiter;
10936
11329
  const pollIntervalMs = this.options.pollIntervalMs ?? 1e3;
10937
11330
  const immediatePollIntervalMs = this.options.immediatePollIntervalMs ?? 100;
11331
+ const delayBetweenWorkers = this.options.pollIntervalMs ?? 1e3;
11332
+ const delay = delayBetweenWorkers * (totalWorkers - workerIndex);
11333
+ await _Worker.delay(delay);
11334
+ this.logger.info("Starting worker loop", {
11335
+ workerIndex,
11336
+ totalWorkers,
11337
+ delay,
11338
+ workerId,
11339
+ taskCount,
11340
+ pollIntervalMs,
11341
+ immediatePollIntervalMs,
11342
+ concurrencyOptions: this.concurrency
11343
+ });
10938
11344
  while (!this.isShuttingDown) {
10939
- if (this.limiter.activeCount + this.limiter.pendingCount >= this.concurrency.limit) {
11345
+ if (limiter.activeCount + limiter.pendingCount >= this.concurrency.limit) {
11346
+ this.logger.debug("Worker at capacity, waiting", {
11347
+ workerId,
11348
+ concurrencyOptions: this.concurrency,
11349
+ activeCount: limiter.activeCount,
11350
+ pendingCount: limiter.pendingCount
11351
+ });
10940
11352
  await _Worker.delay(pollIntervalMs);
10941
11353
  continue;
10942
11354
  }
11355
+ const $taskCount = Math.min(
11356
+ taskCount,
11357
+ this.concurrency.limit - limiter.activeCount - limiter.pendingCount
11358
+ );
10943
11359
  try {
10944
11360
  const items = await this.withHistogram(
10945
11361
  this.metrics.dequeueDuration,
10946
- this.queue.dequeue(taskCount),
11362
+ this.queue.dequeue($taskCount),
10947
11363
  {
10948
11364
  worker_id: workerId,
10949
- task_count: taskCount
11365
+ task_count: $taskCount
10950
11366
  }
10951
11367
  );
10952
11368
  if (items.length === 0) {
11369
+ this.logger.debug("No items to dequeue", {
11370
+ workerId,
11371
+ concurrencyOptions: this.concurrency,
11372
+ activeCount: limiter.activeCount,
11373
+ pendingCount: limiter.pendingCount
11374
+ });
10953
11375
  await _Worker.delay(pollIntervalMs);
10954
11376
  continue;
10955
11377
  }
11378
+ this.logger.debug("Dequeued items", {
11379
+ workerId,
11380
+ itemCount: items.length,
11381
+ concurrencyOptions: this.concurrency,
11382
+ activeCount: limiter.activeCount,
11383
+ pendingCount: limiter.pendingCount
11384
+ });
10956
11385
  for (const item of items) {
10957
- this.limiter(() => this.processItem(item, items.length, workerId)).catch(
10958
- (err) => {
10959
- this.logger.error("Unhandled error in processItem:", { error: err, workerId, item });
10960
- }
10961
- );
11386
+ limiter(
11387
+ () => this.processItem(item, items.length, workerId, limiter)
11388
+ ).catch((err) => {
11389
+ this.logger.error("Unhandled error in processItem:", { error: err, workerId, item });
11390
+ });
10962
11391
  }
10963
11392
  } catch (error) {
10964
11393
  this.logger.error("Error dequeuing items:", { name: this.options.name, error });
@@ -10967,17 +11396,22 @@ var Worker = class _Worker {
10967
11396
  }
10968
11397
  await _Worker.delay(immediatePollIntervalMs);
10969
11398
  }
11399
+ this.logger.info("Worker loop finished", { workerId });
10970
11400
  }
10971
11401
  /**
10972
11402
  * Processes a single item.
10973
11403
  */
10974
- async processItem({ id, job, item, visibilityTimeoutMs, attempt, timestamp, deduplicationKey }, batchSize, workerId) {
11404
+ async processItem({ id, job, item, visibilityTimeoutMs, attempt, timestamp, deduplicationKey }, batchSize, workerId, limiter) {
10975
11405
  const catalogItem = this.options.catalog[job];
10976
11406
  const handler = this.jobs[job];
10977
11407
  if (!handler) {
10978
11408
  this.logger.error(`No handler found for job type: ${job}`);
10979
11409
  return;
10980
11410
  }
11411
+ if (!catalogItem) {
11412
+ this.logger.error(`No catalog item found for job type: ${job}`);
11413
+ return;
11414
+ }
10981
11415
  await startSpan(
10982
11416
  this.tracer,
10983
11417
  "processItem",
@@ -10993,6 +11427,9 @@ var Worker = class _Worker {
10993
11427
  }
10994
11428
  );
10995
11429
  await this.queue.ack(id, deduplicationKey);
11430
+ if (catalogItem.cron) {
11431
+ await this.rescheduleCronJob(job, catalogItem, item);
11432
+ }
10996
11433
  },
10997
11434
  {
10998
11435
  kind: SpanKind.CONSUMER,
@@ -11003,9 +11440,9 @@ var Worker = class _Worker {
11003
11440
  job_timestamp: timestamp.getTime(),
11004
11441
  job_age_in_ms: Date.now() - timestamp.getTime(),
11005
11442
  worker_id: workerId,
11006
- worker_limit_concurrency: this.limiter.concurrency,
11007
- worker_limit_active: this.limiter.activeCount,
11008
- worker_limit_pending: this.limiter.pendingCount,
11443
+ worker_limit_concurrency: limiter.concurrency,
11444
+ worker_limit_active: limiter.activeCount,
11445
+ worker_limit_pending: limiter.pendingCount,
11009
11446
  worker_name: this.options.name,
11010
11447
  batch_size: batchSize
11011
11448
  }
@@ -11039,6 +11476,9 @@ var Worker = class _Worker {
11039
11476
  errorMessage
11040
11477
  });
11041
11478
  await this.queue.moveToDeadLetterQueue(id, errorMessage);
11479
+ if (catalogItem.cron) {
11480
+ await this.rescheduleCronJob(job, catalogItem, item);
11481
+ }
11042
11482
  return;
11043
11483
  }
11044
11484
  const retryDate = new Date(Date.now() + retryDelay);
@@ -11076,20 +11516,108 @@ var Worker = class _Worker {
11076
11516
  });
11077
11517
  }
11078
11518
  async withHistogram(histogram, promise, labels) {
11079
- if (!histogram || !this.metrics.register) {
11519
+ if (!histogram) {
11080
11520
  return promise;
11081
11521
  }
11082
- const end = histogram.startTimer({ worker_name: this.options.name, ...labels });
11522
+ const start = Date.now();
11083
11523
  try {
11084
11524
  return await promise;
11085
11525
  } finally {
11086
- end();
11526
+ const duration = (Date.now() - start) / 1e3;
11527
+ histogram.record(duration, { worker_name: this.options.name, ...labels });
11087
11528
  }
11088
11529
  }
11089
11530
  // A simple helper to delay for a given number of milliseconds.
11090
11531
  static delay(ms) {
11091
11532
  return new Promise((resolve) => setTimeout(resolve, ms));
11092
11533
  }
11534
+ setupCron() {
11535
+ const cronJobs = Object.entries(this.options.catalog).filter(([_, value]) => value.cron);
11536
+ if (cronJobs.length === 0) {
11537
+ return;
11538
+ }
11539
+ this.logger.info("Setting up cron jobs", {
11540
+ cronJobs: cronJobs.map(([job, value]) => ({
11541
+ job,
11542
+ cron: value.cron,
11543
+ jitterInMs: value.jitterInMs
11544
+ }))
11545
+ });
11546
+ const enqueuePromises = cronJobs.map(
11547
+ ([job, value]) => this.enqueueCronJob(value.cron, job, value.jitterInMs)
11548
+ );
11549
+ Promise.allSettled(enqueuePromises).then((results) => {
11550
+ results.forEach((result) => {
11551
+ if (result.status === "fulfilled") {
11552
+ this.logger.info("Enqueued cron job", { result: result.value });
11553
+ } else {
11554
+ this.logger.error("Failed to enqueue cron job", { reason: result.reason });
11555
+ }
11556
+ });
11557
+ });
11558
+ }
11559
+ async enqueueCronJob(cron, job, jitter, lastTimestamp) {
11560
+ const scheduledAt = this.calculateNextScheduledAt(cron, lastTimestamp);
11561
+ const identifier = [job, this.timestampIdentifier(scheduledAt)].join(":");
11562
+ const appliedJitter = typeof jitter === "number" ? Math.random() * jitter - jitter / 2 : 0;
11563
+ const availableAt = new Date(scheduledAt.getTime() + appliedJitter);
11564
+ const enqueued = await this.enqueueOnce({
11565
+ id: identifier,
11566
+ job,
11567
+ payload: {
11568
+ timestamp: scheduledAt.getTime(),
11569
+ lastTimestamp: lastTimestamp?.getTime(),
11570
+ cron
11571
+ },
11572
+ availableAt
11573
+ });
11574
+ this.logger.info("Enqueued cron job", {
11575
+ identifier,
11576
+ cron,
11577
+ job,
11578
+ scheduledAt,
11579
+ enqueued,
11580
+ availableAt,
11581
+ appliedJitter,
11582
+ jitter
11583
+ });
11584
+ return {
11585
+ identifier,
11586
+ cron,
11587
+ job,
11588
+ scheduledAt,
11589
+ enqueued
11590
+ };
11591
+ }
11592
+ async rescheduleCronJob(job, catalogItem, item) {
11593
+ if (!catalogItem.cron) {
11594
+ return;
11595
+ }
11596
+ return this.enqueueCronJob(
11597
+ catalogItem.cron,
11598
+ job,
11599
+ catalogItem.jitterInMs,
11600
+ new Date(item.timestamp)
11601
+ );
11602
+ }
11603
+ calculateNextScheduledAt(cron, lastTimestamp) {
11604
+ const scheduledAt = parseExpression(cron, {
11605
+ currentDate: lastTimestamp
11606
+ }).next().toDate();
11607
+ if (scheduledAt < /* @__PURE__ */ new Date()) {
11608
+ return this.calculateNextScheduledAt(cron);
11609
+ }
11610
+ return scheduledAt;
11611
+ }
11612
+ timestampIdentifier(timestamp) {
11613
+ const year = timestamp.getUTCFullYear();
11614
+ const month = timestamp.getUTCMonth();
11615
+ const day = timestamp.getUTCDate();
11616
+ const hour = timestamp.getUTCHours();
11617
+ const minute = timestamp.getUTCMinutes();
11618
+ const second = timestamp.getUTCSeconds();
11619
+ return `${year}-${month}-${day}-${hour}-${minute}-${second}`;
11620
+ }
11093
11621
  setupSubscriber() {
11094
11622
  const channel = `${this.options.name}:redrive`;
11095
11623
  this.subscriber?.subscribe(channel, (err) => {
@@ -11146,6 +11674,6 @@ var Worker = class _Worker {
11146
11674
  }
11147
11675
  };
11148
11676
 
11149
- export { SimpleQueue, Worker };
11677
+ export { CronSchema, SimpleQueue, Worker };
11150
11678
  //# sourceMappingURL=index.js.map
11151
11679
  //# sourceMappingURL=index.js.map