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 +4 -4
- data/README.md +3 -0
- data/lib/que/scheduler/audit.rb +12 -0
- data/lib/que/scheduler/configuration.rb +30 -6
- data/lib/que/scheduler/db.rb +20 -6
- data/lib/que/scheduler/db_support.rb +29 -4
- data/lib/que/scheduler/defined_job.rb +23 -6
- data/lib/que/scheduler/enqueueing_calculator.rb +11 -0
- data/lib/que/scheduler/jobs/que_scheduler_audit_clear_down_job.rb +10 -1
- data/lib/que/scheduler/migrations.rb +14 -1
- data/lib/que/scheduler/schedule.rb +22 -2
- data/lib/que/scheduler/scheduler_job.rb +29 -1
- data/lib/que/scheduler/scheduler_job_args.rb +6 -0
- data/lib/que/scheduler/state_checks.rb +7 -0
- data/lib/que/scheduler/time_zone.rb +7 -0
- data/lib/que/scheduler/to_enqueue.rb +42 -11
- data/lib/que/scheduler/types.rb +18 -0
- data/lib/que/scheduler/version.rb +3 -1
- data/lib/que/scheduler.rb +2 -0
- data/lib/que-scheduler.rb +2 -0
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bdd3f8eb6dccc6c82cb4ce97f0a64f7acf30394cdc4d7f7f299d86680209777a
|
|
4
|
+
data.tar.gz: ac8c0c66d2fb3def5887a6f7cc27b6577c28692b55549fd067e09ad6767fe68e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/que/scheduler/audit.rb
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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
|
-
|
|
14
|
-
|
|
32
|
+
extend T::Sig
|
|
33
|
+
|
|
34
|
+
sig { returns(Configuration) }
|
|
35
|
+
def configuration
|
|
36
|
+
@configuration ||= Configuration.new
|
|
37
|
+
end
|
|
15
38
|
|
|
16
|
-
|
|
17
|
-
|
|
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 =
|
data/lib/que/scheduler/db.rb
CHANGED
|
@@ -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
|
-
|
|
19
|
+
T.cast(first_value(SCHEDULER_COUNT_SQL), Integer)
|
|
15
20
|
end
|
|
16
21
|
|
|
22
|
+
sig { returns(Time) }
|
|
17
23
|
def now
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
40
|
+
case job_args
|
|
41
|
+
when Hash
|
|
21
42
|
clazz.enqueue(job_args, job_options: job_options)
|
|
22
43
|
else
|
|
23
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
+
case scheduled_at
|
|
171
|
+
when Float
|
|
141
172
|
Que::Scheduler::TimeZone.time_zone.at(scheduled_at)
|
|
142
|
-
|
|
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
|
data/lib/que/scheduler.rb
CHANGED
data/lib/que-scheduler.rb
CHANGED
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
|
|
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-
|
|
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:
|