ealdent-resque-scheduler 2.0.0.e

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,63 @@
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 %z")
14
+ end
15
+
16
+ def queue_from_class_name(class_name)
17
+ Resque.queue_from_class(Resque.constantize(class_name))
18
+ end
19
+ end
20
+
21
+ get "/schedule" do
22
+ Resque.reload_schedule! if Resque::Scheduler.dynamic
23
+ # Is there a better way to specify alternate template locations with sinatra?
24
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/scheduler.erb'))
25
+ end
26
+
27
+ post "/schedule/requeue" do
28
+ config = Resque.schedule[params['job_name']]
29
+ Resque::Scheduler.enqueue_from_config(config)
30
+ redirect u("/overview")
31
+ end
32
+
33
+ get "/delayed" 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.erb'))
36
+ end
37
+
38
+ get "/delayed/:timestamp" do
39
+ # Is there a better way to specify alternate template locations with sinatra?
40
+ erb File.read(File.join(File.dirname(__FILE__), 'server/views/delayed_timestamp.erb'))
41
+ end
42
+
43
+ post "/delayed/queue_now" do
44
+ timestamp = params['timestamp']
45
+ Resque::Scheduler.enqueue_delayed_items_for_timestamp(timestamp.to_i) if timestamp.to_i > 0
46
+ redirect u("/overview")
47
+ end
48
+
49
+ post "/delayed/clear" do
50
+ Resque.reset_delayed_queue
51
+ redirect u('delayed')
52
+ end
53
+
54
+ end
55
+
56
+ end
57
+
58
+ Resque::Server.tabs << 'Schedule'
59
+ Resque::Server.tabs << 'Delayed'
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,48 @@
1
+ <h1>Delayed Jobs</h1>
2
+ <%- size = resque.delayed_queue_schedule_size %>
3
+ <% if size > 0 %>
4
+ <form method="POST" action="<%=u 'delayed/clear'%>" class='clear-delayed'>
5
+ <input type='submit' name='' value='Clear Delayed Jobs' />
6
+ </form>
7
+ <% end %>
8
+
9
+ <p class='intro'>
10
+ This list below contains the timestamps for scheduled delayed jobs.
11
+ </p>
12
+
13
+ <p class='sub'>
14
+ Showing <%= start = params[:start].to_i %> to <%= start + 20 %> of <b><%= size %></b> timestamps
15
+ </p>
16
+
17
+ <table>
18
+ <tr>
19
+ <th></th>
20
+ <th>Timestamp</th>
21
+ <th>Job count</th>
22
+ <th>Class</th>
23
+ <th>Args</th>
24
+ </tr>
25
+ <% resque.delayed_queue_peek(start, 20).each do |timestamp| %>
26
+ <tr>
27
+ <td>
28
+ <form action="<%= u "/delayed/queue_now" %>" method="post">
29
+ <input type="hidden" name="timestamp" value="<%= timestamp.to_i %>">
30
+ <input type="submit" value="Queue now">
31
+ </form>
32
+ </td>
33
+ <td><a href="<%= u "delayed/#{timestamp}" %>"><%= format_time(Time.at(timestamp)) %></a></td>
34
+ <td><%= delayed_timestamp_size = resque.delayed_timestamp_size(timestamp) %></td>
35
+ <% job = resque.delayed_timestamp_peek(timestamp, 0, 1).first %>
36
+ <td>
37
+ <% if job && delayed_timestamp_size == 1 %>
38
+ <%= h(job['class']) %>
39
+ <% else %>
40
+ <a href="<%= u "delayed/#{timestamp}" %>">see details</a>
41
+ <% end %>
42
+ </td>
43
+ <td><%= h(job['args'].inspect) if job && delayed_timestamp_size == 1 %></td>
44
+ </tr>
45
+ <% end %>
46
+ </table>
47
+
48
+ <%= 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,43 @@
1
+ <h1>Schedule</h1>
2
+
3
+ <p class='intro'>
4
+ The list below contains all scheduled jobs. Click &quot;Queue now&quot; 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>Interval</th>
14
+ <th>Class</th>
15
+ <th>Queue</th>
16
+ <th>Arguments</th>
17
+ </tr>
18
+ <% Resque.schedule.keys.sort.each do |name| %>
19
+ <% config = Resque.schedule[name] %>
20
+ <tr>
21
+ <td>
22
+ <form action="<%= u "/schedule/requeue" %>" method="post">
23
+ <input type="hidden" name="job_name" value="<%= h name %>">
24
+ <input type="submit" value="Queue now">
25
+ </form>
26
+ </td>
27
+ <td><%= h name %></td>
28
+ <td><%= h config['description'] %></td>
29
+ <td style="white-space:nowrap"><%= if !config['every'].nil?
30
+ h('every: ' + config['every'])
31
+ elsif !config['cron'].nil?
32
+ h('cron: ' + config['cron'])
33
+ else
34
+ 'Not currently scheduled'
35
+ end %></td>
36
+ <td><%= (config['class'].nil? && !config['custom_job_class'].nil?) ?
37
+ h(config['custom_job_class']) :
38
+ h(config['class']) %></td>
39
+ <td><%= h config['queue'] || queue_from_class_name(config['class']) %></td>
40
+ <td><%= h config['args'].inspect %></td>
41
+ </tr>
42
+ <% end %>
43
+ </table>
@@ -0,0 +1,27 @@
1
+ require 'resque/tasks'
2
+ # will give you the resque tasks
3
+
4
+ namespace :resque do
5
+ task :setup
6
+
7
+ desc "Start Resque Scheduler"
8
+ task :scheduler => :scheduler_setup do
9
+ require 'resque'
10
+ require 'resque_scheduler'
11
+
12
+ File.open(ENV['PIDFILE'], 'w') { |f| f << Process.pid.to_s } if ENV['PIDFILE']
13
+
14
+ Resque::Scheduler.dynamic = true if ENV['DYNAMIC_SCHEDULE']
15
+ Resque::Scheduler.verbose = true if ENV['VERBOSE']
16
+ Resque::Scheduler.run
17
+ end
18
+
19
+ task :scheduler_setup do
20
+ if ENV['INITIALIZER_PATH']
21
+ load ENV['INITIALIZER_PATH'].to_s.strip
22
+ else
23
+ Rake::Task['resque:setup'].invoke
24
+ end
25
+ end
26
+
27
+ end
@@ -0,0 +1,3 @@
1
+ module ResqueScheduler
2
+ Version = '2.0.0.e'
3
+ end
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/resque_scheduler/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "ealdent-resque-scheduler"
6
+ s.version = ResqueScheduler::Version
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Ben VandenBos']
9
+ s.email = ['bvandenbos@gmail.com']
10
+ s.homepage = "http://github.com/bvandenbos/resque-scheduler"
11
+ s.summary = "Light weight job scheduling on top of Resque"
12
+ s.description = %q{Light weight job scheduling on top of Resque.
13
+ Adds methods enqueue_at/enqueue_in to schedule jobs in the future.
14
+ Also supports queueing jobs on a fixed, cron-like schedule.}
15
+
16
+ s.required_rubygems_version = ">= 1.3.6"
17
+ s.add_development_dependency "bundler", ">= 1.0.0"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
21
+ s.require_path = 'lib'
22
+
23
+ s.add_runtime_dependency(%q<redis>, [">= 2.0.1"])
24
+ s.add_runtime_dependency(%q<resque>, [">= 1.15.0"])
25
+ s.add_runtime_dependency(%q<rufus-scheduler>, [">= 0"])
26
+ s.add_development_dependency(%q<mocha>, [">= 0"])
27
+ s.add_development_dependency(%q<rack-test>, [">= 0"])
28
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'resque_scheduler/tasks'
@@ -0,0 +1,254 @@
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.flushall
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_enqueue_at_with_queue_adds_correct_list_and_zset_and_queue
38
+
39
+ timestamp = Time.now - 1 # 1 second ago (in the past, should come out right away)
40
+
41
+ assert_equal(0, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
42
+
43
+ Resque.enqueue_at_with_queue('critical', timestamp, SomeIvarJob, "path")
44
+
45
+ # Confirm the correct keys were added
46
+ assert_equal(1, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
47
+ assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
48
+
49
+ read_timestamp = Resque.next_delayed_timestamp
50
+
51
+ # Confirm the timestamp came out correctly
52
+ assert_equal(timestamp.to_i, read_timestamp, "The timestamp we pull out of redis should match the one we put in")
53
+ item = Resque.next_item_for_timestamp(read_timestamp)
54
+
55
+ # Confirm the item came out correctly
56
+ assert_equal('SomeIvarJob', item['class'], "Should be the same class that we queued")
57
+ assert_equal(["path"], item['args'], "Should have the same arguments that we queued")
58
+ assert_equal('critical', item['queue'], "Should have the queue that we asked for")
59
+
60
+ # And now confirm the keys are gone
61
+ assert(!Resque.redis.exists("delayed:#{timestamp.to_i}"))
62
+ assert_equal(0, Resque.redis.zcard(:delayed_queue_schedule), "delayed queue should be empty")
63
+ end
64
+
65
+ def test_something_in_the_future_doesnt_come_out
66
+ timestamp = Time.now + 600 # 10 minutes from now (in the future, shouldn't come out)
67
+
68
+ assert_equal(0, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should be empty to start")
69
+
70
+ Resque.enqueue_at(timestamp, SomeIvarJob, "path")
71
+
72
+ # Confirm the correct keys were added
73
+ assert_equal(1, Resque.redis.llen("delayed:#{timestamp.to_i}").to_i, "delayed queue should have one entry now")
74
+ assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "The delayed_queue_schedule should have 1 entry now")
75
+
76
+ read_timestamp = Resque.next_delayed_timestamp
77
+
78
+ assert_nil(read_timestamp, "No timestamps should be ready for queueing")
79
+ end
80
+
81
+ def test_something_in_the_future_comes_out_if_you_want_it_to
82
+ timestamp = Time.now + 600 # 10 minutes from now
83
+
84
+ Resque.enqueue_at(timestamp, SomeIvarJob, "path")
85
+
86
+ read_timestamp = Resque.next_delayed_timestamp(timestamp)
87
+
88
+ assert_equal(timestamp.to_i, read_timestamp, "The timestamp we pull out of redis should match the one we put in")
89
+ end
90
+
91
+ def test_enqueue_at_and_enqueue_in_are_equivelent
92
+ timestamp = Time.now + 60
93
+
94
+ Resque.enqueue_at(timestamp, SomeIvarJob, "path")
95
+ Resque.enqueue_in(timestamp - Time.now, SomeIvarJob, "path")
96
+
97
+ assert_equal(1, Resque.redis.zcard(:delayed_queue_schedule), "should have one timestamp in the delayed queue")
98
+ assert_equal(2, Resque.redis.llen("delayed:#{timestamp.to_i}"), "should have 2 items in the timestamp queue")
99
+ end
100
+
101
+ def test_empty_delayed_queue_peek
102
+ assert_equal([], Resque.delayed_queue_peek(0,20))
103
+ end
104
+
105
+ def test_delayed_queue_peek
106
+ t = Time.now
107
+ expected_timestamps = (1..5).to_a.map do |i|
108
+ (t + 60 + i).to_i
109
+ end
110
+
111
+ expected_timestamps.each do |timestamp|
112
+ Resque.delayed_push(timestamp, {:class => SomeIvarJob, :args => 'blah1'})
113
+ end
114
+
115
+ timestamps = Resque.delayed_queue_peek(2,3)
116
+
117
+ assert_equal(expected_timestamps[2,3], timestamps)
118
+ end
119
+
120
+ def test_delayed_queue_schedule_size
121
+ assert_equal(0, Resque.delayed_queue_schedule_size)
122
+ Resque.enqueue_at(Time.now+60, SomeIvarJob)
123
+ assert_equal(1, Resque.delayed_queue_schedule_size)
124
+ end
125
+
126
+ def test_delayed_timestamp_size
127
+ t = Time.now + 60
128
+ assert_equal(0, Resque.delayed_timestamp_size(t))
129
+ Resque.enqueue_at(t, SomeIvarJob)
130
+ assert_equal(1, Resque.delayed_timestamp_size(t))
131
+ assert_equal(0, Resque.delayed_timestamp_size(t.to_i+1))
132
+ end
133
+
134
+ def test_delayed_timestamp_peek
135
+ t = Time.now + 60
136
+ assert_equal([], Resque.delayed_timestamp_peek(t, 0, 1), "make sure it's an empty array, not nil")
137
+ Resque.enqueue_at(t, SomeIvarJob)
138
+ assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 1).length)
139
+ Resque.enqueue_at(t, SomeIvarJob)
140
+ assert_equal(1, Resque.delayed_timestamp_peek(t, 0, 1).length)
141
+ assert_equal(2, Resque.delayed_timestamp_peek(t, 0, 3).length)
142
+
143
+ assert_equal({'args' => [], 'class' => 'SomeIvarJob', 'queue' => 'ivar'}, Resque.delayed_timestamp_peek(t, 0, 1).first)
144
+ end
145
+
146
+ def test_handle_delayed_items_with_no_items
147
+ Resque::Scheduler.expects(:enqueue).never
148
+ Resque::Scheduler.handle_delayed_items
149
+ end
150
+
151
+ def test_handle_delayed_items_with_items
152
+ t = Time.now - 60 # in the past
153
+ Resque.enqueue_at(t, SomeIvarJob)
154
+ Resque.enqueue_at(t, SomeIvarJob)
155
+
156
+ # 2 SomeIvarJob jobs should be created in the "ivar" queue
157
+ Resque::Job.expects(:create).twice.with(:ivar, SomeIvarJob, nil)
158
+ Resque::Scheduler.handle_delayed_items
159
+ end
160
+
161
+ def test_handle_delayed_items_with_items_in_the_future
162
+ t = Time.now + 60 # in the future
163
+ Resque.enqueue_at(t, SomeIvarJob)
164
+ Resque.enqueue_at(t, SomeIvarJob)
165
+
166
+ # 2 SomeIvarJob jobs should be created in the "ivar" queue
167
+ Resque::Job.expects(:create).twice.with(:ivar, SomeIvarJob, nil)
168
+ Resque::Scheduler.handle_delayed_items(t)
169
+ end
170
+
171
+ def test_enqueue_delayed_items_for_timestamp
172
+ t = Time.now + 60
173
+
174
+ Resque.enqueue_at(t, SomeIvarJob)
175
+ Resque.enqueue_at(t, SomeIvarJob)
176
+
177
+ # 2 SomeIvarJob jobs should be created in the "ivar" queue
178
+ Resque::Job.expects(:create).twice.with(:ivar, SomeIvarJob, nil)
179
+
180
+ Resque::Scheduler.enqueue_delayed_items_for_timestamp(t)
181
+
182
+ # delayed queue for timestamp should be empty
183
+ assert_equal(0, Resque.delayed_timestamp_peek(t, 0, 3).length)
184
+ end
185
+
186
+ def test_works_with_out_specifying_queue__upgrade_case
187
+ t = Time.now - 60
188
+ Resque.delayed_push(t, :class => 'SomeIvarJob')
189
+
190
+ # Since we didn't specify :queue when calling delayed_push, it will be forced
191
+ # to load the class to figure out the queue. This is the upgrade case from 1.0.4
192
+ # to 1.0.5.
193
+ Resque::Job.expects(:create).once.with(:ivar, SomeIvarJob, nil)
194
+
195
+ Resque::Scheduler.handle_delayed_items
196
+ end
197
+
198
+ def test_clearing_delayed_queue
199
+ t = Time.now + 120
200
+ 4.times { Resque.enqueue_at(t, SomeIvarJob) }
201
+ 4.times { Resque.enqueue_at(Time.now + rand(100), SomeIvarJob) }
202
+
203
+ Resque.reset_delayed_queue
204
+ assert_equal(0, Resque.delayed_queue_schedule_size)
205
+ end
206
+
207
+ def test_remove_specific_item
208
+ t = Time.now + 120
209
+ Resque.enqueue_at(t, SomeIvarJob)
210
+
211
+ assert_equal(1, Resque.remove_delayed(SomeIvarJob))
212
+ end
213
+
214
+ def test_remove_bogus_item_leaves_the_rest_alone
215
+ t = Time.now + 120
216
+ Resque.enqueue_at(t, SomeIvarJob, "foo")
217
+ Resque.enqueue_at(t, SomeIvarJob, "bar")
218
+ Resque.enqueue_at(t, SomeIvarJob, "bar")
219
+ Resque.enqueue_at(t, SomeIvarJob, "baz")
220
+
221
+ assert_equal(0, Resque.remove_delayed(SomeIvarJob))
222
+ end
223
+
224
+ def test_remove_specific_item_in_group_of_other_items_at_same_timestamp
225
+ t = Time.now + 120
226
+ Resque.enqueue_at(t, SomeIvarJob, "foo")
227
+ Resque.enqueue_at(t, SomeIvarJob, "bar")
228
+ Resque.enqueue_at(t, SomeIvarJob, "bar")
229
+ Resque.enqueue_at(t, SomeIvarJob, "baz")
230
+
231
+ assert_equal(2, Resque.remove_delayed(SomeIvarJob, "bar"))
232
+ assert_equal(1, Resque.delayed_queue_schedule_size)
233
+ end
234
+
235
+ def test_remove_specific_item_in_group_of_other_items_at_different_timestamps
236
+ t = Time.now + 120
237
+ Resque.enqueue_at(t, SomeIvarJob, "foo")
238
+ Resque.enqueue_at(t + 1, SomeIvarJob, "bar")
239
+ Resque.enqueue_at(t + 2, SomeIvarJob, "bar")
240
+ Resque.enqueue_at(t + 3, SomeIvarJob, "baz")
241
+
242
+ assert_equal(2, Resque.remove_delayed(SomeIvarJob, "bar"))
243
+ assert_equal(2, Resque.count_all_scheduled_jobs)
244
+ end
245
+
246
+ def test_invalid_job_class
247
+ assert_raise Resque::NoClassError do
248
+ Resque.enqueue_in(10, nil)
249
+ end
250
+ assert_raise Resque::NoQueueError do
251
+ Resque.enqueue_in(10, String) # string serves as invalid Job class
252
+ end
253
+ end
254
+ end