resque-scheduler 5.0.1 → 5.1.1

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: 7a49c095f83e23c0bb20e1dffe6038c64a50376d89a0123436eae4233a74e390
4
+ data.tar.gz: 80bd2fb3055d7904d0d62f47f3683c5c6a6f5c4d7236d7311b29ee7c6d2f1174
5
5
  SHA512:
6
- metadata.gz: 8b6c3caa8b47c24aee900d2c348cabb20d1a32a5fce88e02a974649ca6c711b1703562393902c7638e14c1a11180b1901b126480e905c7cca8b2dbd47d27daa3
7
- data.tar.gz: 7b2665804103943b3bf9a98b84134ba71a0ac00ace8e0c2ed7c765873445c801bea14c8ae9082fec4133e6f3be0314bdf1485ecd47c7bab8aba6d8d85ef36ec8
6
+ metadata.gz: c206ceb2414921369b7fd89800f95a8e3583226d709fd6d92effbfa69c1284711081cd46189b05aee1d14fdc3161f3d8fee0d7588c4e81a0a2cff6a791c0e415
7
+ data.tar.gz: f99d4d481f7ec3de91bef5a3b5ead66474e0f1df72a68bb705058de727ed448cd60cae39b901fd0b3edd4d02004fb87147e08a7b265517ca2b5109e194d7fb54
@@ -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,36 @@
2
2
 
