que-scheduler 7.0.1 → 7.2.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: b09366936c031d9621c4596d06e3ad7bf590a6d52a2db9cf1b0f9fe31b8d3bba
4
- data.tar.gz: d8caa2bfb5becaee14cbc827a63aa0d138597f702e62d7e59e65acff604baa77
3
+ metadata.gz: bdd3f8eb6dccc6c82cb4ce97f0a64f7acf30394cdc4d7f7f299d86680209777a
4
+ data.tar.gz: ac8c0c66d2fb3def5887a6f7cc27b6577c28692b55549fd067e09ad6767fe68e
5
5
  SHA512:
6
- metadata.gz: a371c6a801e5bcd9fb153faa8190cce580192fb6bc304c4cecdb3e8fb89d782f0ced9b07e05a7a546a631c9a73dec40648fe4c8bc800930e175ac810d4e81d4e
7
- data.tar.gz: 02ff472a2d83564fd197a6e03fea030e8304e1a2ea2f49c900e83afdc9a70d2d07dc2ce88941fd153f53fc4d9ec445c05ba1271aba330b8f8663a76ede2a7264
6
+ metadata.gz: 384c9a75444bac91293d13f4bcfa9c79418df2b43d537a8707b1996705c27d44d44422a7af90135ec34311d872096fa06a1ac32eefbf5bf770223673019c2606
7
+ data.tar.gz: 1f6e7fa846f34937010560a8905c84b56f2e67f87acafd9d0b72d83503f9ec32b36b7ad9c1d77f7396cd7e8ccfe0564a494d2992bb82430fc4ff3cfab76a6f46
data/README.md CHANGED
@@ -123,11 +123,13 @@ A job can have a `schedule_type` assigned to it. Valid values are:
123
123
  times go by during an extended period of downtime (eg a long maintenance window) then only one job
124
124
  will be enqueued when the system starts back up. Multiple missed events are coalesced. This mimics
125
125
  the way resque-scheduler would perform if it were taken down for some time.
126
+ The job's `run_at` is the latest missed cron time.
126
127
  1. `every_event` - Every cron match will result in a job being scheduled. If multiple cron times go
127
128
  by during an extended period of downtime, then a job will be scheduled for every one missed on
128
129
  startup. This `schedule_type` should be used for regular batch jobs that need to know which time
129
130
  they are running a batch for. The job will always be scheduled with an ISO8601 string of the cron
130
131
  that matched as the first argument.
132
+ Its `run_at` is that same cron time, so overdue jobs retain their scheduled order within a priority.
131
133
 
132
134
  An example would be an eventing DailyReportJob which summarises a day's sales. If no jobs run for
133
135
  a few days due to a technical failure, then on recovery a report would still be needed for each
@@ -338,3 +340,4 @@ This gem was inspired by the makers of the excellent [Que](https://github.com/ch
338
340
  * @milgner
339
341
  * @oiahoon
340
342
  * @papodaca
343
+ * @bensynapse
@@ -1,6 +1,9 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
4
+ require "sorbet-runtime"
3
5
  require_relative "to_enqueue"
6
+ require_relative "types"
4
7
 
5
8
  module Que
6
9
  module Scheduler
@@ -23,6 +26,15 @@ module Que
23
26
  }.freeze
24
27
 
25
28
  class << self
29
+ extend T::Sig
30
+
31
+ sig do
32
+ params(
33
+ scheduler_job_id: Integer,
34
+ executed_at: Types::TIME,
35
+ enqueued_jobs: T::Array[Que::Scheduler::EnqueuedJobType]
36
+ ).void
37
+ end
26
38
  def append(scheduler_job_id, executed_at, enqueued_jobs)
27
39
  ::Que::Scheduler::DbSupport.execute(INSERT_AUDIT, [scheduler_job_id, executed_at])
28
40
  enqueued_jobs.each do |j|
@@ -1,23 +1,47 @@
1
+ # typed: true
2
+
3
+ require "pathname"
1
4
  require "que"
5
+ require "sorbet-runtime"
2
6
  require_relative "db_support"
7
+ require_relative "types"
3
8
 
4
9
  module Que
5
10
  module Scheduler
6
11
  # :reek:Attribute
7
12
  class Configuration
8
- attr_accessor :schedule_location, :schedule, :transaction_adapter, :que_scheduler_queue,
9
- :time_zone
13
+ extend T::Sig
14
+
15
+ sig { returns(T.any(String, Pathname)) }
16
+ attr_accessor :schedule_location
17
+
18
+ sig { returns(T.nilable(Types::SCHEDULE_CONFIG)) }
19
+ attr_accessor :schedule
20
+
21
+ sig { returns(T.any(Method, Proc)) }
22
+ attr_accessor :transaction_adapter
23
+
24
+ sig { returns(String) }
25
+ attr_accessor :que_scheduler_queue
26
+
27
+ sig { returns(T.nilable(String)) }
28
+ attr_accessor :time_zone
10
29
  end
