clockwork 1.3.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 66489a780e645c6b88fe4fe8548faaa80615d85c
4
- data.tar.gz: ad94d2f1a2f376d7f5931d177267b91ec6b69841
2
+ SHA256:
3
+ metadata.gz: 81357fe2cd463fcf64f263400fd180f02a2c6e14fc281c32cd5098dbe6971487
4
+ data.tar.gz: 2eec51851477a00654c88adaa7b5e4b5abac252fa808957d1f3cfafda49572f4
5
5
  SHA512:
6
- metadata.gz: 9b1409ea965916002991e9fd1a38d2453350994d027aa9b114a7f4feee4edb1a902ac07076c691d84e462fc9b697fe13989858f524184311d44d3717c7887aba
7
- data.tar.gz: 085f2739c059e90b8ea5875c0fb9b2eb161a7e09114cdd5db9a116a171fb5615db066278e65bfd641b78b1bac4cd77255438e8c27d94679d26bda66d4e2aa518
6
+ metadata.gz: ee46e6ec0c2da3bda8c38ab24ad29b50d01d058e0e1580c6cc2c0f1a5b000a649453ddc780b5d007b37c25f3a8380303f00344b9c171f5febb2742b11127c25c
7
+ data.tar.gz: 1d64979d6c6a494963bf27283aff3da36345441be828c0496fc25858e6c0834d87aaf8283ea3cf7d3bf396dea30e0d27c681b2ea52ab8e04681440491ddbef20
data/.travis.yml CHANGED
@@ -1,13 +1,30 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  cache: bundler
4
+ before_install:
5
+ - gem update --system
6
+ # This is required to support ActiveSupport 4
7
+ # https://docs.travis-ci.com/user/languages/ruby/#bundler-20
8
+ - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
9
+ - gem install bundler -v '< 2'
4
10
  rvm:
5
- - 1.9.3
6
- - 2.0.0
7
- - 2.1
8
- - 2.2
9
- - jruby-19mode
10
- - # rbx-2.2.7
11
+ - 2.3.8
12
+ - 2.4.9
13
+ - 2.5.7
14
+ - 2.6.5
15
+ - 2.7.0
16
+ - 3.0.2
17
+ - jruby-9.1.17.0
18
+ - jruby-9.2.11.0
11
19
  gemfile:
12
- - gemfiles/activesupport3.gemfile
13
20
  - gemfiles/activesupport4.gemfile
