que-scheduler 3.2.7 → 3.4.2

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: 1085b028076fd5380d046d26f49dbc42004e0a0c9a0576e8e92cff93726e980f
4
- data.tar.gz: 61c315d358848616d9bdc74a6704a011e0ca6165ebd5fce104eeac7a1ad830ec
3
+ metadata.gz: a3dcdf3dc34be0c572a49822ddad6b35464ee30f4424e0823c04008ae0b4facb
4
+ data.tar.gz: d46bdd71340462e52ea918aea603cecf27786e9e014c49c25c1459bc6181b383
5
5
  SHA512:
6
- metadata.gz: b11700cb20b528d2cac6b00960736e1107277163d80b22f68eb498fe00b5530260c6f89e1f6c6981f982af33b73bf6d7ebf8fa9273e35fd6495fb8cb0f516aa1
7
- data.tar.gz: aa6b0ca104cbe5daf013c748d854df1e9755a065a7183ef9182783d155707272fbd27d89b58c8097b16ba7368b42f3c68f5802756f4f012851c2392bc36a8640
6
+ metadata.gz: e1bf5bde9c9811a6675a7c3ac5ee983e47131ab9dda803f3961bbd272b64c4f7a79e601ffd5db671709ef36eb77e10552ad7c91b3719b0b51ac056d34565a23f
7
+ data.tar.gz: c60d779f6b2bc7e0f27d6ad8f09549d9a8767aca647ae0330f5e59f95b6a6d013537b34ed2bf1e290e2485843d9aa0a4018871deadf1352b57571b2380ce6629
data/README.md CHANGED
@@ -22,12 +22,13 @@ needs to be run, enqueueing those jobs, then enqueueing itself to check again la
22
22
  look for it is `config/que_schedule.yml`. They are essentially the same as resque-scheduler
23
23
  files, but with additional features.
24
24
 
25
- 1. Add a migration to start the job scheduler and prepare the audit table.
25
+ 1. Add a migration to start the job scheduler and prepare the audit table. Note that this migration
26
+ will fail if Que is set to execute jobs synchronously, i.e. `Que::Job.run_synchronously = true`.
26
27
 
27
28
  ```ruby
28
29
  class CreateQueSchedulerSchema < ActiveRecord::Migration
29
30
  def change
30
- Que::Scheduler::Migrations.migrate!(version: 4)
31
+ Que::Scheduler::Migrations.migrate!(version: 5)
31
32
  end
32
33
  end
33
34
  ```
@@ -65,10 +66,21 @@ BatchOrders:
65
66
  cron: "0 0 * * *"
66
67
  priority: 25
67
68
 
68
- # Specify job arguments
69
+ # Specify array job arguments
69
70
  SendOrders:
70
71
  cron: "0 0 * * *"
71
72
  args: ['open']
73
+
74
+ # Specify hash job arguments. Note, this appears as a single hash to `run`, not as kwargs.
75
+ SendPreorders:
76
+ cron: "0 0 * * *"
77
+ args:
78
+ order_type: special
79
+
80
+ # Specify a single nil argument
81
+ SendPostorders:
82
+ cron: "0 0 * * *"
83
+ args: ~ # See https://stackoverflow.com/a/51990876/1267203
72
84
 
73
85
  # Use simpler cron syntax
74
86
  SendBilling:
@@ -136,8 +148,54 @@ end
136
148
 
137
149
  ## Scheduler Audit
138
150
 
139
- An audit table _que_scheduler_audit_ is written to by the scheduler to keep a history of what jobs
140
- were enqueued when. It is created by the included migration tasks.
151
+ An audit table `que_scheduler_audit` is written to by the scheduler to keep a history of when the
152
+ scheduler ran to calculate what was necessary to run (if anything). It is created by the included
153
+ migration tasks.
154
+
155
+ Additionally, there is the audit table `que_scheduler_audit_enqueued`. This logs every job that
156
+ the scheduler enqueues.
157
+
158
+ When there is a major version (breaking) change, a migration should be run in. The version of the
159
+ migration proceeds at a faster rate than the version of the gem. To run in all the migrations required
160
+ up to a number, just migrate to that number with one line, and it will perform all the intermediary steps.
161
+
162
+ ie, This will perform all migrations necessary up to the latest version, skipping any already
163
+ performed.
164
+
165
+ ```ruby
166
+ class CreateQueSchedulerSchema < ActiveRecord::Migration
167
+ def change
168
+ Que::Scheduler::Migrations.migrate!(version: 5)
169
+ end
170
+ end
171
+ ```
172
+
173
+ The changes in past migrations were:
174
+
175
+ | Version | Changes |
176
+ |:-------:|---------------------------------------------------------------------------------|
177
+ | 1 | Enqueued the main Que::Scheduler. This is the job that performs the scheduling. |
178
+ | 2 | Added the audit table `que_scheduler_audit`. |
179
+ | 3 | Added the audit table `que_scheduler_audit_enqueued`. |
180
+ | 4 | Updated the the audit tables to use bigints |
181
+ | 5 | Dropped an unnecessary index |
182
+
183
+ ## Built in optional job for audit clear down
184
+
185
+ que-scheduler comes with the `QueSchedulerAuditClearDownJob` job built in that you can optionally
186
+ schedule to clear down audit rows if you don't need to retain them indefinitely. You should add this
187
+ to your own scheduler config yaml.
188
+
189
+ For example:
190
+
191
+ ```yaml
192
+ # This will clear down the oldest que-scheduler audit rows. Since que-scheduler
193
+ # runs approximately every minute, 129600 is 90 days.
194
+ Que::Scheduler::Jobs::QueSchedulerAuditClearDownJob:
195
+ cron: "0 0 * * *"
196
+ args:
197
+ retain_row_count: 129600
198
+ ```
141
199
 