11
30
 
12
31
  class << self
13
- # :reek:Attribute
14
- attr_accessor :configuration # rubocop:disable ThreadSafety/ClassAndModuleAttributes
32
+ extend T::Sig
33
+
34
+ sig { returns(Configuration) }
35
+ def configuration
36
+ @configuration ||= Configuration.new
37
+ end
15
38
 
16
- def configure
17
- self.configuration ||= Configuration.new
39
+ sig { params(blk: T.proc.params(config: Configuration).void).void }
40
+ def configure(&blk)
18
41
  yield(configuration)
19
42
  end
20
43
 
44
+ sig { void }
21
45
  def apply_defaults
22
46
  configure do |config|
23
47
  config.schedule_location =
@@ -1,5 +1,7 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
4
+ require "sorbet-runtime"
3
5
  require_relative "configuration"
4
6
 
5
7
  module Que
@@ -10,19 +12,31 @@ module Que
10
12
  NOW_SQL = "SELECT now()"
11
13
 
12
14
  class << self
15
+ extend T::Sig
16
+
17
+ sig { returns(Integer) }
13
18
  def count_schedulers
14
- Que::Scheduler::DbSupport.execute(SCHEDULER_COUNT_SQL).first.values.first.to_i
19
+ T.cast(first_value(SCHEDULER_COUNT_SQL), Integer)
15
20
  end
16
21
 
22
+ sig { returns(Time) }
17
23
  def now
18
- Que::Scheduler::DbSupport.execute(NOW_SQL).first.values.first
24
+ T.cast(first_value(NOW_SQL), Time)
25
+ end
26
+
27
+ sig { params(sql: String).returns(T.anything) }
28
+ private def first_value(sql)
29
+ Que::Scheduler::DbSupport.execute(sql).fetch(0).values.fetch(0)
19
30
  end
20
31
 
21
- # rubocop:disable Style/ExplicitBlockArgument
22
- def transaction
23
- Que::Scheduler.configuration.transaction_adapter.call { yield }
32
+ sig do
33
+ type_parameters(:U)
34
+ .params(blk: T.proc.returns(T.type_parameter(:U)))
35
+ .returns(T.type_parameter(:U))
36
+ end
37
+ def transaction(&blk)
38
+ Que::Scheduler.configuration.transaction_adapter.call(&blk)
24
39
  end
25
- # rubocop:enable Style/ExplicitBlockArgument
26
40
  end
27
41
  end
28
42
  end
@@ -1,39 +1,64 @@
1
+ # typed: true
2
+
1
3
  require "que"
4
+ require "sorbet-runtime"
2
5
 
3
6
  module Que
4
7
  module Scheduler
5
8
  module DbSupport
6
9
  class << self
10
+ extend T::Sig
11
+
12
+ sig { params(enqueued_job: Que::Job).returns(T::Hash[Symbol, T.anything]) }
7
13
  def job_attributes(enqueued_job)
8
14
  enqueued_job.que_attrs.to_h.transform_keys(&:to_sym).tap do |hash|
9
15
  hash[:job_id] = hash.delete(:id)
10
16
  end
11
17
  end
12
18
 
19
+ sig { params(enqueued_job: Que::Job).returns(Integer) }
20
+ def job_id(enqueued_job) = T.cast(job_attributes(enqueued_job).fetch(:job_id), Integer)
21
+
13
22
  # Between Que versions the result of Que execute changed keys from strings to symbols.
14
23
  # Here we wrap the concept and make sure either way produces symbols
24
+ sig do
25
+ params(str: String, args: T::Array[T.anything])
26
+ .returns(T::Array[T::Hash[Symbol, T.anything]])
27
+ end
15
28
  def execute(str, args = [])
16
29
  normalise_array_of_hashes(Que.execute(str, args))
17
30
  end
18
31
 
32
+ sig do
33
+ params(
34
+ clazz: T.class_of(Que::Job),
35
+ job_options: T::Hash[Symbol, T.anything],
36
+ job_args: T.anything
37
+ ).returns(T.nilable(T.any(Que::Job, FalseClass)))
38
+ end
19
39
  def enqueue_a_job(clazz, job_options = {}, job_args = [])
20
- if job_args.is_a?(Hash)
40
+ case job_args
41
+ when Hash
21
42
  clazz.enqueue(job_args, job_options: job_options)
22
43
  else
23
- clazz.enqueue(*job_args, job_options: job_options)
44
+ # Sorbet cannot type a splat of a value of unknown size
45
+ T.unsafe(clazz).enqueue(*job_args, job_options: job_options)
24
46
  end
25
47
  end
26
48
 
49
+ sig { returns(String) }
27
50
  def que_version
28
51
  @que_version ||= que_version_object.to_s
29
52
  end
30
53
 
54
+ sig { returns(Gem::Version) }
31
55
  private def que_version_object
