tickwork 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.ruby-gemset +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +373 -0
- data/Rakefile +9 -0
- data/example.rb +28 -0
- data/gemfiles/activesupport3.gemfile +10 -0
- data/gemfiles/activesupport4.gemfile +11 -0
- data/lib/tickwork/at.rb +62 -0
- data/lib/tickwork/data_store.rb +28 -0
- data/lib/tickwork/event.rb +83 -0
- data/lib/tickwork/manager.rb +174 -0
- data/lib/tickwork.rb +56 -0
- data/test/at_test.rb +116 -0
- data/test/data_stores/fake_store.rb +21 -0
- data/test/event_test.rb +67 -0
- data/test/manager_test.rb +576 -0
- data/test/null_logger.rb +19 -0
- data/test/tickwork_test.rb +91 -0
- data/tickworkwork.gemspec +28 -0
- metadata +172 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f0f55ef5fd21a2a08cc0a9f43a35c3ff6d419de8
|
4
|
+
data.tar.gz: e524021956050752db8dfe4d493313c5791ed065
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f6bdf37dc8d00f9eaa5c2d193007e90ccb8379003b2179609468811f42c41843b204794d167db3b1a0716d1a54df6be7b548094613e33fadb8478566947d2158
|
7
|
+
data.tar.gz: 8e7abceaab3cf06d8e7424fcf7591f86685d4b02de9644b4baf4b8f037109c83cec1c4e5f54bb6b22083fa64600b97d73305aacd1207efaa7c01b60235cbb3a5
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.3.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2010-2014 Adam Wiggins, tomykaira <tomykaira@gmail.com>
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,373 @@
|
|
1
|
+
Tickwork - a scheduler library that requires an external call to tick to run scheduled events
|
2
|
+
===========================================
|
3
|
+
|
4
|
+
[![Build Status](https://secure.travis-ci.org/softwaregravy/tickwork.png?branch=master)](http://travis-ci.org/softwaregravy/tickwork) [![Dependency Status](https://gemnasium.com/softwaregravy/tickwork.png)](https://gemnasium.com/softwaregravy/tickwork)
|
5
|
+
|
6
|
+
This started as a stripped down version of [clockwork](https://github.com/tomykaira/clockwork).
|
7
|
+
|
8
|
+
Tickwork provides a familiar and compatible config file for scheduled jobs, but instead of it being driven by a background process, it relies on regular calls to `Tickwork.run`. `Tickwork.run` efectively ticks the clock forward from the last time it was called scheduling jobs as it goes. By tuning the paramters below, you can call `Tickwork.run` as little or as often as you like.
|
9
|
+
|
10
|
+
Tickwork keeps track of time using a datastore. Right now, nothing is supported.
|
11
|
+
|
12
|
+
Note that clockwork allowed schedules to be dynamically set via the database. This functionality does not exist in Tickwork.
|
13
|
+
|
14
|
+
Quickstart
|
15
|
+
----------
|
16
|
+
|
17
|
+
Create tick.rb:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
require 'tickwork'
|
21
|
+
module Tickwork
|
22
|
+
handler do |job|
|
23
|
+
puts "Running #{job}"
|
24
|
+
end
|
25
|
+
|
26
|
+
# handler receives the time when job is prepared to run in the 2nd argument
|
27
|
+
# handler do |job, time|
|
28
|
+
# puts "Running #{job}, at #{time}"
|
29
|
+
# end
|
30
|
+
|
31
|
+
every(10.seconds, 'frequent.job')
|
32
|
+
every(3.minutes, 'less.frequent.job')
|
33
|
+
every(1.hour, 'hourly.job')
|
34
|
+
|
35
|
+
every(1.day, 'midnight.job', :at => '00:00')
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
If you need to load your entire environment for your jobs, simply add:
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require './config/boot'
|
43
|
+
require './config/environment'
|
44
|
+
```
|
45
|
+
|
46
|
+
under the `require 'tickwork'` declaration.
|
47
|
+
|
48
|
+
Then, somewhere else in your app, you need to regularly call `Tickwork.run`.
|
49
|
+
|
50
|
+
Use with queueing
|
51
|
+
-----------------
|
52
|
+
|
53
|
+
The clock process only makes sense as a place to schedule work to be done, not
|
54
|
+
to do the work. It avoids locking by running as a single process, but this
|
55
|
+
makes it impossible to parallelize. For doing the work, you should be using a
|
56
|
+
job queueing system, such as
|
57
|
+
[Delayed Job](http://www.therailsway.com/2009/7/22/do-it-later-with-delayed-job),
|
58
|
+
[Beanstalk/Stalker](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/),
|
59
|
+
[RabbitMQ/Minion](http://adam.heroku.com/past/2009/9/28/background_jobs_with_rabbitmq_and_minion/),
|
60
|
+
[Resque](http://github.com/blog/542-introducing-resque), or
|
61
|
+
[Sidekiq](https://github.com/mperham/sidekiq). This design allows a
|
62
|
+
simple clock process with no locks, but also offers near infinite horizontal
|
63
|
+
scalability.
|
64
|
+
|
65
|
+
For example, if you're using Beanstalk/Stalker:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
require 'stalker'
|
69
|
+
|
70
|
+
module Tickwork
|
71
|
+
handler { |job| Stalker.enqueue(job) }
|
72
|
+
|
73
|
+
every(1.hour, 'feeds.refresh')
|
74
|
+
every(1.day, 'reminders.send', :at => '01:30')
|
75
|
+
end
|
76
|
+
```
|
77
|
+
|
78
|
+
Using a queueing system which doesn't require that your full application be
|
79
|
+
loaded is preferable, because the clock process can keep a tiny memory
|
80
|
+
footprint. If you're using DJ or Resque, however, you can go ahead and load
|
81
|
+
your full application enviroment, and use per-event blocks to call DJ or Resque
|
82
|
+
enqueue methods. For example, with DJ/Rails:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
require 'config/boot'
|
86
|
+
require 'config/environment'
|
87
|
+
|
88
|
+
every(1.hour, 'feeds.refresh') { Feed.send_later(:refresh) }
|
89
|
+
every(1.day, 'reminders.send', :at => '01:30') { Reminder.send_later(:send_reminders) }
|
90
|
+
```
|
91
|
+
|
92
|
+
|
93
|
+
|
94
|
+
Event Parameters
|
95
|
+
----------
|
96
|
+
|
97
|
+
### :at
|
98
|
+
|
99
|
+
`:at` parameter specifies when to trigger the event:
|
100
|
+
|
101
|
+
#### Valid formats:
|
102
|
+
|
103
|
+
HH:MM
|
104
|
+
H:MM
|
105
|
+
**:MM
|
106
|
+
HH:**
|
107
|
+
(Mon|mon|Monday|monday) HH:MM
|
108
|
+
|
109
|
+
#### Examples
|
110
|
+
|
111
|
+
The simplest example:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
every(1.day, 'reminders.send', :at => '01:30')
|
115
|
+
```
|
116
|
+
|
117
|
+
You can omit the leading 0 of the hour:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
every(1.day, 'reminders.send', :at => '1:30')
|
121
|
+
```
|
122
|
+
|
123
|
+
Wildcards for hour and minute are supported:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
every(1.hour, 'reminders.send', :at => '**:30')
|
127
|
+
every(10.seconds, 'frequent.job', :at => '9:**')
|
128
|
+
```
|
129
|
+
|
130
|
+
You can set more than one timing:
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
every(1.day, 'reminders.send', :at => ['12:00', '18:00'])
|
134
|
+
# send reminders at noon and evening
|
135
|
+
```
|
136
|
+
|
137
|
+
You can specify the day of week to run:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
every(1.week, 'myjob', :at => 'Monday 16:20')
|
141
|
+
```
|
142
|
+
|
143
|
+
If another task is already running at the specified time, clockwork will skip execution of the task with the `:at` option.
|
144
|
+
If this is a problem, please use the `:thread` option to prevent the long running task from blocking clockwork's scheduler.
|
145
|
+
|
146
|
+
### :tz
|
147
|
+
|
148
|
+
`:tz` parameter lets you specify a timezone (default is the local timezone):
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
every(1.day, 'reminders.send', :at => '00:00', :tz => 'UTC')
|
152
|
+
# Runs the job each day at midnight, UTC.
|
153
|
+
# The value for :tz can be anything supported by [TZInfo](http://tzinfo.rubyforge.org/)
|
154
|
+
```
|
155
|
+
|
156
|
+
### :if
|
157
|
+
|
158
|
+
`:if` parameter is invoked every time the task is ready to run, and run if the
|
159
|
+
return value is true.
|
160
|
+
|
161
|
+
Run on every first day of month.
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
Tickwork.every(1.day, 'myjob', :if => lambda { |t| t.day == 1 })
|
165
|
+
```
|
166
|
+
|
167
|
+
The argument is an instance of `ActiveSupport::TimeWithZone` if the `:tz` option is set. Otherwise, it's an instance of `Time`.
|
168
|
+
|
169
|
+
This argument cannot be omitted. Please use _ as placeholder if not needed.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
Tickwork.every(1.second, 'myjob', :if => lambda { |_| true })
|
173
|
+
```
|
174
|
+
|
175
|
+
### :thread
|
176
|
+
|
177
|
+
By default, clockwork runs in a single-process and single-thread.
|
178
|
+
If an event handler takes a long time, the main routine of clockwork is blocked until it ends.
|
179
|
+
Tickwork does not misbehave, but the next event is blocked, and runs when the process is returned to the clockwork routine.
|
180
|
+
|
181
|
+
The `:thread` option is to avoid blocking. An event with `thread: true` runs in a different thread.
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
Tickwork.every(1.day, 'run.me.in.new.thread', :thread => true)
|
185
|
+
```
|
186
|
+
|
187
|
+
If a job is long-running or IO-intensive, this option helps keep the clock precise.
|
188
|
+
|
189
|
+
Configuration
|
190
|
+
-----------------------
|
191
|
+
|
192
|
+
Tickwork exposes a couple of configuration options:
|
193
|
+
|
194
|
+
### :logger
|
195
|
+
|
196
|
+
By default Tickwork logs to `STDOUT`. In case you prefer your
|
197
|
+
own logger implementation you have to specify the `logger` configuration option. See example below.
|
198
|
+
|
199
|
+
### :tz
|
200
|
+
|
201
|
+
This is the default timezone to use for all events. When not specified this defaults to the local
|
202
|
+
timezone. Specifying :tz in the parameters for an event overrides anything set here.
|
203
|
+
|
204
|
+
### :max_threads
|
205
|
+
|
206
|
+
Tickwork runs handlers in threads. If it exceeds `max_threads`, it will warn you (log an error) about missing
|
207
|
+
jobs.
|
208
|
+
|
209
|
+
|
210
|
+
### :thread
|
211
|
+
|
212
|
+
Boolean true or false. Default is false. If set to true, every event will be run in its own thread. Can be overridden on a per event basis (see the ```:thread``` option in the Event Parameters section above)
|
213
|
+
|
214
|
+
### :namespace
|
215
|
+
|
216
|
+
This prefixes keys with a namespace which is useful to prevent colisions if you are using redis or memcache as the datastore. Defautls to `_tickwork_`.
|
217
|
+
|
218
|
+
### Stepping forward in time from the past
|
219
|
+
|
220
|
+
Think about Tickwork as having a concept of now built into it, but rather than now moving with the clock, it only moves forward (ticks forward) when you tell it to. You tell it to tick forward through time by calling `Tickwork.run`. How much it ticks forward is controlled by the following variables.
|
221
|
+
|
222
|
+
If you think of a clock, each tick is 1 second, and you take 1 tick each second. With tickwork, you control the size of the ticks, how many you take, and how often you take them.
|
223
|
+
|
224
|
+
Tickwork will never tick into the future.
|
225
|
+
|
226
|
+
### :tick_size
|
227
|
+
|
228
|
+
This is the interval in seconds that each tick will step forward. The original clockwork implementation would (by default) wake up every second to check for work. Tickwork defaults to 60 seconds. This effectively puts a floor on your frequency of events you can schedule. So if you scheduled something to run every 30 seconds, it would only be run every other time -- so don't do that.
|
229
|
+
|
230
|
+
In general, set this to at least as small as your most frequently run job. If you set this to a value larger than 60, then events schedule to run at a particular time may be missed.
|
231
|
+
|
232
|
+
### :max_ticks
|
233
|
+
|
234
|
+
This is the most number of ticks executed per run. If you have `tick_size` set to 60, then each tick will be 1 minute. If `max_ticks` is set to 10, then a call to `Tickwork.run` could result in as many as 10 minutes worth of jobs being scheduled. If you had 1 job that ran every minute, up to 10 jobs would be run. Tickwork will not tick into the future, so you may run fewer than this number of jobs.
|
235
|
+
|
236
|
+
In any given call to `Tickwork.run`, you can move foward through time at most tick_size * max_ticks
|
237
|
+
|
238
|
+
### :max_catchup
|
239
|
+
|
240
|
+
When running tickwork, the last time you run it is important since that is what now. But what if your system goes down? `max_catchup` sets a floor on how far back Tickwork look back for jobs. This defaults to 3600 which is 1 hour. This means that if you run Tickwork for a day, then turn your system off for a day, then start running Tickwork again, it will start scheduling jobs from 1 hour ago.
|
241
|
+
|
242
|
+
Setting to 0 or nil disables the feature, and Tickwork will start from where it left off.
|
243
|
+
|
244
|
+
If there is no last timestamp, Tickwork starts from now.
|
245
|
+
|
246
|
+
This must be larger than your `tick_size`, and probably significantly larger to avoid missing any jobs.
|
247
|
+
|
248
|
+
### Configuration example
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
module Tickwork
|
252
|
+
configure do |config|
|
253
|
+
config[:logger] = Logger.new(log_file_path)
|
254
|
+
config[:tz] = 'EST'
|
255
|
+
config[:max_threads] = 15
|
256
|
+
config[:thread] = true
|
257
|
+
config[:tick_size] = 60
|
258
|
+
config[:max_ticks] = 10
|
259
|
+
config[:max_catchup] = 3600
|
260
|
+
end
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
### External call frequency & configs
|
265
|
+
|
266
|
+
Since tickwork requires on some external system to make calls into `Tickwork.run`, you must balance whatever that system is against the config settings.
|
267
|
+
|
268
|
+
Lets say you call `Tickwork.run` every 5 minutes and you have no jobs trying to run faster than 1x/min. The default values will work well (`tick_size: 60, max_ticks: 10`). Every 5 minutes, you would expect to run 5 minutes worth of jobs. If you miss 1 period, you will catch up and run 10 minutes worth of jobs. However, if you miss 2 periods, then call back (after 15 min), it will take 2 calls to catch up since there are 15 minutes waiting to run, but `max_ticks` limits this to just 10 per call.
|
269
|
+
|
270
|
+
|
271
|
+
### error_handler
|
272
|
+
|
273
|
+
You can add error_handler to define your own logging or error rescue.
|
274
|
+
|
275
|
+
```ruby
|
276
|
+
module Tickwork
|
277
|
+
error_handler do |error|
|
278
|
+
Airbrake.notify_or_ignore(error)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
```
|
282
|
+
|
283
|
+
Current specifications are as follows.
|
284
|
+
|
285
|
+
- defining error_handler does not disable original logging
|
286
|
+
- errors from error_handler itself are not rescued, and stop clockwork
|
287
|
+
|
288
|
+
Any suggestion about these specifications is welcome.
|
289
|
+
|
290
|
+
|
291
|
+
Anatomy of a tick file
|
292
|
+
-----------------------
|
293
|
+
|
294
|
+
tick.rb is standard Ruby. Since we include the Tickwork module, this
|
295
|
+
exposes a small DSL to define the handler for events, and then the events themselves.
|
296
|
+
|
297
|
+
The handler typically looks like this:
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
handler { |job| enqueue_your_job(job) }
|
301
|
+
```
|
302
|
+
|
303
|
+
This block will be invoked every time an event is triggered, with the job name
|
304
|
+
passed in. In most cases, you should be able to pass the job name directly
|
305
|
+
through to your queueing system.
|
306
|
+
|
307
|
+
The second part of the file, which lists the events, roughly resembles a crontab:
|
308
|
+
|
309
|
+
```ruby
|
310
|
+
every(5.minutes, 'thing.do')
|
311
|
+
every(1.hour, 'otherthing.do')
|
312
|
+
```
|
313
|
+
|
314
|
+
In the first line of this example, an event will be triggered once every five
|
315
|
+
minutes, passing the job name 'thing.do' into the handler. The handler shown
|
316
|
+
above would thus call enqueue_your_job('thing.do').
|
317
|
+
|
318
|
+
You can also pass a custom block to the handler, for job queueing systems that
|
319
|
+
rely on classes rather than job names (i.e. DJ and Resque). In this case, you
|
320
|
+
need not define a general event handler, and instead provide one with each
|
321
|
+
event:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
every(5.minutes, 'thing.do') { Thing.send_later(:do) }
|
325
|
+
```
|
326
|
+
|
327
|
+
If you provide a custom handler for the block, the job name is used only for
|
328
|
+
logging.
|
329
|
+
|
330
|
+
You can also use blocks to do more complex checks:
|
331
|
+
|
332
|
+
```ruby
|
333
|
+
every(1.day, 'check.leap.year') do
|
334
|
+
Stalker.enqueue('leap.year.party') if Date.leap?(Time.now.year)
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
In addition, Tickwork also supports `:before_tick` and `after_tick` callbacks.
|
339
|
+
They are optional, and run every tick (a tick being whatever your `:sleep_timeout`
|
340
|
+
is set to, default is 1 second):
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
on(:before_tick) do
|
344
|
+
puts "tick"
|
345
|
+
end
|
346
|
+
|
347
|
+
on(:after_tick) do
|
348
|
+
puts "tock"
|
349
|
+
end
|
350
|
+
```
|
351
|
+
|
352
|
+
Use cases
|
353
|
+
---------
|
354
|
+
|
355
|
+
Feel free to add your idea or experience and send a pull-request.
|
356
|
+
|
357
|
+
- [Sending errors to Airbrake](https://github.com/tomykaira/clockwork/issues/58)
|
358
|
+
|
359
|
+
Meta
|
360
|
+
----
|
361
|
+
|
362
|
+
Created by Adam Wiggins
|
363
|
+
|
364
|
+
Inspired by [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) and [resque-scheduler](https://github.com/bvandenbos/resque-scheduler)
|
365
|
+
|
366
|
+
Design assistance from Peter van Hardenberg and Matthew Soldo
|
367
|
+
|
368
|
+
Patches contributed by Mark McGranaghan and Lukáš Konarovský
|
369
|
+
|
370
|
+
Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
|
371
|
+
|
372
|
+
http://github.com/tomykaira/clockwork
|
373
|
+
http://github.com/softwaregravy/tickwork
|
data/Rakefile
ADDED
data/example.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'tickwork'
|
2
|
+
|
3
|
+
module Tickwork
|
4
|
+
handler do |job|
|
5
|
+
puts "Queueing job: #{job}"
|
6
|
+
end
|
7
|
+
|
8
|
+
every(10.seconds, 'run.me.every.10.seconds')
|
9
|
+
every(1.minute, 'run.me.every.minute')
|
10
|
+
every(1.hour, 'run.me.every.hour')
|
11
|
+
|
12
|
+
every(1.day, 'run.me.at.midnight', :at => '00:00')
|
13
|
+
|
14
|
+
every(1.day, 'custom.event.handler', :at => '00:30') do
|
15
|
+
puts 'This event has its own handler'
|
16
|
+
end
|
17
|
+
|
18
|
+
# note: callbacks that return nil or false will cause event to not run
|
19
|
+
on(:before_tick) do
|
20
|
+
puts "tick"
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
on(:after_tick) do
|
25
|
+
puts "tock"
|
26
|
+
true
|
27
|
+
end
|
28
|
+
end
|
data/lib/tickwork/at.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Tickwork
|
2
|
+
class At
|
3
|
+
class FailedToParse < StandardError; end
|
4
|
+
|
5
|
+
NOT_SPECIFIED = nil
|
6
|
+
WDAYS = %w[sunday monday tuesday wednesday thursday friday saturday].each.with_object({}).with_index do |(w, wdays), index|
|
7
|
+
[w, w.capitalize, w[0...3], w[0...3].capitalize].each do |k|
|
8
|
+
wdays[k] = index
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.parse(at)
|
13
|
+
return unless at
|
14
|
+
case at
|
15
|
+
when /\A([[:alpha:]]+)\s(.*)\z/
|
16
|
+
if wday = WDAYS[$1]
|
17
|
+
parsed_time = parse($2)
|
18
|
+
parsed_time.wday = wday
|
19
|
+
parsed_time
|
20
|
+
else
|
21
|
+
raise FailedToParse, at
|
22
|
+
end
|
23
|
+
when /\A(\d{1,2}):(\d\d)\z/
|
24
|
+
new($2.to_i, $1.to_i)
|
25
|
+
when /\A\*{1,2}:(\d\d)\z/
|
26
|
+
new($1.to_i)
|
27
|
+
when /\A(\d{1,2}):\*\*\z/
|
28
|
+
new(NOT_SPECIFIED, $1.to_i)
|
29
|
+
else
|
30
|
+
raise FailedToParse, at
|
31
|
+
end
|
32
|
+
rescue ArgumentError
|
33
|
+
raise FailedToParse, at
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_accessor :min, :hour, :wday
|
37
|
+
|
38
|
+
def initialize(min, hour=NOT_SPECIFIED, wday=NOT_SPECIFIED)
|
39
|
+
@min = min
|
40
|
+
@hour = hour
|
41
|
+
@wday = wday
|
42
|
+
raise ArgumentError unless valid?
|
43
|
+
end
|
44
|
+
|
45
|
+
def ready?(t)
|
46
|
+
(@min == NOT_SPECIFIED or t.min == @min) and
|
47
|
+
(@hour == NOT_SPECIFIED or t.hour == @hour) and
|
48
|
+
(@wday == NOT_SPECIFIED or t.wday == @wday)
|
49
|
+
end
|
50
|
+
|
51
|
+
def == other
|
52
|
+
@min == other.min && @hour == other.hour && @wday == other.wday
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def valid?
|
57
|
+
@min == NOT_SPECIFIED || (0..59).cover?(@min) &&
|
58
|
+
@hour == NOT_SPECIFIED || (0..23).cover?(@hour) &&
|
59
|
+
@wday == NOT_SPECIFIED || (0..6).cover?(@wday)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Tickwork
|
2
|
+
class DataStore
|
3
|
+
|
4
|
+
# This is an abstract parent class, ruby style :)
|
5
|
+
#
|
6
|
+
# Tickwork requires a data store to record the last time events ran
|
7
|
+
# Ideally, this would be an optimistic write, but it doesn't really matter.
|
8
|
+
# It doesn't matter because our goal is errrs for at least once, vs. at most or exactly
|
9
|
+
# So we run, we record that we ran. There's a chance that another process also ran at the same time
|
10
|
+
# e.g we both read the same 'last time running'
|
11
|
+
# In practice, this shouldn't happen unless our external ticker is called faster than our jobs can run
|
12
|
+
|
13
|
+
|
14
|
+
# Providers should implement
|
15
|
+
#
|
16
|
+
# def get(key)
|
17
|
+
#
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# def set(key, value)
|
21
|
+
#
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# note: keys will be prefixed with '_tickwork_' both for easy identification and also to
|
25
|
+
# help avoid conflicts with the rest of the app
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Tickwork
|
2
|
+
class Event
|
3
|
+
class IllegalJobName < RuntimeError; end
|
4
|
+
|
5
|
+
attr_accessor :job, :data_store_key
|
6
|
+
|
7
|
+
def initialize(manager, period, job, block, options={})
|
8
|
+
validate_if_option(options[:if])
|
9
|
+
@manager = manager
|
10
|
+
@period = period
|
11
|
+
raise IllegalJobName unless job.is_a?(String) && !job.empty? && Tickwork::Manager::MANAGER_KEY != job
|
12
|
+
@job = job
|
13
|
+
@at = At.parse(options[:at])
|
14
|
+
@block = block
|
15
|
+
@if = options[:if]
|
16
|
+
@thread = options.fetch(:thread, @manager.config[:thread])
|
17
|
+
@timezone = options.fetch(:tz, @manager.config[:tz])
|
18
|
+
namespace = options[:namespace]
|
19
|
+
namespace ||= '_tickwork_'
|
20
|
+
@data_store_key = namespace + @job
|
21
|
+
end
|
22
|
+
|
23
|
+
def last
|
24
|
+
@manager.data_store.get(data_store_key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def last=(value)
|
28
|
+
@manager.data_store.set(data_store_key, value)
|
29
|
+
end
|
30
|
+
|
31
|
+
def convert_timezone(t)
|
32
|
+
@timezone ? t.in_time_zone(@timezone) : t
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_now?(t)
|
36
|
+
t = convert_timezone(t)
|
37
|
+
elapsed_ready(t) and (@at.nil? or @at.ready?(t)) and (@if.nil? or @if.call(t))
|
38
|
+
end
|
39
|
+
|
40
|
+
def elapsed_ready(t)
|
41
|
+
last.nil? || (t - last.to_i).to_i >= @period
|
42
|
+
end
|
43
|
+
|
44
|
+
def thread?
|
45
|
+
@thread
|
46
|
+
end
|
47
|
+
|
48
|
+
def run(t)
|
49
|
+
@manager.log "Triggering '#{self}'"
|
50
|
+
self.last = convert_timezone(t)
|
51
|
+
if thread?
|
52
|
+
if @manager.thread_available?
|
53
|
+
t = Thread.new do
|
54
|
+
execute
|
55
|
+
end
|
56
|
+
t['creator'] = @manager
|
57
|
+
else
|
58
|
+
@manager.log_error "Threads exhausted; skipping #{self}"
|
59
|
+
end
|
60
|
+
else
|
61
|
+
execute
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
job
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def execute
|
71
|
+
@block.call(@job, last)
|
72
|
+
rescue => e
|
73
|
+
@manager.log_error e
|
74
|
+
@manager.handle_error e
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_if_option(if_option)
|
78
|
+
if if_option && !if_option.respond_to?(:call)
|
79
|
+
raise ArgumentError.new(':if expects a callable object, but #{if_option} does not respond to call')
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|