tickwork 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ pkg
2
+ tmp
3
+ /.bundle
4
+ Gemfile.lock
5
+ *.swo
6
+ *.swp
data/.ruby-gemset ADDED
@@ -0,0 +1,2 @@
1
+ tickwork
2
+
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.3.1
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+ sudo: false
3
+ cache: bundler
4
+ rvm:
5
+ - 1.9.3
6
+ - 2.0.0
7
+ - 2.1
8
+ - 2.2
9
+ - jruby-19mode
10
+ - # rbx-2.2.7
11
+ gemfile:
12
+ - gemfiles/activesupport3.gemfile
13
+ - gemfiles/activesupport4.gemfile
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = FileList['test/**/*_test.rb']
6
+ t.verbose = false
7
+ end
8
+
9
+ task :default => :test
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
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ platforms :rbx do
4
+ gem 'rubysl', '~> 2.0'
5
+ gem 'rubysl-test-unit'
6
+ gem 'rubinius-developer_tools'
7
+ end
8
+
9
+ gem 'activesupport', '~> 3.2.14'
10
+ gemspec :path=>"../"
@@ -0,0 +1,11 @@
1
+ source 'https://rubygems.org'
2
+
3
+ platforms :rbx do
4
+ gem 'rubysl', '~> 2.0'
5
+ gem 'rubysl-test-unit'
6
+ gem 'rubinius-developer_tools'
7
+ end
8
+
9
+ gem 'activesupport', '~> 4.0.0'
10
+ gem 'minitest', '~> 4.2'
11
+ gemspec :path=>"../"
@@ -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