21
+ - gemfiles/activesupport5.gemfile
22
+ - gemfiles/activesupport6.gemfile
23
+ matrix:
24
+ exclude:
25
+ - rvm: 2.3.8
26
+ gemfile: gemfiles/activesupport6.gemfile
27
+ - rvm: 2.4.9
28
+ gemfile: gemfiles/activesupport6.gemfile
29
+ - rvm: jruby-9.1.17.0
30
+ gemfile: gemfiles/activesupport6.gemfile
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ ## 3.0.0 ##
2
+
3
+ * BREAKING CHANGE: drop Rubinius support
4
+ * add Ruby 3.0 support (65d71f)
5
+ * fix a bug with OpenStruct (15eb030)
6
+ * allow `:skip_first_run` in database events (429bc0a)
7
+ * add rescue to prevent hung up by exception (cc1b7c9)
8
+ * fix CI errors (16b4e19 & e4480ea)
9
+ * fix a compatibility bug with Rails 7 (5907bc7)
10
+ * Add "Finished" log with duration and error summary (66419ab)
11
+
12
+ ## 2.0.4 ##
13
+
14
+ * Reverts the breaking changes in PR #18 that went out in patch 2.0.3
15
+
16
+ *Javier Julio*
17
+
18
+ ## 2.0.3 (February 15, 2018) ##
19
+
20
+ * [See the version release](https://github.com/Rykian/clockwork/releases) for the commits that were included.
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
- Clockwork - a clock process to replace cron [![Build Status](https://secure.travis-ci.org/tomykaira/clockwork.png?branch=master)](http://travis-ci.org/tomykaira/clockwork) [![Dependency Status](https://gemnasium.com/tomykaira/clockwork.png)](https://gemnasium.com/tomykaira/clockwork)
1
+ Clockwork - a clock process to replace cron [![Build Status](https://api.travis-ci.com/Rykian/clockwork.svg?branch=master)](https://travis-ci.org/Rykian/clockwork)
2
2
  ===========================================
3
3
 
4
4
  Cron is non-ideal for running scheduled application tasks, especially in an app
5
- deployed to multiple machines. [More details.](http://adam.heroku.com/past/2010/4/13/rethinking_cron/)
5
+ deployed to multiple machines. [More details.](http://adam.herokuapp.com/past/2010/4/13/rethinking_cron/)
6
6
 
7
7
  Clockwork is a cron replacement. It runs as a lightweight, long-running Ruby
8
8
  process which sits alongside your web processes (Mongrel/Thin) and your worker
@@ -18,6 +18,8 @@ Create clock.rb:
18
18
 
19
19
  ```ruby
20
20
  require 'clockwork'
21
+ require 'active_support/time' # Allow numeric durations (eg: 1.minutes)
22
+
21
23
  module Clockwork
22
24
  handler do |job|
23
25
  puts "Running #{job}"
@@ -58,8 +60,8 @@ Quickstart for Heroku
58
60
 
59
61
  Clockwork fits well with heroku's cedar stack.
60
62
 
61
- Consider to use [clockwork-init.sh](https://gist.github.com/1312172) to create
62
- a new project for heroku.
63
+ Consider using [clockwork-init.sh](https://gist.github.com/tomykaira/1312172) to create
64
+ a new project for Heroku.
63
65
 
64
66
  Use with queueing
65
67
  -----------------
@@ -69,8 +71,8 @@ to do the work. It avoids locking by running as a single process, but this
69
71
  makes it impossible to parallelize. For doing the work, you should be using a
70
72
  job queueing system, such as
71
73
  [Delayed Job](http://www.therailsway.com/2009/7/22/do-it-later-with-delayed-job),
72
- [Beanstalk/Stalker](http://adam.heroku.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/),
73
- [RabbitMQ/Minion](http://adam.heroku.com/past/2009/9/28/background_jobs_with_rabbitmq_and_minion/),
74
+ [Beanstalk/Stalker](http://adam.herokuapp.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/),
75
+ [RabbitMQ/Minion](http://adam.herokuapp.com/past/2009/9/28/background_jobs_with_rabbitmq_and_minion/),
74
76
  [Resque](http://github.com/blog/542-introducing-resque), or
75
77
  [Sidekiq](https://github.com/mperham/sidekiq). This design allows a
76
78
  simple clock process with no locks, but also offers near infinite horizontal
@@ -80,6 +82,7 @@ For example, if you're using Beanstalk/Stalker:
80
82
 
81
83
  ```ruby
82
84
  require 'stalker'
85
+ require 'active_support/time'
83
86
 
84
87
  module Clockwork
85
88
  handler { |job| Stalker.enqueue(job) }
@@ -92,7 +95,7 @@ end
92
95
  Using a queueing system which doesn't require that your full application be
93
96
  loaded is preferable, because the clock process can keep a tiny memory
94
97
  footprint. If you're using DJ or Resque, however, you can go ahead and load
95
- your full application enviroment, and use per-event blocks to call DJ or Resque
98
+ your full application environment, and use per-event blocks to call DJ or Resque
96
99
  enqueue methods. For example, with DJ/Rails:
97
100
 
98
101
  ```ruby
@@ -142,27 +145,33 @@ module Clockwork
142
145
  end
143
146
  ```
144
147
 
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).
148
+ This tells clockwork to fetch all `ClockworkDatabaseEvent` instances from the database, and create an internal clockwork event for each one. Each clockwork event will be configured based on the instance's `frequency` and, optionally, its `at`, `if?`, `ignored_attributes`, `name`, and, `tz` methods. The code above 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
149
 
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.
150
+ When one of the events is ready to be run (based on it's `frequency`, and possible `at`, `if?`, `ignored attributes`, and `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 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.
148
151
 
149
152
  ### Your Model Classes
150
153
 
151
154
  `ActiveRecord` models are a perfect candidate for the model class. Having said that, the only requirements are:
152
155
 
153
- 1. the class responds to `all` returning an array of instances from the database
156
+ 1. the class responds to `all` returning an array of instances from the database
157
+
158
+ 2. the instances returned respond to:
159
+ - `id` returning a unique identifier (this is needed to track changes to event settings)
160
+ - `frequency` returning the how frequently (in seconds) the database event should be run
161
+
162
+ - `attributes` returning a hash of [attribute name] => [attribute value] values (or really anything that we can use store on registering the event, and then compare again to see if the state has changed later)
154
163
 
155
- 2. the instances returned respond to:
164
+ - `at` *(optional)* return any acceptable clockwork `:at` string
156
165
 
157
- - `id` returning a unique identifier (this is needed to track changes to event settings)
166
+ - `name` *(optional)* returning the name for the event (used to identify it in the Clockwork output)
158
167
 
159
- - `frequency` returning the how frequently (in seconds) the database event should be run
168
+ - `if?`*(optional)* returning either true or false, depending on whether the database event should run at the given time (this method will be passed the time as a parameter, much like the standard clockwork `:if`)
160
169
 
161
- - `at` return nil or `''` if not using `:at`, or otherwise any acceptable clockwork `:at` string
170
+ - `ignored_attributes` *(optional)* returning an array of model attributes (as symbols) to ignore when determining whether the database event has been modified since our last run
162
171
 
163
- - (optionally) `name` returning the name for the event (used to identify it in the Clcockwork output)
172
+ - `tz` *(optional)* returning the timezone to use (default is the local timezone)
164
173
 
165
- - (optionally) `tz` returning the timezone to use (default is the local timezone)
174
+ - `skip_first_run` *(optional)* self explanatory, see [dedicated section](#skip_first_run)
166
175
 
167
176
  #### Example Setup
168
177
 
@@ -220,6 +229,50 @@ end
220
229
  ...
221
230
  ```
222
231
 
232
+ #### Example use of `if?`
233
+
234
+ Database events support the ability to run events if certain conditions are met. This can be used to only run events on a given day, week, or month, or really any criteria you could conceive. Best of all, these criteria e.g. which day to
235
+ run it on can be attributes on your Model, and therefore change dynamically as you change the Model in the database.
236
+
237
+ So for example, if you had a Model that had a `day` and `month` integer attribute, you could specify that the Database event should only run on a particular day of a particular month as follows:
238
+
239
+ ```ruby
240
+ # app/models/clockwork_database_event.rb
241
+ class ClockworkDatabaseEvent < ActiveRecord::Base
242
+
243
+ ...
244
+
245
+ def if?(time)
246
+ time.day == day && time.month == month
247
+ end
248
+
249
+ ...
250
+ end
251
+ ```
252
+
253
+ #### Example use of `ignored_attributes`
254
+
255
+ Clockwork compares all attributes of the model between runs to determine if the model has changed, and if it has, it runs the event if all other conditions are met.
256
+
257
+ However, in certain cases, you may want to store additional attributes in your model that you don't want to affect whether a database event is executed prior to its next interval.
258
+
259
+ So for example, you may update an attribute of your model named `last_scheduled_at` on each run to track the last time it was successfully scheduled. You can tell Clockwork to ignore that attribute in its comparison as follows:
260
+
261
+ ```ruby
262
+ # app/models/clockwork_database_event.rb
263
+ class ClockworkDatabaseEvent < ActiveRecord::Base
264
+
265
+ ...
266
+
267
+ def ignored_attributes
268
+ [ :last_scheduled_at ]
269
+ end
270
+
271
+ ...
272
+ end
273
+ ```
274
+
275
+
223
276
  Event Parameters
224
277
  ----------
225
278
 
@@ -279,7 +332,8 @@ If this is a problem, please use the `:thread` option to prevent the long runnin
279
332
  ```ruby
280
333
  every(1.day, 'reminders.send', :at => '00:00', :tz => 'UTC')
281
334
  # Runs the job each day at midnight, UTC.
282
- # The value for :tz can be anything supported by [TZInfo](http://tzinfo.rubyforge.org/)
335
+ # The value for :tz can be anything supported by [TZInfo](https://github.com/tzinfo/tzinfo)
336
+ # Using the 'tzinfo' gem, run TZInfo::Timezone.all_identifiers to get a list of acceptable identifiers.
283
337
  ```
284
338
 
285
339
  ### :if
@@ -315,6 +369,19 @@ Clockwork.every(1.day, 'run.me.in.new.thread', :thread => true)
315
369
 
316
370
  If a job is long-running or IO-intensive, this option helps keep the clock precise.
317
371
 
372
+ ### :skip_first_run
373
+
374
+ Normally, a clockwork process that is defined to run in a specified period will run at startup.
375
+ This is sometimes undesired behaviour, if the action being run relies on other processes booting which may be slower than clock.
376
+ To avoid this problem, `:skip_first_run` can be used.
377
+
378
+ ```ruby
379
+ Clockwork.every(5.minutes, 'myjob', :skip_first_run => true)
380
+ ```
381
+
382
+ The above job will not run at initial boot, and instead run every 5 minutes after boot.
383
+
384
+
318
385
  Configuration
319
386
  -----------------------
320
387
 
@@ -331,7 +398,6 @@ Clockwork wakes up once a second and performs its duties. To change the number o
331
398
  sleeps, set the `sleep_timeout` configuration option as shown below in the example.
332
399
 
333
400
  From 1.1.0, Clockwork does not accept `sleep_timeout` less than 1 seconds.
334
- This restriction is introduced to solve more severe bug [#135](https://github.com/tomykaira/clockwork/pull/135).
335
401
 
336
402
  ### :tz
337
403
 
@@ -437,8 +503,8 @@ every(1.day, 'check.leap.year') do
437
503
  end
438
504
  ```
439
505
 
440
- In addition, Clockwork also supports `:before_tick` and `after_tick` callbacks.
441
- They are optional, and run every tick (a tick being whatever your `:sleep_timeout`
506
+ In addition, Clockwork also supports `:before_tick`, `:after_tick`, `:before_run`, and `:after_run` callbacks.
507
+ All callbacks are optional. All `before` callbacks must return a truthy value otherwise the job will not run. The `tick` callbacks run every tick (a tick being whatever your `:sleep_timeout`
442
508
  is set to, default is 1 second):
443
509
 
444
510
  ```ruby
@@ -449,6 +515,14 @@ end
449
515
  on(:after_tick) do
450
516
  puts "tock"
451
517
  end
518
+
519
+ on(:before_run) do |event, t|
520
+ puts "job_started: #{event}"
521
+ end
522
+
523
+ on(:after_run) do |event, t|
524
+ puts "job_finished: #{event}"
525
+ end
452
526
  ```
453
527
 
454
528
  Finally, you can use tasks synchronised from a database as described in detail above:
@@ -490,10 +564,17 @@ clockworkd -c YOUR_CLOCK.rb start
490
564
 
491
565
  For more details, you can run `clockworkd -h`.
492
566
 
567
+ Integration Testing
568
+ -------------------
569
+
570
+ You could take a look at:
571
+ * [clockwork-mocks](https://github.com/dpoetzsch/clockwork-mocks) that helps with running scheduled tasks during integration testing.
572
+ * [clockwork-test](https://github.com/kevin-j-m/clockwork-test) which ensures that tasks are triggered at the right time
573
+
493
574
  Issues and Pull requests
494
575
  ------------------------
495
576
 
496
- If you find a bug, please create an issue - [Issues · tomykaira/clockwork](https://github.com/tomykaira/clockwork/issues).
577
+ If you find a bug, please create an issue - [Issues · Rykian/clockwork](https://github.com/Rykian/clockwork/issues).
497
578
 
498
579
  For a bug fix or a feature request, please send a pull-request.
499
580
  Do not forget to add tests to show how your feature works, or what bug is fixed.
@@ -509,8 +590,8 @@ Use cases
509
590
 
510
591
  Feel free to add your idea or experience and send a pull-request.
511
592
 
512
- - [Sending errors to Airbrake](https://github.com/tomykaira/clockwork/issues/58)
513
- - [Read events from a database](https://github.com/tomykaira/clockwork/issues/25)
593
+ - Sending errors to Airbrake
594
+ - Read events from a database
514
595
 
515
596
  Meta
516
597
  ----
@@ -525,4 +606,4 @@ Patches contributed by Mark McGranaghan and Lukáš Konarovský
525
606
 
526
607
  Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
527
608
 
528
- http://github.com/tomykaira/clockwork
609
+ https://github.com/Rykian/clockwork
data/bin/clockwork CHANGED
@@ -4,16 +4,11 @@ STDERR.sync = STDOUT.sync = true
4
4
 
5
5
  require 'clockwork'
6
6
 
7
- usage = 'clockwork <clock.rb>'
7
+ usage = 'Usage: clockwork <clock.rb>'
8
8
  file = ARGV.shift or abort usage
9
9
 
10
10
  file = "./#{file}" unless file.match(/^[\/.]/)
11
11
 
12
12
  require file
13
13
 
14
- trap('INT') do
15
- puts "\rExiting"
16
- exit
17
- end
18
-
19
14
  Clockwork::run
data/bin/clockworkd CHANGED
@@ -4,6 +4,7 @@ STDERR.sync = STDOUT.sync = true
4
4
 
5
5
  require 'clockwork'
6
6
  require 'optparse'
7
+ require 'pathname'
7
8
 
8
9
  begin
9
10
  require 'daemons'
@@ -45,7 +46,7 @@ opts = OptionParser.new do |opts|
45
46
  end
46
47
  opts.on('-c', '--clock=FILE',"Clock .rb file. Default is #{@options[:file]}.") do |clock_file|
47
48
  @options[:file] = clock_file
48
- @options[:file] = "./#{@options[:file]}" unless @options[:file].match(/^[\/.]/)
49
+ @options[:file] = "./#{@options[:file]}" unless Pathname.new(@options[:file]).absolute?
49
50
  @options[:file] = File.expand_path(@options[:file])
50
51
  end
51
52
  opts.on('-d', '--dir=DIR', 'Directory to change to once the process starts') do |dir|
data/clockwork.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "clockwork"
3
- s.version = "1.3.1"
3
+ s.version = "3.0.0"
4
4
 
5
5
  s.authors = ["Adam Wiggins", "tomykaira"]
6
6
  s.license = 'MIT'
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.extra_rdoc_files = [
10
10
  "README.md"
11
11
  ]
12
- s.homepage = "http://github.com/tomykaira/clockwork"
12
+ s.homepage = "http://github.com/Rykian/clockwork"
13
13
  s.summary = "A scheduler process to replace cron."
14
14
 
15
15
  s.files = `git ls-files`.split($/)
@@ -20,9 +20,9 @@ Gem::Specification.new do |s|
20
20
  s.add_dependency(%q<tzinfo>)
21
21
  s.add_dependency(%q<activesupport>)
22
22
 
23
- s.add_development_dependency "bundler", "~> 1.3"
24
23
  s.add_development_dependency "rake"
25
24
  s.add_development_dependency "daemons"
26
25
  s.add_development_dependency "minitest", "~> 5.8"
27
26
  s.add_development_dependency "mocha"
27
+ s.add_development_dependency "test-unit"
28
28
  end
@@ -1,11 +1,5 @@
1
1
  source 'https://rubygems.org'
2
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'
3
+ gem 'activesupport', '~> 4.2'
4
+ gem 'minitest', '~> 5.0'
11
5
  gemspec :path=>"../"
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', '~> 5.0'
4
+ gem 'minitest', '~> 5.0'
5
+ gemspec :path=>"../"
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'activesupport', '~> 6.0'
4
+ gem 'minitest', '~> 5.0'
5
+ gemspec :path=>"../"
@@ -4,12 +4,13 @@ module Clockwork
4
4
 
5
5
  class Event < Clockwork::Event
6
6
 
7
- attr_accessor :event_store, :at
7
+ attr_accessor :event_store, :model_attributes
8
8
 
9
- def initialize(manager, period, job, block, event_store, options={})
9
+ def initialize(manager, period, job, block, event_store, model_attributes, options={})
10
10
  super(manager, period, job, block, options)
11
11
  @event_store = event_store
12
12
  @event_store.register(self, job)
13
+ @model_attributes = model_attributes
13
14
  end
14
15
 
15
16
  def name
@@ -14,9 +14,14 @@ module Clockwork
14
14
  def has_changed?(model)
15
15
  return true if event.nil?
16
16
 
17
- (has_name? && name != model.name) ||
18
- frequency != model.frequency ||
19
- ats != model_ats(model)
17
+ ignored_attributes = model.ignored_attributes if model.respond_to?(:ignored_attributes)
18
+ ignored_attributes ||= []
19
+
20
+ model_attributes = model.attributes.select do |k, _|
21
+ not ignored_attributes.include?(k.to_sym)
22
+ end
23
+
24
+ event.model_attributes != model_attributes
20
25
  end
21
26
 
22
27
  def unregister
@@ -27,33 +32,12 @@ module Clockwork
27
32
 
28
33
  attr_reader :events, :manager
29
34
 
35
+ # All events in the same collection (for a model instance) are equivalent
36
+ # so we can use any of them. Only their @at variable will be different,
37
+ # but we don't care about that here.
30
38
  def event
31
39
  events.first
32
40
  end
33
-
34
- def has_name?
35
- event.job_has_name?
36
- end
37
-
38
- def name
39
- event.name
40
- end
41
-
42
- def frequency
43
- event.frequency
44
- end
45
-
46
- def ats
47
- events.collect(&:at).compact
48
- end
49
-
50
- def model_ats(model)
51
- at_strings_for(model).collect{|at| At.parse(at) }
52
- end
53
-
54
- def at_strings_for(model)
55
- model.at.to_s.split(',').map(&:strip)
56
- end
57
41
  end
58
42
  end
59
43
  end
@@ -112,10 +112,20 @@ module Clockwork
112
112
  options = {
113
113
  :from_database => true,
114
114
  :synchronizer => self,
115
- :at => at_strings_for(model)
115
+ :ignored_attributes => [],
116
116
  }
117
117
 
118
+ options[:at] = at_strings_for(model) if model.respond_to?(:at)
119
+ options[:if] = ->(time){ model.if?(time) } if model.respond_to?(:if?)
118
120
  options[:tz] = model.tz if model.respond_to?(:tz)
121
+ options[:ignored_attributes] = model.ignored_attributes if model.respond_to?(:ignored_attributes)
122
+ options[:skip_first_run] = model.skip_first_run if model.respond_to?(:skip_first_run)
123
+
124
+ # store the state of the model at time of registering so we can
125
+ # easily compare and determine if state has changed later
126
+ options[:model_attributes] = model.attributes.select do |k, v|
127
+ not options[:ignored_attributes].include?(k.to_sym)
128
+ end
119
129
 
120
130
  options
121
131
  end
@@ -10,7 +10,11 @@ module Clockwork
10
10
 
11
11
  def register(period, job, block, options)
12
12
  @events << if options[:from_database]
13
- Clockwork::DatabaseEvents::Event.new(self, period, job, (block || handler), options.fetch(:synchronizer), options)
13
+ synchronizer = options.fetch(:synchronizer)
14
+ model_attributes = options.fetch(:model_attributes)
15
+
16
+ Clockwork::DatabaseEvents::Event.
17
+ new(self, period, job, (block || handler), synchronizer, model_attributes, options)
14
18
  else
15
19
  Clockwork::Event.new(self, period, job, block || handler, options)
16
20
  end
@@ -1,5 +1,3 @@
1
- require_relative '../database_events'
2
-
3
1
  module Clockwork
4
2
 
5
3
  module DatabaseEvents
@@ -8,11 +8,12 @@ module Clockwork
8
8
  @period = period
9
9
  @job = job
10
10
  @at = At.parse(options[:at])
11
- @last = nil
12
11
  @block = block
13
12
  @if = options[:if]
14
13
  @thread = options.fetch(:thread, @manager.config[:thread])
15
14
  @timezone = options.fetch(:tz, @manager.config[:tz])
15
+ @skip_first_run = options[:skip_first_run]
16
+ @last = @skip_first_run ? convert_timezone(Time.now) : nil
16
17
  end
17
18
 
18
19
  def convert_timezone(t)
@@ -21,7 +22,10 @@ module Clockwork
21
22
 
22
23
  def run_now?(t)
23
24
  t = convert_timezone(t)
24
- elapsed_ready(t) and (@at.nil? or @at.ready?(t)) and (@if.nil? or @if.call(t))
25
+ return false unless elapsed_ready?(t)
26
+ return false unless run_at?(t)
27
+ return false unless run_if?(t)
28
+ true
25
29
  end
26
30
 
27
31
  def thread?
@@ -51,16 +55,33 @@ module Clockwork
51
55
 
52
56
  private
53
57
  def execute
58
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
+ error = nil
60
+
54
61
  @block.call(@job, @last)
55
62
  rescue => e
63
+ error = e
56
64
  @manager.log_error e
57
65
  @manager.handle_error e
66
+ ensure
67
+ finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
68
+ duration = ((finish - start) * 1000).round # milliseconds
69
+
70
+ @manager.log "Finished '#{self}' duration_ms=#{duration} error=#{error.inspect}"
58
71
  end
59
72
 
60
- def elapsed_ready(t)
73
+ def elapsed_ready?(t)
61
74
  @last.nil? || (t - @last.to_i).to_i >= @period
62
75
  end
63
76
 
77
+ def run_at?(t)
78
+ @at.nil? || @at.ready?(t)
79
+ end
80
+
81
+ def run_if?(t)
82
+ @if.nil? || @if.call(t)
83
+ end
84
+
64
85
  def validate_if_option(if_option)
65
86
  if if_option && !if_option.respond_to?(:call)
66
87
  raise ArgumentError.new(':if expects a callable object, but #{if_option} does not respond to call')
@@ -9,6 +9,9 @@ module Clockwork
9
9
  @callbacks = {}
10
10
  @config = default_configuration
11
11
  @handler = nil
12
+ @mutex = Mutex.new
13
+ @condvar = ConditionVariable.new
14
+ @finish = false
12
15
  end
13
16
 
14
17
  def thread_available?
@@ -34,7 +37,7 @@ module Clockwork
34
37
 
35
38
  def error_handler(&block)
36
39
  @error_handler = block if block_given?
37
- @error_handler
40
+ @error_handler if instance_variable_defined?("@error_handler")
38
41
  end
39
42
 
40
43
  def on(event, options={}, &block)
@@ -60,10 +63,68 @@ module Clockwork
60
63
 
61
64
  def run
62
65
  log "Starting clock for #{@events.size} events: [ #{@events.map(&:to_s).join(' ')} ]"
63
- loop do
64
- tick
65
- interval = config[:sleep_timeout] - Time.now.subsec + 0.001
66
- sleep(interval) if interval > 0
66
+
67
+ sig_read, sig_write = IO.pipe
68
+
69
+ (%w[INT TERM HUP] & Signal.list.keys).each do |sig|
70
+ trap sig do
71
+ sig_write.puts(sig)
72
+ end
73
+ end
74
+
75
+ run_tick_loop
76
+
77
+ while io = IO.select([sig_read])
78
+ sig = io.first[0].gets.chomp
79
+ handle_signal(sig)
80
+ end
81
+ end
82
+
83
+ def handle_signal(sig)
84
+ logger.debug "Got #{sig} signal"
85
+ case sig
86
+ when 'INT'
87
+ shutdown
88
+ when 'TERM'
89
+ # Heroku sends TERM signal, and waits 10 seconds before exit
90
+ graceful_shutdown
91
+ when 'HUP'
92
+ graceful_shutdown
93
+ end
94
+ end
95
+
96
+ def shutdown
97
+ logger.info 'Shutting down'
98
+ stop_tick_loop
99
+ exit(0)
100
+ end
101
+
102
+ def graceful_shutdown
103
+ logger.info 'Gracefully shutting down'
104
+ stop_tick_loop
105
+ wait_tick_loop_finishes
106
+ exit(0)
107
+ end
108
+
109
+ def stop_tick_loop
110
+ @finish = true
111
+ end
112
+
113
+ def wait_tick_loop_finishes
114
+ @mutex.synchronize do # wait by synchronize
115
+ @condvar.signal
116
+ end
117
+ end
118
+
119
+ def run_tick_loop
120
+ Thread.new do
121
+ @mutex.synchronize do
122
+ until @finish
123
+ tick
124
+ interval = config[:sleep_timeout] - Time.now.subsec + 0.001
125
+ @condvar.wait(@mutex, interval) if interval > 0
126
+ end
127
+ end
67
128
  end
68
129
  end
69
130
 
@@ -81,6 +142,10 @@ module Clockwork
81
142
  events
82
143
  end
83
144
 
145
+ def logger
146
+ config[:logger]
147
+ end
148
+
84
149
  def log_error(e)
85
150
  config[:logger].error(e)
86
151
  end
@@ -95,7 +160,14 @@ module Clockwork
95
160
 
96
161
  private
97
162
  def events_to_run(t)
98
- @events.select{ |event| event.run_now?(t) }
163
+ @events.select do |event|
164
+ begin
165
+ event.run_now?(t)
166
+ rescue => e
167
+ log_error(e)
168
+ false
169
+ end
170
+ end
99
171
  end
100
172
 
101
173
  def register(period, job, block, options)
data/lib/clockwork.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'logger'
2
+ require 'active_support'
2
3
  require 'active_support/time'
3
4
 
4
5
  require 'clockwork/at'
data/test/at_test.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.expand_path('../../lib/clockwork', __FILE__)
2
2
  require "minitest/autorun"
3
- require 'mocha/setup'
3
+ require 'mocha/minitest'
4
4
  require 'time'
5
5
  require 'active_support/time'
6
6
 
@@ -1,6 +1,6 @@
1
1
  require File.expand_path('../../lib/clockwork', __FILE__)
2
2
  require 'minitest/autorun'
3
- require 'mocha/setup'
3
+ require 'mocha/minitest'
4
4
 
5
5
  describe Clockwork do
6
6
  before do
@@ -9,6 +9,7 @@ describe Clockwork do
9
9
  config[:sleep_timeout] = 0
10
10
  config[:logger] = Logger.new(@log_output)
11
11
  end
12
+ IO.stubs(:select)
12
13
  end
13
14
 
14
15
  after do
@@ -21,11 +22,12 @@ describe Clockwork do
21
22
  run = job == 'myjob'
22
23
  end
23
24
  Clockwork.every(1.minute, 'myjob')
24
- Clockwork.manager.expects(:loop).yields.then.returns
25
+ Clockwork.manager.stubs(:run_tick_loop).returns(Clockwork.manager.tick)
25
26
  Clockwork.run
26
27
 
27
28
  assert run
28
29
  assert @log_output.string.include?('Triggering')
30
+ assert @log_output.string.include?('Finished')
29
31
  end
30
32
 
31
33
  it 'should log event correctly' do
@@ -34,10 +36,25 @@ describe Clockwork do
34
36
  run = job == 'an event'
35
37
  end
36
38
  Clockwork.every(1.minute, 'an event')
37
- Clockwork.manager.expects(:loop).yields.then.returns
39
+ Clockwork.manager.stubs(:run_tick_loop).returns(Clockwork.manager.tick)
38
40
  Clockwork.run
39
41
  assert run
40
42
  assert @log_output.string.include?("Triggering 'an event'")
43
+ assert_match /Finished 'an event' duration_ms=\d+ error=nil/, @log_output.string
44
+ end
45
+
46
+ it 'should log exceptions' do
47
+ run = false
48
+ Clockwork.handler do |job|
49
+ run = job == 'an event'
50
+ raise 'boom'
51
+ end
52
+ Clockwork.every(1.minute, 'an event')
53
+ Clockwork.manager.stubs(:run_tick_loop).returns(Clockwork.manager.tick)
54
+ Clockwork.run
55
+ assert run
56
+ assert @log_output.string.include?("Triggering 'an event'")
57
+ assert_match /Finished 'an event' duration_ms=\d+ error=#<RuntimeError: boom>/, @log_output.string
41
58
  end
42
59
 
43
60
  it 'should pass event without modification to handler' do
@@ -47,7 +64,7 @@ describe Clockwork do
47
64
  run = job == event_object
48
65
  end
49
66
  Clockwork.every(1.minute, event_object)
50
- Clockwork.manager.expects(:loop).yields.then.returns
67
+ Clockwork.manager.stubs(:run_tick_loop).returns(Clockwork.manager.tick)
51
68
  Clockwork.run
52
69
  assert run
53
70
  end
@@ -59,14 +76,14 @@ describe Clockwork do
59
76
  config[:sleep_timeout] = 0
60
77
  config[:logger] = Logger.new(@log_output)
61
78
  end
62
- Clockwork.manager.expects(:loop).yields.then.returns
79
+ Clockwork.manager.stubs(:run_tick_loop).returns(Clockwork.manager.tick)
63
80
  Clockwork.run
64
81
  assert @log_output.string.include?('0 events')
65
82
  end
66
83
 
67
84
  it 'should pass all arguments to every' do
68
85
  Clockwork.every(1.second, 'myjob', if: lambda { |_| false }) { }
69
- Clockwork.manager.expects(:loop).yields.then.returns
86
+ Clockwork.manager.stubs(:run_tick_loop).returns(Clockwork.manager.tick)
70
87
  Clockwork.run
71
88
  assert @log_output.string.include?('1 events')
72
89
  assert !@log_output.string.include?('Triggering')
@@ -77,7 +94,7 @@ describe Clockwork do
77
94
  module ::Clockwork
78
95
  every(1.second, 'myjob') { $called = true }
79
96
  end
80
- Clockwork.manager.expects(:loop).yields.then.returns
97
+ Clockwork.manager.stubs(:run_tick_loop).returns(Clockwork.manager.tick)
81
98
  Clockwork.run
82
99
  assert $called
83
100
  end
@@ -1,6 +1,7 @@
1
1
  require "minitest/autorun"
2
2
  require 'clockwork/database_events/event_store'
3
3
  require 'clockwork/database_events/event_collection'
4
+ require 'ostruct'
4
5
 
5
6
  describe Clockwork::DatabaseEvents::EventStore do
6
7
 
@@ -23,9 +23,13 @@ module ActiveRecordFake
23
23
  set_attribute_values_from_options options
24
24
  end
25
25
 
26
+ def attributes
27
+ Hash[instance_variables.map { |name| [name, instance_variable_get(name)] } ]
28
+ end
29
+
26
30
  module ClassMethods
27
- def create *args
28
- new *args
31
+ def create(*args)
32
+ new(*args)
29
33
  end
30
34
 
31
35
  def delete_all
@@ -1,5 +1,5 @@
1
1
  require "minitest/autorun"
2
- require 'mocha/setup'
2
+ require 'mocha/minitest'
3
3
  require 'time'
4
4
  require 'active_support/time'
5
5
 
@@ -10,8 +10,6 @@ require_relative 'test_helpers'
10
10
  describe Clockwork::DatabaseEvents::Synchronizer do
11
11
  before do
12
12
  @now = Time.now
13
- DatabaseEventModel.delete_all
14
- DatabaseEventModel2.delete_all
15
13
 
16
14
  Clockwork.manager = @manager = Clockwork::DatabaseEvents::Manager.new
17
15
  class << @manager
@@ -21,6 +19,9 @@ describe Clockwork::DatabaseEvents::Synchronizer do
21
19
 
22
20
  after do
23
21
  Clockwork.clear!
22
+ DatabaseEventModel.delete_all
23
+ DatabaseEventModel2.delete_all
24
+ DatabaseEventModelWithIf.delete_all
24
25
  end
25
26
 
26
27
  describe "setup" do
@@ -254,6 +255,37 @@ describe Clockwork::DatabaseEvents::Synchronizer do
254
255
  end
255
256
  end
256
257
 
258
+ describe "with model that responds to `if?`" do
259
+
260
+ before do
261
+ @events_run = []
262
+ end
263
+
264
+ describe "when model.if? is true" do
265
+ it 'runs' do
266
+ DatabaseEventModelWithIf.create(:if_state => true, :frequency => 10)
267
+ setup_sync(model: DatabaseEventModelWithIf, :every => 1.minute, :events_run => @events_run)
268
+
269
+ tick_at(@now, :and_every_second_for => 9.seconds)
270
+
271
+ assert_equal 1, @events_run.length
272
+ end
273
+ end
274
+
275
+ describe "when model.if? is false" do
276
+ it 'does not run' do
277
+ DatabaseEventModelWithIf.create(:if_state => false, :frequency => 10, :name => 'model with if?')
278
+ setup_sync(model: DatabaseEventModelWithIf, :every => 1.minute, :events_run => @events_run)
279
+
280
+ tick_at(@now, :and_every_second_for => 1.minute)
281
+
282
+ # require 'byebug'
283
+ # byebug if events_run.length > 0
284
+ assert_equal 0, @events_run.length
285
+ end
286
+ end
287
+ end
288
+
257
289
  describe "with task that responds to `tz`" do
258
290
  before do
259
291
  @events_run = []
@@ -36,23 +36,60 @@ end
36
36
 
37
37
  class DatabaseEventModel
38
38
  include ActiveRecordFake
39
- attr_accessor :name, :frequency, :at, :tz
39
+ attr_accessor :frequency, :at, :tz
40
40
 
41
41
  def name
42
- @name || "#{self.class}:#{id}"
42
+ if instance_variable_defined?("@name")
43
+ @name
44
+ else
45
+ "#{self.class}:#{id}"
46
+ end
47
+ end
48
+
49
+ def name=(name)
50
+ @name = name
43
51
  end
44
52
  end
45
53
 
46
54
  class DatabaseEventModel2
47
55
  include ActiveRecordFake
48
- attr_accessor :name, :frequency, :at, :tz
56
+ attr_accessor :frequency, :at, :tz
49
57
 
50
58
  def name
51
- @name || "#{self.class}:#{id}"
59
+ if instance_variable_defined?("@name")
60
+ @name
61
+ else
62
+ "#{self.class}:#{id}"
63
+ end
64
+ end
65
+
66
+ def name=(name)
67
+ @name = name
52
68
  end
53
69
  end
54
70
 
55
71
  class DatabaseEventModelWithoutName
56
72
  include ActiveRecordFake
57
73
  attr_accessor :frequency, :at
74
+ end
75
+
76
+ class DatabaseEventModelWithIf
77
+ include ActiveRecordFake
78
+ attr_accessor :frequency, :at, :tz, :if_state
79
+
80
+ def name
81
+ if instance_variable_defined?("@name")
82
+ @name
83
+ else
84
+ "#{self.class}:#{id}"
85
+ end
86
+ end
87
+
88
+ def name=(name)
89
+ @name = name
90
+ end
91
+
92
+ def if?(time)
93
+ @if_state
94
+ end
58
95
  end
data/test/event_test.rb CHANGED
@@ -34,4 +34,41 @@ describe Clockwork::Event do
34
34
  end
35
35
  end
36
36
  end
37
+
38
+ describe '#run_now?' do
39
+ before do
40
+ @manager = Class.new
41
+ @manager.stubs(:config).returns({})
42
+ end
43
+
44
+ describe 'event skip_first_run option set to true' do
45
+ it 'returns false on first attempt' do
46
+ event = Clockwork::Event.new(@manager, 1, nil, nil, :skip_first_run => true)
47
+ assert_equal false, event.run_now?(Time.now)
48
+ end
49
+
50
+ it 'returns true on subsequent attempts' do
51
+ event = Clockwork::Event.new(@manager, 1, nil, nil, :skip_first_run => true)
52
+ # first run
53
+ event.run_now?(Time.now)
54
+
55
+ # second run
56
+ assert_equal true, event.run_now?(Time.now + 1)
57
+ end
58
+ end
59
+
60
+ describe 'event skip_first_run option not set' do
61
+ it 'returns true on first attempt' do
62
+ event = Clockwork::Event.new(@manager, 1, nil, nil)
63
+ assert_equal true, event.run_now?(Time.now + 1)
64
+ end
65
+ end
66
+
67
+ describe 'event skip_first_run option set to false' do
68
+ it 'returns true on first attempt' do
69
+ event = Clockwork::Event.new(@manager, 1, nil, nil, :skip_first_run => false)
70
+ assert_equal true, event.run_now?(Time.now)
71
+ end
72
+ end
73
+ end
37
74
  end
data/test/manager_test.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.expand_path('../../lib/clockwork', __FILE__)
2
2
  require "minitest/autorun"
3
- require 'mocha/setup'
3
+ require 'mocha/minitest'
4
4
  require 'time'
5
5
  require 'active_support/time'
6
6
 
@@ -0,0 +1,20 @@
1
+ require 'clockwork'
2
+ require 'active_support/time'
3
+
4
+ module Clockwork
5
+ LOGFILE = File.expand_path('../../tmp/signal_test.log', __FILE__)
6
+
7
+ handler do |job|
8
+ File.write(LOGFILE, 'start')
9
+ sleep 0.1
10
+ File.write(LOGFILE, 'done')
11
+ end
12
+
13
+ configure do |config|
14
+ config[:sleep_timeout] = 0
15
+ config[:logger] = Logger.new(StringIO.new)
16
+ end
17
+
18
+ every(1.seconds, 'run.me.every.1.seconds')
19
+ end
20
+
@@ -0,0 +1,34 @@
1
+ require 'test/unit'
2
+ require 'mocha/minitest'
3
+ require 'fileutils'
4
+
5
+ class SignalTest < Test::Unit::TestCase
6
+ CMD = File.expand_path('../../bin/clockwork', __FILE__)
7
+ SAMPLE = File.expand_path('../samples/signal_test.rb', __FILE__)
8
+ LOGFILE = File.expand_path('../tmp/signal_test.log', __FILE__)
9
+
10
+ setup do
11
+ FileUtils.mkdir_p(File.dirname(LOGFILE))
12
+ @pid = spawn(CMD, SAMPLE)
13
+ until File.exist?(LOGFILE)
14
+ sleep 0.1
15
+ end
16
+ end
17
+
18
+ teardown do
19
+ FileUtils.rm_r(File.dirname(LOGFILE))
20
+ end
21
+
22
+ test 'should gracefully shutdown with SIGTERM' do
23
+ Process.kill(:TERM, @pid)
24
+ sleep 0.2
25
+ assert_equal 'done', File.read(LOGFILE)
26
+ end
27
+
28
+ test 'should forcely shutdown with SIGINT' do
29
+ Process.kill(:INT, @pid)
30
+ sleep 0.2
31
+ assert_equal 'start', File.read(LOGFILE)
32
+ end
33
+ end
34
+
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clockwork
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Wiggins
8
8
  - tomykaira
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-04-15 00:00:00.000000000 Z
12
+ date: 2021-12-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: tzinfo
@@ -39,20 +39,6 @@ dependencies:
39
39
  - - ">="
40
40
  - !ruby/object:Gem::Version
41
41
  version: '0'
42
- - !ruby/object:Gem::Dependency
43
- name: bundler
44
- requirement: !ruby/object:Gem::Requirement
45
- requirements:
46
- - - "~>"
47
- - !ruby/object:Gem::Version
48
- version: '1.3'
49
- type: :development
50
- prerelease: false
51
- version_requirements: !ruby/object:Gem::Requirement
52
- requirements:
53
- - - "~>"
54
- - !ruby/object:Gem::Version
55
- version: '1.3'
56
42
  - !ruby/object:Gem::Dependency
57
43
  name: rake
58
44
  requirement: !ruby/object:Gem::Requirement
@@ -109,6 +95,20 @@ dependencies:
109
95
  - - ">="
110
96
  - !ruby/object:Gem::Version
111
97
  version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: test-unit
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
112
  description: A scheduler process to replace cron, using a more flexible Ruby syntax
113
113
  running as a single long-running process. Inspired by rufus-scheduler and resque-scheduler.
114
114
  email:
@@ -123,6 +123,7 @@ extra_rdoc_files:
123
123
  files:
124
124
  - ".gitignore"
125
125
  - ".travis.yml"
126
+ - CHANGELOG.md
126
127
  - Gemfile
127
128
  - LICENSE
128
129
  - README.md
@@ -132,8 +133,9 @@ files:
132
133
  - clockwork.gemspec
133
134
  - clockworkd.1
134
135
  - example.rb
135
- - gemfiles/activesupport3.gemfile
136
136
  - gemfiles/activesupport4.gemfile
137
+ - gemfiles/activesupport5.gemfile
138
+ - gemfiles/activesupport6.gemfile
137
139
  - lib/clockwork.rb
138
140
  - lib/clockwork/at.rb
139
141
  - lib/clockwork/database_events.rb
@@ -152,11 +154,13 @@ files:
152
154
  - test/database_events/test_helpers.rb
153
155
  - test/event_test.rb
154
156
  - test/manager_test.rb
155
- homepage: http://github.com/tomykaira/clockwork
157
+ - test/samples/signal_test.rb
158
+ - test/signal_test.rb
159
+ homepage: http://github.com/Rykian/clockwork
156
160
  licenses:
157
161
  - MIT
158
162
  metadata: {}
159
- post_install_message:
163
+ post_install_message:
160
164
  rdoc_options: []
161
165
  require_paths:
162
166
  - lib
@@ -171,9 +175,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
171
175
  - !ruby/object:Gem::Version
172
176
  version: '0'
173
177
  requirements: []
174
- rubyforge_project:
175
- rubygems_version: 2.5.1
176
- signing_key:
178
+ rubygems_version: 3.2.22
179
+ signing_key:
177
180
  specification_version: 4
178
181
  summary: A scheduler process to replace cron.
179
182
  test_files:
@@ -185,3 +188,5 @@ test_files:
185
188
  - test/database_events/test_helpers.rb
186
189
  - test/event_test.rb
187
190
  - test/manager_test.rb
191
+ - test/samples/signal_test.rb
192
+ - test/signal_test.rb
@@ -1,10 +0,0 @@
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=>"../"