clockwork 0.7.7 → 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7deeb150ded639846749af6efb04b31157dc430f
4
+ data.tar.gz: d97f85b001f54997b6c5fd5f2c160da31c8d0a73
5
+ SHA512:
6
+ metadata.gz: b6d0b9c662b085d8243aaa23e00fc1c4549810ec3d346ce0b789eef518f7c839ea830d9c8ec7b785d33cdce81a3587e13b097673cd5ca3e133e40f8133007edb
7
+ data.tar.gz: b162bd32440586badc9156aa06738bda2ad581474d23bf7daf946d8c8a6aa7e55d6e63129ebd9776dc100bc02a2bfae02b21b06f31d3510968c0410f1c724f53
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 CHANGED
@@ -103,50 +103,70 @@ every(1.hour, 'feeds.refresh') { Feed.send_later(:refresh) }
103
103
  every(1.day, 'reminders.send', :at => '01:30') { Reminder.send_later(:send_reminders) }
104
104
  ```
105
105
 
106
- Use with database tasks
106
+ Use with database events
107
107
  -----------------------
108
108
 
109
- You can dynamically add tasks from a database to be scheduled along with the regular events in clock.rb.
109
+ In addition to managing static events in your `clock.rb`, you can configure clockwork to synchronise with dynamic events from a database. Like static events, these database-backed events say when they should be run, and how frequently; the difference being that if you change those settings in the database, they will be reflected in clockwork.
110
110
 
111
- To do this, use the `sync_database_tasks` method call:
111
+ To keep the database events in sync with clockwork, a special manager class `DatabaseEvents::Manager` is used. You tell it to sync a database-backed model using the `sync_database_events` method, and then, at the frequency you specify, it will fetch all the events from the database, and ensure clockwork is using the latest settings.
112
+
113
+ ### Example `clock.rb` file
114
+
115
+ Here we're using an `ActiveRecord` model called `ClockworkDatabaseEvent` to store events in the database:
112
116
 
113
117
  ```ruby
114
118
  require 'clockwork'
115
- require 'clockwork/manager_with_database_tasks'
119
+ require 'clockwork/manager_with_database_events'
116
120
  require_relative './config/boot'
117
121
  require_relative './config/environment'
118
-
122
+
119
123
  module Clockwork
120
124
 
121
125
  # required to enable database syncing support
122
- Clockwork.manager = ManagerWithDatabaseTasks.new
126
+ Clockwork.manager = DatabaseEvents::Manager.new
123
127
 
124
- sync_database_tasks model: MyScheduledTask, every: 1.minute do |instance_job_name|
125
- # Where your model will acts as a worker:
126
- id = instance_job_name.split(':').last
127
- task = MyScheduledTask.find(id)
128
- task.perform_async
128
+ sync_database_events model: ClockworkDatabaseEvent, every: 1.minute do |model_instance|
129
129
 
130
- # Or, e.g. if your queue system just needs job names
131
- # Stalker.enqueue(instance_job_name)
130
+ # do some work e.g...
131
+
132
+ # running a DelayedJob task, where #some_action is a method
133
+ # you've defined on the model, which does the work you need
134
+ model_instance.delay.some_action
135
+
136
+ # performing some work with Sidekiq
137
+ YourSidekiqWorkerClass.perform_async
132
138
  end
133
139
 
134
- [...other tasks if you have...]
140
+ [other events if you have]
135
141
 
136
142
  end