32
- @que_version_object ||= Gem.loaded_specs["que"].version
56
+ @que_version_object ||= T.must(Gem.loaded_specs["que"]).version
33
57
  end
34
58
 
59
+ sig { params(array: T::Array[T.anything]).returns(T::Array[T::Hash[Symbol, T.anything]]) }
35
60
  private def normalise_array_of_hashes(array)
36
- array.map { |row| row.to_h.transform_keys(&:to_sym) }
61
+ array.map { |row| T.cast(row, T::Hash[T.any(String, Symbol), T.anything]).to_h.transform_keys(&:to_sym) }
37
62
  end
38
63
  end
39
64
  end
@@ -1,11 +1,16 @@
1
+ # typed: true
2
+
1
3
  require "hashie"
4
+ require "sorbet-runtime"
2
5
  require "fugit"
6
+ require_relative "types"
3
7
 
4
8
  # This is the definition of one scheduleable job in the que-scheduler config yml file.
5
9
  module Que
6
10
  module Scheduler
7
- class DefinedJob < Hashie::Dash
11
+ class DefinedJob < Hashie::Dash # rubocop:disable Metrics/ClassLength -- shrinks with T::Struct
8
12
  include Hashie::Extensions::Dash::PropertyTranslation
13
+ extend T::Sig
9
14
 