142
200
  ## HA Redundancy and DB restores
143
201
 
@@ -172,23 +230,6 @@ then reschedules itself. The flow is as follows:
172
230
  1. After a deploy that changes the schedule, the job notices any new jobs to schedule, and knows which
173
231
  ones to forget. It does not need to be re-enqueued or restarted.
174
232
 
175
- ## DB Migrations
176
-
177
- When there is a major version (breaking) change, a migration should be run in. The version of the
178
- migration proceeds at a faster rate than the version of the gem. To run in all the migrations required
179
- up to a number, just migrate to that number with one line, and it will perform all the intermediary steps.
180
-
181
- ie, `Que::Scheduler::Migrations.migrate!(version: 4)` will perform all migrations necessary to
182
- reach migration version `4`.
183
-
184
- As of migration `4`, two elements are added to the DB for que-scheduler to run.
185
-
186
- 1. The first is the scheduler job itself, which runs forever, re-enqueuing itself to performs its
187
- duties.
188
- 1. The second part comprises the audit table `que_scheduler_audit` and the "enqueued" table
189
- `que_scheduler_audit_enqueued`. The first tracks when the scheduler calculated what was necessary to run
190
- (if anything). The second then logs every job that the scheduler enqueues.
191
-
192
233
  ## Testing Configuration
193
234
 
194
235
  You can add tests to validate your configuration during the spec phase. This will perform a variety