137
143
  ```
138
144
 
139
- This tells clockwork to fetch all MyScheduledTask instances from the database, and create an event for each, configured based on the instances' `frequency`, `name`, and `at` methods. It also says to reload the tasks from the database every 1.minute - we need to frequently do this as they could have changed (but you can choose a sensible reload frequency by changing the `every:` option).
145
+ This tells clockwork to fetch all `ClockworkDatabaseEvent` instances from the database, creating an internal clockwork event for each one, configured based on the instance's `frequency`, `at` and optionally `name` and `tz` methods. It also says to reload the events from the database every `1.minute`; we need pick up any changes in the database frequently (choose a sensible reload frequency by changing the `every:` option).
146
+
147
+ When one of the events is ready to be run (based on it's `frequency`, `at` and possible `tz` methods), clockwork arranges for the block passed to `sync_database_events` to be run. The above example shows how you could use either DelayedJob or Sidekiq to simply kick off a worker job. This approach is good because the ideal is to use clockwork as a simple scheduler, and avoid making it carry out any long-running tasks.
140
148
 
141
- Rails ActiveRecord models are a perfect candidate for the model class, but you could use something else. The only requirements are:
149
+ ### Your Model Classes
150
+
151
+ `ActiveRecord` models are a perfect candidate for the model class. Having said that, the only requirements are:
142
152
 
143
153
  1. the class responds to `all` returning an array of instances from the database
154
+
144
155
  2. the instances returned respond to:
145
- `frequency` returning the how frequently (in seconds) the database task should be run
146
- `name` returning the task's job name (this is what gets passed into the block above)
147
- `at` return nil or `''` if not using `:at`, or otherwise any acceptable clockwork `:at` string
148
156
 
149
- Here's an example of one way of setting up your ActiveRecord models, using Sidekiq for background tasks, and making the model class a worker:
157
+ - `id` returning a unique identifier (this is needed to track changes to event settings)
158
+
159
+ - `frequency` returning the how frequently (in seconds) the database event should be run
160
+
161
+ - `at` return nil or `''` if not using `:at`, or otherwise any acceptable clockwork `:at` string
162
+
163
+ - (optionally) `name` returning the name for the event (used to identify it in the Clcockwork output)
164
+
165
+ - (optionally) `tz` returning the timezone to use (default is the local timezone)
166
+
167
+ #### Example Setup
168
+
169
+ Here's an example of one way of setting up your ActiveRecord models:
150
170
 
151
171
  ```ruby
152
172
  # db/migrate/20140302220659_create_frequency_periods.rb
@@ -160,45 +180,30 @@ class CreateFrequencyPeriods < ActiveRecord::Migration
160
180
  end
161
181
  end
162
182
 
163
- # 20140302221102_create_my_scheduled_tasks.rb
164
- class CreateMyScheduledTasks < ActiveRecord::Migration
183
+ # 20140302221102_create_clockwork_database_events.rb
184
+ class CreateClockworkDatabaseEvents < ActiveRecord::Migration
165
185
  def change
166
- create_table :my_scheduled_tasks do |t|
186
+ create_table :clockwork_database_events do |t|
167
187
  t.integer :frequency_quantity
168
188
  t.references :frequency_period
169
189
  t.string :at
170
190
 
171
191
  t.timestamps
172
192
  end
173
- add_index :my_scheduled_tasks, :frequency_period_id
193
+ add_index :clockwork_database_events, :frequency_period_id
174
194
  end
175
195
  end
176
196
 
177
- # app/models/my_scheduled_task.rb
178
- class MyScheduledTask < ActiveRecord::Base
179
- include Sidekiq::Worker
180
-
197
+ # app/models/clockwork_database_event.rb
198
+ class ClockworkDatabaseEvent < ActiveRecord::Base
181
199
  belongs_to :frequency_period
182
- attr_accessible :frequency_quantity, :frequency_period_id, :at
200
+ attr_accessible :frequency_quantity, :frequency_period_id, :at
183
201
 
184
- # Used by clockwork to schedule how frequently this task should be run
202
+ # Used by clockwork to schedule how frequently this event should be run
185
203
  # Should be the intended number of seconds between executions
186
204
  def frequency
187
205
  frequency_quantity.send(frequency_period.name.pluralize)
188
206
  end
189
-
190
- # Used by clockwork to name this task internally for its logging
191
- # Should return a reference for this task to be used in clockwork
192
- # Include the instance ID if you want to be able to retrieve the
193
- # model instance inside the sync_database_tasks block in clock.rb
194
- def name
195
- "Database_MyScheduledTask:#{id}"
196
- end
197
-
198
- # Method required by Sidekiq
199
- def perform
200
- # the task that will be performed in the background
201
- end
202
207
  end
203
208
 
204
209
  # app/models/frequency_period.rb
@@ -215,8 +220,6 @@ end
215
220
  ...
