resque-scheduler 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +94 -6
- data/Rakefile +5 -4
- data/lib/resque/scheduler.rb +90 -9
- data/lib/resque_scheduler.rb +81 -1
- data/lib/resque_scheduler/server.rb +47 -0
- data/lib/resque_scheduler/server/views/delayed.erb +24 -0
- data/lib/resque_scheduler/server/views/delayed_timestamp.erb +26 -0
- data/lib/resque_scheduler/server/views/scheduler.erb +32 -0
- data/lib/resque_scheduler/tasks.rb +1 -0
- data/lib/resque_scheduler/version.rb +2 -2
- data/test/delayed_queue_test.rb +117 -0
- data/test/redis-test.conf +132 -0
- data/test/scheduler_test.rb +1 -1
- data/test/test_helper.rb +47 -1
- metadata +15 -5
data/README.markdown
CHANGED
@@ -1,13 +1,22 @@
|
|
1
1
|
resque-scheduler
|
2
2
|
===============
|
3
3
|
|
4
|
-
Resque-scheduler is
|
5
|
-
|
6
|
-
|
4
|
+
Resque-scheduler is an extension to Resque that adds support for queueing items
|
5
|
+
in the future.
|
6
|
+
|
7
|
+
Requires redis >=1.1.
|
8
|
+
|
9
|
+
|
10
|
+
Job scheduling is supported in two different way:
|
11
|
+
|
12
|
+
### Recurring (scheduled)
|
13
|
+
|
14
|
+
Recurring (or scheduled) jobs are logically no different than a standard cron
|
15
|
+
job. They are jobs that run based on a fixed schedule which is set at startup.
|
7
16
|
|
8
17
|
The schedule is a list of Resque worker classes with arguments and a
|
9
18
|
schedule frequency (in crontab syntax). The schedule is just a hash, but
|
10
|
-
is most likely stored in a YAML:
|
19
|
+
is most likely stored in a YAML like so:
|
11
20
|
|
12
21
|
queue_documents_for_indexing:
|
13
22
|
cron: "0 0 * * *"
|
@@ -32,8 +41,69 @@ And then set the schedule wherever you configure Resque, like so:
|
|
32
41
|
require 'resque-scheduler'
|
33
42
|
ResqueScheduler.schedule = YAML.load_file(File.join(File.dirname(__FILE__), '../resque_schedule.yml'))
|
34
43
|
|
35
|
-
|
36
|
-
|
44
|
+
Keep in mind, scheduled jobs behave like crons: if your scheduler process (more
|
45
|
+
on that later) is not running when a particular job is supposed to be queued,
|
46
|
+
it will NOT be ran later when the scheduler process is started back up. In that
|
47
|
+
sense, you can sort of think of the scheduler process as crond. Delayed jobs,
|
48
|
+
however, are different.
|
49
|
+
|
50
|
+
A big shout out to [rufus-scheduler](http://github.com/jmettraux/rufus-scheduler) for handling the heavy lifting of the
|
51
|
+
actual scheduling engine.
|
52
|
+
|
53
|
+
### Delayed jobs
|
54
|
+
|
55
|
+
Delayed jobs are one-off jobs that you want to be put into a queue at some point
|
56
|
+
in the future. The classic example is sending email:
|
57
|
+
|
58
|
+
Resque.enqueue_at(5.days.from_now, SendFollowUpEmail, :user_id => current_user.id)
|
59
|
+
|
60
|
+
This will store the job for 5 days in the resque delayed queue at which time the
|
61
|
+
scheduler process will pull it from the delayed queue and put it in the
|
62
|
+
appropriate work queue for the given job and it will be processed as soon as
|
63
|
+
a worker is available.
|
64
|
+
|
65
|
+
NOTE: The job does not fire **exactly** at the time supplied. Rather, once that
|
66
|
+
time is in the past, the job moves from the delayed queue to the actual resque
|
67
|
+
work queue and will be completed as workers as free to process it.
|
68
|
+
|
69
|
+
Also supported is `Resque.enqueue_in` which takes an amount of time in seconds
|
70
|
+
in which to queue the job.
|
71
|
+
|
72
|
+
The delayed queue is stored in redis and is persisted in the same way the
|
73
|
+
standard resque jobs are persisted (redis writing to disk). Delayed jobs differ
|
74
|
+
from scheduled jobs in that if your scheduler process is down or workers are
|
75
|
+
down when a particular job is supposed to be queue, they will simply "catch up"
|
76
|
+
once they are started again. Jobs are guaranteed to run (provided they make it
|
77
|
+
into the delayed queue) after their given queue_at time has passed.
|
78
|
+
|
79
|
+
One other thing to note is that insertion into the delayed queue is O(log(n))
|
80
|
+
since the jobs are stored in a redis sorted set (zset). I can't imagine this
|
81
|
+
being an issue for someone since redis is stupidly fast even at log(n), but full
|
82
|
+
disclosure is always best.
|
83
|
+
|
84
|
+
|
85
|
+
Resque-web additions
|
86
|
+
--------------------
|
87
|
+
|
88
|
+
Resque-scheduler also adds to tabs to the resque-web UI. One is for viewing
|
89
|
+
(and manually queueing) the schedule and one is for viewing pending jobs in
|
90
|
+
the delayed queue.
|
91
|
+
|
92
|
+
The Schedule tab:
|
93
|
+
|
94
|
+
![The Schedule Tab](http://img.skitch.com/20100111-km2f5gmtpbq23enpujbruj6mgk.png)
|
95
|
+
|
96
|
+
The Delayed tab:
|
97
|
+
|
98
|
+
![The Delayed Tab](http://img.skitch.com/20100111-ne4fcqtc5emkcuwc5qtais2kwx.jpg)
|
99
|
+
|
100
|
+
|
101
|
+
The Scheduler process
|
102
|
+
---------------------
|
103
|
+
|
104
|
+
The scheduler process is just a rake task which is responsible for both queueing
|
105
|
+
items from the schedule and polling the delayed queue for items ready to be
|
106
|
+
pushed on to the work queues. For obvious reasons, this process never exits.
|
37
107
|
|
38
108
|
$ rake resque-scheduler
|
39
109
|
|
@@ -42,4 +112,22 @@ You'll need to add this to your rakefile:
|
|
42
112
|
require 'resque_scheduler/tasks'
|
43
113
|
task "resque:setup" => :environment
|
44
114
|
|
115
|
+
Supported environment variables are `VERBOSE` and `MUTE`. If either is set to
|
116
|
+
any nonempty value, they will take effect. `VERBOSE` simply dumps more output
|
117
|
+
to stdout. `MUTE` does the opposite and silences all output. `MUTE` supercedes
|
118
|
+
`VERBOSE`.
|
119
|
+
|
120
|
+
|
121
|
+
Plagurism alert
|
122
|
+
---------------
|
123
|
+
|
124
|
+
This was intended to be an extension to resque and so resulted in a lot of the
|
125
|
+
code looking very similar to resque, particularly in resque-web and the views. I
|
126
|
+
wanted it to be similar enough that someone familiar with resque could easily
|
127
|
+
work on resque-scheduler.
|
128
|
+
|
129
|
+
|
130
|
+
Contributing
|
131
|
+
------------
|
45
132
|
|
133
|
+
For bugs or suggestions, please just open an issue in github.
|
data/Rakefile
CHANGED
@@ -27,14 +27,16 @@ begin
|
|
27
27
|
|
28
28
|
Jeweler::Tasks.new do |gemspec|
|
29
29
|
gemspec.name = "resque-scheduler"
|
30
|
-
gemspec.summary = ""
|
31
|
-
gemspec.description =
|
30
|
+
gemspec.summary = "Light weight job scheduling on top of Resque"
|
31
|
+
gemspec.description = %{Light weight job scheduling on top of Resque.
|
32
|
+
Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
|
33
|
+
Also supports queueing jobs on a fixed, cron-like schedule.}
|
32
34
|
gemspec.email = "bvandenbos@gmail.com"
|
33
35
|
gemspec.homepage = "http://github.com/bvandenbos/resque-scheduler"
|
34
36
|
gemspec.authors = ["Ben VandenBos"]
|
35
37
|
gemspec.version = ResqueScheduler::Version
|
36
38
|
|
37
|
-
gemspec.add_dependency "resque"
|
39
|
+
gemspec.add_dependency "resque", ">= 1.3.0"
|
38
40
|
gemspec.add_dependency "rufus-scheduler"
|
39
41
|
gemspec.add_development_dependency "jeweler"
|
40
42
|
gemspec.add_development_dependency "mocha"
|
@@ -49,5 +51,4 @@ task :publish => [ :test, :gemspec, :build ] do
|
|
49
51
|
system "git push origin master"
|
50
52
|
system "gem push pkg/resque-scheduler-#{ResqueScheduler::Version}.gem"
|
51
53
|
system "git clean -fd"
|
52
|
-
exec "rake pages"
|
53
54
|
end
|
data/lib/resque/scheduler.rb
CHANGED
@@ -10,24 +10,82 @@ module Resque
|
|
10
10
|
|
11
11
|
class << self
|
12
12
|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
13
|
+
# If true, logs more stuff...
|
14
|
+
attr_accessor :verbose
|
15
|
+
|
16
|
+
# If set, produces no output
|
17
|
+
attr_accessor :mute
|
18
|
+
|
19
|
+
# Schedule all jobs and continually look for delayed jobs (never returns)
|
20
|
+
def run
|
21
|
+
|
22
|
+
# trap signals
|
23
|
+
register_signal_handlers
|
24
|
+
|
25
|
+
# Load the schedule into rufus
|
26
|
+
load_schedule!
|
27
|
+
|
28
|
+
# Now start the scheduling part of the loop.
|
29
|
+
loop do
|
30
|
+
handle_delayed_items
|
31
|
+
poll_sleep
|
32
|
+
end
|
33
|
+
|
34
|
+
# never gets here.
|
35
|
+
end
|
36
|
+
|
37
|
+
# For all signals, set the shutdown flag and wait for current
|
38
|
+
# poll/enqueing to finish (should be almost istant). In the
|
39
|
+
# case of sleeping, exit immediately.
|
40
|
+
def register_signal_handlers
|
41
|
+
trap("TERM") { shutdown }
|
42
|
+
trap("INT") { shutdown }
|
43
|
+
trap('QUIT') { shutdown } unless defined? JRUBY_VERSION
|
44
|
+
end
|
45
|
+
|
46
|
+
# Pulls the schedule from Resque.schedule and loads it into the
|
47
|
+
# rufus scheduler instance
|
48
|
+
def load_schedule!
|
49
|
+
log! "Schedule empty! Set Resque.schedule" if Resque.schedule.empty?
|
16
50
|
|
17
51
|
Resque.schedule.each do |name, config|
|
18
|
-
|
52
|
+
log! "Scheduling #{name} "
|
19
53
|
rufus_scheduler.cron config['cron'] do
|
20
|
-
|
54
|
+
log! "queuing #{config['class']} (#{name})"
|
21
55
|
enqueue_from_config(config)
|
22
56
|
end
|
23
57
|
end
|
24
|
-
# sleep baby, sleep
|
25
|
-
ThreadsWait.all_waits(rufus_scheduler.instance_variable_get("@thread")) if wait
|
26
58
|
end
|
27
59
|
|
60
|
+
# Handles queueing delayed items
|
61
|
+
def handle_delayed_items
|
62
|
+
item = nil
|
63
|
+
if timestamp = Resque.next_delayed_timestamp
|
64
|
+
item = nil
|
65
|
+
begin
|
66
|
+
handle_shutdown do
|
67
|
+
if item = Resque.next_item_for_timestamp(timestamp)
|
68
|
+
log "queuing #{item['class']} [delayed]"
|
69
|
+
klass = constantize(item['class'])
|
70
|
+
Resque.enqueue(klass, *item['args'])
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end while !item.nil?
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def handle_shutdown
|
78
|
+
exit if @shutdown
|
79
|
+
yield
|
80
|
+
exit if @shutdown
|
81
|
+
end
|
82
|
+
|
83
|
+
# Enqueues a job based on a config hash
|
28
84
|
def enqueue_from_config(config)
|
29
|
-
|
30
|
-
|
85
|
+
args = config['args'] || config[:args]
|
86
|
+
klass_name = config['class'] || config[:class]
|
87
|
+
params = args.nil? ? [] : Array(args)
|
88
|
+
Resque.enqueue(constantize(klass_name), *params)
|
31
89
|
end
|
32
90
|
|
33
91
|
def rufus_scheduler
|
@@ -42,6 +100,29 @@ module Resque
|
|
42
100
|
rufus_scheduler
|
43
101
|
end
|
44
102
|
|
103
|
+
# Sleeps and returns true
|
104
|
+
def poll_sleep
|
105
|
+
@sleeping = true
|
106
|
+
handle_shutdown { sleep 5 }
|
107
|
+
@sleeping = false
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
# Sets the shutdown flag, exits if sleeping
|
112
|
+
def shutdown
|
113
|
+
@shutdown = true
|
114
|
+
exit if @sleeping
|
115
|
+
end
|
116
|
+
|
117
|
+
def log!(msg)
|
118
|
+
puts "#{Time.now.strftime("%Y-%m-%d %H:%M:%S")} #{msg}" unless mute
|
119
|
+
end
|
120
|
+
|
121
|
+
def log(msg)
|
122
|
+
# add "verbose" logic later
|
123
|
+
log!(msg) if verbose
|
124
|
+
end
|
125
|
+
|
45
126
|
end
|
46
127
|
|
47
128
|
end
|
data/lib/resque_scheduler.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'resque'
|
3
|
+
require 'resque/server'
|
3
4
|
require 'resque_scheduler/version'
|
4
5
|
require 'resque/scheduler'
|
6
|
+
require 'resque_scheduler/server'
|
5
7
|
|
6
8
|
module ResqueScheduler
|
7
9
|
|
@@ -31,6 +33,84 @@ module ResqueScheduler
|
|
31
33
|
@schedule ||= {}
|
32
34
|
end
|
33
35
|
|
36
|
+
# This method is nearly identical to +enqueue+ only it also
|
37
|
+
# takes a timestamp which will be used to schedule the job
|
38
|
+
# for queueing. Until timestamp is in the past, the job will
|
39
|
+
# sit in the schedule list.
|
40
|
+
def enqueue_at(timestamp, klass, *args)
|
41
|
+
delayed_push(timestamp, :class => klass.to_s, :args => args)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Identical to enqueue_at but takes number_of_seconds_from_now
|
45
|
+
# instead of a timestamp.
|
46
|
+
def enqueue_in(number_of_seconds_from_now, klass, *args)
|
47
|
+
enqueue_at(Time.now + number_of_seconds_from_now, klass, *args)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Used internally to stuff the item into the schedule sorted list.
|
51
|
+
# +timestamp+ can be either in seconds or a datetime object
|
52
|
+
# Insertion if O(log(n)).
|
53
|
+
def delayed_push(timestamp, item)
|
54
|
+
# First add this item to the list for this timestamp
|
55
|
+
redis.rpush("delayed:#{timestamp.to_i}", encode(item))
|
56
|
+
|
57
|
+
# Now, add this timestamp to the zsets. The score and the value are
|
58
|
+
# the same since we'll be querying by timestamp, and we don't have
|
59
|
+
# anything else to store.
|
60
|
+
redis.zset_add :delayed_queue_schedule, timestamp.to_i, timestamp.to_i
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns an array of timestamps based on start and count
|
64
|
+
def delayed_queue_peek(start, count)
|
65
|
+
(redis.zrange :delayed_queue_schedule, start, start+count).collect(&:to_i)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns the size of the delayed queue schedule
|
69
|
+
def delayed_queue_schedule_size
|
70
|
+
redis.zcard :delayed_queue_schedule
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns the number of jobs for a given timestamp in the delayed queue schedule
|
74
|
+
def delayed_timestamp_size(timestamp)
|
75
|
+
redis.llen "delayed:#{timestamp.to_i}"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns an array of delayed items for the given timestamp
|
79
|
+
def delayed_timestamp_peek(timestamp, start, count)
|
80
|
+
if 1 == count
|
81
|
+
r = list_range "delayed:#{timestamp.to_i}", start, count
|
82
|
+
r.nil? ? [] : [r]
|
83
|
+
else
|
84
|
+
list_range "delayed:#{timestamp.to_i}", start, count
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Returns the next delayed queue timestamp
|
89
|
+
# (don't call directly)
|
90
|
+
def next_delayed_timestamp
|
91
|
+
timestamp = redis.zrangebyscore(:delayed_queue_schedule, '-inf', Time.now.to_i, 'limit', 0, 1).first
|
92
|
+
timestamp.to_i unless timestamp.nil?
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns the next item to be processed for a given timestamp, nil if
|
96
|
+
# done. (don't call directly)
|
97
|
+
# +timestamp+ can either be in seconds or a datetime
|
98
|
+
def next_item_for_timestamp(timestamp)
|
99
|
+
key = "delayed:#{timestamp.to_i}"
|
100
|
+
|
101
|
+
item = decode redis.lpop(key)
|
102
|
+
|
103
|
+
# If the list is empty, remove it.
|
104
|
+
if 0 == redis.llen(key).to_i
|
105
|
+
redis.del key
|
106
|
+
redis.zrem :delayed_queue_schedule, timestamp.to_i
|
107
|
+
end
|
108
|
+
item
|
109
|
+
end
|
110
|
+
|
34
111
|
end
|
35
112
|
|
36
|
-
Resque.extend ResqueScheduler
|
113
|
+
Resque.extend ResqueScheduler
|
114
|
+
Resque::Server.class_eval do
|
115
|
+
include ResqueScheduler::Server
|
116
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
# Extend Resque::Server to add tabs
|
3
|
+
module ResqueScheduler
|
4
|
+
|
5
|
+
module Server
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
|
9
|
+
base.class_eval do
|
10
|
+
|
11
|
+
helpers do
|
12
|
+
def format_time(t)
|
13
|
+
t.strftime("%Y-%m-%d %H:%M:%S")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
get "/schedule" do
|
18
|
+
# Is there a better way to specify alternate template locations with sinatra?
|
19
|
+
erb File.read(File.join(File.dirname(__FILE__), 'server/views/scheduler.erb'))
|
20
|
+
end
|
21
|
+
|
22
|
+
post "/schedule/requeue" do
|
23
|
+
config = Resque.schedule[params['job_name']]
|
24
|
+
Resque::Scheduler.enqueue_from_config(config)
|
25
|
+
redirect url("/queues")
|
26
|
+
end
|
27
|
+
|
28
|
+
get "/delayed" do
|
29
|
+
# Is there a better way to specify alternate template locations with sinatra?
|
30
|
+
erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed.erb'))
|
31
|
+
end
|
32
|
+
|
33
|
+
get "/delayed/:timestamp" do
|
34
|
+
# Is there a better way to specify alternate template locations with sinatra?
|
35
|
+
erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
Resque::Server.tabs << 'Schedule'
|
43
|
+
Resque::Server.tabs << 'Delayed'
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<h1>Delayed Jobs</h1>
|
2
|
+
|
3
|
+
<p class='intro'>
|
4
|
+
This list below contains the timestamps for scheduled delayed jobs.
|
5
|
+
</p>
|
6
|
+
|
7
|
+
<p class='sub'>
|
8
|
+
Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.delayed_queue_schedule_size %></b> timestamps
|
9
|
+
</p>
|
10
|
+
|
11
|
+
<table>
|
12
|
+
<tr>
|
13
|
+
<th>Timestamp</th>
|
14
|
+
<th>Job count</th>
|
15
|
+
</tr>
|
16
|
+
<% resque.delayed_queue_peek(start, start+20).each do |timestamp| %>
|
17
|
+
<tr>
|
18
|
+
<td><a href="<%= url "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
|
19
|
+
<td><%= resque.delayed_timestamp_size(timestamp) %></td>
|
20
|
+
</tr>
|
21
|
+
<% end %>
|
22
|
+
</table>
|
23
|
+
|
24
|
+
<%= partial :next_more, :start => start, :size => size %>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<% timestamp = params[:timestamp].to_i %>
|
2
|
+
|
3
|
+
<h1>Delayed jobs scheduled for <%= format_time(Time.at(timestamp)) %></h1>
|
4
|
+
|
5
|
+
<p class='sub'>Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%=size = resque.delayed_timestamp_size(timestamp)%></b> jobs</p>
|
6
|
+
|
7
|
+
<table class='jobs'>
|
8
|
+
<tr>
|
9
|
+
<th>Class</th>
|
10
|
+
<th>Args</th>
|
11
|
+
</tr>
|
12
|
+
<% jobs = resque.delayed_timestamp_peek(timestamp, start, 20) %>
|
13
|
+
<% jobs.each do |job| %>
|
14
|
+
<tr>
|
15
|
+
<td class='class'><%= job['class'] %></td>
|
16
|
+
<td class='args'><%=h job['args'].inspect %></td>
|
17
|
+
</tr>
|
18
|
+
<% end %>
|
19
|
+
<% if jobs.empty? %>
|
20
|
+
<tr>
|
21
|
+
<td class='no-data' colspan='2'>There are no pending jobs scheduled for this time.</td>
|
22
|
+
</tr>
|
23
|
+
<% end %>
|
24
|
+
</table>
|
25
|
+
|
26
|
+
<%= partial :next_more, :start => start, :size => size %>
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<h1>Schedule</h1>
|
2
|
+
|
3
|
+
<p class='intro'>
|
4
|
+
The list below contains all scheduled jobs. Click "Queue now" to queue
|
5
|
+
a job immediately.
|
6
|
+
</p>
|
7
|
+
|
8
|
+
<table>
|
9
|
+
<tr>
|
10
|
+
<th></th>
|
11
|
+
<th>Name</th>
|
12
|
+
<th>Description</th>
|
13
|
+
<th>Cron</th>
|
14
|
+
<th>Class</th>
|
15
|
+
<th>Arguments</th>
|
16
|
+
</tr>
|
17
|
+
<% Resque.schedule.each do |name, config| %>
|
18
|
+
<tr>
|
19
|
+
<td>
|
20
|
+
<form action="<%= url "/schedule/requeue" %>" method="post">
|
21
|
+
<input type="hidden" name="job_name" value="<%= h name %>">
|
22
|
+
<input type="submit" value="Queue now">
|
23
|
+
</form>
|
24
|
+
</td>
|
25
|
+
<td><%= h name %></td>
|
26
|
+
<td><%= h config['description'] %></td>
|
27
|
+
<td style="white-space:nowrap"><%= h config['cron'] %></td>
|
28
|
+
<td><%= h config['class'] %></td>
|
29
|
+
<td><%= h config['args'].inspect %></td>
|
30
|
+
</tr>
|
31
|
+
<% end %>
|
32
|
+
</table>
|
@@ -1,3 +1,3 @@
|
|
1
1
|
module ResqueScheduler
|
2
|
-
Version = '0.0
|
3
|
-
end
|
2
|
+
Version = '1.0.0'
|
3
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Resque::DelayedQueueTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
Resque::Scheduler.mute = true
|
7
|
+
Resque.redis.flush_all
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_enqueue_at_adds_correct_list_and_zset
|
11
|
+
|
12
|
+
timestamp = Time.now - 1 # 1 second ago (in the past, should come out right away)
|
13
|
+
|
14
|
+
assert_equal(0, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
|
15
|
+
|
16
|
+
Resque.enqueue_at(timestamp, SomeIvarJob, "path")
|
17
|
+
|
18
|
+
# Confirm the correct keys were added
|
19
|
+
assert_equal(1, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
|
20
|
+
assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
|
21
|
+
|
22
|
+
read_timestamp = Resque.next_delayed_timestamp
|
23
|
+
|
24
|
+
# Confirm the timestamp came out correctly
|
25
|
+
assert_equal(timestamp.to_i, read_timestamp, "The timestamp we pull out of redis should match the one we put in")
|
26
|
+
item = Resque.next_item_for_timestamp(read_timestamp)
|
27
|
+
|
28
|
+
# Confirm the item came out correctly
|
29
|
+
assert_equal('SomeIvarJob', item['class'], "Should be the same class that we queued")
|
30
|
+
assert_equal(["path"], item['args'], "Should have the same arguments that we queued")
|
31
|
+
|
32
|
+
# And now confirm the keys are gone
|
33
|
+
assert(!Resque.redis.exists("delayed:#{timestamp.to_i}"))
|
34
|
+
assert_equal(0, Resque.redis.zcard(:delayed_queue_schedule), "delayed queue should be empty")
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_something_in_the_future_doesnt_come_out
|
38
|
+
timestamp = Time.now + 600 # 10 minutes from now (in the future, shouldn't come out)
|
39
|
+
|
40
|
+
assert_equal(0, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
|
41
|
+
|
42
|
+
Resque.enqueue_at(timestamp, SomeIvarJob, "path")
|
43
|
+
|
44
|
+
# Confirm the correct keys were added
|
45
|
+
assert_equal(1, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
|
46
|
+
assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
|
47
|
+
|
48
|
+
read_timestamp = Resque.next_delayed_timestamp
|
49
|
+
|
50
|
+
assert_nil(read_timestamp, "No timestamps should be ready for queueing")
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_enqueue_at_and_enqueue_in_are_equivelent
|
54
|
+
timestamp = Time.now + 60
|
55
|
+
|
56
|
+
Resque.enqueue_at(timestamp, SomeIvarJob, "path")
|
57
|
+
Resque.enqueue_in(timestamp - Time.now, SomeIvarJob, "path")
|
58
|
+
|
59
|
+
assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "should have one timestamp in the delayed queue")
|
60
|
+
assert_equal(2, Resque.redis.llen("delayed:#{timestamp.to_i}"), "should have 2 items in the timestamp queue")
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_delayed_queue_peek
|
64
|
+
t = Time.now
|
65
|
+
expected_timestamps = (1..5).to_a.map do |i|
|
66
|
+
(t + 60 + i).to_i
|
67
|
+
end
|
68
|
+
|
69
|
+
expected_timestamps.each do |timestamp|
|
70
|
+
Resque.delayed_push(timestamp, {:class => SomeIvarJob, :args => 'blah1'})
|
71
|
+
end
|
72
|
+
|
73
|
+
timestamps = Resque.delayed_queue_peek(2,3)
|
74
|
+
|
75
|
+
assert_equal(expected_timestamps[2,3], timestamps)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_delayed_queue_schedule_size
|
79
|
+
assert_equal(0, Resque.delayed_queue_schedule_size)
|
80
|
+
Resque.enqueue_at(Time.now+60, SomeIvarJob)
|
81
|
+
assert_equal(1, Resque.delayed_queue_schedule_size)
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_delayed_timestamp_size
|
85
|
+
t = Time.now + 60
|
86
|
+
assert_equal(0, Resque.delayed_timestamp_size(t))
|
87
|
+
Resque.enqueue_at(t, SomeIvarJob)
|
88
|
+
assert_equal(1, Resque.delayed_timestamp_size(t))
|
89
|
+
assert_equal(0, Resque.delayed_timestamp_size(t.to_i+1))
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_delayed_timestamp_peek
|
93
|
+
t = Time.now + 60
|
94
|
+
assert_equal([], Resque.delayed_timestamp_peek(t, 0, 1), "make sure it's an empty array, not nil")
|
95
|
+
Resque.enqueue_at(t, SomeIvarJob)
|
96
|
+
assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 1).length)
|
97
|
+
Resque.enqueue_at(t, SomeIvarJob)
|
98
|
+
assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 1).length)
|
99
|
+
assert_equal(2, Resque.delayed_timestamp_peek(t, 0, 3).length)
|
100
|
+
|
101
|
+
assert_equal({'args' => [], 'class' => 'SomeIvarJob'}, Resque.delayed_timestamp_peek(t, 0, 1).first)
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_handle_delayed_items_with_no_items
|
105
|
+
Resque::Scheduler.expects(:enqueue).never
|
106
|
+
Resque::Scheduler.handle_delayed_items
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_handle_delayed_items_with_items
|
110
|
+
t = Time.now - 60 # in the past
|
111
|
+
Resque.enqueue_at(t, SomeIvarJob)
|
112
|
+
Resque.enqueue_at(t, SomeIvarJob)
|
113
|
+
Resque.expects(:enqueue).twice
|
114
|
+
Resque::Scheduler.handle_delayed_items
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# Redis configuration file example
|
2
|
+
|
3
|
+
# By default Redis does not run as a daemon. Use 'yes' if you need it.
|
4
|
+
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
|
5
|
+
daemonize yes
|
6
|
+
|
7
|
+
# When run as a daemon, Redis write a pid file in /var/run/redis.pid by default.
|
8
|
+
# You can specify a custom pid file location here.
|
9
|
+
pidfile ./test/redis-test.pid
|
10
|
+
|
11
|
+
# Accept connections on the specified port, default is 6379
|
12
|
+
port 9736
|
13
|
+
|
14
|
+
# If you want you can bind a single interface, if the bind option is not
|
15
|
+
# specified all the interfaces will listen for connections.
|
16
|
+
#
|
17
|
+
# bind 127.0.0.1
|
18
|
+
|
19
|
+
# Close the connection after a client is idle for N seconds (0 to disable)
|
20
|
+
timeout 300
|
21
|
+
|
22
|
+
# Save the DB on disk:
|
23
|
+
#
|
24
|
+
# save <seconds> <changes>
|
25
|
+
#
|
26
|
+
# Will save the DB if both the given number of seconds and the given
|
27
|
+
# number of write operations against the DB occurred.
|
28
|
+
#
|
29
|
+
# In the example below the behaviour will be to save:
|
30
|
+
# after 900 sec (15 min) if at least 1 key changed
|
31
|
+
# after 300 sec (5 min) if at least 10 keys changed
|
32
|
+
# after 60 sec if at least 10000 keys changed
|
33
|
+
save 900 1
|
34
|
+
save 300 10
|
35
|
+
save 60 10000
|
36
|
+
|
37
|
+
# The filename where to dump the DB
|
38
|
+
dbfilename dump.rdb
|
39
|
+
|
40
|
+
# For default save/load DB in/from the working directory
|
41
|
+
# Note that you must specify a directory not a file name.
|
42
|
+
dir ./test/
|
43
|
+
|
44
|
+
# Set server verbosity to 'debug'
|
45
|
+
# it can be one of:
|
46
|
+
# debug (a lot of information, useful for development/testing)
|
47
|
+
# notice (moderately verbose, what you want in production probably)
|
48
|
+
# warning (only very important / critical messages are logged)
|
49
|
+
loglevel debug
|
50
|
+
|
51
|
+
# Specify the log file name. Also 'stdout' can be used to force
|
52
|
+
# the demon to log on the standard output. Note that if you use standard
|
53
|
+
# output for logging but daemonize, logs will be sent to /dev/null
|
54
|
+
logfile stdout
|
55
|
+
|
56
|
+
# Set the number of databases. The default database is DB 0, you can select
|
57
|
+
# a different one on a per-connection basis using SELECT <dbid> where
|
58
|
+
# dbid is a number between 0 and 'databases'-1
|
59
|
+
databases 16
|
60
|
+
|
61
|
+
################################# REPLICATION #################################
|
62
|
+
|
63
|
+
# Master-Slave replication. Use slaveof to make a Redis instance a copy of
|
64
|
+
# another Redis server. Note that the configuration is local to the slave
|
65
|
+
# so for example it is possible to configure the slave to save the DB with a
|
66
|
+
# different interval, or to listen to another port, and so on.
|
67
|
+
|
68
|
+
# slaveof <masterip> <masterport>
|
69
|
+
|
70
|
+
################################## SECURITY ###################################
|
71
|
+
|
72
|
+
# Require clients to issue AUTH <PASSWORD> before processing any other
|
73
|
+
# commands. This might be useful in environments in which you do not trust
|
74
|
+
# others with access to the host running redis-server.
|
75
|
+
#
|
76
|
+
# This should stay commented out for backward compatibility and because most
|
77
|
+
# people do not need auth (e.g. they run their own servers).
|
78
|
+
|
79
|
+
# requirepass foobared
|
80
|
+
|
81
|
+
################################### LIMITS ####################################
|
82
|
+
|
83
|
+
# Set the max number of connected clients at the same time. By default there
|
84
|
+
# is no limit, and it's up to the number of file descriptors the Redis process
|
85
|
+
# is able to open. The special value '0' means no limts.
|
86
|
+
# Once the limit is reached Redis will close all the new connections sending
|
87
|
+
# an error 'max number of clients reached'.
|
88
|
+
|
89
|
+
# maxclients 128
|
90
|
+
|
91
|
+
# Don't use more memory than the specified amount of bytes.
|
92
|
+
# When the memory limit is reached Redis will try to remove keys with an
|
93
|
+
# EXPIRE set. It will try to start freeing keys that are going to expire
|
94
|
+
# in little time and preserve keys with a longer time to live.
|
95
|
+
# Redis will also try to remove objects from free lists if possible.
|
96
|
+
#
|
97
|
+
# If all this fails, Redis will start to reply with errors to commands
|
98
|
+
# that will use more memory, like SET, LPUSH, and so on, and will continue
|
99
|
+
# to reply to most read-only commands like GET.
|
100
|
+
#
|
101
|
+
# WARNING: maxmemory can be a good idea mainly if you want to use Redis as a
|
102
|
+
# 'state' server or cache, not as a real DB. When Redis is used as a real
|
103
|
+
# database the memory usage will grow over the weeks, it will be obvious if
|
104
|
+
# it is going to use too much memory in the long run, and you'll have the time
|
105
|
+
# to upgrade. With maxmemory after the limit is reached you'll start to get
|
106
|
+
# errors for write operations, and this may even lead to DB inconsistency.
|
107
|
+
|
108
|
+
# maxmemory <bytes>
|
109
|
+
|
110
|
+
############################### ADVANCED CONFIG ###############################
|
111
|
+
|
112
|
+
# Glue small output buffers together in order to send small replies in a
|
113
|
+
# single TCP packet. Uses a bit more CPU but most of the times it is a win
|
114
|
+
# in terms of number of queries per second. Use 'yes' if unsure.
|
115
|
+
glueoutputbuf yes
|
116
|
+
|
117
|
+
# Use object sharing. Can save a lot of memory if you have many common
|
118
|
+
# string in your dataset, but performs lookups against the shared objects
|
119
|
+
# pool so it uses more CPU and can be a bit slower. Usually it's a good
|
120
|
+
# idea.
|
121
|
+
#
|
122
|
+
# When object sharing is enabled (shareobjects yes) you can use
|
123
|
+
# shareobjectspoolsize to control the size of the pool used in order to try
|
124
|
+
# object sharing. A bigger pool size will lead to better sharing capabilities.
|
125
|
+
# In general you want this value to be at least the double of the number of
|
126
|
+
# very common strings you have in your dataset.
|
127
|
+
#
|
128
|
+
# WARNING: object sharing is experimental, don't enable this feature
|
129
|
+
# in production before of Redis 1.0-stable. Still please try this feature in
|
130
|
+
# your development environment so that we can test it better.
|
131
|
+
shareobjects no
|
132
|
+
shareobjectspoolsize 1024
|
data/test/scheduler_test.rb
CHANGED
@@ -15,7 +15,7 @@ class Resque::SchedulerTest < Test::Unit::TestCase
|
|
15
15
|
assert_equal(0, Resque::Scheduler.rufus_scheduler.all_jobs.size)
|
16
16
|
|
17
17
|
Resque.schedule = {:some_ivar_job => {'cron' => "* * * * *", 'class' => 'SomeIvarJob', 'args' => "/tmp"}}
|
18
|
-
Resque::Scheduler.
|
18
|
+
Resque::Scheduler.load_schedule!
|
19
19
|
|
20
20
|
assert_equal(1, Resque::Scheduler.rufus_scheduler.all_jobs.size)
|
21
21
|
end
|
data/test/test_helper.rb
CHANGED
@@ -1,8 +1,54 @@
|
|
1
|
+
|
2
|
+
# Pretty much copied this file from the resque test_helper since we want
|
3
|
+
# to do all the same stuff
|
4
|
+
|
5
|
+
dir = File.dirname(File.expand_path(__FILE__))
|
6
|
+
|
1
7
|
require 'rubygems'
|
2
8
|
require 'test/unit'
|
3
9
|
require 'mocha'
|
10
|
+
require 'resque'
|
11
|
+
require File.join(dir, '../lib/resque_scheduler')
|
4
12
|
$LOAD_PATH.unshift File.dirname(File.expand_path(__FILE__)) + '/../lib'
|
5
|
-
|
13
|
+
|
14
|
+
|
15
|
+
#
|
16
|
+
# make sure we can run redis
|
17
|
+
#
|
18
|
+
|
19
|
+
if !system("which redis-server")
|
20
|
+
puts '', "** can't find `redis-server` in your path"
|
21
|
+
puts "** try running `sudo rake install`"
|
22
|
+
abort ''
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
#
|
27
|
+
# start our own redis when the tests start,
|
28
|
+
# kill it when they end
|
29
|
+
#
|
30
|
+
|
31
|
+
at_exit do
|
32
|
+
next if $!
|
33
|
+
|
34
|
+
if defined?(MiniTest)
|
35
|
+
exit_code = MiniTest::Unit.new.run(ARGV)
|
36
|
+
else
|
37
|
+
exit_code = Test::Unit::AutoRunner.run
|
38
|
+
end
|
39
|
+
|
40
|
+
pid = `ps -e -o pid,command | grep [r]edis-test`.split(" ")[0]
|
41
|
+
puts "Killing test redis server..."
|
42
|
+
`rm -f #{dir}/dump.rdb`
|
43
|
+
Process.kill("KILL", pid.to_i)
|
44
|
+
exit exit_code
|
45
|
+
end
|
46
|
+
|
47
|
+
puts "Starting redis for testing at localhost:9736..."
|
48
|
+
`redis-server #{dir}/redis-test.conf`
|
49
|
+
Resque.redis = 'localhost:9736'
|
50
|
+
|
51
|
+
|
6
52
|
|
7
53
|
class SomeJob
|
8
54
|
def self.perform(repo_id, path)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: resque-scheduler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ben VandenBos
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-01-11 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version:
|
23
|
+
version: 1.3.0
|
24
24
|
version:
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rufus-scheduler
|
@@ -52,7 +52,10 @@ dependencies:
|
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: "0"
|
54
54
|
version:
|
55
|
-
description:
|
55
|
+
description: |-
|
56
|
+
Light weight job scheduling on top of Resque.
|
57
|
+
Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
|
58
|
+
Also supports queueing jobs on a fixed, cron-like schedule.
|
56
59
|
email: bvandenbos@gmail.com
|
57
60
|
executables: []
|
58
61
|
|
@@ -66,9 +69,15 @@ files:
|
|
66
69
|
- Rakefile
|
67
70
|
- lib/resque/scheduler.rb
|
68
71
|
- lib/resque_scheduler.rb
|
72
|
+
- lib/resque_scheduler/server.rb
|
73
|
+
- lib/resque_scheduler/server/views/delayed.erb
|
74
|
+
- lib/resque_scheduler/server/views/delayed_timestamp.erb
|
75
|
+
- lib/resque_scheduler/server/views/scheduler.erb
|
69
76
|
- lib/resque_scheduler/tasks.rb
|
70
77
|
- lib/resque_scheduler/version.rb
|
71
78
|
- tasks/resque_scheduler.rake
|
79
|
+
- test/delayed_queue_test.rb
|
80
|
+
- test/redis-test.conf
|
72
81
|
- test/scheduler_test.rb
|
73
82
|
- test/test_helper.rb
|
74
83
|
has_rdoc: true
|
@@ -98,7 +107,8 @@ rubyforge_project:
|
|
98
107
|
rubygems_version: 1.3.5
|
99
108
|
signing_key:
|
100
109
|
specification_version: 3
|
101
|
-
summary:
|
110
|
+
summary: Light weight job scheduling on top of Resque
|
102
111
|
test_files:
|
112
|
+
- test/delayed_queue_test.rb
|
103
113
|
- test/scheduler_test.rb
|
104
114
|
- test/test_helper.rb
|