@@ -234,3 +275,6 @@ This gem was inspired by the makers of the excellent [Que](https://github.com/ch
234
275
 
235
276
  * @jish
236
277
  * @joehorsnell
278
+ * @bnauta
279
+ * @papodaca
280
+ * @krzyzak
@@ -1,3 +1,3 @@
1
1
  # rubocop:disable Naming/FileName
2
- require 'que/scheduler'
2
+ require "que/scheduler"
3
3
  # rubocop:enable Naming/FileName
@@ -1,7 +1,8 @@
1
- require 'que/scheduler/version'
2
- require 'que/scheduler/version_support'
3
- require 'que/scheduler/config'
4
- require 'que/scheduler/scheduler_job'
5
- require 'que/scheduler/db'
6
- require 'que/scheduler/audit'
7
- require 'que/scheduler/migrations'
1
+ require "que/scheduler/version"
2
+ require "que/scheduler/version_support"
3
+ require "que/scheduler/config"
4
+ require "que/scheduler/scheduler_job"
5
+ require "que/scheduler/db"
6
+ require "que/scheduler/audit"
7
+ require "que/scheduler/migrations"
8
+ require "que/scheduler/jobs/que_scheduler_audit_clear_down_job"
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "to_enqueue"
4
+
3
5
  module Que
4
6
  module Scheduler
5
7
  module Audit
6
- TABLE_NAME = 'que_scheduler_audit'
7
- ENQUEUED_TABLE_NAME = 'que_scheduler_audit_enqueued'
8
+ TABLE_NAME = "que_scheduler_audit"
9
+ ENQUEUED_TABLE_NAME = "que_scheduler_audit_enqueued"
8
10
  INSERT_AUDIT = %{
9
11
  INSERT INTO #{TABLE_NAME} (scheduler_job_id, executed_at)
10
12
  VALUES ($1::bigint, $2::timestamptz)
@@ -24,11 +26,10 @@ module Que
24
26
  def append(scheduler_job_id, executed_at, enqueued_jobs)
25
27
  ::Que::Scheduler::VersionSupport.execute(INSERT_AUDIT, [scheduler_job_id, executed_at])
26
28
  enqueued_jobs.each do |j|
27
- attrs = Que::Scheduler::VersionSupport.job_attributes(j)
28
29
  inserted = ::Que::Scheduler::VersionSupport.execute(
29
30
  INSERT_AUDIT_ENQUEUED,
30
31
  [scheduler_job_id] +
31
- attrs.values_at(:job_class, :queue, :priority, :args, :job_id, :run_at)
32
+ j.values_at(:job_class, :queue, :priority, :args, :job_id, :run_at)
32
33
  )
33
34
  raise "Cannot save audit row #{scheduler_job_id} #{executed_at} #{j}" if inserted.empty?
34
35
  end
@@ -1,5 +1,5 @@
1
- require 'que'
2
- require_relative 'version_support'
1
+ require "que"
2
+ require_relative "version_support"
3
3
 
4
4
  module Que
5
5
  module Scheduler
@@ -21,7 +21,7 @@ module Que
21
21
  end
22
22
 
23
23
  Que::Scheduler.configure do |config|
24
- config.schedule_location = ENV.fetch('QUE_SCHEDULER_CONFIG_LOCATION', 'config/que_schedule.yml')
24
+ config.schedule_location = ENV.fetch("QUE_SCHEDULER_CONFIG_LOCATION", "config/que_schedule.yml")
25
25
  config.transaction_adapter = ::Que.method(:transaction)
26
26
  config.que_scheduler_queue = Que::Scheduler::VersionSupport.default_scheduler_queue
27
27
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'config'
3
+ require_relative "config"
4
4
 
5
5
  module Que
6
6
  module Scheduler
7
7
  module Db
8
8
  SCHEDULER_COUNT_SQL =
9
9
  "SELECT COUNT(*) FROM que_jobs WHERE job_class = 'Que::Scheduler::SchedulerJob'"
10
- NOW_SQL = 'SELECT now()'
10
+ NOW_SQL = "SELECT now()"
11
11
 
12
12
  class << self
13
13
  def count_schedulers
@@ -1,6 +1,5 @@
1
- require 'hashie'
2
- require 'fugit'
3
- require 'backports/2.4.0/hash/compact'
1
+ require "hashie"
2
+ require "fugit"
4
3
 
5
4
  # This is the definition of one scheduleable job in the que-scheduler config yml file.
6
5
  module Que
@@ -10,23 +9,23 @@ module Que
10
9
 
11
10
  DEFINED_JOB_TYPES = [
12
11
  DEFINED_JOB_TYPE_DEFAULT = :default,
13
- DEFINED_JOB_TYPE_EVERY_EVENT = :every_event
12
+ DEFINED_JOB_TYPE_EVERY_EVENT = :every_event,
14
13
  ].freeze
15
14
 
16
- property :name, required: true
17
- property :job_class, required: true, transform_with: lambda { |v|
18
- job_class = Object.const_get(v)
19
- job_class < Que::Job ? job_class : err_field(:job_class, v)
20
- }
21
- property :cron, required: true, transform_with: lambda { |v|
22
- Fugit::Cron.parse(v) || err_field(:cron, v)
23
- }
24
- property :queue, transform_with: ->(v) { v.is_a?(String) ? v : err_field(:queue, v) }
25
- property :priority, transform_with: ->(v) { v.is_a?(Integer) ? v : err_field(:priority, v) }
26
- property :args
27
- property :schedule_type, default: DEFINED_JOB_TYPE_DEFAULT, transform_with: lambda { |v|
28
- v.to_sym.tap { |vs| DEFINED_JOB_TYPES.include?(vs) || err_field(:schedule_type, v) }
29
- }
15
+ property :name
16
+ property :job_class, transform_with: ->(v) { Object.const_get(v) }
17
+ property :cron, transform_with: ->(v) { Fugit::Cron.parse(v) }
18
+ property :queue
19
+ property :priority
20
+ property :args_array
21
+ property :schedule_type, default: DEFINED_JOB_TYPE_DEFAULT
22
+
23
+ class << self
24
+ def create(options)
25
+ defined_job = new(options.compact)
26
+ defined_job.freeze.tap { |dj| dj.validate(options) }
27
+ end
28
+ end
30
29
 
31
30
  # Given a "last time", return the next Time the event will occur, or nil if it
32
31
  # is after "to".
@@ -49,36 +48,80 @@ module Que
49
48
  generate_to_enqueue_list(missed_times)
50
49
  end
51
50
 
52
- class << self
53
- private
51
+ def validate(options)
52
+ validate_fields_presence(options)
53
+ validate_fields_types(options)
54
+ validate_job_class_related(options)
55
+ end
56
+
57
+ private
54
58
 
55
- def err_field(field, value)
56
- schedule = Que::Scheduler.configuration.schedule_location
57
- raise "Invalid #{field} '#{value}' in que-scheduler schedule #{schedule}"
59
+ # rubocop:disable Style/GuardClause This reads better as a conditional
60
+ def validate_fields_types(options)
61
+ unless queue.nil? || queue.is_a?(String)
62
+ err_field(:queue, options, "queue must be a string")
63
+ end
64
+ unless priority.nil? || priority.is_a?(Integer)
65
+ err_field(:priority, options, "priority must be an integer")
66
+ end
67
+ unless DEFINED_JOB_TYPES.include?(schedule_type)
68
+ err_field(:schedule_type, options, "Not in #{DEFINED_JOB_TYPES}")
58
69
  end
59
70
  end
71
+ # rubocop:enable Style/GuardClause
60
72
 
61
- private
73
+ def validate_fields_presence(options)
74
+ err_field(:name, options, "name must be present") if name.nil?
75
+ err_field(:job_class, options, "job_class must be present") if job_class.nil?
76
+ # An invalid cron is nil
77
+ err_field(:cron, options, "cron must be present") if cron.nil?
78
+ end
79
+
80
+ def validate_job_class_related(options)
81
+ # Only support known job engines
82
+ unless Que::Scheduler::ToEnqueue.valid_job_class?(job_class)
83
+ err_field(:job_class, options, "Job #{job_class} was not a supported job type")
84
+ end
85
+
86
+ # queue name is only supported for a subrange of ActiveJob versions. Print this out as a
87
+ # warning.
88
+ if queue &&
89
+ Que::Scheduler::ToEnqueue.active_job_sufficient_version? &&
90
+ job_class < ::ActiveJob::Base &&
91
+ Que::Scheduler::ToEnqueue.active_job_version < Gem::Version.create("6.0.3")
92
+ puts <<-ERR
93
+ WARNING from que-scheduler....
94
+ Between versions 4.2.3 and 6.0.2 (inclusive) Rails did not support setting queue names
95
+ on que jobs with ActiveJob, so que-scheduler cannot support it.
96
+ See removed in Rails 4.2.3
97
+ https://github.com/rails/rails/pull/19498
98
+ And readded in Rails 6.0.3
99
+ https://github.com/rails/rails/pull/38635
100
+
101
+ Please remove all "queue" keys from ActiveJobs defined in the que-scheduler.yml config.
102
+ Specifically #{queue} for job #{name}.
103
+ ERR
104
+ end
105
+ end
62
106
 
63
- class ToEnqueue < Hashie::Dash
64
- property :args, required: true, default: []
65
- property :queue
66
- property :priority
67
- property :job_class, required: true
107
+ def err_field(field, options, reason = "")
108
+ schedule = Que::Scheduler.configuration.schedule_location
109
+ value = options[field]
110
+ raise "Invalid #{field} '#{value}' for '#{name}' in que-scheduler schedule #{schedule}.\n" \
111
+ "#{reason}"
68
112
  end
69
113
 
70
114
  def generate_to_enqueue_list(missed_times)
71
115
  return [] if missed_times.empty?
72
116
 
73
117
  options = to_h.slice(:args, :queue, :priority, :job_class).compact
74
- args_array = args.is_a?(Array) ? args : Array(args)
75
118
 
76
119
  if schedule_type == DefinedJob::DEFINED_JOB_TYPE_EVERY_EVENT
77
120
  missed_times.map do |time_missed|
78
- ToEnqueue.new(options.merge(args: [time_missed] + args_array))
121
+ ToEnqueue.create(options.merge(args: [time_missed] + args_array))
79
122
  end
80
123
  else
81
- [ToEnqueue.new(options.merge(args: args_array))]
124
+ [ToEnqueue.create(options.merge(args: args_array))]
82
125
  end
83
126
  end
84
127
  end
@@ -1,4 +1,4 @@
1
- require 'fugit'
1
+ require "fugit"
2
2
 
3
3
  module Que
4
4
  module Scheduler
@@ -0,0 +1,45 @@
1
+ require "que"
2
+
3
+ # This job can optionally be scheduled to clear down the que-scheduler audit log if it
4
+ # isn't required in the long term.
5
+ module Que
6
+ module Scheduler
7
+ module Jobs
8
+ class QueSchedulerAuditClearDownJob < Que::Job
9
+ class << self
10
+ def build_sql(table_name)
11
+ <<-SQL
12
+ WITH deleted AS (
13
+ DELETE FROM #{table_name}
14
+ WHERE scheduler_job_id <= (
15
+ SELECT scheduler_job_id FROM que_scheduler_audit
16
+ ORDER BY scheduler_job_id DESC
17
+ LIMIT 1 OFFSET $1
18
+ ) RETURNING *
19
+ ) SELECT count(*) FROM deleted;
20
+ SQL
21
+ end
22
+ end
23
+
24
+ DELETE_AUDIT_ENQUEUED_SQL = build_sql("que_scheduler_audit_enqueued").freeze
25
+ DELETE_AUDIT_SQL = build_sql("que_scheduler_audit").freeze
26
+
27
+ # Very low priority
28
+ Que::Scheduler::VersionSupport.set_priority(self, 100)
29
+
30
+ def run(options)
31
+ retain_row_count = options.fetch(:retain_row_count)
32
+ Que::Scheduler::Db.transaction do
33
+ # This may delete zero or more than `retain_row_count` depending on if anything was
34
+ # scheduled in each of the past schedule runs
35
+ Que::Scheduler::VersionSupport.execute(DELETE_AUDIT_ENQUEUED_SQL, [retain_row_count])
36
+ # This will delete all but `retain_row_count` oldest rows
37
+ count = Que::Scheduler::VersionSupport.execute(DELETE_AUDIT_SQL, [retain_row_count])
38
+ log = "#{self.class} cleared down #{count.first.fetch(:count)} rows"
39
+ ::Que.log(event: "que-scheduler".to_sym, message: log)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX index_que_scheduler_audit_on_scheduler_job_id ON que_scheduler_audit USING btree (scheduler_job_id);
@@ -0,0 +1 @@
1
+ DROP INDEX index_que_scheduler_audit_on_scheduler_job_id;
@@ -1,4 +1,4 @@
1
- require_relative 'defined_job'
1
+ require_relative "defined_job"
2
2
 
3
3
  module Que
4
4
  module Scheduler
@@ -24,20 +24,36 @@ module Que
24
24
  end.to_h
25
25
  end
26
26
 
27
- private
28
-
29
27
  def hash_item_to_defined_job(name, defined_job_hash)
30
- Que::Scheduler::DefinedJob.new(
31
- {
32
- name: name,
33
- job_class: defined_job_hash['class'] || name,
34
- queue: defined_job_hash['queue'],
35
- args: defined_job_hash['args'],
36
- priority: defined_job_hash['priority'],
37
- cron: defined_job_hash['cron'],
38
- schedule_type: defined_job_hash['schedule_type'],
39
- }.compact
40
- ).freeze
28
+ # Que stores arguments as a json array. If the args we have to provide are already an
29
+ # array we can can simply pass them through. If it is a single non-nil value, then we make
30
+ # an array with one item which is that value (this includes if it is a hash). It could
31
+ # also be a single nil value.
32
+ args_array =
33
+ if !defined_job_hash.key?("args")
34
+ # No args were requested
35
+ []
36
+ else
37
+ args = defined_job_hash["args"]
38
+ if args.is_a?(Array)
39
+ # An array of args was requested
40
+ args
41
+ else
42
+ # A single value, a nil, or a hash was requested. que expects this to
43
+ # be enqueued as an array of 1 item
44
+ [args]
45
+ end
46
+ end
47
+
48
+ Que::Scheduler::DefinedJob.create(
49
+ name: name,
50
+ job_class: defined_job_hash["class"] || name,
51
+ queue: defined_job_hash["queue"],
52
+ args_array: args_array,
53
+ priority: defined_job_hash["priority"],
54
+ cron: defined_job_hash["cron"],
55
+ schedule_type: defined_job_hash["schedule_type"]&.to_sym
56
+ )
41
57
  end
42
58
  end
43
59
  end
@@ -1,10 +1,13 @@
1
- require 'que'
1
+ require "que"
2
+ require "active_support"
3
+ require "active_support/core_ext/time"
2
4
 
3
- require_relative 'schedule'
4
- require_relative 'enqueueing_calculator'
5
- require_relative 'scheduler_job_args'
6
- require_relative 'state_checks'
7
- require_relative 'version_support'
5
+ require_relative "schedule"
6
+ require_relative "enqueueing_calculator"
7
+ require_relative "scheduler_job_args"
8
+ require_relative "state_checks"
9
+ require_relative "to_enqueue"
10
+ require_relative "version_support"
8
11
 
9
12
  # The main job that runs every minute, determining what needs to be enqueued, enqueues the required
10
13
  # jobs, then re-enqueues itself.
@@ -13,8 +16,8 @@ module Que
13
16
  class SchedulerJob < Que::Job
14
17
  SCHEDULER_FREQUENCY = 60
15
18
 
16
- # Always highest possible priority.
17
19
  Que::Scheduler::VersionSupport.set_priority(self, 0)
20
+ Que::Scheduler::VersionSupport.apply_retry_semantics(self)
18
21
 
19
22
  def run(options = nil)
20
23
  Que::Scheduler::Db.transaction do
@@ -22,46 +25,36 @@ module Que
22
25
 
23
26
  scheduler_job_args = SchedulerJobArgs.build(options)
24
27
  logs = ["que-scheduler last ran at #{scheduler_job_args.last_run_time}."]
25
-
26
28
  result = EnqueueingCalculator.parse(Scheduler.schedule.values, scheduler_job_args)
27
29
  enqueued_jobs = enqueue_required_jobs(result, logs)
28
30
  enqueue_self_again(
29
31
  scheduler_job_args, scheduler_job_args.as_time, result.job_dictionary, enqueued_jobs
30
32
  )
31
-
32
33
  # Only now we're sure nothing errored, log the results
33
- logs.each { |str| ::Que.log(event: 'que-scheduler'.to_sym, message: str) }
34
+ logs.each { |str| ::Que.log(event: "que-scheduler".to_sym, message: str) }
34
35
  destroy
35
36
  end
36
37
  end
37
38
 
38
- def enqueue_required_jobs(result, logs)
39
- result.missed_jobs.map do |to_enqueue|
40
- job_class = to_enqueue.job_class
41
- args = to_enqueue.args
42
- remaining_hash = to_enqueue.except(:job_class, :args)
43
- enqueued_job =
44
- if args.is_a?(Hash)
45
- job_class.enqueue(args.merge(remaining_hash))
46
- else
47
- job_class.enqueue(*args, remaining_hash)
48
- end
49
- check_enqueued_job(enqueued_job, job_class, args, logs)
39
+ def enqueue_required_jobs(calculator_result, logs)
40
+ calculator_result.missed_jobs.map do |to_enqueue|
41
+ to_enqueue.enqueue.tap do |enqueued_job|
42
+ check_enqueued_job(to_enqueue, enqueued_job, logs)
43
+ end
50
44
  end.compact
51
45
  end
52
46
 
53
47
  private
54
48
 
55
- def check_enqueued_job(enqueued_job, job_class, args, logs)
56
- if enqueued_job.is_a?(Que::Job)
57
- job_id = Que::Scheduler::VersionSupport.job_attributes(enqueued_job).fetch(:job_id)
58
- logs << "que-scheduler enqueueing #{job_class} #{job_id} with args: #{args}"
59
- enqueued_job
60
- else
61
- # This can happen if a middleware nixes the enqueue call
62
- logs << "que-scheduler called enqueue on #{job_class} but did not receive a #{Que::Job}"
63
- nil
64
- end
49
+ def check_enqueued_job(to_enqueue, enqueued_job, logs)
50
+ logs << if enqueued_job.present?
51
+ "que-scheduler enqueueing #{enqueued_job.job_class} " \
52
+ "#{enqueued_job.job_id} with args: #{enqueued_job.args}"
53
+ else
54
+ # This can happen if a middleware nixes the enqueue call
55
+ "que-scheduler called enqueue on #{to_enqueue.job_class} " \
56
+ "but it reported no job was scheduled. Has `enqueue` been overridden?"
57
+ end
65
58
  end
66
59
 
67
60
  def enqueue_self_again(scheduler_job_args, last_full_execution, job_dictionary, enqueued_jobs)
@@ -79,8 +72,8 @@ module Que
79
72
  )
80
73
 
81
74
  # rubocop:disable Style/GuardClause This reads better as a conditional
82
- unless check_enqueued_job(enqueued_job, SchedulerJob, {}, [])
83
- raise 'SchedulerJob could not self-schedule. Has `.enqueue` been monkey patched?'
75
+ unless Que::Scheduler::VersionSupport.job_attributes(enqueued_job).fetch(:job_id)
76
+ raise "SchedulerJob could not self-schedule. Has `.enqueue` been monkey patched?"
84
77
  end
85
78
  # rubocop:enable Style/GuardClause
86
79
  end
@@ -1,6 +1,6 @@
1
- require 'hashie'
2
- require 'active_support'
3
- require 'active_support/time_with_zone'
1
+ require "hashie"
2
+ require "active_support"
3
+ require "active_support/time_with_zone"
4
4
 
5
5
  # These are the args that are used for this particular run of the scheduler.
6
6
  module Que
@@ -1,6 +1,6 @@
1
- require_relative 'audit'
2
- require_relative 'db'
3
- require_relative 'migrations'
1
+ require_relative "audit"
2
+ require_relative "db"
3
+ require_relative "migrations"
4
4
 
5
5
  module Que
6
6
  module Scheduler
@@ -17,9 +17,22 @@ module Que
17
17
  db_version = Que::Scheduler::Migrations.db_version
18
18
  return if db_version == Que::Scheduler::Migrations::MAX_VERSION
19
19
 
20
+ sync_err =
21
+ if Que::Scheduler::VersionSupport.running_synchronously? && db_version.zero?
22
+ code = Que::Scheduler::VersionSupport.running_synchronously_code?
23
+ <<-ERR_SYNC
24
+ You currently have Que to run in synchronous mode using
25
+ #{code}, so it is most likely this error
26
+ has happened during an initial migration. You should disable synchronous mode and
27
+ try again. Note, que-scheduler uses "forward time" scheduled jobs, so will not work
28
+ in synchronous mode.
29
+
30
+ ERR_SYNC
31
+ end
32
+
20
33
  raise(<<-ERR)
21
34
  The que-scheduler db migration state was found to be #{db_version}. It should be #{Que::Scheduler::Migrations::MAX_VERSION}.
22
-
35
+ #{sync_err}
23
36
  que-scheduler adds some tables to the DB to provide an audit history of what was
24
37
  enqueued when, and with what options and arguments. The structure of these tables is
25
38
  versioned, and should match that version required by the gem.
@@ -42,6 +55,9 @@ module Que
42
55
  Que::Scheduler::Migrations.migrate!(version: #{Que::Scheduler::Migrations::MAX_VERSION})
43
56
  end
44
57
  end
58
+
59
+ It is also possible that you are running a migration with Que set up to execute jobs
60
+ synchronously. This will fail as que-scheduler needs the above tables to work.
45
61
  ERR
46
62
  end
47
63
 
@@ -0,0 +1,157 @@
1
+ require "que"
2
+
3
+ # This module uses polymorphic dispatch to centralise the differences between supporting Que::Job
4
+ # and other job systems.
5
+ module Que
6
+ module Scheduler
7
+ class ToEnqueue < Hashie::Dash
8
+ property :args, required: true, default: []
9
+ property :queue
10
+ property :priority
11
+ property :run_at, required: true
12
+ property :job_class, required: true
13
+
14
+ class << self
15
+ def create(options)
16
+ type_from_job_class(options.fetch(:job_class)).new(
17
+ options.merge(run_at: Que::Scheduler::Db.now)
18
+ )
19
+ end
20
+
21
+ def valid_job_class?(job_class)
22
+ type_from_job_class(job_class).present?
23
+ end
24
+
25
+ def active_job_version
26
+ Gem.loaded_specs["activejob"]&.version
27
+ end
28
+
29
+ def active_job_sufficient_version?
30
+ # ActiveJob 4.x does not support job_ids correctly
31
+ # https://github.com/rails/rails/pull/20056/files
32
+ active_job_version && active_job_version > Gem::Version.create("5")
33
+ end
34
+
35
+ def active_job_version_supports_queues?
36
+ # Supporting queue name in ActiveJob was removed in Rails 4.2.3
37
+ # https://github.com/rails/rails/pull/19498
38
+ # and readded in Rails 6.0.3
39
+ # https://github.com/rails/rails/pull/38635
40
+ ToEnqueue.active_job_version && ToEnqueue.active_job_version >=
41
+ Gem::Version.create("6.0.3")
42
+ end
43
+
44
+ private
45
+
46
+ def type_from_job_class(job_class)
47
+ types.each do |type, implementation|
48
+ return implementation if job_class < type
49
+ end
50
+ nil
51
+ end
52
+
53
+ def types
54
+ @types ||=
55
+ begin
56
+ hash = {
57
+ ::Que::Job => QueJobType,
58
+ }
59
+ hash[::ActiveJob::Base] = ActiveJobType if ToEnqueue.active_job_sufficient_version?
60
+ hash
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ # For jobs of type Que::Job
67
+ class QueJobType < ToEnqueue
68
+ def enqueue
69
+ job_settings = to_h.slice(:queue, :priority, :run_at).compact
70
+ job =
71
+ if args.is_a?(Hash)
72
+ job_class.enqueue(**args.merge(job_settings))
73
+ else
74
+ job_class.enqueue(*args, **job_settings)
75
+ end
76
+
77
+ return nil if job.nil? || !job # nil in Rails < 6.1, false after.
78
+
79
+ # Now read the just inserted job back out of the DB to get the actual values that will
80
+ # be used when the job is worked.
81
+ values = Que::Scheduler::VersionSupport.job_attributes(job).slice(
82
+ :args, :queue, :priority, :run_at, :job_class, :job_id
83
+ )
84
+ EnqueuedJobType.new(values)
85
+ end
86
+ end
87
+
88
+ # For jobs of type ActiveJob
89
+ class ActiveJobType < ToEnqueue
90
+ def enqueue
91
+ job = enqueue_active_job
92
+
93
+ return nil if job.nil? || !job # nil in Rails < 6.1, false after.
94
+
95
+ enqueued_values = calculate_enqueued_values(job)
96
+ EnqueuedJobType.new(enqueued_values)
97
+ end
98
+
99
+ def calculate_enqueued_values(job)
100
+ # Now read the just inserted job back out of the DB to get the actual values that will
101
+ # be used when the job is worked.
102
+ data = JSON.parse(job.to_json, symbolize_names: true)
103
+
104
+ # ActiveJob scheduled_at is returned as a float, where we want a Time for consistency
105
+ scheduled_at =
106
+ begin
107
+ scheduled_at_float = data[:scheduled_at]
108
+ scheduled_at_float ? Time.zone.at(scheduled_at_float) : nil
109
+ end
110
+
111
+ # Rails didn't support queues for ActiveJob for a while
112
+ used_queue = data[:queue_name] if ToEnqueue.active_job_version_supports_queues?
113
+
114
+ # We can't get the priority out of the DB, as the returned `job` doesn't give us access
115
+ # to the underlying ActiveJob that was scheduled. We have no option but to assume
116
+ # it was what we told it to use. If no priority was specified, we must assume it was
117
+ # the Que default, which is 100 t.ly/1jRK5
118
+ assume_used_priority = priority.nil? ? 100 : priority
119
+
120
+ {
121
+ args: data.fetch(:arguments),
122
+ queue: used_queue,
123
+ priority: assume_used_priority,
124
+ run_at: scheduled_at,
125
+ job_class: job_class.to_s,
126
+ job_id: data.fetch(:provider_job_id),
127
+ }
128
+ end
129
+
130
+ def enqueue_active_job
131
+ job_settings = {
132
+ priority: priority,
133
+ wait_until: run_at,
134
+ queue: queue || Que::Scheduler::VersionSupport.default_scheduler_queue,
135
+ }.compact
136
+
137
+ job_class_set = job_class.set(**job_settings)
138
+ if args.is_a?(Hash)
139
+ job_class_set.perform_later(**args)
140
+ else
141
+ job_class_set.perform_later(*args)
142
+ end
143
+ end
144
+ end
145
+
146
+ # A value object returned after a job has been enqueued. This is necessary as Que (normal) and
147
+ # ActiveJob return very different objects from the `enqueue` call.
148
+ class EnqueuedJobType < Hashie::Dash
149
+ property :args
150
+ property :queue
151
+ property :priority
152
+ property :run_at, required: true
153
+ property :job_class, required: true
154
+ property :job_id, required: true
155
+ end
156
+ end
157
+ end
@@ -1,5 +1,5 @@
1
1
  module Que
2
2
  module Scheduler
3
- VERSION = '3.2.7'.freeze
3
+ VERSION = "3.4.2".freeze
4
4
  end
5
5
  end
@@ -1,24 +1,41 @@
1
- require 'que'
1
+ require "que"
2
2
 
3
3
  # The purpose of this module is to centralise the differences when supporting both que 0.x and
4
4
  # 1.x with the same gem.
5
5
  module Que
6
6
  module Scheduler
7
7
  module VersionSupport
8
+ RETRY_PROC = proc { |count|
9
+ # Maximum one hour, otherwise use the default backoff
10
+ count > 7 ? (60 * 60) : ((count**4) + 3)
11
+ }
12
+
8
13
  class << self
14
+ # Ensure que-scheduler runs at the highest priority. This is because its priority is a
15
+ # the top of all jobs it enqueues.
9
16
  def set_priority(context, priority)
10
17
  if zero_major?
11
- context.instance_variable_set('@priority', priority)
18
+ context.instance_variable_set("@priority", priority)
12
19
  else
13
20
  context.priority = priority
14
21
  end
15
22
  end
16
23
 
24
+ # Ensure the job runs at least once an hour when it is backing off due to errors
25
+ def apply_retry_semantics(context)
26
+ if zero_major?
27
+ context.instance_variable_set("@retry_interval", RETRY_PROC)
28
+ else
29
+ context.maximum_retry_count = 1 << 128 # Heat death of universe
30
+ context.retry_interval = RETRY_PROC
31
+ end
32
+ end
33
+
17
34
  def job_attributes(enqueued_job)
18
35
  if zero_major?
19
- enqueued_job.attrs.transform_keys(&:to_sym)
36
+ enqueued_job.attrs.to_h.transform_keys(&:to_sym)
20
37
  else
21
- enqueued_job.que_attrs.transform_keys(&:to_sym).tap do |hash|
38
+ enqueued_job.que_attrs.to_h.transform_keys(&:to_sym).tap do |hash|
22
39
  hash[:job_id] = hash.delete(:id)
23
40
  end
24
41
  end
@@ -31,22 +48,26 @@ module Que
31
48
  end
32
49
 
33
50
  def default_scheduler_queue
34
- if zero_major?
35
- ''
36
- else
37
- Que::DEFAULT_QUEUE
38
- end
51
+ zero_major? ? "" : Que::DEFAULT_QUEUE
52
+ end
53
+
54
+ def running_synchronously?
55
+ zero_major? ? (Que.mode == :sync) : Que.run_synchronously
56
+ end
57
+
58
+ def running_synchronously_code?
59
+ zero_major? ? "Que.mode == :sync" : "Que.run_synchronously = true"
39
60
  end
40
61
 
41
62
  def zero_major?
42
63
  # This is the only way to handle beta releases too
43
- @zero_major ||= Gem.loaded_specs['que'].version.to_s.split('.').first.to_i.zero?
64
+ @zero_major ||= Gem.loaded_specs["que"].version.to_s.split(".").first.to_i.zero?
44
65
  end
45
66
 
46
67
  private
47
68
 
48
69
  def normalise_array_of_hashes(array)
49
- array.map { |row| row.transform_keys(&:to_sym) }
70
+ array.map { |row| row.to_h.transform_keys(&:to_sym) }
50
71
  end
51
72
  end
52
73
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: que-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.7
4
+ version: 3.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Lascelles
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-10-19 00:00:00.000000000 Z
11
+ date: 2020-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,20 +24,6 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.0'
27
- - !ruby/object:Gem::Dependency
28
- name: backports
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '3.10'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '3.10'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: fugit
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -62,16 +48,22 @@ dependencies:
62
48
  name: hashie
63
49
  requirement: !ruby/object:Gem::Requirement
64
50
  requirements:
65
- - - "~>"
51
+ - - ">="
66
52
  - !ruby/object:Gem::Version
67
53
  version: '3'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '5'
68
57
  type: :runtime
69
58
  prerelease: false
70
59
  version_requirements: !ruby/object:Gem::Requirement
71
60
  requirements:
72
- - - "~>"
61
+ - - ">="
73
62
  - !ruby/object:Gem::Version
74
63
  version: '3'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '5'
75
67
  - !ruby/object:Gem::Dependency
76
68
  name: que
77
69
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +73,7 @@ dependencies:
81
73
  version: '0.12'
82
74
  - - "<="
83
75
  - !ruby/object:Gem::Version
84
- version: 1.0.0.beta3
76
+ version: 1.0.0.beta4
85
77
  type: :runtime
86
78
  prerelease: false
87
79
  version_requirements: !ruby/object:Gem::Requirement
@@ -91,7 +83,7 @@ dependencies:
91
83
  version: '0.12'
92
84
  - - "<="
93
85
  - !ruby/object:Gem::Version
94
- version: 1.0.0.beta3
86
+ version: 1.0.0.beta4
95
87
  - !ruby/object:Gem::Dependency
96
88
  name: activerecord
97
89
  requirement: !ruby/object:Gem::Requirement
@@ -222,16 +214,16 @@ dependencies:
222
214
  name: reek
223
215
  requirement: !ruby/object:Gem::Requirement
224
216
  requirements:
225
- - - '='
217
+ - - ">="
226
218
  - !ruby/object:Gem::Version
227
- version: 5.0.2
219
+ version: '0'
228
220
  type: :development
229
221
  prerelease: false
230
222
  version_requirements: !ruby/object:Gem::Requirement
231
223
  requirements:
232
- - - '='
224
+ - - ">="
233
225
  - !ruby/object:Gem::Version
234
- version: 5.0.2
226
+ version: '0'
235
227
  - !ruby/object:Gem::Dependency
236
228
  name: rspec
237
229
  requirement: !ruby/object:Gem::Requirement
@@ -250,16 +242,30 @@ dependencies:
250
242
  name: rubocop
251
243
  requirement: !ruby/object:Gem::Requirement
252
244
  requirements:
253
- - - "~>"
245
+ - - '='
254
246
  - !ruby/object:Gem::Version
255
- version: 0.68.1
247
+ version: 0.84.0
256
248
  type: :development
257
249
  prerelease: false
258
250
  version_requirements: !ruby/object:Gem::Requirement
259
251
  requirements:
260
- - - "~>"
252
+ - - '='
253
+ - !ruby/object:Gem::Version
254
+ version: 0.84.0
255
+ - !ruby/object:Gem::Dependency
256
+ name: rubocop-rspec
257
+ requirement: !ruby/object:Gem::Requirement
258
+ requirements:
259
+ - - ">="
260
+ - !ruby/object:Gem::Version
261
+ version: '0'
262
+ type: :development
263
+ prerelease: false
264
+ version_requirements: !ruby/object:Gem::Requirement
265
+ requirements:
266
+ - - ">="
261
267
  - !ruby/object:Gem::Version
262
- version: 0.68.1
268
+ version: '0'
263
269
  - !ruby/object:Gem::Dependency
264
270
  name: sqlite3
265
271
  requirement: !ruby/object:Gem::Requirement
@@ -317,6 +323,7 @@ files:
317
323
  - lib/que/scheduler/db.rb
318
324
  - lib/que/scheduler/defined_job.rb
319
325
  - lib/que/scheduler/enqueueing_calculator.rb
326
+ - lib/que/scheduler/jobs/que_scheduler_audit_clear_down_job.rb
320
327
  - lib/que/scheduler/migrations.rb
321
328
  - lib/que/scheduler/migrations/1/down.sql
322
329
  - lib/que/scheduler/migrations/1/up.sql
@@ -326,10 +333,13 @@ files:
326
333
  - lib/que/scheduler/migrations/3/up.sql
327
334
  - lib/que/scheduler/migrations/4/down.sql
328
335
  - lib/que/scheduler/migrations/4/up.sql
336
+ - lib/que/scheduler/migrations/5/down.sql
337
+ - lib/que/scheduler/migrations/5/up.sql
329
338
  - lib/que/scheduler/schedule.rb
330
339
  - lib/que/scheduler/scheduler_job.rb
331
340
  - lib/que/scheduler/scheduler_job_args.rb
332
341
  - lib/que/scheduler/state_checks.rb
342
+ - lib/que/scheduler/to_enqueue.rb
333
343
  - lib/que/scheduler/version.rb
334
344
  - lib/que/scheduler/version_support.rb
335
345
  homepage: https://github.com/hlascelles/que-scheduler
@@ -356,8 +366,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
356
366
  - !ruby/object:Gem::Version
357
367
  version: '0'
358
368
  requirements: []
359
- rubyforge_project:
360
- rubygems_version: 2.7.6.2
369
+ rubygems_version: 3.0.3
361
370
  signing_key:
362
371
  specification_version: 4
363
372
  summary: A cron scheduler for Que