216
221
  ```
217
222
 
218
- You could, of course, create a separate Sidekiq or DelayedJob worker class under app/workers, and simply use the model referenced by clockwork to trigger that worker to run asynchronously.
219
-
220
223
  Event Parameters
221
224
  ----------
222
225
 
@@ -297,7 +300,11 @@ Clockwork.every(1.second, 'myjob', :if => lambda { |_| true })
297
300
 
298
301
  ### :thread
299
302
 
300
- An event with `:thread => true` runs in a different thread.
303
+ In default, clockwork runs in single-process, single-thread.
304
+ If an event handler takes long time, the main routine of clockwork is blocked until it ends.
305
+ Clockwork does not misbehave, but the next event is blocked, and runs when the process is returned to the clockwork routine.
306
+
307
+ This `:thread` option is to avoid blocking. An event with `:thread => true` runs in a different thread.
301
308
 
302
309
  ```ruby
303
310
  Clockwork.every(1.day, 'run.me.in.new.thread', :thread => true)
@@ -441,12 +448,12 @@ end
441
448
  Finally, you can use tasks synchronised from a database as described in detail above:
442
449
 
443
450
  ```ruby
444
- sync_database_tasks model: MyScheduledTask, every: 1.minute do |instance_job_name|
451
+ sync_database_events model: MyEvent, every: 1.minute do |instance_job_name|
445
452
  # what to do with each instance
446
453
  end
447
454
  ```
448
455
 
