que-scheduler 0.7.0 → 0.8.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 +9 -3
- data/lib/que/scheduler/defined_job.rb +3 -2
- data/lib/que/scheduler/enqueueing_calculator.rb +76 -0
- data/lib/que/scheduler/schedule_parser.rb +24 -61
- data/lib/que/scheduler/scheduler_job.rb +43 -66
- data/lib/que/scheduler/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38355bcd24a4b7a7ec92449a833bcbf4977c0504
|
4
|
+
data.tar.gz: b01c3e96f925e9cceab9e3e1f8a6cac606fa8e25
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fb6dcde2cfd75d95c90dab1c5d72d678196ce6f5d288dbe9b86c45dbdfa78e6522c031e36959c5856399699c0327753d523e2de2938467695a367b50f8bbea18
|
7
|
+
data.tar.gz: 07e5303c3a9e0b41f9e8742c83e9bc9e68932875e0d70894679c343b8b342efa152d148b5c8398b0b68866fcd1c27e18576001f8b18f75e4e9b10d14ac580a0d
|
data/README.md
CHANGED
@@ -50,17 +50,17 @@ queue_documents_for_indexing:
|
|
50
50
|
cron: "0 0 * * *"
|
51
51
|
class: QueueDocuments
|
52
52
|
|
53
|
-
# Specify job
|
53
|
+
# Specify the job queue
|
54
54
|
ReportOrders:
|
55
55
|
cron: "0 0 * * *"
|
56
56
|
queue: reporting
|
57
57
|
|
58
|
-
# Specify job priority using Que's number system
|
58
|
+
# Specify the job priority using Que's number system
|
59
59
|
BatchOrders:
|
60
60
|
cron: "0 0 * * *"
|
61
61
|
priority: 25
|
62
62
|
|
63
|
-
# Specify job
|
63
|
+
# Specify job arguments
|
64
64
|
SendOrders:
|
65
65
|
cron: "0 0 * * *"
|
66
66
|
args: ['open']
|
@@ -95,6 +95,12 @@ are no HA concerns to worry about and no namespace collisions between different
|
|
95
95
|
|
96
96
|
Additionally, like Que, when your database is backed up, your scheduling state is stored too.
|
97
97
|
|
98
|
+
## Multiple scheduler detection
|
99
|
+
|
100
|
+
No matter how many tasks you have defined in your config, you will only ever need one que-scheduler
|
101
|
+
job enqueued. que-scheduler knows this, and it will check before performing any operations that
|
102
|
+
there is only one of itself present.
|
103
|
+
|
98
104
|
## How it works
|
99
105
|
|
100
106
|
que-scheduler is a job that reads a config file, enqueues any jobs it determines that need to be run,
|
@@ -1,14 +1,15 @@
|
|
1
1
|
require 'hashie'
|
2
2
|
require 'fugit'
|
3
3
|
|
4
|
-
# This is the definition of one
|
4
|
+
# This is the definition of one scheduleable job in the que-scheduler config yml file.
|
5
5
|
module Que
|
6
6
|
module Scheduler
|
7
7
|
class DefinedJob < Hashie::Dash
|
8
8
|
include Hashie::Extensions::Dash::PropertyTranslation
|
9
9
|
|
10
10
|
def self.err_field(f, v)
|
11
|
-
suffix =
|
11
|
+
suffix = 'in que-scheduler config ' \
|
12
|
+
"#{Que::Scheduler::ScheduleParser::QUE_SCHEDULER_CONFIG_LOCATION}"
|
12
13
|
raise "Invalid #{f} '#{v}' #{suffix}"
|
13
14
|
end
|
14
15
|
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'fugit'
|
2
|
+
require 'backports/2.4.0/hash/compact'
|
3
|
+
|
4
|
+
module Que
|
5
|
+
module Scheduler
|
6
|
+
EnqueueingCalculatorResult = Struct.new(:missed_jobs, :schedule_dictionary)
|
7
|
+
|
8
|
+
class EnqueueingCalculator
|
9
|
+
class << self
|
10
|
+
def parse(scheduler_config, scheduler_job_args)
|
11
|
+
missed_jobs = {}
|
12
|
+
schedule_dictionary = []
|
13
|
+
|
14
|
+
# For each scheduled item, we need not schedule a job it if it has no history, as it is
|
15
|
+
# new. Otherwise, check how many times we have missed the job since the last run time.
|
16
|
+
# If it is "unmissable" then we schedule all of them, with the missed time as an arg,
|
17
|
+
# otherwise just schedule it once.
|
18
|
+
scheduler_config.each do |desc|
|
19
|
+
job_name = desc.name
|
20
|
+
schedule_dictionary << job_name
|
21
|
+
|
22
|
+
# If we have never seen this job before, we don't want to scheduled any jobs for it.
|
23
|
+
# But we have added it to the dictionary, so it will be used to enqueue jobs next time.
|
24
|
+
next unless scheduler_job_args.job_dictionary.include?(job_name)
|
25
|
+
|
26
|
+
# This has been seen before. We should check if we have missed any executions.
|
27
|
+
missed = calculate_missed_runs(
|
28
|
+
desc, scheduler_job_args.last_run_time, scheduler_job_args.as_time
|
29
|
+
)
|
30
|
+
missed_jobs[desc.job_class] = missed unless missed.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
EnqueueingCalculatorResult.new(missed_jobs, schedule_dictionary)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Given a job description, the last scheduler run time, and this run time, return all
|
39
|
+
# the instances that should be enqueued for that job class.
|
40
|
+
def calculate_missed_runs(desc, last_run_time, as_time)
|
41
|
+
missed_times = []
|
42
|
+
last_time = last_run_time
|
43
|
+
while (next_run = desc.next_run_time(last_time, as_time))
|
44
|
+
missed_times << next_run
|
45
|
+
last_time = next_run
|
46
|
+
end
|
47
|
+
|
48
|
+
generate_required_jobs_list(desc, missed_times)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Given a job description, and the timestamps of the missed events, generate a list of jobs
|
52
|
+
# that can be enqueued as an array of arrays of args.
|
53
|
+
def generate_required_jobs_list(desc, missed_times)
|
54
|
+
jobs_for_class = []
|
55
|
+
|
56
|
+
unless missed_times.empty?
|
57
|
+
options = {
|
58
|
+
args: desc.args,
|
59
|
+
queue: desc.queue,
|
60
|
+
priority: desc.priority
|
61
|
+
}.compact
|
62
|
+
|
63
|
+
if desc.unmissable
|
64
|
+
missed_times.each do |time_missed|
|
65
|
+
jobs_for_class << options.merge(args: [time_missed] + (desc.args || []))
|
66
|
+
end
|
67
|
+
else
|
68
|
+
jobs_for_class << options
|
69
|
+
end
|
70
|
+
end
|
71
|
+
jobs_for_class
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,74 +1,37 @@
|
|
1
|
-
require '
|
1
|
+
require 'yaml'
|
2
2
|
require 'backports/2.4.0/hash/compact'
|
3
3
|
|
4
|
+
require_relative 'defined_job'
|
5
|
+
|
4
6
|
module Que
|
5
7
|
module Scheduler
|
6
|
-
|
8
|
+
module ScheduleParser
|
9
|
+
QUE_SCHEDULER_CONFIG_LOCATION =
|
10
|
+
ENV.fetch('QUE_SCHEDULER_CONFIG_LOCATION', 'config/que_schedule.yml')
|
7
11
|
|
8
|
-
class ScheduleParser
|
9
12
|
class << self
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
# For each scheduled item, we need not schedule a job it if it has no history, as it is
|
15
|
-
# new. Otherwise, check how many times we have missed the job since the last run time.
|
16
|
-
# If it is "unmissable" then we schedule all of them, with the missed time as an arg,
|
17
|
-
# otherwise just schedule it once.
|
18
|
-
scheduler_config.each do |desc|
|
19
|
-
job_name = desc.name
|
20
|
-
schedule_dictionary << job_name
|
21
|
-
|
22
|
-
# If we have never seen this job before, we don't want to scheduled any jobs for it.
|
23
|
-
# But we have added it to the dictionary, so it will be used to enqueue jobs next time.
|
24
|
-
next unless scheduler_job_args.job_dictionary.include?(job_name)
|
25
|
-
|
26
|
-
# This has been seen before. We should check if we have missed any executions.
|
27
|
-
missed = calculate_missed_runs(
|
28
|
-
desc, scheduler_job_args.last_run_time, scheduler_job_args.as_time
|
29
|
-
)
|
30
|
-
missed_jobs[desc.job_class] = missed unless missed.empty?
|
31
|
-
end
|
32
|
-
|
33
|
-
ScheduleParserResult.new(missed_jobs, schedule_dictionary)
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
# Given a job description, the last scheduler run time, and this run time, return all
|
39
|
-
# the instances that should be enqueued for that job class.
|
40
|
-
def calculate_missed_runs(desc, last_run_time, as_time)
|
41
|
-
missed_times = []
|
42
|
-
last_time = last_run_time
|
43
|
-
while (next_run = desc.next_run_time(last_time, as_time))
|
44
|
-
missed_times << next_run
|
45
|
-
last_time = next_run
|
13
|
+
def scheduler_config
|
14
|
+
@scheduler_config ||= begin
|
15
|
+
jobs_list(YAML.load_file(QUE_SCHEDULER_CONFIG_LOCATION))
|
46
16
|
end
|
47
|
-
|
48
|
-
generate_required_jobs_list(desc, missed_times)
|
49
17
|
end
|
50
18
|
|
51
|
-
#
|
52
|
-
#
|
53
|
-
def
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
67
|
-
else
|
68
|
-
jobs_for_class << options
|
69
|
-
end
|
19
|
+
# Convert the config hash into a list of real classes and args, parsing the cron and
|
20
|
+
# "unmissable" parameters.
|
21
|
+
def jobs_list(schedule)
|
22
|
+
schedule.map do |k, v|
|
23
|
+
Que::Scheduler::DefinedJob.new(
|
24
|
+
{
|
25
|
+
name: k,
|
26
|
+
job_class: Object.const_get(v['class'] || k),
|
27
|
+
queue: v['queue'],
|
28
|
+
args: v['args'],
|
29
|
+
priority: v['priority'],
|
30
|
+
cron: v['cron'],
|
31
|
+
unmissable: v['unmissable']
|
32
|
+
}.compact
|
33
|
+
)
|
70
34
|
end
|
71
|
-
jobs_for_class
|
72
35
|
end
|
73
36
|
end
|
74
37
|
end
|
@@ -1,17 +1,13 @@
|
|
1
1
|
require 'que'
|
2
|
-
require 'yaml'
|
3
|
-
require 'backports/2.4.0/hash/compact'
|
4
2
|
|
5
|
-
require_relative 'defined_job'
|
6
3
|
require_relative 'schedule_parser'
|
4
|
+
require_relative 'enqueueing_calculator'
|
7
5
|
require_relative 'scheduler_job_args'
|
8
6
|
|
9
7
|
module Que
|
10
8
|
module Scheduler
|
11
|
-
QUE_SCHEDULER_CONFIG_LOCATION =
|
12
|
-
ENV.fetch('QUE_SCHEDULER_CONFIG_LOCATION', 'config/que_schedule.yml')
|
13
|
-
|
14
9
|
class SchedulerJob < Que::Job
|
10
|
+
SCHEDULER_COUNT_SQL = "SELECT COUNT(*) FROM que_jobs WHERE job_class = '#{name}'".freeze
|
15
11
|
SCHEDULER_FREQUENCY = 60
|
16
12
|
|
17
13
|
# Highest possible priority.
|
@@ -22,16 +18,17 @@ module Que
|
|
22
18
|
options = { last_run_time: options, job_dictionary: oldarg } if oldarg.present?
|
23
19
|
|
24
20
|
::ActiveRecord::Base.transaction do
|
21
|
+
assert_one_scheduler_job
|
25
22
|
scheduler_job_args = SchedulerJobArgs.prepare_scheduler_job_args(options)
|
26
23
|
logs = ["que-scheduler last ran at #{scheduler_job_args.last_run_time}."]
|
27
24
|
|
28
25
|
# It's possible one worker node has severe clock skew, and reports a time earlier than
|
29
26
|
# the last run. If so, log, and rescheduled with the same last run at.
|
30
27
|
if scheduler_job_args.as_time < scheduler_job_args.last_run_time
|
31
|
-
|
28
|
+
handle_clock_skew(scheduler_job_args, logs)
|
32
29
|
else
|
33
30
|
# Otherwise, run as normal
|
34
|
-
|
31
|
+
handle_normal_call(scheduler_job_args, logs)
|
35
32
|
end
|
36
33
|
|
37
34
|
# Only now we're sure nothing errored, log the results
|
@@ -42,70 +39,50 @@ module Que
|
|
42
39
|
|
43
40
|
private
|
44
41
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
end
|
51
|
-
|
52
|
-
# Convert the config hash into a list of real classes and args, parsing the cron and
|
53
|
-
# "unmissable" parameters.
|
54
|
-
def jobs_list(schedule)
|
55
|
-
schedule.map do |k, v|
|
56
|
-
Que::Scheduler::DefinedJob.new(
|
57
|
-
{
|
58
|
-
name: k,
|
59
|
-
job_class: Object.const_get(v['class'] || k),
|
60
|
-
queue: v['queue'],
|
61
|
-
args: v['args'],
|
62
|
-
priority: v['priority'],
|
63
|
-
cron: v['cron'],
|
64
|
-
unmissable: v['unmissable']
|
65
|
-
}.compact
|
66
|
-
)
|
67
|
-
end
|
68
|
-
end
|
42
|
+
def assert_one_scheduler_job
|
43
|
+
schedulers = ActiveRecord::Base.connection.execute(SCHEDULER_COUNT_SQL).first['count'].to_i
|
44
|
+
return if schedulers == 1
|
45
|
+
raise "Only one #{self.class.name} should be enqueued. #{schedulers} were found."
|
46
|
+
end
|
69
47
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
48
|
+
def handle_normal_call(scheduler_job_args, logs)
|
49
|
+
result = enqueue_required_jobs(scheduler_job_args, logs)
|
50
|
+
enqueue_self_again(
|
51
|
+
scheduler_job_args.as_time,
|
52
|
+
scheduler_job_args.as_time,
|
53
|
+
result.schedule_dictionary
|
54
|
+
)
|
55
|
+
end
|
78
56
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
57
|
+
def enqueue_required_jobs(scheduler_job_args, logs)
|
58
|
+
# Obtain the hash of missed jobs. Keys are the job classes, and the values are arrays
|
59
|
+
# each containing more arrays for the arguments of that instance.
|
60
|
+
result = EnqueueingCalculator.parse(ScheduleParser.scheduler_config, scheduler_job_args)
|
61
|
+
result.missed_jobs.each do |job_class, args_arrays|
|
62
|
+
args_arrays.each do |args|
|
63
|
+
logs << "que-scheduler enqueueing #{job_class} with args: #{args}"
|
64
|
+
job_class.enqueue(*args)
|
88
65
|
end
|
89
|
-
result
|
90
66
|
end
|
67
|
+
result
|
68
|
+
end
|
91
69
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
70
|
+
def handle_clock_skew(scheduler_job_args, logs)
|
71
|
+
logs << 'que-scheduler detected worker with time older than last run. ' \
|
72
|
+
'Rescheduling without enqueueing jobs.'
|
73
|
+
enqueue_self_again(
|
74
|
+
scheduler_job_args.last_run_time,
|
75
|
+
scheduler_job_args.as_time,
|
76
|
+
scheduler_job_args.job_dictionary
|
77
|
+
)
|
78
|
+
end
|
99
79
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
scheduler_job_args.job_dictionary
|
107
|
-
)
|
108
|
-
end
|
80
|
+
def enqueue_self_again(last_full_execution, this_run_time, new_job_dictionary)
|
81
|
+
SchedulerJob.enqueue(
|
82
|
+
last_run_time: last_full_execution.iso8601,
|
83
|
+
job_dictionary: new_job_dictionary,
|
84
|
+
run_at: this_run_time.beginning_of_minute + SCHEDULER_FREQUENCY
|
85
|
+
)
|
109
86
|
end
|
110
87
|
end
|
111
88
|
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: 0.
|
4
|
+
version: 0.8.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: 2017-11-
|
11
|
+
date: 2017-11-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -90,6 +90,7 @@ files:
|
|
90
90
|
- README.md
|
91
91
|
- lib/que/scheduler.rb
|
92
92
|
- lib/que/scheduler/defined_job.rb
|
93
|
+
- lib/que/scheduler/enqueueing_calculator.rb
|
93
94
|
- lib/que/scheduler/schedule_parser.rb
|
94
95
|
- lib/que/scheduler/scheduler_job.rb
|
95
96
|
- lib/que/scheduler/scheduler_job_args.rb
|