clockwork 0.7.7 → 1.0.0

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: 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