449
- You can use multiple `sync_database_tasks` if you wish, so long as you use different model classes for each (ActiveRecord Single Table Inheritance could be a good idea if you're doing this).
456
+ You can use multiple `sync_database_events` if you wish, so long as you use different model classes for each (ActiveRecord Single Table Inheritance could be a good idea if you're doing this).
450
457
 
451
458
  In production
452
459
  -------------
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new do |t|
5
- t.test_files = FileList['test/*_test.rb']
5
+ t.test_files = FileList['test/**/*_test.rb']
6
6
  t.verbose = false
7
7
  end
8
8
 
data/clockwork.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "clockwork"
3
- s.version = "0.7.7"
3
+ s.version = "1.0.0"
4
4
 
5
5
  s.authors = ["Adam Wiggins", "tomykaira"]
6
6
  s.license = 'MIT'
data/clockworkd.1 ADDED
@@ -0,0 +1,62 @@
1
+ .TH CLOCKWORKD 1 "August 2014" "Ruby Gem" "clockwork"
2
+
3
+ .SH NAME
4
+ clockworkd - daemon executing clockwork scripts
5
+
6
+ .SH SYNOPSIS
7
+ \fBclockworkd\fR [-c \fIFILE\fR] [\fIOPTIONS\fR] {start|stop|restart|run}
8
+
9
+ .SH DESCRIPTION
10
+ \fBclockworkd\fR executes clockwork script as a daemon.
11
+
12
+ You will need the \fBdaemons\fR gem to use \fBclockworkd\fR. It is not automatically installed, please install by yourself.
13
+
14
+ .SH OPTIONS
15
+ .TP
16
+ \fB--pid-dir\fR=\fIDIR\fR
17
+ Alternate directory in which to store the process ids. Default is \fIGEM_LOCATION\fR/tmp.
18
+
19
+ .TP
20
+ \fB-i\fR, \fB--identifier\fR=\fISTR\fR
21
+ An identifier for the process. Default is clock file name.
22
+
23
+ .TP
24
+ \fB-l\fR, \fB--log\fR
25
+ Redirect both STDOUT and STDERR to a logfile named clockworkd[.\fIidentifier\fR].output in the pid-file directory.
26
+
27
+ .TP
28
+ \fB--log-dir\fR=\fIDIR\fR
29
+ A specific directory to put the log files into. Default location is pid directory.
30
+
31
+ .TP
32
+ \fB-m\fR, \fB--monitor\fR
33
+ Start monitor process.
34
+
35
+ .TP
36
+ \fB-c\fR, \fB--clock\fR=\fIFILE\fR
37
+ Clock .rb file. Default is \fIGEM_LOCATION\fR/clock.rb.
38
+
39
+ .TP
40
+ \fB-d\fR, \fB--dir\fR=\fIDIR\fR
41
+ Directory to change to once the process starts
42
+
43
+ .TP
44
+ \fB-h\fR, \fB--help\fR
45
+ Show help message.
46
+
47
+ .SH BUGS
48
+ If you find a bug, please create an issue \fIhttps://github.com/tomykaira/clockwork/issues\fR.
49
+
50
+ For a bug fix or a feature request, please send a pull-request. Do not forget to add tests to show how your feature works, or what bug is fixed. All existing tests and new tests must pass (TravisCI is watching).
51
+
52
+ .SH AUTHORS
53
+ Created by Adam Wiggins.
54
+
55
+ Inspired by rufus-scheduler and resque-scheduler.
56
+
57
+ Design assistance from Peter van Hardenberg and Matthew Soldo.
58
+
59
+ Patches contributed by Mark McGranaghan and Lukáš Konarovský.
60
+
61
+ .SH SEE ALSO
62
+ \fIhttps://github.com/tomykaira/clockwork\fR.
data/lib/clockwork/at.rb CHANGED
@@ -33,7 +33,7 @@ module Clockwork
33
33
  raise FailedToParse, at
34
34
  end
35
35
 
36
- attr_writer :min, :hour, :wday
36
+ attr_accessor :min, :hour, :wday
37
37
 
38
38
  def initialize(min, hour=NOT_SPECIFIED, wday=NOT_SPECIFIED)
39
39
  @min = min
@@ -48,6 +48,10 @@ module Clockwork
48
48
  (@wday == NOT_SPECIFIED or t.wday == @wday)
49
49
  end
50
50
 
51
+ def == other
52
+ @min == other.min && @hour == other.hour && @wday == other.wday
53
+ end
54
+
51
55
  private
52
56
  def valid?
53
57
  @min == NOT_SPECIFIED || (0..59).cover?(@min) &&
@@ -0,0 +1,26 @@
1
+ require_relative 'database_events/event'
2
+ require_relative 'database_events/sync_performer'
3
+ require_relative 'database_events/registry'
4
+ require_relative 'database_events/manager'
5
+
6
+ # TERMINOLOGY
7
+ #
8
+ # For clarity, we have chosen to define terms as follows for better communication in the code, and when
9
+ # discussing the database event implementation.
10
+ #
11
+ # "Event": "Native" Clockwork events, whether Clockwork::Event or Clockwork::DatabaseEvents::Event
12
+ # "Model": Database-backed model instances representing events to be created in Clockwork
13
+
14
+ module Clockwork
15
+
16
+ module Methods
17
+ def sync_database_events(options={}, &block)
18
+ DatabaseEvents::SyncPerformer.setup(options, &block)
19
+ end
20
+ end
21
+
22
+ extend Methods
23
+
24
+ module DatabaseEvents
25
+ end
26
+ end
@@ -0,0 +1,38 @@
1
+ module Clockwork
2
+
3
+ module DatabaseEvents
4
+
5
+ class Event < Clockwork::Event
6
+
7
+ attr_accessor :sync_performer, :at
8
+
9
+ def initialize(manager, period, job, block, sync_performer, options={})
10
+ super(manager, period, job, block, options)
11
+ @sync_performer = sync_performer
12
+ @sync_performer.register(self, job)
13
+ end
14
+
15
+ def name
16
+ (job.respond_to?(:name) && job.name) ? job.name : "#{job.class}:#{job.id}"
17
+ end
18
+
19
+ def to_s
20
+ name
21
+ end
22
+
23
+ def name_or_frequency_has_changed?(model)
24
+ name_has_changed?(model) || frequency_has_changed?(model)
25
+ end
26
+
27
+ private
28
+ def name_has_changed?(model)
29
+ !job.respond_to?(:name) || job.name != model.name
30
+ end
31
+
32
+ def frequency_has_changed?(model)
33
+ @period != model.frequency
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,20 @@
1
+ module Clockwork
2
+
3
+ module DatabaseEvents
4
+
5
+ class Manager < Clockwork::Manager
6
+
7
+ def unregister(event)
8
+ @events.delete(event)
9
+ end
10
+
11
+ def register(period, job, block, options)
12
+ @events << if options[:from_database]
13
+ Clockwork::DatabaseEvents::Event.new(self, period, job, (block || handler), options.fetch(:sync_performer), options)
14
+ else
15
+ Clockwork::Event.new(self, period, job, block || handler, options)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,39 @@
1
+ module Clockwork
2
+
3
+ module DatabaseEvents
4
+
5
+ class Registry
6
+
7
+ def initialize
8
+ @events = Hash.new []
9
+ end
10
+
11
+ def register(event, model)
12
+ @events[model.id] = @events[model.id] + [event]
13
+ end
14
+
15
+ def unregister(model)
16
+ unregister_by_id(model.id)
17
+ end
18
+
19
+ def unregister_by_id(id)
20
+ @events[id].each{|e| Clockwork.manager.unregister(e) }
21
+ @events.delete(id)
22
+ end
23
+
24
+ def unregister_all_except(ids)
25
+ (@events.keys - ids).each{|id| unregister_by_id(id) }
26
+ end
27
+
28
+ # all events of same id will have same frequency/name, just different ats
29
+ def event_for(model)
30
+ events_for(model).first
31
+ end
32
+
33
+ def events_for(model)
34
+ @events[model.id]
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,94 @@
1
+ require_relative '../database_events'
2
+
3
+ module Clockwork
4
+
5
+ module DatabaseEvents
6
+
7
+ class SyncPerformer
8
+
9
+ PERFORMERS = []
10
+
11
+ def self.setup(options={}, &block)
12
+ model_class = options.fetch(:model) { raise KeyError, ":model must be set to the model class" }
13
+ every = options.fetch(:every) { raise KeyError, ":every must be set to the database sync frequency" }
14
+
15
+ sync_performer = self.new(model_class, &block)
16
+
17
+ # create event that syncs clockwork events with events coming from database-backed model
18
+ Clockwork.manager.every every, "sync_database_events_for_model_#{model_class}" do
19
+ sync_performer.sync
20
+ end
21
+ end
22
+
23
+ def initialize(model_class, &proc)
24
+ @model_class = model_class
25
+ @block = proc
26
+ @database_event_registry = Registry.new
27
+
28
+ PERFORMERS << self
29
+ end
30
+
31
+ # delegates to Registry
32
+ def register(event, model)
33
+ @database_event_registry.register(event, model)
34
+ end
35
+
36
+ # Ensure clockwork events reflect events from database-backed model
37
+ # Adds any new events, modifies updated ones, and delets removed ones
38
+ def sync
39
+ model_ids_that_exist = []
40
+
41
+ @model_class.all.each do |model|
42
+ model_ids_that_exist << model.id
43
+ if are_different?(@database_event_registry.event_for(model), model)
44
+ create_or_recreate_event(model)
45
+ end
46
+ end
47
+ @database_event_registry.unregister_all_except(model_ids_that_exist)
48
+ end
49
+
50
+ private
51
+ def are_different?(event, model)
52
+ return true if event.nil?
53
+ event.name_or_frequency_has_changed?(model) || ats_have_changed?(model)
54
+ end
55
+
56
+ def ats_have_changed?(model)
57
+ model_ats = ats_array_for_event(model)
58
+ event_ats = ats_array_from_model(model)
59
+
60
+ model_ats != event_ats
61
+ end
62
+
63
+ def ats_array_for_event(model)
64
+ @database_event_registry.events_for(model).collect{|event| event.at }.compact
65
+ end
66
+
67
+ def ats_array_from_model(model)
68
+ (at_strings_for(model) || []).collect{|at| At.parse(at) }
69
+ end
70
+
71
+ def at_strings_for(model)
72
+ model.at.to_s.empty? ? nil : model.at.split(',').map(&:strip)
73
+ end
74
+
75
+ def create_or_recreate_event(model)
76
+ if @database_event_registry.event_for(model)
77
+ @database_event_registry.unregister(model)
78
+ end
79
+
80
+ options = {
81
+ :from_database => true,
82
+ :sync_performer => self,
83
+ :at => at_strings_for(model)
84
+ }
85
+
86
+ options[:tz] = model.tz if model.respond_to?(:tz)
87
+
88
+ # we pass actual model instance as the job, rather than just name
89
+ Clockwork.manager.every model.frequency, model, options, &@block
90
+ end
91
+ end
92
+
93
+ end
94
+ end