sidetiq 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -7,3 +7,4 @@ doc
7
7
  Gemfile.lock
8
8
  pkg
9
9
  tmp
10
+ dump.rdb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ 0.3.0
2
+ -----
3
+
4
+ - Add `Sidetiq.schedules`.
5
+ - Add `Sidetiq.workers`.
6
+ - Add `Sidetiq.scheduled`.
7
+ - Add `Sidetiq.retries`.
8
+ - Add `Sidetiq.logger`. This defaults to the Sidekiq logger.
9
+ - Add support for job backfills.
10
+ - Clean up tests.
11
+ - Sidetiq::Schedule no longer inherits from IceCube::Schedule.
12
+
1
13
  0.2.0
2
14
  -----
3
15
 
data/README.md CHANGED
@@ -6,7 +6,27 @@ Sidetiq
6
6
 
7
7
  Recurring jobs for [Sidekiq](http://mperham.github.com/sidekiq/).
8
8
 
9
- ## DESCRIPTION
9
+ Table Of Contents
10
+ -----------------
11
+
12
+ * [Overview](#section_Overview)
13
+ * [Dependencies](#section_Dependencies)
14
+ * [Installation](#section_Installation)
15
+ * [Introduction](#section_Introduction)
16
+ * [Backfills](#section_Backfills)
17
+ * [Configuration](#section_Configuration)
18
+ * [Logging](#section_Configuration_Logging)
19
+ * [API](#section_API)
20
+ * [Polling](#section_Polling)
21
+ * [Known Issues](#section_Known_Issues)
22
+ * [Web Extension](#section_Web_Extension)
23
+ * [Contribute](#section_Contribute)
24
+ * [License](#section_License)
25
+ * [Author](#section_Author)
26
+
27
+ <a name='section_Overview'></a>
28
+ Overview
29
+ --------
10
30
 
11
31
  Sidetiq provides a simple API for defining recurring workers for Sidekiq.
12
32
 
@@ -20,12 +40,18 @@ Sidetiq provides a simple API for defining recurring workers for Sidekiq.
20
40
  each other (tested with sub-second polling of scheduled jobs by Sidekiq and
21
41
  Sidetiq clock rates above 100hz).
22
42
 
23
- ## DEPENDENCIES
43
+ Detailed API documentation is available on [rubydoc.info](http://rdoc.info/github/tobiassvn/sidetiq/).
44
+
45
+ <a name='section_Dependencies'></a>
46
+ Dependencies
47
+ ------------
24
48
 
25
49
  - [Sidekiq](http://mperham.github.com/sidekiq/)
26
50
  - [ice_cube](http://seejohnrun.github.com/ice_cube/)
27
51
 
28
- ## INSTALLATION
52
+ <a name='section_Installation'></a>
53
+ Installation
54
+ ------------
29
55
 
30
56
  The best way to install Sidetiq is with RubyGems:
31
57
 
@@ -36,7 +62,9 @@ to pick up all the gems ([more info](http://gembundler.com/bundle_install.html))
36
62
 
37
63
  $ bundle install
38
64
 
39
- ## GETTING STARTED
65
+ <a name='section_Introduction'></a>
66
+ Introduction
67
+ ------------
40
68
 
41
69
  Defining recurring jobs is simple:
42
70
 
@@ -47,6 +75,10 @@ class MyWorker
47
75
 
48
76
  # Daily at midnight
49
77
  tiq { daily }
78
+
79
+ def perform
80
+ # do stuff ...
81
+ end
50
82
  end
51
83
  ```
52
84
 
@@ -64,6 +96,10 @@ class MyWorker
64
96
  # Every second year in February
65
97
  yearly(2).month_of_year(:february)
66
98
  end
99
+
100
+ def perform
101
+ # do stuff ...
102
+ end
67
103
  end
68
104
  ```
69
105
 
@@ -76,6 +112,28 @@ class MyWorker
76
112
 
77
113
  # Every other month on the first monday and last tuesday at 12 o'clock.
78
114
  tiq { monthly(2).day_of_week(1 => [1], 2 => [-1]).hour_of_day(12) }
115
+
116
+ def perform
117
+ # do stuff ...
118
+ end
119
+ end
120
+ ```
121
+
122
+ Additionally, the last and current occurrence time (as a `Float`) can be
123
+ passed to the worker simply by adding arguments to `#perform`. Sidetiq
124
+ will check the method arity before enqueuing the job:
125
+
126
+ ```ruby
127
+ class MyWorker
128
+ include Sidekiq::Worker
129
+ include Sidetiq::Schedulable
130
+
131
+ tiq { daily }
132
+
133
+ # Receive last and current occurrence times.
134
+ def perform(last_occurrence, current_occurrence)
135
+ # do stuff ...
136
+ end
79
137
  end
80
138
  ```
81
139
 
@@ -91,7 +149,35 @@ end
91
149
  Additionally, Sidetiq includes a middleware that will check if the clock
92
150
  thread is still alive and restart it if necessary.
93
151
 
94
- ## CONFIGURATION
152
+ <a name='section_Backfills''></a>
153
+ Backfills
154
+ ---------
155
+
156
+ In certain cases it is desirable that missed jobs will be enqueued
157
+ retroactively, for example when a critical, hourly job isn't run due to
158
+ server downtime. To solve this, `#tiq` takes a *backfill* option. If
159
+ missing job occurrences have been detected, Sidetiq will then enqueue
160
+ the jobs automatically. It will also ensure that the timestamps passed to
161
+ `#perform` are as expected:
162
+
163
+ ```ruby
164
+ class MyWorker
165
+ include Sidekiq::Worker
166
+ include Sidetiq::Schedulable
167
+
168
+ tiq backfill: true do
169
+ hourly
170
+ end
171
+
172
+ def perform(last_occurrence, current_occurrence)
173
+ # do stuff ...
174
+ end
175
+ end
176
+ ```
177
+
178
+ <a name='section_Configuration'></a>
179
+ Configuration
180
+ -------------
95
181
 
96
182
  ```ruby
97
183
  Sidetiq.configure do |config|
@@ -109,8 +195,71 @@ Sidetiq.configure do |config|
109
195
  config.utc = false
110
196
  end
111
197
  ```
198
+ <a name='section_Configuration_Logging'></a>
199
+ ### Logging
200
+
201
+ By default Sidetiq uses Sidekiq's logger. However, this is configuration:
202
+
203
+ ```ruby
204
+ Sidetiq.logger = Logger.new(STDOUT)
205
+ ```
206
+
207
+ The logger should implement Ruby's [Logger API](http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html).
208
+
209
+ <a name='section_API'></a>
210
+ API
211
+ ---
212
+
213
+ Sidetiq implements a simple API to support reflection of recurring jobs at
214
+ runtime:
215
+
216
+ `Sidetiq.schedules` returns a `Hash` with the `Sidekiq::Worker` class as the
217
+ key and the Sidetiq::Schedule object as the value:
218
+
219
+ ```ruby
220
+ Sidetiq.schedules
221
+ # => { MyWorker => #<Sidetiq::Schedule> }
222
+ ```
223
+
224
+ `Sidetiq.workers` returns an `Array` of all workers currently tracked by
225
+ Sidetiq (workers which include `Sidetiq::Schedulable` and a `.tiq` call):
226
+
227
+ ```ruby
228
+ Sidetiq.workers
229
+ # => [MyWorker, AnotherWorker]
230
+ ```
231
+
232
+ `Sidetiq.scheduled` returns an `Array` of currently scheduled Sidetiq jobs
233
+ as `Sidekiq::SortedEntry` (`Sidekiq::Job`) objects. Optionally, it is
234
+ possible to pass a block to which each job will be yielded:
235
+
236
+ ```ruby
237
+ Sidetiq.scheduled do |job|
238
+ # do stuff ...
239
+ end
240
+ ```
241
+
242
+ This list can further be filtered by passing the worker class to `#scheduled`,
243
+ either as a String or the constant itself:
244
+
245
+ ```ruby
246
+ Sidetiq.scheduled(MyWorker) do |job|
247
+ # do stuff ...
248
+ end
249
+ ```
112
250
 
113
- ## NOTES
251
+ The same can be done for recurring jobs currently scheduled for retries
252
+ (`.retries` wraps `Sidekiq::RetrySet` instead of `Sidekiq::ScheduledSet`):
253
+
254
+ ```ruby
255
+ Sidetiq.retries(MyWorker) do |job|
256
+ # do stuff ...
257
+ end
258
+ ```
259
+
260
+ <a name='section_Polling'></a>
261
+ Polling
262
+ -------
114
263
 
115
264
  By default Sidekiq uses a 15 second polling interval to check if scheduled
116
265
  jobs are due. If a recurring job has to run more often than that you should
@@ -120,7 +269,43 @@ lower this value.
120
269
  Sidekiq.options[:poll_interval] = 1
121
270
  ```
122
271
 
123
- ## WEB EXTENSION
272
+ More information about this can be found in the
273
+ [Sidekiq Wiki](https://github.com/mperham/sidekiq/wiki/Scheduled-Jobs).
274
+
275
+ <a name='section_Known_Issues'></a>
276
+ Known Issues
277
+ ------------
278
+
279
+ Unfortunately, using ice_cube's interval methods is terribly slow on
280
+ start-up (it tends to eat up 100% CPU for quite a while). This is due to it
281
+ calculating every possible occurrence since the schedule's start time. The way
282
+ around is to avoid using them.
283
+
284
+ For example, instead of defining a job that should run every 15 minutes like this:
285
+
286
+ ```ruby
287
+ class MyWorker
288
+ include Sidekiq::Worker
289
+ include Sidetiq::Schedulable
290
+
291
+ tiq { minutely(15) }
292
+ end
293
+ ```
294
+
295
+ It is better to use the more explicit way:
296
+
297
+ ```ruby
298
+ class MyWorker
299
+ include Sidekiq::Worker
300
+ include Sidetiq::Schedulable
301
+
302
+ tiq { hourly.minute_of_hour(0, 15, 30, 45) }
303
+ end
304
+ ```
305
+
306
+ <a name='section_Web_Extension'></a>
307
+ Web Extension
308
+ -------------
124
309
 
125
310
  Sidetiq includes an extension for Sidekiq's web interface. It will not be
126
311
  loaded by default, so it will have to be required manually:
@@ -133,7 +318,9 @@ require 'sidetiq/web'
133
318
 
134
319
  ![Screenshot](http://f.cl.ly/items/1P2u1v091F3V1n381g2I/Screen%20Shot%202013-02-01%20at%2012.16.17.png)
135
320
 
136
- ## CONTRIBUTE
321
+ <a name='section_Contribute'></a>
322
+ Contribute
323
+ ----------
137
324
 
138
325
  If you'd like to contribute to Sidetiq, start by forking my repo on GitHub:
139
326
 
@@ -152,6 +339,15 @@ your changes merged back into core is as follows:
152
339
  1. Push the branch up to GitHub
153
340
  1. Send a pull request to the tobiassvn/sidetiq project.
154
341
 
155
- ## LICENSE
342
+ <a name='section_License'></a>
343
+ License
344
+ -------
156
345
 
157
346
  Sidetiq is released under the MIT License. See LICENSE for further details.
347
+
348
+ <a name='section_Author'></a>
349
+ Author
350
+ ------
351
+
352
+ Tobias Svensson, [@tobiassvn](https://twitter.com/tobiassvn), [http://github.com/tobiassvn](http://github.com/tobiassvn)
353
+
data/lib/sidetiq.rb CHANGED
@@ -20,5 +20,122 @@ require 'sidetiq/version'
20
20
 
21
21
  # The Sidetiq namespace.
22
22
  module Sidetiq
23
- end
23
+ class << self
24
+ # Public: Setter for the Sidetiq logger.
25
+ attr_writer :logger
26
+
27
+ # Public: Reader for the Sidetiq logger.
28
+ #
29
+ # Defaults to `Sidekiq.logger`.
30
+ def logger
31
+ @logger ||= Sidekiq.logger
32
+ end
33
+
34
+ # Public: Returns an Array of workers including Sidetiq::Schedulable.
35
+ def workers
36
+ schedules.keys
37
+ end
38
+
39
+ # Public: Returns a Hash of Sidetiq::Schedule instances.
40
+ def schedules
41
+ Clock.synchronize do
42
+ Clock.schedules.dup
43
+ end
44
+ end
45
+
46
+ # Public: Currently scheduled recurring jobs.
47
+ #
48
+ # worker - A Sidekiq::Worker class or String of the class name (optional)
49
+ # block - An optional block that can be given to which each
50
+ # Sidekiq::SortedEntry instance corresponding to a scheduled job will
51
+ # be yielded.
52
+ #
53
+ # Examples
54
+ #
55
+ # Sidetiq.scheduled
56
+ # # => [#<Sidekiq::SortedEntry>, ...]
57
+ #
58
+ # Sidetiq.scheduled(MyWorker)
59
+ # # => [#<Sidekiq::SortedEntry>, ...]
60
+ #
61
+ # Sidetiq.scheduled("MyWorker")
62
+ # # => [#<Sidekiq::SortedEntry>, ...]
63
+ #
64
+ # Sidetiq.scheduled do |job|
65
+ # # do stuff ...
66
+ # end
67
+ # # => [#<Sidekiq::SortedEntry>, ...]
68
+ #
69
+ # Sidetiq.scheduled(MyWorker) do |job|
70
+ # # do stuff ...
71
+ # end
72
+ # # => [#<Sidekiq::SortedEntry>, ...]
73
+ #
74
+ # Sidetiq.scheduled("MyWorker") do |job|
75
+ # # do stuff ...
76
+ # end
77
+ # # => [#<Sidekiq::SortedEntry>, ...]
78
+ #
79
+ # Yields each Sidekiq::SortedEntry instance.
80
+ # Returns an Array of Sidekiq::SortedEntry objects.
81
+ def scheduled(worker = nil, &block)
82
+ filter_set(Sidekiq::ScheduledSet.new, worker, &block)
83
+ end
84
+
85
+ # Public: Recurring jobs currently scheduled for retries.
86
+ #
87
+ # worker - A Sidekiq::Worker class or String of the class name (optional)
88
+ # block - An optional block that can be given to which each
89
+ # Sidekiq::SortedEntry instance corresponding to a scheduled job will
90
+ # be yielded.
91
+ #
92
+ # Examples
93
+ #
94
+ # Sidetiq.retries
95
+ # # => [#<Sidekiq::SortedEntry>, ...]
96
+ #
97
+ # Sidetiq.retries(MyWorker)
98
+ # # => [#<Sidekiq::SortedEntry>, ...]
99
+ #
100
+ # Sidetiq.retries("MyWorker")
101
+ # # => [#<Sidekiq::SortedEntry>, ...]
102
+ #
103
+ # Sidetiq.retries do |job|
104
+ # # do stuff ...
105
+ # end
106
+ # # => [#<Sidekiq::SortedEntry>, ...]
107
+ #
108
+ # Sidetiq.retries(MyWorker) do |job|
109
+ # # do stuff ...
110
+ # end
111
+ # # => [#<Sidekiq::SortedEntry>, ...]
112
+ #
113
+ # Sidetiq.retries("MyWorker") do |job|
114
+ # # do stuff ...
115
+ # end
116
+ # # => [#<Sidekiq::SortedEntry>, ...]
117
+ #
118
+ # Yields each Sidekiq::SortedEntry instance.
119
+ # Returns an Array of Sidekiq::SortedEntry objects.
120
+ def retries(worker = nil, &block)
121
+ filter_set(Sidekiq::RetrySet.new, worker, &block)
122
+ end
24
123
 
124
+ private
125
+
126
+ def filter_set(set, worker, &block)
127
+ worker = worker.constantize if worker.kind_of?(String)
128
+
129
+ jobs = set.select do |job|
130
+ klass = job.klass.constantize
131
+ ret = klass.include?(Schedulable)
132
+ ret = ret && klass == worker if worker
133
+ ret
134
+ end
135
+
136
+ jobs.each(&block) if block_given?
137
+
138
+ jobs
139
+ end
140
+ end
141
+ end
data/lib/sidetiq/clock.rb CHANGED
@@ -11,14 +11,10 @@ module Sidetiq
11
11
  include Singleton
12
12
  include MonitorMixin
13
13
 
14
- # Public: Start time offset from epoch used for calculating run
15
- # times in the Sidetiq schedules.
16
- START_TIME = Sidetiq.config.utc ? Time.utc(2010, 1, 1) : Time.local(2010, 1, 1)
17
-
18
- # Public: Returns a hash of Sidetiq::Schedule instances.
14
+ # Internal: Returns a hash of Sidetiq::Schedule instances.
19
15
  attr_reader :schedules
20
16
 
21
- # Public: Returns the clock thread.
17
+ # Internal: Returns the clock thread.
22
18
  attr_reader :thread
23
19
 
24
20
  def self.method_missing(meth, *args, &block) # :nodoc:
@@ -41,7 +37,7 @@ module Sidetiq
41
37
  #
42
38
  # Returns a Sidetiq::Schedule instances.
43
39
  def schedule_for(worker)
44
- schedules[worker] ||= Sidetiq::Schedule.new(START_TIME)
40
+ schedules[worker] ||= Sidetiq::Schedule.new
45
41
  end
46
42
 
47
43
  # Public: Issue a single clock tick.
@@ -53,12 +49,18 @@ module Sidetiq
53
49
  #
54
50
  # Returns a hash of Sidetiq::Schedule instances.
55
51
  def tick
56
- @tick = gettime
52
+ tick = gettime
57
53
  synchronize do
58
- schedules.each do |worker, schedule|
59
- if schedule.schedule_next?(@tick)
60
- enqueue(worker, schedule.next_occurrence(@tick))
61
- end
54
+ schedules.each do |worker, sched|
55
+ synchronize_clockworks(worker) do |redis|
56
+ if sched.backfill? && (last = worker.last_scheduled_occurrence) > 0
57
+ last = Sidetiq.config.utc ? Time.at(last).utc : Time.at(last)
58
+ sched.occurrences_between(last + 1, tick).each do |past_t|
59
+ enqueue(worker, past_t, redis)
60
+ end
61
+ end
62
+ enqueue(worker, sched.next_occurrence(tick), redis)
63
+ end if sched.schedule_next?(tick)
62
64
  end
63
65
  end
64
66
  end
@@ -89,7 +91,8 @@ module Sidetiq
89
91
  def start!
90
92
  return if ticking?
91
93
 
92
- Sidekiq.logger.info "Sidetiq::Clock start"
94
+ Sidetiq.logger.info "Sidetiq::Clock start"
95
+
93
96
  @thread = Thread.start { clock { tick } }
94
97
  @thread.abort_on_exception = true
95
98
  @thread.priority = Sidetiq.config.priority
@@ -107,7 +110,7 @@ module Sidetiq
107
110
  def stop!
108
111
  if ticking?
109
112
  @thread.kill
110
- Sidekiq.logger.info "Sidetiq::Clock stop"
113
+ Sidetiq.logger.info "Sidetiq::Clock stop"
111
114
  end
112
115
  end
113
116
 
@@ -129,36 +132,35 @@ module Sidetiq
129
132
 
130
133
  private
131
134
 
132
- def enqueue(worker, time)
133
- key = "sidetiq:#{worker.name}"
134
- time_f = time.to_f
135
-
136
- synchronize_clockworks("#{key}:lock") do |redis|
137
- next_run = (redis.get("#{key}:next") || -1).to_f
135
+ def enqueue(worker, time, redis)
136
+ key = "sidetiq:#{worker.name}"
137
+ time_f = time.to_f
138
+ next_run = (redis.get("#{key}:next") || -1).to_f
138
139
 
139
- if next_run < time_f
140
- Sidekiq.logger.info "Sidetiq::Clock enqueue #{worker.name} (at: #{time_f}) (last: #{next_run})"
140
+ if next_run < time_f
141
+ Sidetiq.logger.info "Sidetiq::Clock enqueue #{worker.name} (at: #{time_f}) (last: #{next_run})"
141
142
 
142
- redis.mset("#{key}:last", next_run, "#{key}:next", time_f)
143
+ redis.mset("#{key}:last", next_run, "#{key}:next", time_f)
143
144
 
144
- arity = [worker.instance_method(:perform).arity - 1, -1].max
145
- args = [next_run, time_f][0..arity]
145
+ arity = [worker.instance_method(:perform).arity - 1, -1].max
146
+ args = [next_run, time_f][0..arity]
146
147
 
147
- worker.perform_at(time, *args)
148
- end
148
+ worker.perform_at(time, *args)
149
149
  end
150
150
  end
151
151
 
152
- def synchronize_clockworks(lock)
152
+ def synchronize_clockworks(klass)
153
+ lock = "sidetiq:#{klass.name}:lock"
154
+
153
155
  Sidekiq.redis do |redis|
154
156
  if redis.setnx(lock, 1)
155
- Sidekiq.logger.debug "Sidetiq::Clock lock #{lock}"
157
+ Sidetiq.logger.debug "Sidetiq::Clock lock #{lock}"
156
158
 
157
159
  redis.pexpire(lock, Sidetiq.config.lock_expire)
158
160
  yield redis
159
161
  redis.del(lock)
160
162
 
161
- Sidekiq.logger.debug "Sidetiq::Clock unlock #{lock}"
163
+ Sidetiq.logger.debug "Sidetiq::Clock unlock #{lock}"
162
164
  end
163
165
  end
164
166
  end
@@ -181,3 +183,4 @@ module Sidetiq
181
183
  end
182
184
  end
183
185
  end
186
+
@@ -7,7 +7,7 @@ module Sidetiq
7
7
  def call(*args)
8
8
  # Restart the clock if the thread died.
9
9
  if !@clock.ticking?
10
- Sidekiq.logger.warn "Sidetiq::Clock thread died. Restarting..."
10
+ Sidetiq.logger.warn "Sidetiq::Clock thread died. Restarting..."
11
11
  @clock.start!
12
12
  end
13
13
  yield
@@ -4,30 +4,34 @@ module Sidetiq
4
4
  # Examples
5
5
  #
6
6
  # class MyWorker
7
- # include Sidekiq::Worker
8
- # include Sidetiq::Schedulable
7
+ # include Sidekiq::Worker
8
+ # include Sidetiq::Schedulable
9
9
  #
10
- # # Daily at midnight
11
- # tiq { daily }
12
- # end
10
+ # # Daily at midnight
11
+ # tiq { daily }
12
+ # end
13
13
  module Schedulable
14
14
  module ClassMethods
15
+ # Public: Returns a Float timestamp of the last scheduled run.
15
16
  def last_scheduled_occurrence
16
17
  get_timestamp "last"
17
18
  end
18
19
 
20
+ # Public: Returns a Float timestamp of the next scheduled run.
19
21
  def next_scheduled_occurrence
20
22
  get_timestamp "next"
21
23
  end
22
24
 
23
- def tiq(&block) # :nodoc:
25
+ def tiq(options = {}, &block) # :nodoc:
24
26
  clock = Sidetiq::Clock.instance
25
27
  clock.synchronize do
26
- clock.schedule_for(self).instance_eval(&block)
28
+ schedule = clock.schedule_for(self)
29
+ schedule.instance_eval(&block)
30
+ schedule.set_options(options)
27
31
  end
28
32
  end
29
33
 
30
- private
34
+ private
31
35
 
32
36
  def get_timestamp(key)
33
37
  Sidekiq.redis do |redis|
@@ -41,3 +45,4 @@ module Sidetiq
41
45
  end
42
46
  end
43
47
  end
48
+
@@ -1,23 +1,75 @@
1
1
  module Sidetiq
2
- # Internal: Recurrence schedules.
3
- class Schedule < IceCube::Schedule
4
- def method_missing(meth, *args, &block)
2
+ # Public: Recurrence schedule.
3
+ class Schedule
4
+ # :nodoc:
5
+ attr_reader :last_occurrence
6
+
7
+ # Public: Writer for backfilling option.
8
+ attr_writer :backfill
9
+
10
+ # Public: Start time offset from epoch used for calculating run
11
+ # times in the Sidetiq schedules.
12
+ START_TIME = Sidetiq.config.utc ? Time.utc(2010, 1, 1) : Time.local(2010, 1, 1)
13
+
14
+ def initialize # :nodoc:
15
+ @schedule = IceCube::Schedule.new(START_TIME)
16
+ end
17
+
18
+ def method_missing(meth, *args, &block) # :nodoc:
5
19
  if IceCube::Rule.respond_to?(meth)
6
20
  rule = IceCube::Rule.send(meth, *args, &block)
7
- add_recurrence_rule(rule)
21
+ @schedule.add_recurrence_rule(rule)
8
22
  rule
23
+ elsif @schedule.respond_to?(meth)
24
+ @schedule.send(meth, *args, &block)
9
25
  else
10
26
  super
11
27
  end
12
28
  end
13
29
 
30
+ # Public: Checks if a job is due to be scheduled.
31
+ #
32
+ # Returns true if a job is due, otherwise false.
14
33
  def schedule_next?(time)
15
- if @last_scheduled != (no = next_occurrence(time))
16
- @last_scheduled = no
34
+ next_occurrence = @schedule.next_occurrence(time)
35
+ if @last_scheduled != next_occurrence
36
+ @last_scheduled = next_occurrence
17
37
  return true
18
38
  end
19
39
  false
20
40
  end
41
+
42
+ # Public: Schedule to String.
43
+ #
44
+ # Examples
45
+ #
46
+ # class MyWorker
47
+ # include Sidekiq::Worker
48
+ # include Sidetiq::Schedulable
49
+ #
50
+ # tiq { daily }
51
+ #
52
+ # def perform
53
+ # end
54
+ # end
55
+ #
56
+ # Sidetiq.schedules[MyWorker].to_s
57
+ # # => "Daily"
58
+ #
59
+ # Returns a String representing the schedule.
60
+ def to_s
61
+ @schedule.to_s
62
+ end
63
+
64
+ # Public: Inquirer for backfilling option.
65
+ def backfill?
66
+ !!@backfill
67
+ end
68
+
69
+ # Internal: Set schedule options.
70
+ def set_options(hash)
71
+ self.backfill = hash[:backfill] if !hash[:backfill].nil?
72
+ end
21
73
  end
22
74
  end
23
75
 
@@ -5,7 +5,7 @@ module Sidetiq
5
5
  MAJOR = 0
6
6
 
7
7
  # Public: Sidetiq minor version number.
8
- MINOR = 2
8
+ MINOR = 3
9
9
 
10
10
  # Public: Sidetiq patch level.
11
11
  PATCH = 0
@@ -0,0 +1,11 @@
1
+ class BackfillWorker
2
+ include Sidekiq::Worker
3
+ include Sidetiq::Schedulable
4
+
5
+ tiq backfill: true do
6
+ daily
7
+ end
8
+
9
+ def perform
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class LastAndScheduledTicksWorker
2
+ def perform(last_tick, scheduled_tick)
3
+ end
4
+ end
5
+
@@ -0,0 +1,5 @@
1
+ class LastTickWorker
2
+ def perform(last_tick)
3
+ end
4
+ end
5
+
@@ -0,0 +1,16 @@
1
+ class ScheduledWorker
2
+ include Sidekiq::Worker
3
+ include Sidetiq::Schedulable
4
+
5
+ tiq do
6
+ daily.hour_of_day(1)
7
+ yearly.month_of_year(2)
8
+ monthly.day_of_month(3)
9
+
10
+ add_exception_rule yearly.month_of_year(:february)
11
+ end
12
+
13
+ def perform
14
+ end
15
+ end
16
+
@@ -0,0 +1,8 @@
1
+ class SimpleWorker
2
+ include Sidekiq::Worker
3
+ include Sidetiq::Schedulable
4
+
5
+ def perform
6
+ end
7
+ end
8
+
@@ -0,0 +1,5 @@
1
+ class SplatArgsWorker
2
+ def perform(arg1, *args)
3
+ end
4
+ end
5
+
data/test/helper.rb CHANGED
@@ -13,8 +13,12 @@ require 'sidekiq/testing'
13
13
  require 'sidetiq'
14
14
  require 'sidetiq/web'
15
15
 
16
- # Keep the test output clean
17
- Sidekiq.logger = Logger.new(nil)
16
+ # Keep the test output clean.
17
+ Sidetiq.logger = Logger.new(nil)
18
+
19
+ Dir[File.join(File.dirname(__FILE__), 'fixtures/**/*.rb')].each do |fixture|
20
+ require fixture
21
+ end
18
22
 
19
23
  class Sidetiq::TestCase < MiniTest::Unit::TestCase
20
24
  def setup
@@ -24,5 +28,16 @@ class Sidetiq::TestCase < MiniTest::Unit::TestCase
24
28
  def clock
25
29
  @clock ||= Sidetiq::Clock.instance
26
30
  end
31
+
32
+ # Blatantly stolen from Sidekiq's test suite.
33
+ def add_retry(worker = 'SimpleWorker', jid = 'bob', at = Time.now.to_f)
34
+ payload = Sidekiq.dump_json('class' => worker,
35
+ 'args' => [], 'queue' => 'default', 'jid' => jid,
36
+ 'retry_count' => 2, 'failed_at' => Time.now.utc)
37
+
38
+ Sidekiq.redis do |conn|
39
+ conn.zadd('retry', at.to_s, payload)
40
+ end
41
+ end
27
42
  end
28
43
 
data/test/test_clock.rb CHANGED
@@ -1,11 +1,6 @@
1
1
  require_relative 'helper'
2
2
 
3
3
  class TestClock < Sidetiq::TestCase
4
- class FakeWorker
5
- def perform
6
- end
7
- end
8
-
9
4
  def test_delegates_to_instance
10
5
  Sidetiq::Clock.instance.expects(:foo).once
11
6
  Sidetiq::Clock.foo
@@ -45,13 +40,28 @@ class TestClock < Sidetiq::TestCase
45
40
  Sidetiq.config.utc = false
46
41
  end
47
42
 
43
+ def test_backfilling
44
+ BackfillWorker.jobs.clear
45
+ start = Sidetiq::Schedule::START_TIME
46
+
47
+ BackfillWorker.stubs(:last_scheduled_occurrence).returns(start.to_f)
48
+ clock.stubs(:gettime).returns(start)
49
+ clock.tick
50
+
51
+ BackfillWorker.jobs.clear
52
+
53
+ clock.stubs(:gettime).returns(start + 86400 * 10 + 1)
54
+ clock.tick
55
+ assert_equal 10, BackfillWorker.jobs.length
56
+ end
57
+
48
58
  def test_enqueues_jobs_by_schedule
49
- schedule = Sidetiq::Schedule.new(Sidetiq::Clock::START_TIME)
59
+ schedule = Sidetiq::Schedule.new
50
60
  schedule.daily
51
61
 
52
- clock.stubs(:schedules).returns(FakeWorker => schedule)
62
+ clock.stubs(:schedules).returns(SimpleWorker => schedule)
53
63
 
54
- FakeWorker.expects(:perform_at).times(10)
64
+ SimpleWorker.expects(:perform_at).times(10)
55
65
 
56
66
  10.times do |i|
57
67
  clock.stubs(:gettime).returns(Time.local(2011, 1, i + 1, 1))
@@ -64,13 +74,8 @@ class TestClock < Sidetiq::TestCase
64
74
  clock.tick
65
75
  end
66
76
 
67
- class LastTickWorker
68
- def perform last_tick
69
- end
70
- end
71
-
72
77
  def test_enqueues_jobs_with_default_last_tick_arg_on_first_run
73
- schedule = Sidetiq::Schedule.new(Sidetiq::Clock::START_TIME)
78
+ schedule = Sidetiq::Schedule.new
74
79
  schedule.hourly
75
80
 
76
81
  time = Time.local(2011, 1, 1, 1, 30)
@@ -82,19 +87,15 @@ class TestClock < Sidetiq::TestCase
82
87
  expected_second_tick = expected_first_tick + 3600
83
88
 
84
89
  LastTickWorker.expects(:perform_at).with(expected_first_tick, -1).once
85
- LastTickWorker.expects(:perform_at).with(expected_second_tick, expected_first_tick.to_f).once
90
+ LastTickWorker.expects(:perform_at).with(expected_second_tick,
91
+ expected_first_tick.to_f).once
86
92
 
87
93
  clock.tick
88
94
  clock.tick
89
95
  end
90
96
 
91
- class LastAndScheduledTicksWorker
92
- def perform last_tick, scheduled_tick
93
- end
94
- end
95
-
96
97
  def test_enqueues_jobs_with_last_run_timestamp_and_next_run_timestamp
97
- schedule = Sidetiq::Schedule.new(Sidetiq::Clock::START_TIME)
98
+ schedule = Sidetiq::Schedule.new
98
99
  schedule.hourly
99
100
 
100
101
  time = Time.local(2011, 1, 1, 1, 30)
@@ -105,20 +106,20 @@ class TestClock < Sidetiq::TestCase
105
106
  expected_first_tick = time + 1800
106
107
  expected_second_tick = expected_first_tick + 3600
107
108
 
108
- LastAndScheduledTicksWorker.expects(:perform_at).with(expected_first_tick, -1, expected_first_tick.to_f).once
109
- clock.tick
109
+ LastAndScheduledTicksWorker.expects(:perform_at)
110
+ .with(expected_first_tick, -1, expected_first_tick.to_f).once
110
111
 
111
- LastAndScheduledTicksWorker.expects(:perform_at).with(expected_second_tick, expected_first_tick.to_f, expected_second_tick.to_f).once
112
112
  clock.tick
113
- end
114
113
 
115
- class SplatArgsWorker
116
- def perform arg1, *args
117
- end
114
+ LastAndScheduledTicksWorker.expects(:perform_at)
115
+ .with(expected_second_tick, expected_first_tick.to_f,
116
+ expected_second_tick.to_f).once
117
+
118
+ clock.tick
118
119
  end
119
120
 
120
121
  def test_enqueues_jobs_correctly_for_splat_args_perform_methods
121
- schedule = Sidetiq::Schedule.new(Sidetiq::Clock::START_TIME)
122
+ schedule = Sidetiq::Schedule.new
122
123
  schedule.hourly
123
124
 
124
125
  time = Time.local(2011, 1, 1, 1, 30)
@@ -128,7 +129,8 @@ class TestClock < Sidetiq::TestCase
128
129
 
129
130
  expected_first_tick = time + 1800
130
131
 
131
- SplatArgsWorker.expects(:perform_at).with(expected_first_tick, -1, expected_first_tick.to_f).once
132
+ SplatArgsWorker.expects(:perform_at)
133
+ .with(expected_first_tick, -1, expected_first_tick.to_f).once
132
134
  clock.tick
133
135
  end
134
136
  end
@@ -1,10 +1,6 @@
1
1
  require_relative 'helper'
2
2
 
3
3
  class TestSchedule < Sidetiq::TestCase
4
- def test_super
5
- assert_equal IceCube::Schedule, Sidetiq::Schedule.superclass
6
- end
7
-
8
4
  def test_method_missing
9
5
  sched = Sidetiq::Schedule.new
10
6
  sched.daily
@@ -21,5 +17,22 @@ class TestSchedule < Sidetiq::TestCase
21
17
  assert sched.schedule_next?(Time.now + (2 * 24 * 60 * 60))
22
18
  refute sched.schedule_next?(Time.now + (2 * 24 * 60 * 60))
23
19
  end
20
+
21
+ def test_backfill
22
+ sched = Sidetiq::Schedule.new
23
+ refute sched.backfill?
24
+ sched.backfill = true
25
+ assert sched.backfill?
26
+ end
27
+
28
+ def test_set_options
29
+ sched = Sidetiq::Schedule.new
30
+
31
+ sched.set_options(backfill: true)
32
+ assert sched.backfill?
33
+
34
+ sched.set_options(backfill: false)
35
+ refute sched.backfill?
36
+ end
24
37
  end
25
38
 
@@ -0,0 +1,125 @@
1
+ require_relative 'helper'
2
+
3
+ class TestSidetiq < Sidetiq::TestCase
4
+ def test_schedules
5
+ schedules = Sidetiq.schedules
6
+
7
+ assert_equal 2, schedules.length
8
+
9
+ assert_includes schedules.keys, ScheduledWorker
10
+ assert_includes schedules.keys, BackfillWorker
11
+
12
+ assert_kind_of Sidetiq::Schedule, schedules[ScheduledWorker]
13
+ assert_kind_of Sidetiq::Schedule, schedules[BackfillWorker]
14
+ end
15
+
16
+ def test_workers
17
+ workers = Sidetiq.workers
18
+
19
+ assert_includes workers, ScheduledWorker
20
+ assert_includes workers, BackfillWorker
21
+ assert_equal 2, workers.length
22
+ end
23
+
24
+ def test_scheduled
25
+ SimpleWorker.perform_at(Time.local(2011, 1, 1, 1))
26
+ SimpleWorker.client_push_old(SimpleWorker.jobs.first)
27
+
28
+ scheduled = Sidetiq.scheduled
29
+
30
+ assert_kind_of Array, scheduled
31
+ assert_kind_of Sidekiq::SortedEntry, scheduled.first
32
+ assert_equal 1, scheduled.length
33
+ end
34
+
35
+ def test_scheduled_on_empty_set
36
+ assert_equal 0, Sidetiq.scheduled.length
37
+ end
38
+
39
+ def test_scheduled_given_arguments
40
+ SimpleWorker.perform_at(Time.local(2011, 1, 1, 1))
41
+ SimpleWorker.client_push_old(SimpleWorker.jobs.first)
42
+
43
+ assert_equal 1, Sidetiq.scheduled(SimpleWorker).length
44
+ assert_equal 0, Sidetiq.scheduled(ScheduledWorker).length
45
+
46
+ assert_equal 1, Sidetiq.scheduled("SimpleWorker").length
47
+ assert_equal 0, Sidetiq.scheduled("ScheduledWorker").length
48
+ end
49
+
50
+ def test_scheduled_yields_each_job
51
+ SimpleWorker.perform_at(Time.local(2011, 1, 1, 1))
52
+ SimpleWorker.client_push_old(SimpleWorker.jobs.first)
53
+
54
+ ScheduledWorker.perform_at(Time.local(2011, 1, 1, 1))
55
+ ScheduledWorker.client_push_old(ScheduledWorker.jobs.first)
56
+
57
+ jobs = []
58
+ Sidetiq.scheduled { |job| jobs << job }
59
+ assert_equal 2, jobs.length
60
+
61
+ jobs = []
62
+ Sidetiq.scheduled(SimpleWorker) { |job| jobs << job }
63
+ assert_equal 1, jobs.length
64
+
65
+ jobs = []
66
+ Sidetiq.scheduled("ScheduledWorker") { |job| jobs << job }
67
+ assert_equal 1, jobs.length
68
+ end
69
+
70
+ def test_scheduled_with_invalid_class
71
+ assert_raises(NameError) do
72
+ Sidetiq.scheduled("Foobar")
73
+ end
74
+ end
75
+
76
+ def test_retries
77
+ add_retry('SimpleWorker', 'foo')
78
+ add_retry('ScheduledWorker', 'bar')
79
+
80
+ retries = Sidetiq.retries
81
+
82
+ assert_kind_of Array, retries
83
+ assert_kind_of Sidekiq::SortedEntry, retries[0]
84
+ assert_kind_of Sidekiq::SortedEntry, retries[1]
85
+ assert_equal 2, retries.length
86
+ end
87
+
88
+ def test_retries_on_empty_set
89
+ assert_equal 0, Sidetiq.retries.length
90
+ end
91
+
92
+ def test_retries_given_arguments
93
+ add_retry('SimpleWorker', 'foo')
94
+
95
+ assert_equal 1, Sidetiq.retries(SimpleWorker).length
96
+ assert_equal 0, Sidetiq.retries(ScheduledWorker).length
97
+
98
+ assert_equal 1, Sidetiq.retries("SimpleWorker").length
99
+ assert_equal 0, Sidetiq.retries("ScheduledWorker").length
100
+ end
101
+
102
+ def test_retries_yields_each_job
103
+ add_retry('SimpleWorker', 'foo')
104
+ add_retry('ScheduledWorker', 'foo')
105
+
106
+ jobs = []
107
+ Sidetiq.retries { |job| jobs << job }
108
+ assert_equal 2, jobs.length
109
+
110
+ jobs = []
111
+ Sidetiq.retries(SimpleWorker) { |job| jobs << job }
112
+ assert_equal 1, jobs.length
113
+
114
+ jobs = []
115
+ Sidetiq.retries("ScheduledWorker") { |job| jobs << job }
116
+ assert_equal 1, jobs.length
117
+ end
118
+
119
+ def test_retries_with_invalid_class
120
+ assert_raises(NameError) do
121
+ Sidetiq.retries("Foobar")
122
+ end
123
+ end
124
+ end
125
+
data/test/test_web.rb CHANGED
@@ -3,19 +3,6 @@ require_relative 'helper'
3
3
  class TestWeb < Sidetiq::TestCase
4
4
  include Rack::Test::Methods
5
5
 
6
- class Worker
7
- include Sidekiq::Worker
8
- include Sidetiq::Schedulable
9
-
10
- tiq do
11
- daily(1)
12
- yearly(2)
13
- monthly(3)
14
-
15
- add_exception_rule yearly.month_of_year(:february)
16
- end
17
- end
18
-
19
6
  def app
20
7
  Sidekiq::Web
21
8
  end
@@ -26,7 +13,7 @@ class TestWeb < Sidetiq::TestCase
26
13
 
27
14
  def setup
28
15
  super
29
- Worker.jobs.clear
16
+ ScheduledWorker.jobs.clear
30
17
  end
31
18
 
32
19
  def test_home_tab
@@ -47,9 +34,9 @@ class TestWeb < Sidetiq::TestCase
47
34
  end
48
35
 
49
36
  def test_details_page
50
- get "/sidetiq/#{Worker.name}"
37
+ get "/sidetiq/ScheduledWorker"
51
38
  assert_equal 200, last_response.status
52
- schedule = clock.schedules[Worker]
39
+ schedule = clock.schedules[ScheduledWorker]
53
40
 
54
41
  schedule.recurrence_rules.each do |rule|
55
42
  assert_match /#{rule.to_s}/, last_response.body
@@ -65,10 +52,10 @@ class TestWeb < Sidetiq::TestCase
65
52
  end
66
53
 
67
54
  def test_trigger
68
- post "/sidetiq/#{Worker.name}/trigger"
55
+ post "/sidetiq/ScheduledWorker/trigger"
69
56
  assert_equal 302, last_response.status
70
57
  assert_equal "http://#{host}/sidetiq", last_response.location
71
- assert_equal 1, Worker.jobs.size
58
+ assert_equal 1, ScheduledWorker.jobs.size
72
59
  end
73
60
  end
74
61
 
data/test/test_worker.rb CHANGED
@@ -22,4 +22,8 @@ class TestWorker < Sidetiq::TestCase
22
22
  assert FakeWorker.last_scheduled_occurrence == last_run
23
23
  assert FakeWorker.next_scheduled_occurrence == next_run
24
24
  end
25
+
26
+ def test_options
27
+ assert Sidetiq.schedules[BackfillWorker].backfill?
28
+ end
25
29
  end
metadata CHANGED
@@ -2,14 +2,14 @@
2
2
  name: sidetiq
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.2.0
5
+ version: 0.3.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Tobias Svensson
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-08 00:00:00.000000000 Z
12
+ date: 2013-03-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  version_requirements: !ruby/object:Gem::Requirement
@@ -73,12 +73,19 @@ files:
73
73
  - lib/sidetiq/views/sidetiq_details.slim
74
74
  - lib/sidetiq/web.rb
75
75
  - sidetiq.gemspec
76
+ - test/fixtures/backfill_worker.rb
77
+ - test/fixtures/last_and_scheduled_ticks_worker.rb
78
+ - test/fixtures/last_tick_worker.rb
79
+ - test/fixtures/scheduled_worker.rb
80
+ - test/fixtures/simple_worker.rb
81
+ - test/fixtures/splat_args_worker.rb
76
82
  - test/helper.rb
77
83
  - test/test_clock.rb
78
84
  - test/test_config.rb
79
85
  - test/test_errors.rb
80
86
  - test/test_middleware.rb
81
87
  - test/test_schedule.rb
88
+ - test/test_sidetiq.rb
82
89
  - test/test_version.rb
83
90
  - test/test_web.rb
84
91
  - test/test_worker.rb
@@ -108,12 +115,19 @@ signing_key:
108
115
  specification_version: 3
109
116
  summary: Recurring jobs for Sidekiq
110
117
  test_files:
118
+ - test/fixtures/backfill_worker.rb
119
+ - test/fixtures/last_and_scheduled_ticks_worker.rb
120
+ - test/fixtures/last_tick_worker.rb
121
+ - test/fixtures/scheduled_worker.rb
122
+ - test/fixtures/simple_worker.rb
123
+ - test/fixtures/splat_args_worker.rb
111
124
  - test/helper.rb
112
125
  - test/test_clock.rb
113
126
  - test/test_config.rb
114
127
  - test/test_errors.rb
115
128
  - test/test_middleware.rb
116
129
  - test/test_schedule.rb
130
+ - test/test_sidetiq.rb
117
131
  - test/test_version.rb
118
132
  - test/test_web.rb
119
133
  - test/test_worker.rb