allora 0.0.1
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +184 -0
- data/Rakefile +1 -0
- data/allora.gemspec +27 -0
- data/lib/allora/backend/memory.rb +50 -0
- data/lib/allora/backend/redis.rb +126 -0
- data/lib/allora/backend.rb +52 -0
- data/lib/allora/cron_line.rb +244 -0
- data/lib/allora/job/cron_job.rb +46 -0
- data/lib/allora/job/every_job.rb +48 -0
- data/lib/allora/job.rb +57 -0
- data/lib/allora/scheduler.rb +130 -0
- data/lib/allora/version.rb +26 -0
- data/lib/allora.rb +57 -0
- metadata +79 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2012 Flippa.com Pty. Ltd.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Allora: A distributed cron daemon in ruby
|
|
2
|
+
|
|
3
|
+
Allora (*Italian: at that time*) allows you to run a cron scheduler (yes, the actual
|
|
4
|
+
scheduler) on multiple machines within a network, without the worry of multiple
|
|
5
|
+
machines processing the same job on the schedule.
|
|
6
|
+
|
|
7
|
+
A centralized backend is used (by default redis) in order to maintain a shared state.
|
|
8
|
+
Allora also provides a basic in-memory backend, which can be used to expressly allow
|
|
9
|
+
jobs to run on more than one machine (e.g. to perform some file cleanup operations
|
|
10
|
+
directly on the machine).
|
|
11
|
+
|
|
12
|
+
I am a firm believer in keeping it simple, so you'll find Allora weighs in at just
|
|
13
|
+
a couple of hundred SLOC and doesn't provide a bajillion features you aren't likely
|
|
14
|
+
to use.
|
|
15
|
+
|
|
16
|
+
Schedules are written in pure ruby, not YAML.
|
|
17
|
+
|
|
18
|
+
The scheduler process is reentrant. That is to say, if the scheduler is due to run
|
|
19
|
+
a job at midnight and the process is stopped at 23:59, then restarted at 00:01, the
|
|
20
|
+
midnight job will still run. Reentry is smart, however: it catches back up as soon
|
|
21
|
+
as it has processed any overdue jobs (so it doesn't get stuck in the past).
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Via rubygems:
|
|
26
|
+
|
|
27
|
+
gem install allora
|
|
28
|
+
|
|
29
|
+
## Creating a schedule
|
|
30
|
+
|
|
31
|
+
The schedule is a ruby file. You execute this ruby file to start the daemon running.
|
|
32
|
+
|
|
33
|
+
If you don't have ActiveSupport available, replace `1.hour`, for example with `3600`
|
|
34
|
+
(seconds).
|
|
35
|
+
|
|
36
|
+
Create a file, for example "schedule.rb":
|
|
37
|
+
|
|
38
|
+
Allora.start(:join => true) do |s|
|
|
39
|
+
# a job that runs hourly
|
|
40
|
+
s.add("empty_cache", :every => 1.hour) { `rm -f /path/to/cache/*` }
|
|
41
|
+
|
|
42
|
+
# a job that runs based on a cron string
|
|
43
|
+
s.add("update_stats", :cron => "0 2,14 * * *") { Resque.enqueue(UpdateStatsJob) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
When you run this file with ruby, it will remain in the foreground, providing log
|
|
47
|
+
output. It is *currently* your responsibility to daemonize the process.
|
|
48
|
+
|
|
49
|
+
Note that in the above example, we're only using the in-memory backend, so this
|
|
50
|
+
probably shouldn't be run on multiple machines.
|
|
51
|
+
|
|
52
|
+
In the following example, we specify to use a Redis backend, which is safe to run on
|
|
53
|
+
multiple machines:
|
|
54
|
+
|
|
55
|
+
Allora.start(:backend => :redis, :host => "redis.lan", :join => true) do |s|
|
|
56
|
+
# a job that runs hourly
|
|
57
|
+
s.add("empty_cache", :every => 1.hour) { `rm -f /path/to/cache/*` }
|
|
58
|
+
|
|
59
|
+
# a job that runs based on a cron string
|
|
60
|
+
s.add("update_stats", :cron => "0 2,14 * * *") { Resque.enqueue(UpdateStatsJob) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
We specify a redis host (and port) so that schedule data can be shared.
|
|
64
|
+
|
|
65
|
+
## Accessing your application environment
|
|
66
|
+
|
|
67
|
+
Allora will not make any assumptions about your application. It is your responsibility
|
|
68
|
+
to load it, if you need it. For Rails 3.x applications, add the following to the top
|
|
69
|
+
of your schedule:
|
|
70
|
+
|
|
71
|
+
require File.expand_path("../config/environment", __FILE__)
|
|
72
|
+
|
|
73
|
+
Assuming "../config/environment" resolves to the actual path where your environment.rb is
|
|
74
|
+
found.
|
|
75
|
+
|
|
76
|
+
## Implementation notes
|
|
77
|
+
|
|
78
|
+
Disclaimer: The scheduler is not intended to be 100% accurate. A job set to run every
|
|
79
|
+
second will probably run every second, but occasionally, if polling is slow, 2 seconds
|
|
80
|
+
may pass between runs. If this is a problem for your application, you should not use
|
|
81
|
+
this gem. The focus of this gem is to support running the scheduler on multiple machines.
|
|
82
|
+
|
|
83
|
+
In order to run the scheduler on more than one machine, Allora uses a `Backend` class to
|
|
84
|
+
maintain state. The timestamp at which a job should next run is kept in the backend.
|
|
85
|
+
When the scheduler polls, it asks the backend to return any jobs that can be run *and*
|
|
86
|
+
update the time at which they should next run. A locking strategy is used to ensure no
|
|
87
|
+
two machines update the schedule information at the same time.
|
|
88
|
+
|
|
89
|
+
In short, whichever running scheduler finds a job to do is the same scheduler the sets the
|
|
90
|
+
next time that job should run.
|
|
91
|
+
|
|
92
|
+
Jobs are executed in forked children, so that if they crash, the scheduler does not
|
|
93
|
+
exit.
|
|
94
|
+
|
|
95
|
+
## Custom Job classes
|
|
96
|
+
|
|
97
|
+
Allora offers two types of Job, which make sense for scheduling work at set intervals.
|
|
98
|
+
These are the `:every` and `:cron` types of job, which map to `Allora::Job::EveryJob` and
|
|
99
|
+
`Allora::Job::CronJob` internally. You may write your own subclass of `Allora::Job`, if
|
|
100
|
+
you have some specific need that is not met by either of these job types.
|
|
101
|
+
|
|
102
|
+
Job classes simply need to implement the `#next_at` method, which accepts a `Time` as
|
|
103
|
+
input and returns a time after that at which the job should run. `Allora::Job` will
|
|
104
|
+
handle the execution of the job itself.
|
|
105
|
+
|
|
106
|
+
Here's the implementation of the `EveryJob` class:
|
|
107
|
+
|
|
108
|
+
module Allora
|
|
109
|
+
class Job::EveryJob < Job
|
|
110
|
+
def initialize(n, &block)
|
|
111
|
+
@duration = n
|
|
112
|
+
|
|
113
|
+
super(&block)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def next_at(from_time)
|
|
117
|
+
from_time + @duration
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Quite simply it adds whatever the duration is to the given time.
|
|
123
|
+
|
|
124
|
+
To use your custom Job class, pass the instance to `Scheduler#add`:
|
|
125
|
+
|
|
126
|
+
s.add("foo", MyJob.new { puts "Running custom job" })
|
|
127
|
+
|
|
128
|
+
## Custom Backend classes
|
|
129
|
+
|
|
130
|
+
It is more likely that you will wish to write a custom backend, than a custom job. In
|
|
131
|
+
particular if you do not wish to use Redis, which is currently the only provided option.
|
|
132
|
+
|
|
133
|
+
Backend classes subclass `Allora::Backend` and implement `#reschedule`. The `#reschedule`
|
|
134
|
+
method accepts a Hash of jobs to check and does two things:
|
|
135
|
+
|
|
136
|
+
1. Returns a new Hash containing any jobs that can run now
|
|
137
|
+
2. Internally updates the time at which the job should next run
|
|
138
|
+
|
|
139
|
+
A locking strategy should be used in order to ensure the backend supports running on
|
|
140
|
+
multiple machines.
|
|
141
|
+
|
|
142
|
+
For the sake of clarity and brevity, here is a pseudo-code example:
|
|
143
|
+
|
|
144
|
+
class MyBackend < Allora::Backend
|
|
145
|
+
def reschedule(jobs)
|
|
146
|
+
now = Time.now
|
|
147
|
+
jobs.select do |name, job|
|
|
148
|
+
schedule_if_new(name, job.next_at(now))
|
|
149
|
+
lock_job(name) do # returns the result of the block only if successful
|
|
150
|
+
next_run = scheduled_time(name)
|
|
151
|
+
if next_run <= now
|
|
152
|
+
update_schedule(name, job.next_at(now))
|
|
153
|
+
true
|
|
154
|
+
else
|
|
155
|
+
false
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
The backend sets a new time into its internal schedule if none is present for that job.
|
|
163
|
+
|
|
164
|
+
It then tries to gain a lock on the schedule information for that job, returning false
|
|
165
|
+
if not possible (and thus not selecting the job from the input Hash).
|
|
166
|
+
|
|
167
|
+
If a lock was acquired, the time at which the job should run is checked. If it is in the
|
|
168
|
+
past, the scheule information is advanced to the next time at which the job should run and
|
|
169
|
+
the job is selected, else the job is not selected.
|
|
170
|
+
|
|
171
|
+
## Credits
|
|
172
|
+
|
|
173
|
+
Big thanks for jmettraux for rufus-scheduler, which I have borrowed the cron parsing logic
|
|
174
|
+
from.
|
|
175
|
+
|
|
176
|
+
## Disclaimer
|
|
177
|
+
|
|
178
|
+
Most of this work is the result of a quick code spike on a Sunday afternoon. There are no
|
|
179
|
+
specs right now. Use at your own risk. I will add specs in the next day or two, if you
|
|
180
|
+
prefer to wait.
|
|
181
|
+
|
|
182
|
+
## Copyright & License
|
|
183
|
+
|
|
184
|
+
Copyright © 2012 Flippa.com Pty. Ltd. See LICENSE file for details.
|
data/Rakefile
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
data/allora.gemspec
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
|
3
|
+
require "allora/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "allora"
|
|
7
|
+
s.version = Allora::VERSION
|
|
8
|
+
s.authors = ["d11wtq"]
|
|
9
|
+
s.email = ["chris@w3style.co.uk"]
|
|
10
|
+
s.homepage = "https://github.com/flippa/allora"
|
|
11
|
+
s.summary = %q{A ruby scheduler that keeps it simple, with support for distributed schedules}
|
|
12
|
+
s.description = %q{Allora (Italian for "at that time") provides a replacement for the classic UNIX
|
|
13
|
+
cron, using nothing but ruby. It is very small, easy to follow and relatively
|
|
14
|
+
feature-light. It does support a locking mechanism, backed by Redis, or any
|
|
15
|
+
other custom implementation, which makes it possible to run the scheduler on
|
|
16
|
+
more than one server, without worrying about jobs executing more than once per
|
|
17
|
+
scheduled time.}
|
|
18
|
+
|
|
19
|
+
s.rubyforge_project = "allora"
|
|
20
|
+
|
|
21
|
+
s.files = `git ls-files`.split("\n")
|
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
24
|
+
s.require_paths = ["lib"]
|
|
25
|
+
|
|
26
|
+
s.add_development_dependency "redis"
|
|
27
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
# A basic, single-process backend using a Hash.
|
|
26
|
+
#
|
|
27
|
+
# You should not run this on multiple machines on the same network.
|
|
28
|
+
class Backend::Memory < Backend
|
|
29
|
+
# Initialize a new Memory backend.
|
|
30
|
+
#
|
|
31
|
+
# @params [Hash] opts
|
|
32
|
+
# this backend does not accept any options
|
|
33
|
+
def initialize(opts = {})
|
|
34
|
+
super
|
|
35
|
+
|
|
36
|
+
@schedule = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reschedule(jobs)
|
|
40
|
+
current_time = Time.now
|
|
41
|
+
last_time = (@last_time ||= Time.now)
|
|
42
|
+
@last_time = current_time
|
|
43
|
+
|
|
44
|
+
jobs.select do |name, job|
|
|
45
|
+
@schedule[name] ||= job.next_at(last_time)
|
|
46
|
+
@schedule[name] < current_time && @schedule[name] = job.next_at(current_time)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
# A backend that uses Redis to maintain schedule state.
|
|
26
|
+
#
|
|
27
|
+
# When using this backend, it is possible to run the scheduler process
|
|
28
|
+
# on more than one machine in the network, connected to the same Redis
|
|
29
|
+
# instace. Whichever scheduler finds a runnable job first updates the
|
|
30
|
+
# 'next run time' information in Redis, using an optimistic locking
|
|
31
|
+
# strategy, then executes the job if the write succeeds. No two
|
|
32
|
+
# machines will ever run the same job twice.
|
|
33
|
+
class Backend::Redis < Backend
|
|
34
|
+
attr_reader :redis
|
|
35
|
+
attr_reader :prefix
|
|
36
|
+
|
|
37
|
+
# Initialize the Redis backed with the given options.
|
|
38
|
+
#
|
|
39
|
+
# Options:
|
|
40
|
+
# client: an already instantiated Redis client object.
|
|
41
|
+
# host: the hostname of a Redis server
|
|
42
|
+
# port: the port number of a Redis server
|
|
43
|
+
# prefix: a namespace prefix to use
|
|
44
|
+
#
|
|
45
|
+
# @param [Hash] opts
|
|
46
|
+
# options for the Redis backend
|
|
47
|
+
def initialize(opts = {})
|
|
48
|
+
@redis = create_redis(opts)
|
|
49
|
+
@prefix = opts.fetch(:prefix, "allora")
|
|
50
|
+
|
|
51
|
+
reset!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def reschedule(jobs)
|
|
55
|
+
current_time = Time.now
|
|
56
|
+
last_time = send(:last_time)
|
|
57
|
+
set_last_time(current_time)
|
|
58
|
+
|
|
59
|
+
jobs.select do |name, job|
|
|
60
|
+
redis.hsetnx(schedule_info_key, name, 1)
|
|
61
|
+
redis.setnx(job_info_key(name), time_to_int(job.next_at(last_time)))
|
|
62
|
+
update_job_info(job, name, current_time)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def create_redis(opts)
|
|
69
|
+
return opts[:client] if opts.key?(:client)
|
|
70
|
+
|
|
71
|
+
::Redis.new(:host => opts.fetch(:host, "localhost"), :port => opts.fetch(:port, 6379))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Forces all job data to be re-entered into Redis at the next poll
|
|
75
|
+
def reset!
|
|
76
|
+
redis.hgetall(schedule_info_key) { |name, t| redis.del(job_info_key(name)) }
|
|
77
|
+
redis.del(schedule_info_key)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns a Boolean specifying if the job can be run and no race condition occurred updating its info
|
|
81
|
+
def update_job_info(job, name, time)
|
|
82
|
+
redis.watch(job_info_key(name))
|
|
83
|
+
run_at = int_to_time(redis.get(job_info_key(name)))
|
|
84
|
+
|
|
85
|
+
if run_at <= time
|
|
86
|
+
redis.multi
|
|
87
|
+
redis.set(job_info_key(name), time_to_int(job.next_at(time)))
|
|
88
|
+
redis.exec
|
|
89
|
+
else
|
|
90
|
+
redis.unwatch && false
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def schedule_info_key
|
|
95
|
+
"#{prefix}_schedule"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def job_info_key(name)
|
|
99
|
+
"#{prefix}_job_#{name}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def last_time_key
|
|
103
|
+
"#{prefix}_last_run"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns the last time at which polling occurred
|
|
107
|
+
#
|
|
108
|
+
# This is used as a re-entry mechanism if the scheduler stops
|
|
109
|
+
def last_time
|
|
110
|
+
redis.setnx(last_time_key, time_to_int(Time.now))
|
|
111
|
+
int_to_time(redis.get(last_time_key))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def set_last_time(t)
|
|
115
|
+
redis.set(last_time_key, time_to_int(t))
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def time_to_int(t)
|
|
119
|
+
t.to_i
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def int_to_time(i)
|
|
123
|
+
Time.at(i.to_i)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
class Backend
|
|
26
|
+
attr_reader :options
|
|
27
|
+
|
|
28
|
+
# Initialize the backend with the given options Hash.
|
|
29
|
+
#
|
|
30
|
+
# @param [Hash] opts
|
|
31
|
+
# options for the backend, if the backend requires any
|
|
32
|
+
def initialize(opts = {})
|
|
33
|
+
@options = opts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Reschedules jobs in the given Hash and returns those that should run now.
|
|
37
|
+
#
|
|
38
|
+
# Subclasses should take an approach that tracks the run time information and updates
|
|
39
|
+
# it in a way that avoids race conditions. The job should not be run until it can be
|
|
40
|
+
# guaranteed that it has been rescheduled for a future time and no other scheduler
|
|
41
|
+
# process executed the job first.
|
|
42
|
+
#
|
|
43
|
+
# @param [Hash] jobs
|
|
44
|
+
# a Hash mapping job names with their job classes
|
|
45
|
+
#
|
|
46
|
+
# @return [Hash]
|
|
47
|
+
# a Hash containing the jobs to be run now, if any
|
|
48
|
+
def reschedule(jobs)
|
|
49
|
+
raise NotImplementedError, "Abstract method #reschedule must be implemented by subclass"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2006-2012, John Mettraux, jmettraux@gmail.com
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
20
|
+
# THE SOFTWARE.
|
|
21
|
+
#
|
|
22
|
+
# Made in Japan.
|
|
23
|
+
#++
|
|
24
|
+
|
|
25
|
+
# This is almost entirely lifted from https://github.com/jmettraux/rufus-scheduler/blob/master/lib/rufus/sc/cronline.rb
|
|
26
|
+
module Allora
|
|
27
|
+
# Parses a crontab string to determine the times it represents.
|
|
28
|
+
class CronLine
|
|
29
|
+
attr_reader :seconds
|
|
30
|
+
attr_reader :minutes
|
|
31
|
+
attr_reader :hours
|
|
32
|
+
attr_reader :days
|
|
33
|
+
attr_reader :months
|
|
34
|
+
attr_reader :weekdays
|
|
35
|
+
attr_reader :monthdays
|
|
36
|
+
attr_reader :timezone
|
|
37
|
+
|
|
38
|
+
def initialize(line)
|
|
39
|
+
items = line.split
|
|
40
|
+
|
|
41
|
+
raise ArgumentError.new("not a valid cronline : '#{line}'") \
|
|
42
|
+
unless items.length == 5 or items.length == 6
|
|
43
|
+
|
|
44
|
+
offset = items.length - 5
|
|
45
|
+
|
|
46
|
+
@seconds = offset == 1 ? parse_item(items[0], 0, 59) : [0]
|
|
47
|
+
@minutes = parse_item(items[0 + offset], 0, 59)
|
|
48
|
+
@hours = parse_item(items[1 + offset], 0, 24)
|
|
49
|
+
@days = parse_item(items[2 + offset], 1, 31)
|
|
50
|
+
@months = parse_item(items[3 + offset], 1, 12)
|
|
51
|
+
|
|
52
|
+
@weekdays, @monthdays = parse_weekdays(items[4 + offset])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns the next time that this cron line is supposed to 'fire'
|
|
56
|
+
#
|
|
57
|
+
# Note that the time instance returned will be in the same time zone that
|
|
58
|
+
# the given start point Time (thus a result in the local time zone will
|
|
59
|
+
# be passed if no start time is specified (search start time set to
|
|
60
|
+
# Time.now))
|
|
61
|
+
#
|
|
62
|
+
# @example
|
|
63
|
+
#
|
|
64
|
+
# Allora::CronLine.new('30 7 * * *').next_time(
|
|
65
|
+
# Time.mktime(2008, 10, 24, 7, 29))
|
|
66
|
+
# #=> Fri Oct 24 07:30:00 -0500 2008
|
|
67
|
+
#
|
|
68
|
+
# Allora::CronLine.new('30 7 * * *').next_time(
|
|
69
|
+
# Time.utc(2008, 10, 24, 7, 29))
|
|
70
|
+
# #=> Fri Oct 24 07:30:00 UTC 2008
|
|
71
|
+
#
|
|
72
|
+
# Allora::CronLine.new('30 7 * * *').next_time(
|
|
73
|
+
# Time.utc(2008, 10, 24, 7, 29)).localtime
|
|
74
|
+
# #=> Fri Oct 24 02:30:00 -0500 2008
|
|
75
|
+
#
|
|
76
|
+
# @param [Time] time
|
|
77
|
+
# the time from which to compute the next time
|
|
78
|
+
#
|
|
79
|
+
# @return [Time]
|
|
80
|
+
# the next time after the given Time
|
|
81
|
+
def next_time(time)
|
|
82
|
+
# little adjustment before starting
|
|
83
|
+
time = time + 1
|
|
84
|
+
|
|
85
|
+
loop do
|
|
86
|
+
unless date_match?(time)
|
|
87
|
+
time += (24 - time.hour) * 3600 - time.min * 60 - time.sec
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
unless sub_match?(time.hour, @hours)
|
|
91
|
+
time += (60 - time.min) * 60 - time.sec
|
|
92
|
+
next
|
|
93
|
+
end
|
|
94
|
+
unless sub_match?(time.min, @minutes)
|
|
95
|
+
time += 60 - time.sec
|
|
96
|
+
next
|
|
97
|
+
end
|
|
98
|
+
unless sub_match?(time.sec, @seconds)
|
|
99
|
+
time += 1
|
|
100
|
+
next
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
break
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
time
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def to_array
|
|
110
|
+
[
|
|
111
|
+
@seconds,
|
|
112
|
+
@minutes,
|
|
113
|
+
@hours,
|
|
114
|
+
@days,
|
|
115
|
+
@months,
|
|
116
|
+
@weekdays,
|
|
117
|
+
@monthdays
|
|
118
|
+
]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
WEEKDAYS = %w[ sun mon tue wed thu fri sat ]
|
|
124
|
+
|
|
125
|
+
def parse_weekdays(item)
|
|
126
|
+
return nil if item == '*'
|
|
127
|
+
|
|
128
|
+
items = item.downcase.split(',')
|
|
129
|
+
|
|
130
|
+
weekdays = nil
|
|
131
|
+
monthdays = nil
|
|
132
|
+
|
|
133
|
+
items.each do |it|
|
|
134
|
+
if it.match(/#[12345]$/)
|
|
135
|
+
raise ArgumentError.new(
|
|
136
|
+
"ranges are not supported for monthdays (#{it})"
|
|
137
|
+
) if it.index('-')
|
|
138
|
+
|
|
139
|
+
(monthdays ||= []) << it
|
|
140
|
+
else
|
|
141
|
+
WEEKDAYS.each_with_index { |a, i| it.gsub!(/#{a}/, i.to_s) }
|
|
142
|
+
|
|
143
|
+
its = it.index('-') ? parse_range(it, 0, 7) : [Integer(it)]
|
|
144
|
+
its = its.collect { |i| i == 7 ? 0 : i }
|
|
145
|
+
|
|
146
|
+
(weekdays ||= []).concat(its)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
weekdays = weekdays.uniq if weekdays
|
|
151
|
+
|
|
152
|
+
[weekdays, monthdays]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def parse_item(item, min, max)
|
|
156
|
+
return nil if item == '*'
|
|
157
|
+
return parse_list(item, min, max) if item.index(',')
|
|
158
|
+
return parse_range(item, min, max) if item.index('*') or item.index('-')
|
|
159
|
+
|
|
160
|
+
i = item.to_i
|
|
161
|
+
|
|
162
|
+
i = min if i < min
|
|
163
|
+
i = max if i > max
|
|
164
|
+
|
|
165
|
+
[i]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parse_list(item, min, max)
|
|
169
|
+
item.split(',').inject([]) { |r, i|
|
|
170
|
+
r.push(parse_range(i, min, max))
|
|
171
|
+
}.flatten
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_range(item, min, max)
|
|
175
|
+
i = item.index('-')
|
|
176
|
+
j = item.index('/')
|
|
177
|
+
|
|
178
|
+
return item.to_i if (not i and not j)
|
|
179
|
+
|
|
180
|
+
inc = j ? item[j + 1..-1].to_i : 1
|
|
181
|
+
|
|
182
|
+
istart = -1
|
|
183
|
+
iend = -1
|
|
184
|
+
|
|
185
|
+
if i
|
|
186
|
+
istart = item[0..i - 1].to_i
|
|
187
|
+
iend = if j
|
|
188
|
+
item[i + 1..j - 1].to_i
|
|
189
|
+
else
|
|
190
|
+
item[i + 1..-1].to_i
|
|
191
|
+
end
|
|
192
|
+
else # case */x
|
|
193
|
+
istart = min
|
|
194
|
+
iend = max
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
istart = min if istart < min
|
|
198
|
+
iend = max if iend > max
|
|
199
|
+
|
|
200
|
+
result = []
|
|
201
|
+
|
|
202
|
+
value = istart
|
|
203
|
+
loop do
|
|
204
|
+
result << value
|
|
205
|
+
value = value + inc
|
|
206
|
+
break if value > iend
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
result
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def sub_match?(value, values)
|
|
213
|
+
values.nil? || values.include?(value)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def monthday_match(monthday, monthdays)
|
|
217
|
+
return true if monthdays == nil
|
|
218
|
+
return true if monthdays.include?(monthday)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def date_match?(date)
|
|
222
|
+
return false unless sub_match?(date.day, @days)
|
|
223
|
+
return false unless sub_match?(date.month, @months)
|
|
224
|
+
return false unless sub_match?(date.wday, @weekdays)
|
|
225
|
+
return false unless sub_match?(CronLine.monthday(date), @monthdays)
|
|
226
|
+
true
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
DAY_IN_SECONDS = 7 * 24 * 3600
|
|
230
|
+
|
|
231
|
+
def self.monthday(date)
|
|
232
|
+
count = 1
|
|
233
|
+
date2 = date.dup
|
|
234
|
+
|
|
235
|
+
loop do
|
|
236
|
+
date2 = date2 - DAY_IN_SECONDS
|
|
237
|
+
break if date2.month != date.month
|
|
238
|
+
count = count + 1
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
"#{WEEKDAYS[date.wday]}##{count}"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
# A classic cron style job, with support for seconds.
|
|
26
|
+
class Job::CronJob < Job
|
|
27
|
+
# Initialize the CronJob with the given cron string.
|
|
28
|
+
#
|
|
29
|
+
# @param [String] cron_str
|
|
30
|
+
# any valid cron string, which may include seconds
|
|
31
|
+
#
|
|
32
|
+
# @example
|
|
33
|
+
# CronJob.new("*/5 * * * * *") # every 5s
|
|
34
|
+
# CronJob.new("0,30 * * * *") # the 0th and 30th min of each hour
|
|
35
|
+
# CronJob.new("0 3-6 * * *") # on the hour, every hour between 3am and 6am
|
|
36
|
+
def initialize(cron_str, &block)
|
|
37
|
+
super(&block)
|
|
38
|
+
|
|
39
|
+
@cron_line = CronLine.new(cron_str)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def next_at(from_time)
|
|
43
|
+
@cron_line.next_time(from_time)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
# A very simple job type that simply repeats every +n+ seconds.
|
|
26
|
+
class Job::EveryJob < Job
|
|
27
|
+
# Initialize the job to run every +n+ seconds.
|
|
28
|
+
#
|
|
29
|
+
# @param [Integer] n
|
|
30
|
+
# the number of seconds to wait between executions
|
|
31
|
+
#
|
|
32
|
+
# You may use ActiveSupport's numeric helpers, if you have ActiveSupport
|
|
33
|
+
# available.
|
|
34
|
+
#
|
|
35
|
+
# @example Using ActiveSupport
|
|
36
|
+
# EveryJob.new(15.seconds) { puts "Boo!" }
|
|
37
|
+
#
|
|
38
|
+
def initialize(n, &block)
|
|
39
|
+
@duration = n
|
|
40
|
+
|
|
41
|
+
super(&block)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def next_at(from_time)
|
|
45
|
+
from_time + @duration
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/allora/job.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
# Abstract job class providing a wrapper around job execution.
|
|
26
|
+
#
|
|
27
|
+
# Subclasses must be able to provide a time for the job to run,
|
|
28
|
+
# given a start time.
|
|
29
|
+
class Job
|
|
30
|
+
attr_reader :block
|
|
31
|
+
|
|
32
|
+
# Initialize the job with the given block to invoke during execution.
|
|
33
|
+
def initialize(&block)
|
|
34
|
+
@block = block
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Execute the job.
|
|
38
|
+
#
|
|
39
|
+
# Execution happens inside a forked and detached child.
|
|
40
|
+
def execute
|
|
41
|
+
Process.detach(fork { @block.call })
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns the next time at which this job should run.
|
|
45
|
+
#
|
|
46
|
+
# Subclasses must implement this method.
|
|
47
|
+
#
|
|
48
|
+
# @param [Time] from_time
|
|
49
|
+
# the time from which to calculate the next run time
|
|
50
|
+
#
|
|
51
|
+
# @return [Time]
|
|
52
|
+
# the time at which the job should next run
|
|
53
|
+
def next_at(from_time)
|
|
54
|
+
raise NotImplementedError, "Abstract method #next_at must be overridden by subclasses"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
# Worker daemon, dealing with a Backend to execute jobs at regular intervals.
|
|
26
|
+
class Scheduler
|
|
27
|
+
attr_reader :jobs
|
|
28
|
+
attr_reader :backend
|
|
29
|
+
|
|
30
|
+
# Initialize the Scheduler with the given options.
|
|
31
|
+
#
|
|
32
|
+
# Options:
|
|
33
|
+
# backend: an instance of any Backend class (defaults to Memory)
|
|
34
|
+
# interval: a floating point specifying how frequently to poll (defaults to 0.333)
|
|
35
|
+
# logger: an instance of a ruby Logger, or nil to disable
|
|
36
|
+
# *other: any additonal parameters are passed to the Backend
|
|
37
|
+
#
|
|
38
|
+
# @param [Hash] options
|
|
39
|
+
# options for the scheduler, if any
|
|
40
|
+
def initialize(opts = {})
|
|
41
|
+
require "logger"
|
|
42
|
+
|
|
43
|
+
@backend = create_backend(opts)
|
|
44
|
+
@interval = opts.fetch(:interval, 0.333)
|
|
45
|
+
@logger = opts.fetch(:logger, Logger.new(STDOUT))
|
|
46
|
+
@jobs = {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Register a new job for the given options.
|
|
50
|
+
#
|
|
51
|
+
# @example
|
|
52
|
+
# s.add("foo", :every => 5.seconds) { puts "Running!" }
|
|
53
|
+
# s.add("bar", :cron => "*/15 * 1,10,20 * *") { puts "Bonus!" }
|
|
54
|
+
#
|
|
55
|
+
# @param [String] name
|
|
56
|
+
# a unique name to give this job (used for locking)
|
|
57
|
+
#
|
|
58
|
+
# @param [Hash, Job] opts_or_job
|
|
59
|
+
# options specifying when to run the job (:every, or :cron), or a Job instance.
|
|
60
|
+
#
|
|
61
|
+
# @return [Job]
|
|
62
|
+
# the job instance added to the schedule
|
|
63
|
+
def add(name, opts_or_job, &block)
|
|
64
|
+
jobs[name.to_s] = create_job(opts_or_job, &block)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Starts running the scheduler in a new Thread, and returns that Thread.
|
|
68
|
+
#
|
|
69
|
+
# @return [Thread]
|
|
70
|
+
# the scheduler polling Thread
|
|
71
|
+
def start
|
|
72
|
+
log "Starting scheduler process, using #{@backend.class}"
|
|
73
|
+
|
|
74
|
+
@thread = Thread.new do
|
|
75
|
+
loop do
|
|
76
|
+
@backend.reschedule(@jobs).each do |name, job|
|
|
77
|
+
log "Running job '#{name}'"
|
|
78
|
+
job.execute
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sleep(@interval)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Stop the currently running scheduler Thread
|
|
87
|
+
def stop
|
|
88
|
+
log "Exiting scheduler process"
|
|
89
|
+
@thread.exit
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Join the currently running scheduler Thread.
|
|
93
|
+
#
|
|
94
|
+
# This should be invoked to prevent the parent Thread from terminating.
|
|
95
|
+
def join
|
|
96
|
+
@thread.join
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def create_job(opts, &block)
|
|
102
|
+
return opts if Job === opts
|
|
103
|
+
|
|
104
|
+
raise ArgumentError "Missing schedule key (either :cron, or :every)" \
|
|
105
|
+
unless opts.key?(:cron) || opts.key?(:every)
|
|
106
|
+
|
|
107
|
+
if opts.key?(:every)
|
|
108
|
+
Job::EveryJob.new(opts[:every], &block)
|
|
109
|
+
elsif opts.key?(:cron)
|
|
110
|
+
Job::CronJob.new(opts[:cron], &block)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def create_backend(opts)
|
|
115
|
+
return Backend::Memory.new unless opts.key?(:backend)
|
|
116
|
+
|
|
117
|
+
case opts[:backend]
|
|
118
|
+
when :memory then Backend::Memory.new
|
|
119
|
+
when :redis then Backend::Redis.new(opts)
|
|
120
|
+
when Class then opts[:backend].new(opts)
|
|
121
|
+
when Backend then opts[:backend]
|
|
122
|
+
else raise "Unsupported backend '#{opts[:backend].inspect}'"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def log(str)
|
|
127
|
+
@logger.info("Allora: #{str}") if @logger
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
module Allora
|
|
25
|
+
VERSION = "0.0.1"
|
|
26
|
+
end
|
data/lib/allora.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
##
|
|
2
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
|
3
|
+
# a copy of this software and associated documentation files (the
|
|
4
|
+
# "Software"), to deal in the Software without restriction, including
|
|
5
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
|
6
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
|
7
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
|
8
|
+
# the following conditions:
|
|
9
|
+
#
|
|
10
|
+
# The above copyright notice and this permission notice shall be
|
|
11
|
+
# included in all copies or substantial portions of the Software.
|
|
12
|
+
#
|
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
14
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
15
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
16
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
17
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
18
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
19
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
20
|
+
#
|
|
21
|
+
# Copyright © 2012 Flippa.com Pty. Ltd.
|
|
22
|
+
##
|
|
23
|
+
|
|
24
|
+
require "allora/version"
|
|
25
|
+
|
|
26
|
+
require "allora/scheduler"
|
|
27
|
+
|
|
28
|
+
require "allora/backend"
|
|
29
|
+
require "allora/backend/memory"
|
|
30
|
+
require "allora/backend/redis"
|
|
31
|
+
|
|
32
|
+
require "allora/cron_line"
|
|
33
|
+
|
|
34
|
+
require "allora/job"
|
|
35
|
+
require "allora/job/every_job"
|
|
36
|
+
require "allora/job/cron_job"
|
|
37
|
+
|
|
38
|
+
module Allora
|
|
39
|
+
class << self
|
|
40
|
+
# Create a new Scheduler, yield it and then start it.
|
|
41
|
+
#
|
|
42
|
+
# If the `:join` option is specified, the scheduler Thread is joined.
|
|
43
|
+
#
|
|
44
|
+
# @params [Hash] opts
|
|
45
|
+
# options specifying a Backend to use, and any backend-specific options
|
|
46
|
+
#
|
|
47
|
+
# @return [Scheduler]
|
|
48
|
+
# the running scheduler
|
|
49
|
+
def start(opts = {})
|
|
50
|
+
Scheduler.new(opts).tap do |s|
|
|
51
|
+
yield s
|
|
52
|
+
s.start
|
|
53
|
+
s.join if opts[:join]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: allora
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
prerelease:
|
|
6
|
+
platform: ruby
|
|
7
|
+
authors:
|
|
8
|
+
- d11wtq
|
|
9
|
+
autorequire:
|
|
10
|
+
bindir: bin
|
|
11
|
+
cert_chain: []
|
|
12
|
+
date: 2012-03-11 00:00:00.000000000 Z
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: redis
|
|
16
|
+
requirement: &70298724477660 !ruby/object:Gem::Requirement
|
|
17
|
+
none: false
|
|
18
|
+
requirements:
|
|
19
|
+
- - ! '>='
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '0'
|
|
22
|
+
type: :development
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: *70298724477660
|
|
25
|
+
description: ! "Allora (Italian for \"at that time\") provides a replacement for the
|
|
26
|
+
classic UNIX\n cron, using nothing but ruby. It is very small,
|
|
27
|
+
easy to follow and relatively\n feature-light. It does support
|
|
28
|
+
a locking mechanism, backed by Redis, or any\n other custom
|
|
29
|
+
implementation, which makes it possible to run the scheduler on\n more
|
|
30
|
+
than one server, without worrying about jobs executing more than once per\n scheduled
|
|
31
|
+
time."
|
|
32
|
+
email:
|
|
33
|
+
- chris@w3style.co.uk
|
|
34
|
+
executables: []
|
|
35
|
+
extensions: []
|
|
36
|
+
extra_rdoc_files: []
|
|
37
|
+
files:
|
|
38
|
+
- .gitignore
|
|
39
|
+
- Gemfile
|
|
40
|
+
- LICENSE
|
|
41
|
+
- README.md
|
|
42
|
+
- Rakefile
|
|
43
|
+
- allora.gemspec
|
|
44
|
+
- lib/allora.rb
|
|
45
|
+
- lib/allora/backend.rb
|
|
46
|
+
- lib/allora/backend/memory.rb
|
|
47
|
+
- lib/allora/backend/redis.rb
|
|
48
|
+
- lib/allora/cron_line.rb
|
|
49
|
+
- lib/allora/job.rb
|
|
50
|
+
- lib/allora/job/cron_job.rb
|
|
51
|
+
- lib/allora/job/every_job.rb
|
|
52
|
+
- lib/allora/scheduler.rb
|
|
53
|
+
- lib/allora/version.rb
|
|
54
|
+
homepage: https://github.com/flippa/allora
|
|
55
|
+
licenses: []
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
none: false
|
|
62
|
+
requirements:
|
|
63
|
+
- - ! '>='
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '0'
|
|
66
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
67
|
+
none: false
|
|
68
|
+
requirements:
|
|
69
|
+
- - ! '>='
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '0'
|
|
72
|
+
requirements: []
|
|
73
|
+
rubyforge_project: allora
|
|
74
|
+
rubygems_version: 1.8.11
|
|
75
|
+
signing_key:
|
|
76
|
+
specification_version: 3
|
|
77
|
+
summary: A ruby scheduler that keeps it simple, with support for distributed schedules
|
|
78
|
+
test_files: []
|
|
79
|
+
has_rdoc:
|