allora 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|