3
3
  **ATTN**: This project uses [semantic versioning](http://semver.org/).
4
4
 
5
+ ## [5.1.1] - 2026-11-02
6
+ ### Fixed
7
+ * Enqueue batched delayed jobs outside the Redis transaction so a conflicted batch is no longer enqueued twice and enqueue hooks can use Redis by @neuromanskie in #850
8
+ * Skip due delayed items when not master instead of spinning on them by @neuromanskie in #852
9
+ * Load the web UI after Rails initialization to avoid a circular require warning by @neuromanskie in #854
10
+
11
+ ## [5.1.0] - 2026-10-28
12
+ ### Breaking Changes
13
+ * Ruby requirement: `>= 3.0.0` → `>= 3.2.0` by @neuromanskie in #837
14
+ * rufus-scheduler dependency: `~> 3.2, != 3.3` → `~> 3.9` by @neuromanskie in #837
15
+ * 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
16
+
17
+ ### Added
18
+ * `before_recurring` and `after_recurring` hooks for jobs enqueued from the schedule by @top-sigrid in #844
19
+ * `DISABLE_DELAYED_REQUEUE_BATCHES` to opt out of batched delayed job enqueues by @onyxraven in #796
20
+ * Search delayed jobs by args in the web UI by @bmo in #839 (originally #714)
21
+ * Allow a number of seconds for `every` by @carsonreinke in #838 (originally #607)
22
+ * Railtie to load resque-scheduler automatically in Rails apps by @josh-m-sharpe in #694
23
+ * Add Ruby 4.0 and redis-rb 5.4 to the CI matrix by @neuromanskie in #837
24
+
25
+ ### Changed
26
+ * Upgrade RuboCop to 1.91 and run it as a CI job by @neuromanskie in #837
27
+
28
+ ### Fixed
29
+ * Delayed jobs no longer run `before_schedule`/`after_schedule` a second time when they become due by @top-sigrid in #844
30
+ * Skip schedule hooks when `enqueue_at` is given a timestamp that is not in the future by @top-sigrid in #817
31
+ * Schedule Active Job classes through `perform_later` by @neuromanskie and @josh-m-sharpe in #842 (originally #696)
32
+ * Schedule delayed test jobs far enough out to not race the clock by @neuromanskie in #843
33
+ * Fail CI when tests fail by @neuromanskie in #837
34
+
5
35
  ## [5.0.1] - 2026-10-11
6
36
  ### Changed
7
37
  * 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`)
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,10 +780,10 @@ 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
 
758
-
759
787
  ### Contributing
760
788
 
761
789
  See [CONTRIBUTING.md](CONTRIBUTING.md)
@@ -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,13 @@
1
+ module Resque
2
+ module Scheduler
3
+ class Railtie < Rails::Railtie
4
+ config.after_initialize do
5
+ require 'resque/scheduler/server'
6
+ end
7
+
8
+ rake_tasks do
9
+ require 'resque/scheduler/tasks'
10
+ end
11
+ end
12
+ end
13
+ 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.1'.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
 
@@ -196,7 +198,7 @@ module Resque
196
198
  # at_time - Time to start scheduling items (default: now).
197
199
  def handle_delayed_items(at_time = nil)
198
200
  timestamp = Resque.next_delayed_timestamp(at_time)
199
- if timestamp
201
+ if timestamp && am_master
200
202
  procline 'Processing Delayed Items'
201
203
  until timestamp.nil?
202
204
  enqueue_delayed_items_for_timestamp(timestamp)
@@ -210,40 +212,48 @@ 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
- # Continually check that it is still the master
230
238
  if am_master
231
- actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp,
232
- batch_size)
239
+ if batch_delayed_items?
240
+ actual_batch_size = enqueue_items_in_batch_for_timestamp(timestamp, batch_size)
241
+ log "queued batch of #{actual_batch_size} jobs"
242
+ else
243
+ item = enqueue_next_item(timestamp)
244
+ actual_batch_size = item.nil? ? 0 : 1
245
+ end
233
246
  end
234
247
  end
235
248
 
236
249
  count += actual_batch_size
237
- log "queued #{count} jobs" if actual_batch_size != -1
238
250
 
239
251
  # continue processing until there are no more items in this
240
252
  # timestamp. If we don't have a full batch, this is the last one.
241
- # This also breaks us in the event of a redis transaction failure
242
- # i.e. enqueue_items_in_batch_for_timestamp returned -1
243
253
  break if actual_batch_size < batch_size
244
254
  end
245
255
 
246
- log "finished queueing #{count} total jobs for timestamp #{timestamp}" if count != -1
256
+ log "finished queueing #{count} total jobs for timestamp #{timestamp}" if count > 0
247
257
  end
248
258
 
249
259
  def timestamp_key(timestamp)
@@ -253,49 +263,40 @@ module Resque
253
263
  def enqueue_items_in_batch_for_timestamp(timestamp, batch_size)
254
264
  timestamp_bucket_key = timestamp_key(timestamp)
255
265
 
256
- encoded_jobs_to_requeue = Resque.redis.lrange(timestamp_bucket_key, 0, batch_size - 1)
257
-
258
- # Watch is used to ensure that the timestamp bucket we are operating on
259
- # is not altered by any other clients between the watch call and when we call exec
260
- # (to execute the multi block). We should error catch on the redis.exec return value
261
- # as that will indicate if the entire transaction was aborted or not. Though we should
262
- # be safe as our ltrim is inside the multi block and therefore also would have been
263
- # aborted. So nothing would have been queued, but also nothing lost from the bucket.
264
- watch_result = Resque.redis.watch(timestamp_bucket_key) do
265
- Resque.redis.multi do |pipeline|
266
- encoded_jobs_to_requeue.each do |encoded_job|
267
- pipeline.srem("timestamps:#{encoded_job}", timestamp_bucket_key)
268
-
269
- decoded_job = Resque.decode(encoded_job)
270
- enqueue(decoded_job)
271
- end
266
+ return 0 unless (encoded_jobs = take_delayed_batch(timestamp_bucket_key, batch_size))
272
267
 
273
- pipeline.ltrim(timestamp_bucket_key, batch_size, -1)
274
- end
268
+ encoded_jobs.each do |encoded_job|
269
+ enqueue(Resque.decode(encoded_job), :delayed)
275
270
  end
276
271
 
277
- # Did the multi block successfully remove from this timestamp and enqueue the jobs?
278
- success = !watch_result.nil?
279
-
280
- # If this was the last batch in this timestamp bucket, clean up
281
- if success && encoded_jobs_to_requeue.count < batch_size
272
+ if encoded_jobs.count < batch_size
282
273
  Resque.clean_up_timestamp(timestamp_bucket_key, timestamp)
283
274
  end
284
275
 
285
- unless success
286
- # Our batched transaction failed in Redis due to the timestamp_bucket_key value
287
- # being modified while we built our multi block. We return -1 to ensure we break
288
- # out of the loop iterating on this timestamp so it can be re-processed via the
289
- # loop in handle_delayed_items.
290
- return -1
291
- end
276
+ encoded_jobs.count
277
+ end
278
+
279
+ # Returns nil if the timestamp bucket changed before the batch was taken.
280
+ def take_delayed_batch(timestamp_bucket_key, batch_size)
281
+ Resque.redis.watch(timestamp_bucket_key) do
282
+ encoded_jobs = Resque.redis.lrange(timestamp_bucket_key, 0, batch_size - 1)
283
+ taken = Resque.redis.multi do |transaction|
284
+ encoded_jobs.each do |encoded_job|
285
+ transaction.srem("timestamps:#{encoded_job}", [timestamp_bucket_key])
286
+ end
287
+ transaction.ltrim(timestamp_bucket_key, batch_size, -1)
288
+ end
292
289
 
293
- # will return 0 if none were left to batch
294
- encoded_jobs_to_requeue.count
290
+ encoded_jobs if taken
291
+ end
295
292
  end
296
293
 
297
- def enqueue(config)
298
- enqueue_from_config(config)
294
+ # Enqueues a job like enqueue_from_config, but hands errors to the failure
295
+ # handler instead of raising them. Pass :delayed as hooks for a job that
296
+ # left the delayed queue or :recurring for a schedule entry to run the
297
+ # matching hooks.
298
+ def enqueue(config, hooks = nil)
299
+ enqueue_from_config(config, hooks)
299
300
  rescue => e
300
301
  Resque::Scheduler.failure_handler.on_enqueue_failure(config, e)
301
302
  end
@@ -306,8 +307,11 @@ module Resque
306
307
  exit if @shutdown
307
308
  end
308
309
 
309
- # Enqueues a job based on a config hash
310
- def enqueue_from_config(job_config)
310
+ # Enqueues a job based on a config hash. Pass :delayed as hooks for a job
311
+ # that left the delayed queue or :recurring for a schedule entry to run
312
+ # the matching hooks.
313
+ def enqueue_from_config(job_config, hooks = nil)
314
+ Resque::Scheduler::Plugin.validate_enqueue_hooks(hooks)
311
315
  args = job_config['args'] || job_config[:args]
312
316
 
313
317
  klass_name = job_config['class'] || job_config[:class]
@@ -318,9 +322,13 @@ module Resque
318
322
  end
319
323
 
320
324
  params = args.is_a?(Hash) ? [args] : Array(args)
321
- queue = job_config['queue'] ||
322
- job_config[:queue] ||
323
- Resque.queue_from_class(klass)
325
+ queue = job_config['queue'] || job_config[:queue]
326
+
327
+ if Resque::Scheduler::Util.active_job_class?(klass)
328
+ return klass.set(**{ queue: queue }.compact).perform_later(*params)
329
+ end
330
+
331
+ queue ||= Resque.queue_from_class(klass)
324
332
  # Support custom job classes like those that inherit from
325
333
  # Resque::JobWithStatus (resque-status)
326
334
  job_klass = job_config['custom_job_class']
@@ -343,8 +351,7 @@ module Resque
343
351
  # for non-existent classes (for example: running scheduler in
344
352
  # one app that schedules for another.
345
353
  if Class === klass
346
- Resque::Scheduler::Plugin.process_schedule_hooks(klass, *params) do
347
- Resque::Scheduler::Plugin.run_before_delayed_enqueue_hooks(klass, *params)
354
+ Resque::Scheduler::Plugin.process_enqueue_hooks(hooks, klass, *params) do
348
355
  # If the class is a custom job class, call self#scheduled on it.
349
356
  # This allows you to do things like Resque.enqueue_at(timestamp,
350
357
  # CustomJobClass). Otherwise, pass off to Resque.
@@ -504,7 +511,7 @@ module Resque
504
511
  def enqueue_recurring(name, config)
505
512
  if am_master
506
513
  log! "queueing #{config['class']} (#{name})"
507
- enqueue(config)
514
+ enqueue(config, :recurring)
508
515
  Resque.last_enqueued_at(name, Time.now.to_s)
509
516
  end
510
517
  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'
@@ -51,6 +52,7 @@ Gem::Specification.new do |spec|
51
52
  spec.add_development_dependency 'ostruct'
52
53
  spec.add_development_dependency 'pry'
53
54
  spec.add_development_dependency 'rack-test'
55
+ spec.add_development_dependency 'railties'
54
56
  spec.add_development_dependency 'rake'
55
57
  spec.add_development_dependency 'simplecov'
56
58
  spec.add_development_dependency 'test-unit'
@@ -59,14 +61,12 @@ Gem::Specification.new do |spec|
59
61
 
60
62
  # We pin rubocop because new cops have a tendency to result in false-y
61
63
  # positives for new contributors, which is not a nice experience.
62
- spec.add_development_dependency 'rubocop', '~> 0.80'
64
+ spec.add_development_dependency 'rubocop', '~> 1.91'
63
65
 
64
66
  spec.add_runtime_dependency 'base64', '~> 0.1'
65
67
  spec.add_runtime_dependency 'logger'
66
68
  spec.add_runtime_dependency 'mono_logger', '~> 1.0'
67
69
  spec.add_runtime_dependency 'redis', '>= 4.0'
68
70
  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'
71
+ spec.add_runtime_dependency 'rufus-scheduler', '~> 3.9'
72
72
  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.1
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
@@ -126,6 +140,20 @@ dependencies:
126
140
  - - ">="
127
141
  - !ruby/object:Gem::Version
128
142
  version: '0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: railties
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
129
157
  - !ruby/object:Gem::Dependency
130
158
  name: rake
131
159
  requirement: !ruby/object:Gem::Requirement
@@ -202,14 +230,14 @@ dependencies:
202
230
  requirements:
203
231
  - - "~>"
204
232
  - !ruby/object:Gem::Version
205
- version: '0.80'
233
+ version: '1.91'
206
234
  type: :development
207
235
  prerelease: false
208
236
  version_requirements: !ruby/object:Gem::Requirement
209
237
  requirements:
210
238
  - - "~>"
211
239
  - !ruby/object:Gem::Version
212
- version: '0.80'
240
+ version: '1.91'
213
241
  - !ruby/object:Gem::Dependency
214
242
  name: base64
215
243
  requirement: !ruby/object:Gem::Requirement
@@ -286,20 +314,14 @@ dependencies:
286
314
  requirements:
287
315
  - - "~>"
288
316
  - !ruby/object:Gem::Version
289
- version: '3.2'
290
- - - "!="
291
- - !ruby/object:Gem::Version
292
- version: '3.3'
317
+ version: '3.9'
293
318
  type: :runtime
294
319
  prerelease: false
295
320
  version_requirements: !ruby/object:Gem::Requirement
296
321
  requirements:
297
322
  - - "~>"
298
323
  - !ruby/object:Gem::Version
299
- version: '3.2'
300
- - - "!="
301
- - !ruby/object:Gem::Version
302
- version: '3.3'
324
+ version: '3.9'
303
325
  description: |2
304
326
  Light weight job scheduling on top of Resque.
305
327
  Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
@@ -321,8 +343,6 @@ files:
321
343
  - ".devcontainer/devcontainer.json"
322
344
  - ".devcontainer/docker-compose.yml"
323
345
  - ".github/dependabot.yml"
324
- - ".github/workflows/codeql-analysis.yml"
325
- - ".github/workflows/rubocop.yml"
326
346
  - ".github/workflows/ruby.yml"
327
347
  - AUTHORS.md
328
348
  - CHANGELOG.md
@@ -348,6 +368,7 @@ files:
348
368
  - lib/resque/scheduler/locking.rb
349
369
  - lib/resque/scheduler/logger_builder.rb
350
370
  - lib/resque/scheduler/plugin.rb
371
+ - lib/resque/scheduler/railtie.rb
351
372
  - lib/resque/scheduler/scheduling_extensions.rb
352
373
  - lib/resque/scheduler/server.rb
353
374
  - lib/resque/scheduler/server/views/delayed.erb
@@ -374,7 +395,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
374
395
  requirements:
375
396
  - - ">="
376
397
  - !ruby/object:Gem::Version
377
- version: 3.0.0
398
+ version: 3.2.0
378
399
  required_rubygems_version: !ruby/object:Gem::Requirement
379
400
  requirements:
380
401
  - - ">="
@@ -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