sidetiq 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore 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