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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a0e9f48536b268d1387f247f8a7ee52a55dbfef5
4
- data.tar.gz: 148ab173c0eb5c22a6cfd6bf83fb7e846570c705
3
+ metadata.gz: 38355bcd24a4b7a7ec92449a833bcbf4977c0504
4
+ data.tar.gz: b01c3e96f925e9cceab9e3e1f8a6cac606fa8e25
5
5
  SHA512:
6
- metadata.gz: 508e5271f7f35b62b6704a4f7633d026e149ede22f7f6497af8296aed7fd5c1bfcd8d1fbdd6b4ce4ec03adc59dedae195500830453038bfe0f33fb77c99f49ec
7
- data.tar.gz: f18b35343f7a9075db5771b2c2f2917fb49df8efcd81fe055e5c048e2742d21623051291d81f5527af7b15664e178cd3eeac9516cc96d5c11060cf1e233e593f
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 queues
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 queues
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 scheduled job in the yml file.
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 = "in que-scheduler config #{QUE_SCHEDULER_CONFIG_LOCATION}"
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 'fugit'
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
- ScheduleParserResult = Struct.new(:missed_jobs, :schedule_dictionary)
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 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
- 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
- # 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
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
- SchedulerJob.handle_clock_skew(scheduler_job_args, logs)
28
+ handle_clock_skew(scheduler_job_args, logs)
32
29
  else
33
30
  # Otherwise, run as normal
34
- SchedulerJob.handle_normal_call(scheduler_job_args, logs)
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
- class << self
46
- def scheduler_config
47
- @scheduler_config ||= begin
48
- jobs_list(YAML.load_file(QUE_SCHEDULER_CONFIG_LOCATION))
49
- end
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
- def handle_normal_call(scheduler_job_args, logs)
71
- result = enqueue_required_jobs(scheduler_job_args, logs)
72
- enqueue_self_again(
73
- scheduler_job_args.as_time,
74
- scheduler_job_args.as_time,
75
- result.schedule_dictionary
76
- )
77
- end
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
- def enqueue_required_jobs(scheduler_job_args, logs)
80
- # Obtain the hash of missed jobs. Keys are the job classes, and the values are arrays
81
- # each containing more arrays for the arguments of that instance.
82
- result = ScheduleParser.parse(SchedulerJob.scheduler_config, scheduler_job_args)
83
- result.missed_jobs.each do |job_class, args_arrays|
84
- args_arrays.each do |args|
85
- logs << "que-scheduler enqueueing #{job_class} with options: #{args}"
86
- job_class.enqueue(*args)
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
- def enqueue_self_again(last_full_execution, this_run_time, new_job_dictionary)
93
- SchedulerJob.enqueue(
94
- last_run_time: last_full_execution.iso8601,
95
- job_dictionary: new_job_dictionary,
96
- run_at: this_run_time.beginning_of_minute + SCHEDULER_FREQUENCY
97
- )
98
- end
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
- def handle_clock_skew(scheduler_job_args, logs)
101
- logs << 'que-scheduler detected worker with time older than last run. ' \
102
- 'Rescheduling without enqueueing jobs.'
103
- enqueue_self_again(
104
- scheduler_job_args.last_run_time,
105
- scheduler_job_args.as_time,
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
@@ -1,5 +1,5 @@
1
1
  module Que
2
2
  module Scheduler
3
- VERSION = '0.7.0'.freeze
3
+ VERSION = '0.8.0'.freeze
4
4
  end
5
5
  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.7.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-05 00:00:00.000000000 Z
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