10
15
  DEFINED_JOB_TYPES = [
11
16
  DEFINED_JOB_TYPE_DEFAULT = :default,
@@ -21,6 +26,9 @@ module Que
21
26
  property :schedule_type, default: DEFINED_JOB_TYPE_DEFAULT
22
27
 
23
28
  class << self
29
+ extend T::Sig
30
+
31
+ sig { params(options: T::Hash[Symbol, T.anything]).returns(DefinedJob) }
24
32
  def create(options)
25
33
  defined_job = new(options.compact)
26
34
  defined_job.freeze.tap { |dj| dj.validate(options) }
@@ -29,6 +37,7 @@ module Que
29
37
 
30
38
  # Given a "last time", return the next Time the event will occur, or nil if it
31
39
  # is after "to".
40
+ sig { params(from: Types::TIME, to: Types::TIME).returns(T.nilable(ActiveSupport::TimeWithZone)) }
32
41
  def next_run_time(from, to)
33
42
  next_time = cron.next_time(from)
34
43
  next_run = next_time.to_local_time.in_time_zone(next_time.zone)
@@ -37,9 +46,10 @@ module Que
37
46
 
38
47
  # Given the last scheduler run time, and this run time, return all
39
48
  # the instances that should be enqueued for the job class.
49
+ sig { params(last_run_time: Types::TIME, as_time: Types::TIME).returns(T::Array[ToEnqueue]) }
40
50
  def calculate_missed_runs(last_run_time, as_time)
41
- missed_times = []
42
- last_time = last_run_time
51
+ missed_times = T.let([], T::Array[ActiveSupport::TimeWithZone])
52
+ last_time = T.let(last_run_time, Types::TIME)
43
53
  while (next_run = next_run_time(last_time, as_time))
44
54
  missed_times << next_run
45
55
  last_time = next_run
@@ -48,6 +58,7 @@ module Que
48
58
  generate_to_enqueue_list(missed_times)
49
59
  end
50
60
 
61
+ sig { params(options: T::Hash[Symbol, T.anything]).void }
51
62
  def validate(options)
52
63
  validate_fields_presence(options)
53
64
  validate_fields_types(options)
@@ -55,6 +66,7 @@ module Que
55
66
  end
56
67
 
57
68
  # rubocop:disable Style/GuardClause -- This reads better as a conditional
69
+ sig { params(options: T::Hash[Symbol, T.anything]).void }
58
70
  private def validate_fields_types(options)
59
71
  unless queue.nil? || queue.is_a?(String)
60
72
  err_field(:queue, options, "queue must be a string")
@@ -68,6 +80,7 @@ module Que
68
80
  end
69
81
  # rubocop:enable Style/GuardClause
70
82
 
83
+ sig { params(options: T::Hash[Symbol, T.anything]).void }
71
84
  private def validate_fields_presence(options)
72
85
  err_field(:name, options, "name must be present") if name.nil?
73
86
  err_field(:job_class, options, "job_class must be present") if job_class.nil?
@@ -75,6 +88,7 @@ module Que
75
88
  err_field(:cron, options, "cron must be present") if cron.nil?
76
89
  end
77
90
 
91
+ sig { params(options: T::Hash[Symbol, T.anything]).void }
78
92
  private def validate_job_class_related(options)
79
93
  # Only support known job engines
80
94
  unless Que::Scheduler::ToEnqueue.valid_job_class?(job_class)
@@ -86,7 +100,7 @@ module Que
86
100
  if queue &&
87
101
  Que::Scheduler::ToEnqueue.active_job_defined? &&
88
102
  job_class < ::ActiveJob::Base &&
89
- Que::Scheduler::ToEnqueue.active_job_version < Gem::Version.create("6.0.3")
103
+ T.must(Que::Scheduler::ToEnqueue.active_job_version) < Gem::Version.create("6.0.3")
90
104
  puts <<~ERR
91
105
  WARNING from que-scheduler....
92
106
  Between versions 4.2.3 and 6.0.2 (inclusive) Rails did not support setting queue names
@@ -102,6 +116,7 @@ module Que
102
116
  end
103
117
  end
104
118
 
119
+ sig { params(field: Symbol, options: T::Hash[Symbol, T.anything], reason: String).returns(T.noreturn) }
105
120
  private def err_field(field, options, reason = "")
106
121
  schedule = Que::Scheduler.configuration.schedule_location
107
122
  value = options[field]
@@ -109,6 +124,7 @@ module Que
109
124
  "#{reason}"
110
125
  end
111
126
 
127
+ sig { params(missed_times: T::Array[ActiveSupport::TimeWithZone]).returns(T::Array[ToEnqueue]) }
112
128
  private def generate_to_enqueue_list(missed_times)
113
129
  return [] if missed_times.empty?
114
130
 
@@ -116,10 +132,11 @@ module Que
116
132
 
117
133
  if schedule_type == DefinedJob::DEFINED_JOB_TYPE_EVERY_EVENT
118
134
  missed_times.map do |time_missed|
119
- ToEnqueue.create(options.merge(args: [time_missed.iso8601] + args_array))
135
+ ToEnqueue.create(options.merge(run_at: time_missed,
136
+ args: [time_missed.iso8601] + args_array))
120
137
  end
121
138
  else
122
- [ToEnqueue.create(options.merge(args: args_array))]
139
+ [ToEnqueue.create(options.merge(run_at: missed_times.last, args: args_array))]
123
140
  end
124
141
  end
125
142
  end
@@ -1,4 +1,7 @@
1
+ # typed: true
2
+
1
3
  require "fugit"
4
+ require "sorbet-runtime"
2
5
 
3
6
  module Que
4
7
  module Scheduler
@@ -9,6 +12,14 @@ module Que
9
12
  end
10
13
 
11
14
  class << self
15
+ extend T::Sig
16
+
17
+ sig do
18
+ params(
19
+ scheduler_config: T::Array[DefinedJob],
20
+ scheduler_job_args: SchedulerJobArgs
21
+ ).returns(Result)
22
+ end
12
23
  def parse(scheduler_config, scheduler_job_args)
13
24
  job_dictionary = []
14
25
 
@@ -1,4 +1,7 @@
1
+ # typed: true
2
+
1
3
  require "que"
4
+ require "sorbet-runtime"
2
5
 
3
6
  # This job can optionally be scheduled to clear down the que-scheduler audit log if it
4
7
  # isn't required in the long term.
@@ -6,7 +9,12 @@ module Que
6
9
  module Scheduler
7
10
  module Jobs
8
11
  class QueSchedulerAuditClearDownJob < Que::Job
12
+ extend T::Sig
13
+
9
14
  class << self
15
+ extend T::Sig
16
+
17
+ sig { params(table_name: String).returns(String) }
10
18
  def build_sql(table_name)
11
19
  <<~SQL
12
20
  WITH deleted AS (
@@ -28,6 +36,7 @@ module Que
28
36
  self.priority = 100
29
37
  self.queue = -> { Que::Scheduler.configuration.que_scheduler_queue }
30
38
 
39
+ sig { params(options: T::Hash[T.any(String, Symbol), T.anything]).void }
31
40
  def run(options)
32
41
  retain_row_count = options.symbolize_keys.fetch(:retain_row_count)
33
42
  Que::Scheduler::Db.transaction do
@@ -36,7 +45,7 @@ module Que
36
45
  Que::Scheduler::DbSupport.execute(DELETE_AUDIT_ENQUEUED_SQL, [retain_row_count])
37
46
  # This will delete all but `retain_row_count` oldest rows
38
47
  count = Que::Scheduler::DbSupport.execute(DELETE_AUDIT_SQL, [retain_row_count])
39
- log = "#{self.class} cleared down #{count.first.fetch(:count)} rows"
48
+ log = "#{self.class} cleared down #{count.fetch(0).fetch(:count)} rows"
40
49
  ::Que.log(event: :"que-scheduler", message: log)
41
50
  end
42
51
  end
@@ -1,5 +1,8 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
4
+ require "sorbet-runtime"
5
+
3
6
  module Que
4
7
  module Scheduler
5
8
  module Migrations
@@ -12,6 +15,9 @@ module Que
12
15
  MAX_VERSION = Dir.glob("#{__dir__}/migrations/*").map { |d| File.basename(d) }.map(&:to_i).max
13
16
 
14
17
  class << self
18
+ extend T::Sig
19
+
20
+ sig { params(version: Integer).void }
15
21
  def migrate!(version:)
16
22
  # Like que, Do not migrate test DBs.
17
23
  return if defined?(Que::Testing)
@@ -27,9 +33,11 @@ module Que
27
33
  end
28
34
 
29
35
  # rubocop:disable MagicNumbers/NoReturn
36
+ sig { returns(Integer) }
30
37
  def db_version
31
38
  if audit_table_exists?
32
- return Que::Scheduler::DbSupport.execute(TABLE_COMMENT).first[:description].to_i
39
+ comment = Que::Scheduler::DbSupport.execute(TABLE_COMMENT).fetch(0).fetch(:description)
40
+ return T.cast(comment, T.nilable(String)).to_i
33
41
  end
34
42
 
35
43
  # At this point we used to be able to tell if it was 0 or 1 by the presence of the
@@ -38,6 +46,7 @@ module Que
38
46
  end
39
47
  # rubocop:enable MagicNumbers/NoReturn
40
48
 
49
+ sig { returns(T::Boolean) }
41
50
  def audit_table_exists?
42
51
  result = Que::Scheduler::DbSupport.execute(<<-SQL)
43
52
  SELECT * FROM information_schema.tables WHERE table_name = '#{AUDIT_TABLE_NAME}';
@@ -47,6 +56,7 @@ module Que
47
56
 
48
57
  # This method must be used during initial installation of que-scheduler and if the
49
58
  # project migrations are squashed.
59
+ sig { void }
50
60
  def reenqueue_scheduler_if_missing
51
61
  raise <<~MSG unless Que::Migrations.db_version >= 6
52
62
  Cannot (re)enqueue the que-scheduler worker unless the Que migrations have been run to at least version 6.
@@ -64,15 +74,18 @@ module Que
64
74
  Que::Scheduler::DbSupport.enqueue_a_job(Que::Scheduler::SchedulerJob)
65
75
  end
66
76
 
77
+ sig { params(current: Integer, version: Integer).void }
67
78
  private def migrate_up(current, version)
68
79
  execute_step(current += 1, :up) until current == version
69
80
  end
70
81
 
82
+ sig { params(current: Integer, version: Integer).void }
71
83
  private def migrate_down(current, version)
72
84
  current += 1
73
85
  execute_step(current -= 1, :down) until current == version + 1
74
86
  end
75
87
 
88
+ sig { params(number: Integer, direction: Symbol).void }
76
89
  private def execute_step(number, direction)
77
90
  sql = File.read("#{__dir__}/migrations/#{number}/#{direction}.sql")
78
91
  Que::Scheduler::DbSupport.execute(sql)
@@ -1,21 +1,29 @@
1
+ # typed: true
2
+
3
+ require "sorbet-runtime"
1
4
  require_relative "configuration"
2
5
  require_relative "defined_job"
6
+ require_relative "types"
3
7
 
4
8
  module Que
5
9
  module Scheduler
6
10
  class Schedule
7
11
  class << self
12
+ extend T::Sig
13
+
8
14
  # The main method for determining the schedule. It has to evaluate the schedule as late as
9
15
  # possible (ie just as it is about to be used) as we cannot guarantee we are in a Rails
10
16
  # app with initializers. In a future release this may change to "fast fail" in Rails by
11
17
  # checking the config up front.
18
+ sig { returns(T::Hash[String, DefinedJob]) }
12
19
  def schedule
13
20
  @schedule ||=
14
21
  begin
15
22
  configuration = Que::Scheduler.configuration
16
- if !configuration.schedule.nil?
23
+ explicit_schedule = configuration.schedule
24
+ if !explicit_schedule.nil?
17
25
  # If an explicit schedule as a hash has been defined, use that.
18
- from_hash(configuration.schedule)
26
+ from_hash(explicit_schedule)
19
27
  elsif File.exist?(configuration.schedule_location)
20
28
  # If the schedule is defined as a file location, then load it and return it.
21
29
  from_file(configuration.schedule_location)
@@ -26,21 +34,30 @@ module Que
26
34
  end
27
35
  end
28
36
 
37
+ sig { params(location: T.any(String, Pathname)).returns(T::Hash[String, DefinedJob]) }
29
38
  def from_file(location)
30
39
  from_yaml(File.read(location))
31
40
  end
32
41
 
42
+ sig { params(config: String).returns(T::Hash[String, DefinedJob]) }
33
43
  def from_yaml(config)
34
44
  config_hash = YAML.safe_load(config)
35
45
  from_hash(config_hash)
36
46
  end
37
47
 
48
+ sig { params(config_hash: Types::SCHEDULE_CONFIG).returns(T::Hash[String, DefinedJob]) }
38
49
  def from_hash(config_hash)
39
50
  config_hash
40
51
  .map { |name, defined_job_hash| hash_item_to_defined_job(name.to_s, defined_job_hash) }
41
52
  .index_by(&:name)
42
53
  end
43
54
 
55
+ sig do
56
+ params(
57
+ name: String,
58
+ defined_job_hash_in: T::Hash[T.any(String, Symbol), T.anything]
59
+ ).returns(DefinedJob)
60
+ end
44
61
  def hash_item_to_defined_job(name, defined_job_hash_in)
45
62
  defined_job_hash = defined_job_hash_in.stringify_keys
46
63
  # Que stores arguments as a json array. If the args we have to provide are already an
@@ -77,6 +94,9 @@ module Que
77
94
  end
78
95
 
79
96
  class << self
97
+ extend T::Sig
98
+
99
+ sig { returns(T::Hash[String, DefinedJob]) }
80
100
  def schedule
81
101
  Schedule.schedule
82
102
  end
@@ -1,4 +1,7 @@
1
+ # typed: true
2
+
1
3
  require "que"
4
+ require "sorbet-runtime"
2
5
  require "active_support"
3
6
  require "active_support/core_ext/time"
4
7
 
@@ -8,12 +11,15 @@ require_relative "scheduler_job_args"
8
11
  require_relative "state_checks"
9
12
  require_relative "to_enqueue"
10
13
  require_relative "db_support"
14
+ require_relative "types"
11
15
 
12
16
  # The main job that runs every minute, determining what needs to be enqueued, enqueues the required
13
17
  # jobs, then re-enqueues itself.
14
18
  module Que
15
19
  module Scheduler
16
20
  class SchedulerJob < Que::Job
21
+ extend T::Sig
22
+
17
23
  SCHEDULER_FREQUENCY = 60
18
24
  RETRY_PROC = proc { |count|
19
25
  # Maximum one hour, otherwise use the default backoff
@@ -24,6 +30,7 @@ module Que
24
30
  self.maximum_retry_count = 1 << 128 # Heat death of universe
25
31
  self.retry_interval = RETRY_PROC
26
32
 
33
+ sig { params(options: T.nilable(T::Hash[T.any(String, Symbol), T.anything])).void }
27
34
  def run(options = nil)
28
35
  Que::Scheduler::Db.transaction do
29
36
  Que::Scheduler::StateChecks.check
@@ -42,6 +49,12 @@ module Que
42
49
  end
43
50
  end
44
51
 
52
+ sig do
53
+ params(
54
+ calculator_result: EnqueueingCalculator::Result,
55
+ logs: T::Array[String]
56
+ ).returns(T::Array[EnqueuedJobType])
57
+ end
45
58
  def enqueue_required_jobs(calculator_result, logs)
46
59
  calculator_result.missed_jobs.filter_map do |to_enqueue|
47
60
  to_enqueue.enqueue.tap do |enqueued_job|
@@ -50,6 +63,13 @@ module Que
50
63
  end
51
64
  end
52
65
 
66
+ sig do
67
+ params(
68
+ to_enqueue: ToEnqueue,
69
+ enqueued_job: T.nilable(EnqueuedJobType),
70
+ logs: T::Array[String]
71
+ ).void
72
+ end
53
73
  private def check_enqueued_job(to_enqueue, enqueued_job, logs)
54
74
  logs << if enqueued_job.present?
55
75
  "que-scheduler enqueueing #{enqueued_job.job_class} " \
@@ -61,10 +81,18 @@ module Que
61
81
  end
62
82
  end
63
83
 
84
+ sig do
85
+ params(
86
+ scheduler_job_args: SchedulerJobArgs,
87
+ last_full_execution: Types::TIME,
88
+ job_dictionary: T::Array[String],
89
+ enqueued_jobs: T::Array[EnqueuedJobType]
90
+ ).void
91
+ end
64
92
  private def enqueue_self_again(scheduler_job_args, last_full_execution, job_dictionary,
65
93
  enqueued_jobs)
66
94
  # Log last run...
67
- job_id = DbSupport.job_attributes(self).fetch(:job_id)
95
+ job_id = DbSupport.job_id(self)
68
96
  Audit.append(job_id, scheduler_job_args.as_time, enqueued_jobs)
69
97
 
70
98
  # And rerun...
@@ -1,4 +1,7 @@
1
+ # typed: true
2
+
1
3
  require "hashie"
4
+ require "sorbet-runtime"
2
5
  require "active_support"
3
6
  require "active_support/time_with_zone"
4
7
 
@@ -10,6 +13,9 @@ module Que
10
13
  property :job_dictionary, required: true
11
14
  property :as_time, required: true
12
15
 
16
+ extend T::Sig
17
+
18
+ sig { params(options: T.nilable(T::Hash[T.any(String, Symbol), T.anything])).returns(SchedulerJobArgs) }
13
19
  def self.build(options)
14
20
  now = Que::Scheduler::Db.now
15
21
  parsed =
@@ -1,3 +1,6 @@
1
+ # typed: true
2
+
3
+ require "sorbet-runtime"
1
4
  require_relative "audit"
2
5
  require_relative "db"
3
6
  require_relative "migrations"
@@ -6,11 +9,15 @@ module Que
6
9
  module Scheduler
7
10
  module StateChecks
8
11
  class << self
12
+ extend T::Sig
13
+
14
+ sig { void }
9
15
  def check
10
16
  assert_db_migrated
11
17
  end
12
18
 
13
19
  # rubocop:disable Metrics/MethodLength
20
+ sig { void }
14
21
  private def assert_db_migrated
15
22
  db_version = Que::Scheduler::Migrations.db_version
16
23
  return if db_version == Que::Scheduler::Migrations::MAX_VERSION
@@ -1,3 +1,7 @@
1
+ # typed: true
2
+
3
+ require "sorbet-runtime"
4
+
1
5
  module Que
2
6
  module Scheduler
3
7
  module TimeZone
@@ -35,6 +39,9 @@ module Que
35
39
  ERR
36
40
 
37
41
  class << self
42
+ extend T::Sig
43
+
44
+ sig { returns(ActiveSupport::TimeZone) }
38
45
  def time_zone
39
46
  @time_zone ||=
40
47
  begin
@@ -1,44 +1,63 @@
1
+ # typed: true
2
+
1
3
  require "que"
4
+ require "sorbet-runtime"
2
5
 
3
6
  # This module uses polymorphic dispatch to centralise the differences between supporting Que::Job
4
7
  # and other job systems.
5
8
  module Que
6
9
  module Scheduler
7
10
  class ToEnqueue < Hashie::Dash
11
+ extend T::Sig
12
+ extend T::Helpers
13
+
14
+ abstract!
15
+
8
16
  property :args, required: true, default: []
9
17
  property :queue
10
18
  property :priority
11
19
  property :run_at, required: true
12
20
  property :job_class, required: true
13
21
 
22
+ sig { abstract.returns(T.nilable(EnqueuedJobType)) }
23
+ def enqueue; end
24
+
14
25
  class << self
26
+ extend T::Sig
27
+
28
+ sig { params(options: T::Hash[Symbol, T.anything]).returns(ToEnqueue) }
15
29
  def create(options)
16
- type_from_job_class(options.fetch(:job_class)).new(
17
- options.merge(run_at: Que::Scheduler::Db.now)
30
+ T.must(type_from_job_class(T.cast(options.fetch(:job_class), Class))).new(
31
+ options.merge(run_at: options.fetch(:run_at) { Que::Scheduler::Db.now })
18
32
  )
19
33
  end
20
34
 
35
+ sig { params(job_class: Class).returns(T::Boolean) }
21
36
  def valid_job_class?(job_class)
22
37
  type_from_job_class(job_class).present?
23
38
  end
24
39
 
40
+ sig { returns(T::Boolean) }
25
41
  def active_job_defined?
26
42
  Object.const_defined?(:ActiveJob)
27
43
  end
28
44
 
45
+ sig { returns(T.nilable(Gem::Version)) }
29
46
  def active_job_version
30
47
  Gem.loaded_specs["activejob"]&.version
31
48
  end
32
49
 
50
+ sig { returns(T::Boolean) }
33
51
  def active_job_version_supports_queues?
34
52
  # Supporting queue name in ActiveJob was removed in Rails 4.2.3
35
53
  # https://github.com/rails/rails/pull/19498
36
54
  # and readded in Rails 6.0.3
37
55
  # https://github.com/rails/rails/pull/38635
38
- ToEnqueue.active_job_version && ToEnqueue.active_job_version >=
39
- Gem::Version.create("6.0.3")
56
+ version = ToEnqueue.active_job_version
57
+ !version.nil? && version >= Gem::Version.create("6.0.3")
40
58
  end
41
59
 
60
+ sig { params(job_class: Class).returns(T.nilable(T.class_of(ToEnqueue))) }
42
61
  private def type_from_job_class(job_class)
43
62
  types.each do |type, implementation|
44
63
  return implementation if job_class < type
@@ -46,6 +65,7 @@ module Que
46
65
  nil
47
66
  end
48
67
 
68
+ sig { returns(T::Hash[Class, T.class_of(ToEnqueue)]) }
49
69
  private def types
50
70
  @types ||=
51
71
  begin
@@ -61,10 +81,11 @@ module Que
61
81
 
62
82
  # For jobs of type Que::Job
63
83
  class QueJobType < ToEnqueue
84
+ sig { override.returns(T.nilable(EnqueuedJobType)) }
64
85
  def enqueue
65
86
  job_settings = to_h.slice(:queue, :priority, :run_at).compact
66
87
  job = Que::Scheduler::DbSupport.enqueue_a_job(
67
- job_class,
88
+ T.cast(job_class, T.class_of(Que::Job)),
68
89
  job_settings,
69
90
  args
70
91
  )
@@ -82,6 +103,7 @@ module Que
82
103
 
83
104
  # For jobs of type ActiveJob
84
105
  class ActiveJobType < ToEnqueue
106
+ sig { override.returns(T.nilable(EnqueuedJobType)) }
85
107
  def enqueue
86
108
  job = enqueue_active_job
87
109
 
@@ -91,6 +113,7 @@ module Que
91
113
  EnqueuedJobType.new(enqueued_values)
92
114
  end
93
115
 
116
+ sig { params(job: ::ActiveJob::Base).returns(T::Hash[Symbol, T.anything]) }
94
117
  def calculate_enqueued_values(job)
95
118
  # Now read the just inserted job back out of the DB to get the actual values that will
96
119
  # be used when the job is worked.
@@ -117,6 +140,7 @@ module Que
117
140
  }
118
141
  end
119
142
 
143
+ sig { returns(T.nilable(T.any(::ActiveJob::Base, FalseClass))) }
120
144
  def enqueue_active_job
121
145
  job_settings = {
122
146
  priority: priority,
@@ -124,22 +148,29 @@ module Que
124
148
  queue: queue || Que::DEFAULT_QUEUE,
125
149
  }.compact
126
150
 
127
- job_class_set = job_class.set(**job_settings)
128
- if args.is_a?(Hash)
129
- job_class_set.perform_later(**args)
151
+ job_class_set = T.cast(job_class, T.class_of(::ActiveJob::Base)).set(**job_settings)
152
+ job_args = args
153
+ case job_args
154
+ when Hash
155
+ job_class_set.perform_later(**job_args)
130
156
  else
131
- job_class_set.perform_later(*args)
157
+ # Sorbet cannot type a splat of a value of unknown size
158
+ T.unsafe(job_class_set).perform_later(*job_args)
132
159
  end
133
160
  end
134
161
 
135
162
  class << self
163
+ extend T::Sig
164
+
136
165
  # ActiveJob scheduled_at is returned as a float, or a string post Rails 7.1,
137
166
  # and we want a Time for consistency
167
+ sig { params(scheduled_at: T.anything).returns(T.nilable(ActiveSupport::TimeWithZone)) }
138
168
  def extract_scheduled_at(scheduled_at)
139
169
  # rubocop:disable Style/EmptyElse
140
- if scheduled_at.is_a?(Float)
170
+ case scheduled_at
171
+ when Float
141
172
  Que::Scheduler::TimeZone.time_zone.at(scheduled_at)
142
- elsif scheduled_at.is_a?(String)
173
+ when String
143
174
  Que::Scheduler::TimeZone.time_zone.parse(scheduled_at)
144
175
  else
145
176
  nil
@@ -0,0 +1,18 @@
1
+ # typed: true
2
+
3
+ require "sorbet-runtime"
4
+ require "active_support"
5
+ require "active_support/time_with_zone"
6
+
7
+ module Que
8
+ module Scheduler
9
+ module Types
10
+ # Times come from both Postgres (Time) and the configured zone (TimeWithZone)
11
+ TIME = T.type_alias { T.any(Time, ActiveSupport::TimeWithZone) }
12
+ # A schedule as given in config: job name to its options
13
+ SCHEDULE_CONFIG = T.type_alias do
14
+ T::Hash[T.any(String, Symbol), T::Hash[T.any(String, Symbol), T.anything]]
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,7 @@
1
+ # typed: true
2
+
1
3
  module Que
2
4
  module Scheduler
3
- VERSION = "7.0.1".freeze
5
+ VERSION = "7.2.0".freeze
4
6
  end
5
7
  end
data/lib/que/scheduler.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # typed: true
2
+
1
3
  require "que/scheduler/version"
2
4
  require "que/scheduler/db_support"
3
5
  require "que/scheduler/configuration"
data/lib/que-scheduler.rb CHANGED
@@ -1 +1,3 @@
1
+ # typed: true
2
+
1
3
  require "que/scheduler"
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: 7.0.1
4
+ version: 7.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Lascelles
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-21 00:00:00.000000000 Z
11
+ date: 2026-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -84,6 +84,20 @@ dependencies:
84
84
  - - "<"
85
85
  - !ruby/object:Gem::Version
86
86
  version: '3.0'
87
+ - !ruby/object:Gem::Dependency
88
+ name: sorbet-runtime
89
+ requirement: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0.5'
94
+ type: :runtime
95
+ prerelease: false
96
+ version_requirements: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0.5'
87
101
  - !ruby/object:Gem::Dependency
88
102
  name: activerecord
89
103
  requirement: !ruby/object:Gem::Requirement
@@ -452,6 +466,7 @@ files:
452
466
  - lib/que/scheduler/state_checks.rb
453
467
  - lib/que/scheduler/time_zone.rb
454
468
  - lib/que/scheduler/to_enqueue.rb
469
+ - lib/que/scheduler/types.rb
455
470
  - lib/que/scheduler/version.rb
456
471
  homepage: https://github.com/hlascelles/que-scheduler
457
472
  licenses: