resque-scheduler 5.0.1 → 5.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 046040c8fa18b0cb7aabc7fdc46ff008f7b0c30f9aa0500137f5c6c987bdbabc
4
- data.tar.gz: b3e243c3c9ccefa0c1282b492b70a9b26677fa2b4be6ead096e339c721d9f86a
3
+ metadata.gz: 6aa6e1f1a8fe48f091032409157797e48011e473fca30c8fd12d18d05517de49
4
+ data.tar.gz: cbcc8d19930096797a1d028a9765b9c7c0b65ae023011ccec64cf8005420f183
5
5
  SHA512:
6
- metadata.gz: 8b6c3caa8b47c24aee900d2c348cabb20d1a32a5fce88e02a974649ca6c711b1703562393902c7638e14c1a11180b1901b126480e905c7cca8b2dbd47d27daa3
7
- data.tar.gz: 7b2665804103943b3bf9a98b84134ba71a0ac00ace8e0c2ed7c765873445c801bea14c8ae9082fec4133e6f3be0314bdf1485ecd47c7bab8aba6d8d85ef36ec8
6
+ metadata.gz: a3842dbcedbb2768e715aef2355f11333cadfa101ccb5a1475dbda155005d494408e0554c027ba5cf72afcabc7dcd3778125b9f512680850c875efbccb94f64f
7
+ data.tar.gz: 9623d1a81de5c0e1179e7be932ad6a19641dbcbc57c7b9034fc531160ae6e896374611d5d0f1dc64a710c27293cbc0e1db0eda9f6e5ad4c5ed8208398e20cdae
@@ -6,9 +6,11 @@ on:
6
6
  pull_request:
7
7
  branches: [master]
8
8
 
9
+ permissions:
10
+ contents: read
11
+
9
12
  jobs:
10
13
  test:
11
- continue-on-error: true
12
14
  runs-on: ubuntu-latest
13
15
  services:
14
16
  redis:
@@ -19,20 +21,16 @@ jobs:
19
21
  fail-fast: false
20
22
  matrix:
21
23
  ruby-version:
22
- - "3.0"
23
- - 3.1
24
24
  - 3.2
25
25
  - 3.3
26
26
  - 3.4
27
+ - "4.0"
27
28
  - head
28
29
  resque-version:
29
30
  - "master"
30
- rufus-scheduler:
31
- - "3.4"
32
- - "3.5"
33
- - "3.6"
34
31
  redis-version:
35
32
  - "4.8"
33
+ - "5.4"
36
34
  - "latest"
37
35
  rack-version:
38
36
  - 2
@@ -40,15 +38,24 @@ jobs:
40
38
  env:
41
39
  REDIS_VERSION: "${{ matrix.redis-version }}"
42
40
  RESQUE: "${{ matrix.resque-version }}"
43
- RUFUS_SCHEDULER: "${{ matrix.rufus-scheduler }}"
44
41
  RACK_VERSION: "${{ matrix.rack-version }}"
45
42
  COVERAGE: 1
46
43
 
47
44
  steps:
48
- - uses: actions/checkout@v4
45
+ - uses: actions/checkout@v7
49
46
  - uses: ruby/setup-ruby@v1
50
47
  with:
51
48
  ruby-version: "${{ matrix.ruby-version }}"
52
49
  bundler-cache: true
53
50
  - run: bundle exec rake
54
51
  - run: bin/bundle_console_test.sh
52
+
53
+ rubocop:
54
+ runs-on: ubuntu-latest
55
+ steps:
56
+ - uses: actions/checkout@v7
57
+ - uses: ruby/setup-ruby@v1
58
+ with:
59
+ ruby-version: 3.4
60
+ bundler-cache: true
61
+ - run: bundle exec rubocop
data/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  **ATTN**: This project uses [semantic versioning](http://semver.org/).
4
4
 
5
+ ## [5.1.0] - 2026-10-28
6
+ ### Breaking Changes
7
+ * Ruby requirement: `>= 3.0.0` → `>= 3.2.0` by @neuromanskie in #837
8
+ * rufus-scheduler dependency: `~> 3.2, != 3.3` → `~> 3.9` by @neuromanskie in #837
9
+ * Schedule entries (`cron`/`every`) no longer run `before_schedule`, `after_schedule` or `before_delayed_enqueue`. Move that code to `before_recurring`/`after_recurring` by @top-sigrid in #844
10
+
11
+ ### Added
12
+ * `before_recurring` and `after_recurring` hooks for jobs enqueued from the schedule by @top-sigrid in #844
13
+ * `DISABLE_DELAYED_REQUEUE_BATCHES` to opt out of batched delayed job enqueues by @onyxraven in #796
14
+ * Search delayed jobs by args in the web UI by @bmo in #839 (originally #714)
15
+ * Allow a number of seconds for `every` by @carsonreinke in #838 (originally #607)
16
+ * Railtie to load resque-scheduler automatically in Rails apps by @josh-m-sharpe in #694
17
+ * Add Ruby 4.0 and redis-rb 5.4 to the CI matrix by @neuromanskie in #837
18
+
19
+ ### Changed
20
+ * Upgrade RuboCop to 1.91 and run it as a CI job by @neuromanskie in #837
21
+
22
+ ### Fixed
23
+ * Delayed jobs no longer run `before_schedule`/`after_schedule` a second time when they become due by @top-sigrid in #844
24
+ * Skip schedule hooks when `enqueue_at` is given a timestamp that is not in the future by @top-sigrid in #817
25
+ * Schedule Active Job classes through `perform_later` by @neuromanskie and @josh-m-sharpe in #842 (originally #696)
26
+ * Schedule delayed test jobs far enough out to not race the clock by @neuromanskie in #843
27
+ * Fail CI when tests fail by @neuromanskie in #837
28
+
5
29
  ## [5.0.1] - 2026-10-11
6
30
  ### Changed
7
31
  * Pipeline `count_all_scheduled_jobs` for significantly faster execution by @top-sigrid in #822
data/Gemfile CHANGED
@@ -8,13 +8,6 @@ else
8
8
  gem 'resque', resque_version
9
9
  end
10
10
 
11
- case rufus_scheduler_version = ENV.fetch('RUFUS_SCHEDULER', '3.6')
12
- when 'master'
13
- gem 'rufus-scheduler', git: 'https://github.com/jmettraux/rufus-scheduler'
14
- else
15
- gem 'rufus-scheduler', rufus_scheduler_version
16
- end
17
-
18
11
  case redis_version = ENV.fetch('REDIS_VERSION', 'latest')
19
12
  when 'master'
20
13
  gem 'redis', git: 'https://github.com/redis/redis-rb'
data/README.md CHANGED
@@ -48,6 +48,9 @@ Adding the resque:scheduler rake task:
48
48
  require 'resque/scheduler/tasks'
49
49
  ```
50
50
 
51
+ In a Rails app, you can skip this step. A Railtie loads the
52
+ `resque:scheduler` rake task and the Resque web UI tabs automatically.
53
+
51
54
  ### Rake integration
52
55
 
53
56
  By default, `resque-scheduler` depends on the "resque:setup" rake task.
@@ -131,6 +134,8 @@ environment variables:
131
134
  * `APP_NAME` - Application name used in procline (`$0`) (default empty)
132
135
  * `BACKGROUND` - [Run in the background](#running-in-the-background) if
133
136
  non-empty (via `Process.daemon`, if supported) (default `false`)
137
+ * `DELAYED_REQUEUE_BATCH_SIZE` - Set the delayed job batch size if enabled (default `100`). If `<= 1`, this disables batching.
138
+ * `DISABLE_DELAYED_REQUEUE_BATCHES` - Disable batched delayed job queuing (default `false`) - [See section below on consequences](#batched-delayed-job-and-resque-enqueue-hooks)
134
139
  * `DYNAMIC_SCHEDULE` - Enables [dynamic scheduling](#dynamic-schedules)
135
140
  if non-empty (default `false`)
136
141
  * `RAILS_ENV` - Environment to use in procline (`$0`) (default empty)
@@ -199,6 +204,19 @@ Resque.enqueue_at_with_queue(
199
204
  )
200
205
  ```
201
206
 
207
+ #### Active Job
208
+
209
+ `ActiveJob::Base` subclasses can be passed to `enqueue_at`, `enqueue_in` and
210
+ their `_with_queue` variants, and used as the `class` of a scheduled job. They
211
+ are handed to `perform_later` (with `set(wait_until:)` for delayed jobs), so
212
+ Active Job must be using the `:resque` adapter. The job's `queue_as` applies
213
+ unless a queue is given.
214
+
215
+ Since the stored class is the adapter's `JobWrapper`, `delayed?`,
216
+ `remove_delayed` and `find_delayed_selection` won't match Active Job classes,
217
+ and [hooks](#hooks) defined on them aren't called. Use Active Job's callbacks
218
+ instead.
219
+
202
220
  The delayed queue is stored in redis and is persisted in the same way the
203
221
  standard resque jobs are persisted (redis writing to disk). Delayed jobs differ
204
222
  from scheduled jobs in that if your scheduler process is down or workers are
@@ -457,6 +475,8 @@ config[:persist] = true
457
475
  Resque.set_schedule(name, config)
458
476
  ```
459
477
 
478
+ `every` also accepts a number of seconds, so `config[:every] = 1.hour` works too.
479
+
460
480
  Schedules can later be removed by passing their name to the `remove_schedule` method:
461
481
 
462
482
  ```ruby
@@ -506,13 +526,18 @@ Similar to the `before_enqueue`- and `after_enqueue`-hooks provided in Resque
506
526
  the delayed queue. If the hook returns `false`, the job will not be placed on
507
527
  the queue.
508
528
  * `after_schedule`: Called with the job args after a job is placed on the
509
- delayed queue. Any exception raised propagates up to the code with queued the
529
+ delayed queue. Any exception raised propagates up to the code that queued the
510
530
  job.
511
531
  * `before_delayed_enqueue`: Called with the job args after the job has been
512
- removed from the delayed queue, but not yet put on a normal queue. It is
532
+ removed from the delayed queue, but not yet put on its work queue. It is
513
533
  called before `before_enqueue`-hooks, and on the same job instance as the
514
534
  `before_enqueue`-hooks will be invoked on. Return values are ignored.
515
- * `on_enqueue_failure`: Called with the job args and the exception that was raised
535
+ * `before_recurring`: Called with the job args before a job from the schedule
536
+ (`cron`, `every` or "Queue now" in resque-web) is put on its work queue. If
537
+ the hook returns `false`, the job will not be enqueued.
538
+ * `after_recurring`: Called with the job args after a job from the schedule has
539
+ been put on its work queue, after the `after_enqueue`-hooks.
540
+ * `on_enqueue_failure`: Called with the job config and the exception that was raised
516
541
  while enqueueing a job to resque or external application fails. Return
517
542
  values are ignored. For example:
518
543
 
@@ -610,6 +635,9 @@ The Delayed tab:
610
635
 
611
636
  ![The Delayed Tab](http://img.skitch.com/20100111-ne4fcqtc5emkcuwc5qtais2kwx.jpg)
612
637
 
638
+ The Delayed tab's search matches job class names, or job arguments when
639
+ "Search Args?" is checked.
640
+
613
641
  #### How do I get the schedule tabs to show up???
614
642
 
615
643
  To get these to show up you need to pass a file to `resque-web` to tell it to
@@ -752,9 +780,38 @@ This table explains the version requirements for rufus-scheduler
752
780
 
753
781
  | resque-scheduler | rufus-scheduler |
754
782
  |:-----------------|----------------:|
783
+ | `>= 5.1` | `~> 3.9` |
755
784
  | `~> 4.0` | `~> 3.0` |
756
785
  | `< 4.0` | `~> 2.0` |
757
786
 
787
+ ##### Batched delayed job and resque enqueue hooks
788
+
789
+ Batching delayed job queuing can speed up when per-second job counts grows,
790
+ avoiding situations that may cause delayed enqueues to fall behind. This
791
+ batching wraps enqueues in a `multi` pipeline, making far fewer roundtrips to
792
+ the server.
793
+
794
+ However, in `redis` gem `>= 4.0`, any operations to Redis within the `multi`
795
+ block must use the multi handle so that the actions are captured. Resque's hooks
796
+ do not currently have a way to pass this around, and so compatibility with other
797
+ Resque plugins or hooks which access Redis at enqueue time is impacted with
798
+ batch mode (A main class of plugins this affects are single runner locks which
799
+ use redis for their locking mechanism). In these cases, you should consider
800
+ disabling the batching by setting the `DISABLE_DELAYED_REQUEUE_BATCHES`
801
+ environment variable to `true`.
802
+
803
+ Detecting when this occurs can be tricky, you must watch for logs
804
+ emitted by your `resque-scheduler` process such as:
805
+
806
+ ```text
807
+ Redis::CommandError: ERR
808
+ MULTI calls can not be nested
809
+ NoMethodError: undefined method nil? for <Redis::Future
810
+ NoMethodError: undefined method `to_i' for <Redis::Future
811
+ ```
812
+
813
+ If you're in this situation, you will also see delayed jobs to not be enqueued
814
+ (they will get dropped). Disabling batching is the workaround.
758
815
 
759
816
  ### Contributing
760
817
 
@@ -16,7 +16,9 @@ module Resque
16
16
  pidfile: 'PIDFILE',
17
17
  poll_sleep_amount: 'RESQUE_SCHEDULER_INTERVAL',
18
18
  verbose: 'VERBOSE',
19
- lock_timeout: 'LOCK_TIMEOUT'
19
+ lock_timeout: 'LOCK_TIMEOUT',
20
+ delayed_requeue_batch_size: 'DELAYED_REQUEUE_BATCH_SIZE',
21
+ disable_delayed_requeue_batches: 'DISABLE_DELAYED_REQUEUE_BATCHES'
20
22
  }.freeze
21
23
 
22
24
  class Cli
@@ -74,6 +76,20 @@ module Resque
74
76
  {
75
77
  args: ['-v', '--verbose', 'Run with verbose output [VERBOSE]'],
76
78
  callback: ->(options) { ->(v) { options[:verbose] = v } }
79
+ },
80
+ {
81
+ args: [
82
+ '--delayed-requeue-batch-size [DELAYED_REQUEUE_BATCH_SIZE]',
83
+ 'Delayed requeue batch size'
84
+ ],
85
+ callback: ->(options) { ->(d) { options[:delayed_requeue_batch_size] = d } }
86
+ },
87
+ {
88
+ args: [
89
+ '--disable-delayed-requeue-batches',
90
+ 'Disable delayed requeue batches [DISABLE_DELAYED_REQUEUE_BATCHES]'
91
+ ],
92
+ callback: ->(options) { ->(d) { options[:disable_delayed_requeue_batches] = d } }
77
93
  }
78
94
  ].freeze
79
95
 
@@ -65,10 +65,23 @@ module Resque
65
65
  @app_name ||= environment['APP_NAME']
66
66
  end
67
67
 
68
+ # Batch size for requeueing delayed jobs. If <= 1, this disables batching.
69
+ # Defaults to 100.
70
+ attr_writer :delayed_requeue_batch_size
71
+
68
72
  def delayed_requeue_batch_size
69
73
  @delayed_requeue_batch_size ||= \
70
- ENV['DELAYED_REQUEUE_BATCH_SIZE'].to_i if environment['DELAYED_REQUEUE_BATCH_SIZE']
71
- @delayed_requeue_batch_size ||= 100
74
+ Integer(environment.fetch('DELAYED_REQUEUE_BATCH_SIZE', '100'))
75
+ end
76
+
77
+ attr_writer :disable_delayed_requeue_batches
78
+
79
+ def disable_delayed_requeue_batches
80
+ if @disable_delayed_requeue_batches.nil?
81
+ to_bool(environment['DISABLE_DELAYED_REQUEUE_BATCHES'])
82
+ else
83
+ @disable_delayed_requeue_batches
84
+ end
72
85
  end
73
86
 
74
87
  # Amount of time in seconds to sleep between polls of the delayed
@@ -10,6 +10,10 @@ module Resque
10
10
  # for queueing. Until timestamp is in the past, the job will
11
11
  # sit in the schedule list.
12
12
  def enqueue_at(timestamp, klass, *args)
13
+ if Resque::Scheduler::Util.active_job_class?(klass)
14
+ return enqueue_at_with_queue(nil, timestamp, klass, *args)
15
+ end
16
+
13
17
  validate(klass)
14
18
  enqueue_at_with_queue(
15
19
  queue_from_class(klass), timestamp, klass, *args
@@ -21,19 +25,29 @@ module Resque
21
25
  # timestamp has passed. It respects Resque.inline option, by
22
26
  # creating the job right away instead of adding to the queue.
23
27
  def enqueue_at_with_queue(queue, timestamp, klass, *args)
24
- plugin.process_schedule_hooks(klass, *args) do
25
- if Resque.inline? || timestamp.to_i <= Time.now.to_i
26
- # Just create the job and let resque perform it right away with
27
- # inline. If the class is a custom job class, call self#scheduled
28
- # on it. This allows you to do things like
29
- # Resque.enqueue_at(timestamp, CustomJobClass, :opt1 => val1).
30
- # Otherwise, pass off to Resque.
31
- if klass.respond_to?(:scheduled)
32
- klass.scheduled(queue, klass.to_s, *args)
33
- else
34
- Resque.enqueue_to(queue, klass, *args)
35
- end
28
+ if Resque::Scheduler::Util.active_job_class?(klass)
29
+ return klass.set(**{ wait_until: timestamp, queue: queue }.compact)
30
+ .perform_later(*args)
31
+ end
32
+
33
+ if Resque.inline? || timestamp.to_i <= Time.now.to_i
34
+ # Just create the job and let resque perform it right away with
35
+ # inline. If the class is a custom job class, call self#scheduled
36
+ # on it. This allows you to do things like
37
+ # Resque.enqueue_at(timestamp, CustomJobClass, :opt1 => val1).
38
+ # Otherwise, pass off to Resque.
39
+ #
40
+ # Note: schedule hooks are NOT called here because the job is not
41
+ # actually being added to the delayed queue.
42
+ if klass.respond_to?(:scheduled)
43
+ klass.scheduled(queue, klass.to_s, *args)
36
44
  else
45
+ Resque.enqueue_to(queue, klass, *args)
46
+ end
47
+ else
48
+ # Job is being added to the delayed queue, so we call the
49
+ # before_schedule and after_schedule hooks.
50
+ plugin.process_schedule_hooks(klass, *args) do
37
51
  delayed_push(timestamp, job_to_hash_with_queue(queue, klass, args))
38
52
  end
39
53
  end
@@ -173,14 +187,14 @@ module Resque
173
187
  def enqueue_delayed(klass, *args)
174
188
  hash = job_to_hash(klass, args)
175
189
  remove_delayed(klass, *args).times do
176
- Resque::Scheduler.enqueue_from_config(hash)
190
+ Resque::Scheduler.enqueue_from_config(hash, :delayed)
177
191
  end
178
192
  end
179
193
 
180
194
  def enqueue_delayed_with_queue(klass, queue, *args)
181
195
  hash = job_to_hash_with_queue(queue, klass, args)
182
196
  remove_delayed_in_queue(klass, queue, *args).times do
183
- Resque::Scheduler.enqueue_from_config(hash)
197
+ Resque::Scheduler.enqueue_from_config(hash, :delayed)
184
198
  end
185
199
  end
186
200
 
@@ -74,6 +74,14 @@ module Resque
74
74
  end
75
75
 
76
76
  c.verbose = !!options[:verbose] if options.key?(:verbose)
77
+
78
+ if options.key?(:delayed_requeue_batch_size)
79
+ c.delayed_requeue_batch_size = Integer(options[:delayed_requeue_batch_size])
80
+ end
81
+
82
+ if options.key?(:disable_delayed_requeue_batches)
83
+ c.disable_delayed_requeue_batches = !!options[:disable_delayed_requeue_batches]
84
+ end
77
85
  end
78
86
  end
79
87
  # rubocop:enable Metrics/AbcSize
@@ -3,6 +3,8 @@
3
3
  module Resque
4
4
  module Scheduler
5
5
  module Plugin
6
+ ENQUEUE_HOOKS = %i[delayed recurring].freeze
7
+
6
8
  def self.process_schedule_hooks(klass, *args)
7
9
  # the documentation states that if any before_schedule hook returns
8
10
  # false, the process should not be enqueued
@@ -12,6 +14,32 @@ module Resque
12
14
  run_after_schedule_hooks(klass, *args)
13
15
  end
14
16
 
17
+ def self.process_recurring_hooks(klass, *args)
18
+ return unless run_before_recurring_hooks(klass, *args)
19
+
20
+ enqueued = yield
21
+ run_after_recurring_hooks(klass, *args) if enqueued
22
+ enqueued
23
+ end
24
+
25
+ def self.validate_enqueue_hooks(hooks)
26
+ return if hooks.nil? || ENQUEUE_HOOKS.include?(hooks)
27
+
28
+ raise ArgumentError, "unknown hooks: #{hooks.inspect}"
29
+ end
30
+
31
+ def self.process_enqueue_hooks(hooks, klass, *args, &block)
32
+ case hooks
33
+ when :delayed
34
+ run_before_delayed_enqueue_hooks(klass, *args)
35
+ yield
36
+ when :recurring
37
+ process_recurring_hooks(klass, *args, &block)
38
+ else
39
+ yield
40
+ end
41
+ end
42
+
15
43
  def self.hooks(job, pattern)
16
44
  job.methods.grep(/^#{pattern}/).sort
17
45
  end
@@ -35,6 +63,14 @@ module Resque
35
63
  def self.run_after_schedule_hooks(klass, *args)
36
64
  run_hooks(klass, 'after_schedule', *args)
37
65
  end
66
+
67
+ def self.run_before_recurring_hooks(klass, *args)
68
+ run_hooks(klass, 'before_recurring', *args)
69
+ end
70
+
71
+ def self.run_after_recurring_hooks(klass, *args)
72
+ run_hooks(klass, 'after_recurring', *args)
73
+ end
38
74
  end
39
75
  end
40
76
  end
@@ -0,0 +1,11 @@
1
+ require 'resque/scheduler/server'
2
+
3
+ module Resque
4
+ module Scheduler
5
+ class Railtie < Rails::Railtie
6
+ rake_tasks do
7
+ require 'resque/scheduler/tasks'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,4 +1,6 @@
1
1
  <form method="POST" action="<%= u 'delayed/search' %>">
2
+ <input type='checkbox' id='search_args' name='search_args' value="checked" <%= 'checked' if params[:search_args] == 'checked' %>/>
3
+ <label for="search_args"> Search Args?</label><br>
2
4
  <input type='input' name='search' value="<%= h params[:search] %>"/>
3
5
  <input type='submit' value='Search'/>
4
6
  </form>
@@ -48,7 +48,7 @@ module Resque
48
48
  if @parameters
49
49
  erb scheduler_template('requeue-params')
50
50
  else
51
- Resque::Scheduler.enqueue_from_config(config)
51
+ Resque::Scheduler.enqueue_from_config(config, :recurring)
52
52
  redirect u('/overview')
53
53
  end
54
54
  end
@@ -68,7 +68,7 @@ module Resque
68
68
 
69
69
  # Insert the args hash into config and queue the resque job
70
70
  config = config.merge('args' => config_args)
71
- Resque::Scheduler.enqueue_from_config(config)
71
+ Resque::Scheduler.enqueue_from_config(config, :recurring)
72
72
  redirect u('/overview')
73
73
  end
74
74
 
@@ -99,7 +99,8 @@ module Resque
99
99
  end
100
100
 
101
101
  def delayed_search
102
- @jobs = find_job(params[:search])
102
+ field = params[:search_args] == 'checked' ? 'args' : 'class'
103
+ @jobs = find_job(params[:search], field)
103
104
  erb scheduler_template('search')
104
105
  end
105
106
 
@@ -153,21 +154,21 @@ module Resque
153
154
  )
154
155
  end
155
156
 
156
- def find_job(worker)
157
- worker = worker.downcase
158
- results = working_jobs_for_worker(worker)
157
+ def find_job(term, field = 'class')
158
+ term = term.downcase
159
+ matches = ->(job) { job[field].to_s.downcase.include?(term) }
160
+ results = working_jobs_matching(matches)
159
161
 
160
- dels = delayed_jobs_for_worker(worker)
162
+ dels = delayed_jobs_for_worker(term)
161
163
  results += dels.select do |j|
162
- j['class'].downcase.include?(worker) &&
163
- j.merge!('where_at' => 'delayed')
164
+ matches.call(j) && j.merge!('where_at' => 'delayed')
164
165
  end
165
166
 
166
167
  Resque.queues.each do |queue|
167
168
  queued = Resque.peek(queue, 0, Resque.size(queue))
168
169
  queued = [queued] unless queued.is_a?(Array)
169
170
  results += queued.select do |j|
170
- j['class'].downcase.include?(worker) &&
171
+ matches.call(j) &&
171
172
  j.merge!('queue' => queue, 'where_at' => 'queued')
172
173
  end
173
174
  end
@@ -187,13 +188,13 @@ module Resque
187
188
 
188
189
  def schedule_interval_every(every)
189
190
  every = [*every]
190
- s = 'every: ' << every.first
191
+ s = 'every: ' << "#{every.first}#{'s' if every.first.is_a?(Numeric)}"
191
192
 
192
193
  return s unless every.length > 1
193
194
 
194
195
  s << ' ('
195
196
  meta = every.last.map do |key, value|
196
- "#{key.to_s.tr('_', ' ')} #{value}"
197
+ "#{key.to_s.tr('_', ' ')} #{value}#{'s' if value.is_a?(Numeric)}"
197
198
  end
198
199
  s << meta.join(', ') << ')'
199
200
  end
@@ -228,12 +229,11 @@ module Resque
228
229
 
229
230
  private
230
231
 
231
- def working_jobs_for_worker(worker)
232
+ def working_jobs_matching(matches)
232
233
  [].tap do |results|
233
234
  working = [*Resque.working]
234
235
  work = working.select do |w|
235
- w.job && w.job['payload'] &&
236
- w.job['payload']['class'].downcase.include?(worker)
236
+ w.job && w.job['payload'] && matches.call(w.job['payload'])
237
237
  end
238
238
  work.each do |w|
239
239
  results += [
@@ -34,6 +34,11 @@ module Resque
34
34
  def self.classify(dashed_word)
35
35
  dashed_word.split('-').map(&:capitalize).join
36
36
  end
37
+
38
+ def self.active_job_class?(klass)
39
+ defined?(::ActiveJob::Base) && klass.is_a?(Class) &&
40
+ klass < ::ActiveJob::Base
41
+ end
37
42
  end
38
43
  end
39
44
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Resque
4
4
  module Scheduler
5
- VERSION = '5.0.1'.freeze
5
+ VERSION = '5.1.0'.freeze
6
6
  end
7
7
  end
@@ -8,6 +8,8 @@ require_relative 'scheduler/logger_builder'
8
8
  require_relative 'scheduler/signal_handling'
9
9
  require_relative 'scheduler/failure_handler'
10
10
 
11
+ require 'resque/scheduler/railtie' if defined?(Rails::Railtie)
12
+
11
13
  module Resque
12
14
  module Scheduler
13
15
  autoload :Cli, 'resque/scheduler/cli'
@@ -151,7 +153,7 @@ module Resque
151
153
  interval_defined = false
152
154
  interval_types = %w(cron every)
153
155
  interval_types.each do |interval_type|
154
- next unless !config[interval_type].nil? && !config[interval_type].empty?
156
+ next unless !config[interval_type].nil? && !config[interval_type].to_s.empty?
155
157
  args = optionizate_interval_value(config[interval_type])
156
158
  args = [args, nil, job: true] if args.is_a?(::String)
157
159
 
@@ -210,31 +212,42 @@ module Resque
210
212
 
211
213
  if item
212
214
  log "queuing #{item['class']} [delayed]"
213
- enqueue(item)
215
+ enqueue(item, :delayed)
214
216
  end
215
217
 
216
218
  item
217
219
  end
218
220
 
221
+ def batch_delayed_items?
222
+ !disable_delayed_requeue_batches && delayed_requeue_batch_size > 1
223
+ end
224
+
219
225
  # Enqueues all delayed jobs for a timestamp
220
226
  def enqueue_delayed_items_for_timestamp(timestamp)
221
227
  count = 0
222
- batch_size = delayed_requeue_batch_size
223
- actual_batch_size = nil
228
+ batch_size = batch_delayed_items? ? delayed_requeue_batch_size : 1
224
229
 
225
- log "Processing delayed items for timestamp #{timestamp}, in batches of #{batch_size}"
230
+ message = "Processing delayed items for timestamp #{timestamp}"
231
+ message += ", in batches of #{batch_size}" if batch_delayed_items?
232
+ log message
226
233
 
227
234
  loop do
235
+ actual_batch_size = 0
236
+
228
237
  handle_shutdown do
229
238
  # Continually check that it is still the master
230
239
  if am_master
231
- actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp,
232
- batch_size)
240
+ if batch_delayed_items?
241
+ actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp, batch_size)
242
+ log "queued batch of #{actual_batch_size} jobs" if actual_batch_size != -1
243
+ else
244
+ item = enqueue_next_item(timestamp)
245
+ actual_batch_size = item.nil? ? 0 : 1
246
+ end
233
247
  end
234
248
  end
235
249
 
236
250
  count += actual_batch_size
237
- log "queued #{count} jobs" if actual_batch_size != -1
238
251
 
239
252
  # continue processing until there are no more items in this
240
253
  # timestamp. If we don't have a full batch, this is the last one.
@@ -243,7 +256,7 @@ module Resque
243
256
  break if actual_batch_size < batch_size
244
257
  end
245
258
 
246
- log "finished queueing #{count} total jobs for timestamp #{timestamp}" if count != -1
259
+ log "finished queueing #{count} total jobs for timestamp #{timestamp}" if count > 0
247
260
  end
248
261
 
249
262
  def timestamp_key(timestamp)
@@ -267,7 +280,7 @@ module Resque
267
280
  pipeline.srem("timestamps:#{encoded_job}", timestamp_bucket_key)
268
281
 
269
282
  decoded_job = Resque.decode(encoded_job)
270
- enqueue(decoded_job)
283
+ enqueue(decoded_job, :delayed)
271
284
  end
272
285
 
273
286
  pipeline.ltrim(timestamp_bucket_key, batch_size, -1)
@@ -294,8 +307,12 @@ module Resque
294
307
  encoded_jobs_to_requeue.count
295
308
  end
296
309
 
297
- def enqueue(config)
298
- enqueue_from_config(config)
310
+ # Enqueues a job like enqueue_from_config, but hands errors to the failure
311
+ # handler instead of raising them. Pass :delayed as hooks for a job that
312
+ # left the delayed queue or :recurring for a schedule entry to run the
313
+ # matching hooks.
314
+ def enqueue(config, hooks = nil)
315
+ enqueue_from_config(config, hooks)
299
316
  rescue => e
300
317
  Resque::Scheduler.failure_handler.on_enqueue_failure(config, e)
301
318
  end
@@ -306,8 +323,11 @@ module Resque
306
323
  exit if @shutdown
307
324
  end
308
325
 
309
- # Enqueues a job based on a config hash
310
- def enqueue_from_config(job_config)
326
+ # Enqueues a job based on a config hash. Pass :delayed as hooks for a job
327
+ # that left the delayed queue or :recurring for a schedule entry to run
328
+ # the matching hooks.
329
+ def enqueue_from_config(job_config, hooks = nil)
330
+ Resque::Scheduler::Plugin.validate_enqueue_hooks(hooks)
311
331
  args = job_config['args'] || job_config[:args]
312
332
 
313
333
  klass_name = job_config['class'] || job_config[:class]
@@ -318,9 +338,13 @@ module Resque
318
338
  end
319
339
 
320
340
  params = args.is_a?(Hash) ? [args] : Array(args)
321
- queue = job_config['queue'] ||
322
- job_config[:queue] ||
323
- Resque.queue_from_class(klass)
341
+ queue = job_config['queue'] || job_config[:queue]
342
+
343
+ if Resque::Scheduler::Util.active_job_class?(klass)
344
+ return klass.set(**{ queue: queue }.compact).perform_later(*params)
345
+ end
346
+
347
+ queue ||= Resque.queue_from_class(klass)
324
348
  # Support custom job classes like those that inherit from
325
349
  # Resque::JobWithStatus (resque-status)
326
350
  job_klass = job_config['custom_job_class']
@@ -343,8 +367,7 @@ module Resque
343
367
  # for non-existent classes (for example: running scheduler in
344
368
  # one app that schedules for another.
345
369
  if Class === klass
346
- Resque::Scheduler::Plugin.process_schedule_hooks(klass, *params) do
347
- Resque::Scheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
370
+ Resque::Scheduler::Plugin.process_enqueue_hooks(hooks, klass, *params) do
348
371
  # If the class is a custom job class, call self#scheduled on it.
349
372
  # This allows you to do things like Resque.enqueue_at(timestamp,
350
373
  # CustomJobClass). Otherwise, pass off to Resque.
@@ -504,7 +527,7 @@ module Resque
504
527
  def enqueue_recurring(name, config)
505
528
  if am_master
506
529
  log! "queueing #{config['class']} (#{name})"
507
- enqueue(config)
530
+ enqueue(config, :recurring)
508
531
  Resque.last_enqueued_at(name, Time.now.to_s)
509
532
  end
510
533
  end
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.license = 'MIT'
33
33
  spec.metadata['rubygems_mfa_required'] = 'true'
34
34
 
35
- spec.required_ruby_version = '>= 3.0.0'
35
+ spec.required_ruby_version = '>= 3.2.0'
36
36
 
37
37
  spec.files = `git ls-files -z`.split("\0").reject do |f|
38
38
  f.match(%r{^(test|spec|features|examples|bin|tasks)/}) ||
@@ -43,6 +43,7 @@ Gem::Specification.new do |spec|
43
43
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
44
44
  spec.require_paths = %w(lib)
45
45
 
46
+ spec.add_development_dependency 'activejob'
46
47
  spec.add_development_dependency 'bundler'
47
48
  spec.add_development_dependency 'json'
48
49
  spec.add_development_dependency 'irb'
@@ -59,14 +60,12 @@ Gem::Specification.new do |spec|
59
60
 
60
61
  # We pin rubocop because new cops have a tendency to result in false-y
61
62
  # positives for new contributors, which is not a nice experience.
62
- spec.add_development_dependency 'rubocop', '~> 0.80'
63
+ spec.add_development_dependency 'rubocop', '~> 1.91'
63
64
 
64
65
  spec.add_runtime_dependency 'base64', '~> 0.1'
65
66
  spec.add_runtime_dependency 'logger'
66
67
  spec.add_runtime_dependency 'mono_logger', '~> 1.0'
67
68
  spec.add_runtime_dependency 'redis', '>= 4.0'
68
69
  spec.add_runtime_dependency 'resque', '>= 3.0'
69
- # rufus-scheduler v3.7 causes a failure in test/multi_process_test.rb
70
- # rufus-scheduler v3.3 is missing a to_local method which fails tests
71
- spec.add_runtime_dependency 'rufus-scheduler', '~> 3.2', '!= 3.3'
70
+ spec.add_runtime_dependency 'rufus-scheduler', '~> 3.9'
72
71
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resque-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.1
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben VandenBos
@@ -14,6 +14,20 @@ bindir: exe
14
14
  cert_chain: []
15
15
  date: 1980-01-02 00:00:00.000000000 Z
16
16
  dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: activejob
19
+ requirement: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :development
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
17
31
  - !ruby/object:Gem::Dependency
18
32
  name: bundler
19
33
  requirement: !ruby/object:Gem::Requirement
@@ -202,14 +216,14 @@ dependencies:
202
216
  requirements:
203
217
  - - "~>"
204
218
  - !ruby/object:Gem::Version
205
- version: '0.80'
219
+ version: '1.91'
206
220
  type: :development
207
221
  prerelease: false
208
222
  version_requirements: !ruby/object:Gem::Requirement
209
223
  requirements:
210
224
  - - "~>"
211
225
  - !ruby/object:Gem::Version
212
- version: '0.80'
226
+ version: '1.91'
213
227
  - !ruby/object:Gem::Dependency
214
228
  name: base64
215
229
  requirement: !ruby/object:Gem::Requirement
@@ -286,20 +300,14 @@ dependencies:
286
300
  requirements:
287
301
  - - "~>"
288
302
  - !ruby/object:Gem::Version
289
- version: '3.2'
290
- - - "!="
291
- - !ruby/object:Gem::Version
292
- version: '3.3'
303
+ version: '3.9'
293
304
  type: :runtime
294
305
  prerelease: false
295
306
  version_requirements: !ruby/object:Gem::Requirement
296
307
  requirements:
297
308
  - - "~>"
298
309
  - !ruby/object:Gem::Version
299
- version: '3.2'
300
- - - "!="
301
- - !ruby/object:Gem::Version
302
- version: '3.3'
310
+ version: '3.9'
303
311
  description: |2
304
312
  Light weight job scheduling on top of Resque.
305
313
  Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
@@ -321,8 +329,6 @@ files:
321
329
  - ".devcontainer/devcontainer.json"
322
330
  - ".devcontainer/docker-compose.yml"
323
331
  - ".github/dependabot.yml"
324
- - ".github/workflows/codeql-analysis.yml"
325
- - ".github/workflows/rubocop.yml"
326
332
  - ".github/workflows/ruby.yml"
327
333
  - AUTHORS.md
328
334
  - CHANGELOG.md
@@ -348,6 +354,7 @@ files:
348
354
  - lib/resque/scheduler/locking.rb
349
355
  - lib/resque/scheduler/logger_builder.rb
350
356
  - lib/resque/scheduler/plugin.rb
357
+ - lib/resque/scheduler/railtie.rb
351
358
  - lib/resque/scheduler/scheduling_extensions.rb
352
359
  - lib/resque/scheduler/server.rb
353
360
  - lib/resque/scheduler/server/views/delayed.erb
@@ -374,7 +381,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
374
381
  requirements:
375
382
  - - ">="
376
383
  - !ruby/object:Gem::Version
377
- version: 3.0.0
384
+ version: 3.2.0
378
385
  required_rubygems_version: !ruby/object:Gem::Requirement
379
386
  requirements:
380
387
  - - ">="
@@ -1,59 +0,0 @@
1
- name: "CodeQL"
2
-
3
- on:
4
- push:
5
- branches: [ master, '*-stable' ]
6
- pull_request:
7
- # The branches below must be a subset of the branches above
8
- branches: [ master ]
9
- schedule:
10
- - cron: '44 16 * * 6'
11
-
12
- jobs:
13
- analyze:
14
- name: Analyze
15
- runs-on: ubuntu-latest
16
- permissions:
17
- actions: read
18
- contents: read
19
- security-events: write
20
-
21
- strategy:
22
- fail-fast: false
23
- matrix:
24
- language: [ 'ruby' ]
25
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
26
- # Learn more about CodeQL language support at https://git.io/codeql-language-support
27
-
28
- steps:
29
- - name: Checkout repository
30
- uses: actions/checkout@v4
31
-
32
- # Initializes the CodeQL tools for scanning.
33
- - name: Initialize CodeQL
34
- uses: github/codeql-action/init@v3
35
- with:
36
- languages: ${{ matrix.language }}
37
- # If you wish to specify custom queries, you can do so here or in a config file.
38
- # By default, queries listed here will override any specified in a config file.
39
- # Prefix the list here with "+" to use these queries and those in the config file.
40
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
41
-
42
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
43
- # If this step fails, then you should remove it and run the build manually (see below)
44
- - name: Autobuild
45
- uses: github/codeql-action/autobuild@v3
46
-
47
- # ℹ️ Command-line programs to run using the OS shell.
48
- # 📚 https://git.io/JvXDl
49
-
50
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
51
- # and modify them (or add more) to build your code if your project
52
- # uses a compiled language
53
-
54
- #- run: |
55
- # make bootstrap
56
- # make release
57
-
58
- - name: Perform CodeQL Analysis
59
- uses: github/codeql-action/analyze@v3
@@ -1,27 +0,0 @@
1
- name: Rubocop
2
-
3
- on:
4
- push:
5
- branches: [master]
6
- pull_request:
7
- branches: [master]
8
-
9
- jobs:
10
- rubocop:
11
- name: Rubocop
12
- runs-on: ${{ matrix.os }}
13
- strategy:
14
- matrix:
15
- os: [ubuntu-latest]
16
- ruby: [
17
- 3.4
18
- ]
19
-
20
- steps:
21
- - uses: actions/checkout@v4
22
- - uses: ruby/setup-ruby@v1
23
- with:
24
- ruby-version: ${{ matrix.ruby }}
25
- bundler-cache: true
26
- - name: Ruby linter
27
- run: bundle exec rubocop