que-scheduler 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +90 -0
- data/lib/que/scheduler.rb +2 -0
- data/lib/que/scheduler/schedule_parser.rb +64 -0
- data/lib/que/scheduler/scheduler_job.rb +53 -0
- data/lib/que/scheduler/version.rb +5 -0
- metadata +203 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 34c186335cf0386ab154de58d4ca489dbb186ce5
|
4
|
+
data.tar.gz: 8dea367b4b83245873e6657ec4191fa778d4698b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e5fa939bcc60786ac68de29c384165a7cdb21a054095e185f748bbd2732e24fd70d2f9ae49ee1b20251034600268567610df7e148867ec5e47fce84dbc69efd0
|
7
|
+
data.tar.gz: bc60257fed3c568e362a030277f32a033a36cbdb1a4cf7b1ab3b1e7307d49935d0d25615b0ad215b4edf4633805e3b65e7a5d78d1118dcc48fdf7ddeffa41e46
|
data/README.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
que-scheduler
|
2
|
+
================
|
3
|
+
|
4
|
+
[![Build Status](https://travis-ci.org/resque/que-scheduler.svg?branch=master)](https://travis-ci.org/hlascelles/que-scheduler)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/hlascelles/sque-scheduler/badges/gpa.svg)](https://codeclimate.com/github/hlascelles/que-scheduler)
|
6
|
+
|
7
|
+
### Description
|
8
|
+
|
9
|
+
que-scheduler is an extension to [Que](https://github.com/chanks/que) that adds support for scheduling
|
10
|
+
items using a cron style configuration file. It works by running as a que job itself, determining what
|
11
|
+
needs to be run, enqueueing those jobs, then enqueueing itself to check again later.
|
12
|
+
|
13
|
+
### Installation
|
14
|
+
|
15
|
+
To install, add the gem to your Gemfile:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'que-scheduler'
|
19
|
+
```
|
20
|
+
|
21
|
+
You will need to specify a schedule config (see below). The default location that que-scheduler will
|
22
|
+
look for it is `config/que_schedule.yml`
|
23
|
+
|
24
|
+
Finally, add a migration to start the job scheduler.
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
Que::Scheduler::SchedulerJob.enqueue
|
28
|
+
```
|
29
|
+
|
30
|
+
### Environment Variables
|
31
|
+
|
32
|
+
You can configure some aspects of the gem with environment variables.
|
33
|
+
|
34
|
+
* `QUE_SCHEDULER_CONFIG_LOCATION` - The location of the schedule configuration (default config/que_schedule.yml)
|
35
|
+
|
36
|
+
#### Schedule configuration
|
37
|
+
|
38
|
+
The schedule file is a list of que job classes with arguments and a schedule frequency (in crontab
|
39
|
+
syntax). The format is a superset of the resque-scheduler config format, so it they can be used
|
40
|
+
as-is with no modification, assuming the job classes are migrated from Resque to Que.
|
41
|
+
|
42
|
+
It has one additional feature, `unmissable: true`. This is set on a job that must be run for every
|
43
|
+
single matching cron time that goes by, even if the system is offline over more than one match. To better process these unmissable jobs, they are always enqueued with the first
|
44
|
+
argument being the time that they were supposed to be processed.
|
45
|
+
|
46
|
+
For example:
|
47
|
+
|
48
|
+
```yaml
|
49
|
+
CancelAbandonedOrders:
|
50
|
+
cron: "*/5 * * * *"
|
51
|
+
|
52
|
+
queue_documents_for_indexing:
|
53
|
+
cron: "0 0 * * *"
|
54
|
+
# By default the job name (hash key) will be taken as worker class name.
|
55
|
+
# If you want to have a different job name and class name, provide the 'class' option
|
56
|
+
class: "QueueDocuments"
|
57
|
+
queue: high
|
58
|
+
args:
|
59
|
+
|
60
|
+
clear_leaderboards_contributors:
|
61
|
+
cron: "30 6 * * 1"
|
62
|
+
class: "ClearLeaderboards"
|
63
|
+
queue: low
|
64
|
+
args: contributors
|
65
|
+
|
66
|
+
DailyBatchReport:
|
67
|
+
cron: "0 3 * * *"
|
68
|
+
# This job will be run every day, and if workers are offline for several days, then the backlog
|
69
|
+
# will all be scheduled when they are restored, each with that events timestamp.
|
70
|
+
unmissable: true
|
71
|
+
```
|
72
|
+
|
73
|
+
### Redundancy and Fail-Over
|
74
|
+
|
75
|
+
Because of the way que-scheduler works, it requires no additional processes. It is, itself, a Que job.
|
76
|
+
As long as there are Que workers functioning, then jobs will continue to be scheduled correctly.
|
77
|
+
|
78
|
+
### How it works
|
79
|
+
|
80
|
+
que-scheduler is a job that reads a config file, then schedules itself endlessly on a delay, enqueueing
|
81
|
+
any jobs it determines that need to be run. The flow is as follows
|
82
|
+
|
83
|
+
1. The job for the very first time.
|
84
|
+
1. que-scheduler loads the config file, and notices it is new. It will not schedule any other jobs, except itself.
|
85
|
+
1. Some time later it runs again. It knows what jobs it should be monitoring, and notices that some have are due. It enqueues those jobs and itself.
|
86
|
+
1. After a deploy that changes the config, it notices a new job to monitor, and one to forget.
|
87
|
+
|
88
|
+
### Thanks
|
89
|
+
|
90
|
+
This gem was inspired by the makers of the excellent [Que](https://github.com/chanks/que) job scheduler gem.
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'fugit'
|
2
|
+
|
3
|
+
module Que
|
4
|
+
module Scheduler
|
5
|
+
ScheduleParserResult = Struct.new(:missed_jobs, :schedule_dictionary, :seconds_until_next_job)
|
6
|
+
|
7
|
+
class ScheduleParser
|
8
|
+
SCHEDULER_FREQUENCY = 60
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def parse(jobs_list, as_time, last_time, known_jobs)
|
12
|
+
missed_jobs = {}
|
13
|
+
schedule_dictionary = []
|
14
|
+
|
15
|
+
# For each scheduled item, we need not schedule a job it if it has no history, as it is
|
16
|
+
# new. Otherwise, check how many times we have missed the job since the last run time.
|
17
|
+
# If it is "unmissable" then we schedule all of them, with the missed time as an arg,
|
18
|
+
# otherwise just schedule it once.
|
19
|
+
jobs_list.each do |desc|
|
20
|
+
schedule_dictionary << desc[:name]
|
21
|
+
|
22
|
+
next unless known_jobs.include?(desc[:name])
|
23
|
+
# This has been seen before. We should check if we have missed any executions.
|
24
|
+
missed = calculate_missed_runs(desc, last_time, as_time)
|
25
|
+
missed_jobs[desc[:clazz]] = missed unless missed.empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
seconds_until_next_job = SCHEDULER_FREQUENCY # TODO: make it 1 sec after next known run
|
29
|
+
ScheduleParserResult.new(missed_jobs, schedule_dictionary, seconds_until_next_job)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def calculate_missed_runs(desc, last_scheduler_run_time, as_time)
|
35
|
+
jobs_for_class = []
|
36
|
+
missed_times = []
|
37
|
+
last_time = last_scheduler_run_time
|
38
|
+
while (next_run = next_run_time(desc[:cron], last_time, as_time))
|
39
|
+
missed_times << next_run
|
40
|
+
last_time = next_run
|
41
|
+
end
|
42
|
+
|
43
|
+
unless missed_times.empty?
|
44
|
+
if desc[:unmissable]
|
45
|
+
missed_times.each do |time_missed|
|
46
|
+
jobs_for_class << [time_missed] + desc[:args]
|
47
|
+
end
|
48
|
+
else
|
49
|
+
jobs_for_class << desc[:args]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
jobs_for_class
|
53
|
+
end
|
54
|
+
|
55
|
+
def next_run_time(cron, from, to)
|
56
|
+
fugit_cron = Fugit::Cron.parse(cron)
|
57
|
+
next_time = fugit_cron.next_time(from)
|
58
|
+
next_run = next_time.to_local_time
|
59
|
+
next_run <= to ? next_run : nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'que'
|
2
|
+
require 'yaml'
|
3
|
+
require_relative 'schedule_parser'
|
4
|
+
|
5
|
+
module Que
|
6
|
+
module Scheduler
|
7
|
+
class SchedulerJob < Que::Job
|
8
|
+
# Highest possible priority.
|
9
|
+
@priority = 0
|
10
|
+
|
11
|
+
def run(last_time = nil, known_jobs = [])
|
12
|
+
::ActiveRecord::Base.transaction do
|
13
|
+
last_time = last_time.nil? ? Time.now : Time.parse(last_time)
|
14
|
+
as_time = Time.now
|
15
|
+
|
16
|
+
result =
|
17
|
+
ScheduleParser.parse(SchedulerJob.scheduler_config, as_time, last_time, known_jobs)
|
18
|
+
result.missed_jobs.each do |job_class, args_arrays|
|
19
|
+
args_arrays.each { |args| job_class.enqueue(*args) }
|
20
|
+
end
|
21
|
+
SchedulerJob.enqueue(as_time + result.seconds_until_next_job, result.schedule_dictionary)
|
22
|
+
destroy
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class << self
|
27
|
+
def scheduler_config
|
28
|
+
@scheduler_config ||= begin
|
29
|
+
location = ENV.fetch('QUE_SCHEDULER_CONFIG_LOCATION', 'config/que_schedule.yml')
|
30
|
+
jobs_list(YAML.load_file(location))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Convert the config hash into a list of real classes and args, parsing the cron and
|
35
|
+
# unmissable parameters.
|
36
|
+
def jobs_list(schedule)
|
37
|
+
schedule.map do |k, v|
|
38
|
+
clazz = Object.const_get(v['class'] || k)
|
39
|
+
args = v.key?('args') ? v.fetch('args') : []
|
40
|
+
unmissable = v['unmissable'] == true
|
41
|
+
{
|
42
|
+
name: k,
|
43
|
+
clazz: clazz,
|
44
|
+
args: args,
|
45
|
+
cron: v.fetch('cron'),
|
46
|
+
unmissable: unmissable
|
47
|
+
}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: que-scheduler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Harry Lascelles
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: fugit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: que
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.12'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: activesupport
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '4.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '4.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: bundler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.15'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.15'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry-byebug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: que-testing
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.1'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '10.0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '10.0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: rubocop
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.51'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.51'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: timecop
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.7'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.7'
|
167
|
+
description: A lightweight cron scheduler for the async job worker Que
|
168
|
+
email:
|
169
|
+
- harry@harrylascelles.com
|
170
|
+
executables: []
|
171
|
+
extensions: []
|
172
|
+
extra_rdoc_files: []
|
173
|
+
files:
|
174
|
+
- README.md
|
175
|
+
- lib/que/scheduler.rb
|
176
|
+
- lib/que/scheduler/schedule_parser.rb
|
177
|
+
- lib/que/scheduler/scheduler_job.rb
|
178
|
+
- lib/que/scheduler/version.rb
|
179
|
+
homepage: https://rubygems.org/gems/que-scheduler
|
180
|
+
licenses:
|
181
|
+
- MIT
|
182
|
+
metadata: {}
|
183
|
+
post_install_message:
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: '0'
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
197
|
+
requirements: []
|
198
|
+
rubyforge_project:
|
199
|
+
rubygems_version: 2.6.14
|
200
|
+
signing_key:
|
201
|
+
specification_version: 4
|
202
|
+
summary: A cron scheduler for Que
|
203
|
+
test_files: []
|