diff --git a/CHANGELOG.md b/CHANGELOG.md index 0482102..8f5886a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,24 @@ -## 3.0.0 ## +## 4.0.0 -* BREAKING CHANGE: drop Rubinius support -* add Ruby 3.0 support (65d71f) -* fix a bug with OpenStruct (15eb030) -* allow `:skip_first_run` in database events (429bc0a) -* add rescue to prevent hung up by exception (cc1b7c9) -* fix CI errors (16b4e19 & e4480ea) -* fix a compatibility bug with Rails 7 (5907bc7) -* Add "Finished" log with duration and error summary (66419ab) +- BREAKING CHANGE: `error_handler` now receives job as first parameter -## 2.0.4 ## +## 3.0.0 -* Reverts the breaking changes in PR #18 that went out in patch 2.0.3 +- BREAKING CHANGE: drop Rubinius support +- add Ruby 3.0 support (65d71f) +- fix a bug with OpenStruct (15eb030) +- allow `:skip_first_run` in database events (429bc0a) +- add rescue to prevent hung up by exception (cc1b7c9) +- fix CI errors (16b4e19 & e4480ea) +- fix a compatibility bug with Rails 7 (5907bc7) +- Add "Finished" log with duration and error summary (66419ab) - *Javier Julio* +## 2.0.4 -## 2.0.3 (February 15, 2018) ## +- Reverts the breaking changes in PR #18 that went out in patch 2.0.3 -* [See the version release](https://github.com/Rykian/clockwork/releases) for the commits that were included. + _Javier Julio_ + +## 2.0.3 (February 15, 2018) + +- [See the version release](https://github.com/Rykian/clockwork/releases) for the commits that were included. diff --git a/README.md b/README.md index f5432dc..dbe416e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ -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) -=========================================== +# 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) Cron is non-ideal for running scheduled application tasks, especially in an app -deployed to multiple machines. [More details.](http://adam.herokuapp.com/past/2010/4/13/rethinking_cron/) +deployed to multiple machines. [More details.](http://adam.herokuapp.com/past/2010/4/13/rethinking_cron/) -Clockwork is a cron replacement. It runs as a lightweight, long-running Ruby +Clockwork is a cron replacement. It runs as a lightweight, long-running Ruby process which sits alongside your web processes (Mongrel/Thin) and your worker processes (DJ/Resque/Minion/Stalker) to schedule recurring work at particular -times or dates. For example, refreshing feeds on an hourly basis, or send +times or dates. For example, refreshing feeds on an hourly basis, or send reminder emails on a nightly basis, or generating invoices once a month on the 1st. -Quickstart ----------- +## Quickstart Create clock.rb: @@ -55,28 +57,21 @@ require './config/environment' under the `require 'clockwork'` declaration. -Quickstart for Heroku ---------------------- +## Quickstart for Heroku Clockwork fits well with heroku's cedar stack. Consider using [clockwork-init.sh](https://gist.github.com/tomykaira/1312172) to create a new project for Heroku. -Use with queueing ------------------ +## Use with queueing -The clock process only makes sense as a place to schedule work to be done, not -to do the work. It avoids locking by running as a single process, but this -makes it impossible to parallelize. For doing the work, you should be using a -job queueing system, such as +The clock process only makes sense as a place to schedule work to be done, not to do the work. It avoids locking by running as a single process, but this makes it impossible to parallelize. For doing the work, you should be using a job queueing system, such as [Delayed Job](http://www.therailsway.com/2009/7/22/do-it-later-with-delayed-job), [Beanstalk/Stalker](http://adam.herokuapp.com/past/2010/4/24/beanstalk_a_simple_and_fast_queueing_backend/), [RabbitMQ/Minion](http://adam.herokuapp.com/past/2009/9/28/background_jobs_with_rabbitmq_and_minion/), [Resque](http://github.com/blog/542-introducing-resque), or -[Sidekiq](https://github.com/mperham/sidekiq). This design allows a -simple clock process with no locks, but also offers near infinite horizontal -scalability. +[Sidekiq](https://github.com/mperham/sidekiq). This design allows a simple clock process with no locks, but also offers near infinite horizontal scalability. For example, if you're using Beanstalk/Stalker: @@ -92,11 +87,8 @@ module Clockwork end ``` -Using a queueing system which doesn't require that your full application be -loaded is preferable, because the clock process can keep a tiny memory -footprint. If you're using DJ or Resque, however, you can go ahead and load -your full application environment, and use per-event blocks to call DJ or Resque -enqueue methods. For example, with DJ/Rails: +Using a queueing system which doesn't require that your full application be loaded is preferable, because the clock process can keep a tiny memory footprint. If you're using DJ or Resque, however, you can go ahead and load your full application environment, and use per-event blocks to call DJ or Resque +enqueue methods. For example, with DJ/Rails: ```ruby require 'config/boot' @@ -106,8 +98,7 @@ every(1.hour, 'feeds.refresh') { Feed.send_later(:refresh) } every(1.day, 'reminders.send', :at => '01:30') { Reminder.send_later(:send_reminders) } ``` -Use with database events ------------------------ +## Use with database events 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. @@ -156,22 +147,23 @@ When one of the events is ready to be run (based on it's `frequency`, and possib 1. the class responds to `all` returning an array of instances from the database 2. the instances returned respond to: + - `id` returning a unique identifier (this is needed to track changes to event settings) - `frequency` returning the how frequently (in seconds) the database event should be run - `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) - - `at` *(optional)* return any acceptable clockwork `:at` string + - `at` _(optional)_ return any acceptable clockwork `:at` string - - `name` *(optional)* returning the name for the event (used to identify it in the Clockwork output) + - `name` _(optional)_ returning the name for the event (used to identify it in the Clockwork output) - - `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`) + - `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`) - - `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 + - `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 - - `tz` *(optional)* returning the timezone to use (default is the local timezone) + - `tz` _(optional)_ returning the timezone to use (default is the local timezone) - - `skip_first_run` *(optional)* self explanatory, see [dedicated section](#skip_first_run) + - `skip_first_run` _(optional)_ self explanatory, see [dedicated section](#skip_first_run) #### Example Setup @@ -272,9 +264,7 @@ class ClockworkDatabaseEvent < ActiveRecord::Base end ``` - -Event Parameters ----------- +## Event Parameters ### :at @@ -349,7 +339,7 @@ Clockwork.every(1.day, 'myjob', :if => lambda { |t| t.day == 1 }) The argument is an instance of `ActiveSupport::TimeWithZone` if the `:tz` option is set. Otherwise, it's an instance of `Time`. -This argument cannot be omitted. Please use _ as placeholder if not needed. +This argument cannot be omitted. Please use \_ as placeholder if not needed. ```ruby Clockwork.every(1.second, 'myjob', :if => lambda { |_| true }) @@ -381,9 +371,7 @@ Clockwork.every(5.minutes, 'myjob', :skip_first_run => true) The above job will not run at initial boot, and instead run every 5 minutes after boot. - -Configuration ------------------------ +## Configuration Clockwork exposes a couple of configuration options: @@ -401,18 +389,17 @@ From 1.1.0, Clockwork does not accept `sleep_timeout` less than 1 seconds. ### :tz -This is the default timezone to use for all events. When not specified this defaults to the local -timezone. Specifying :tz in the parameters for an event overrides anything set here. +This is the default timezone to use for all events. When not specified this defaults to the local +timezone. Specifying :tz in the parameters for an event overrides anything set here. ### :max_threads Clockwork runs handlers in threads. If it exceeds `max_threads`, it will warn you (log an error) about missing jobs. - ### :thread -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) +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) ### Configuration example @@ -434,8 +421,8 @@ You can add error_handler to define your own logging or error rescue. ```ruby module Clockwork - error_handler do |error| - Airbrake.notify_or_ignore(error) + error_handler do |job_name, error| + Honeybadger.notify(error, context: { job_name: job_name }) end end ``` @@ -447,18 +434,14 @@ Current specifications are as follows. Any suggestion about these specifications is welcome. -Old style ---------- +## Old style `include Clockwork` is old style. The old style is still supported, though not recommended, because it pollutes the global namespace. +## Anatomy of a clock file - -Anatomy of a clock file ------------------------ - -clock.rb is standard Ruby. Since we include the Clockwork module (the +clock.rb is standard Ruby. Since we include the Clockwork module (the clockwork executable does this automatically, or you can do it explicitly), this exposes a small DSL to define the handler for events, and then the events themselves. @@ -469,7 +452,7 @@ handler { |job| enqueue_your_job(job) } ``` This block will be invoked every time an event is triggered, with the job name -passed in. In most cases, you should be able to pass the job name directly +passed in. In most cases, you should be able to pass the job name directly through to your queueing system. The second part of the file, which lists the events, roughly resembles a crontab: @@ -480,11 +463,11 @@ every(1.hour, 'otherthing.do') ``` In the first line of this example, an event will be triggered once every five -minutes, passing the job name 'thing.do' into the handler. The handler shown +minutes, passing the job name 'thing.do' into the handler. The handler shown above would thus call enqueue_your_job('thing.do'). You can also pass a custom block to the handler, for job queueing systems that -rely on classes rather than job names (i.e. DJ and Resque). In this case, you +rely on classes rather than job names (i.e. DJ and Resque). In this case, you need not define a general event handler, and instead provide one with each event: @@ -535,26 +518,24 @@ end 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). -In production -------------- +## In production Only one clock process should ever be running across your whole application -deployment. For example, if your app is running on three VPS machines (two app +deployment. For example, if your app is running on three VPS machines (two app servers and one database), your app machines might have the following process topography: -* App server 1: 3 web (thin start), 3 workers (rake jobs:work), 1 clock (clockwork clock.rb) -* App server 2: 3 web (thin start), 3 workers (rake jobs:work) +- App server 1: 3 web (thin start), 3 workers (rake jobs:work), 1 clock (clockwork clock.rb) +- App server 2: 3 web (thin start), 3 workers (rake jobs:work) You should use [Monit](http://mmonit.com/monit/), [God](https://github.com/mojombo/god), [Upstart](http://upstart.ubuntu.com/), or [Inittab](http://www.tldp.org/LDP/sag/html/config-init.html) to keep your clock process running the same way you keep your web and workers running. -Daemonization -------------- +## Daemonization Thanks to @fddayan, `clockworkd` executes clockwork script as a daemon. -You will need the [daemons gem](https://github.com/ghazel/daemons) to use `clockworkd`. It is not automatically installed, please install by yourself. +You will need the [daemons gem](https://github.com/ghazel/daemons) to use `clockworkd`. It is not automatically installed, please install by yourself. Then, @@ -564,15 +545,14 @@ clockworkd -c YOUR_CLOCK.rb start For more details, you can run `clockworkd -h`. -Integration Testing -------------------- +## Integration Testing You could take a look at: -* [clockwork-mocks](https://github.com/dpoetzsch/clockwork-mocks) that helps with running scheduled tasks during integration testing. -* [clockwork-test](https://github.com/kevin-j-m/clockwork-test) which ensures that tasks are triggered at the right time -Issues and Pull requests ------------------------- +- [clockwork-mocks](https://github.com/dpoetzsch/clockwork-mocks) that helps with running scheduled tasks during integration testing. +- [clockwork-test](https://github.com/kevin-j-m/clockwork-test) which ensures that tasks are triggered at the right time + +## Issues and Pull requests If you find a bug, please create an issue - [Issues · Rykian/clockwork](https://github.com/Rykian/clockwork/issues). @@ -585,18 +565,17 @@ In most cases, directly operating `Manager` realizes an idea, without touching t If you discover a new way to use Clockwork, please create a gist page or an article on your website, then add it to the following "Use cases" section. This tool is already used in various environment, so backward-incompatible requests will be mostly rejected. -Use cases ---------- +## Use cases Feel free to add your idea or experience and send a pull-request. - Sending errors to Airbrake - Read events from a database -Meta ----- +## Meta -Created by Adam Wiggins +Created by Adam Wiggins. +Version 4 released by Rafael Baldasso Audibert while working at [LeadSimple](https://leadsimple.com) Inspired by [rufus-scheduler](https://github.com/jmettraux/rufus-scheduler) and [resque-scheduler](https://github.com/bvandenbos/resque-scheduler) @@ -606,4 +585,4 @@ Patches contributed by Mark McGranaghan and Lukáš Konarovský Released under the MIT License: http://www.opensource.org/licenses/mit-license.php -https://github.com/Rykian/clockwork +https://github.com/LeadSimple/clockwork diff --git a/clockwork.gemspec b/clockwork.gemspec index 9b9c87f..a77ec15 100644 --- a/clockwork.gemspec +++ b/clockwork.gemspec @@ -1,28 +1,26 @@ Gem::Specification.new do |s| - s.name = "clockwork" - s.version = "3.0.0" + s.name = 'clockwork' + s.version = '4.0.0' - s.authors = ["Adam Wiggins", "tomykaira"] + s.authors = ['Adam Wiggins', 'tomykaira', 'Rafael Baldasso Audibert'] s.license = 'MIT' - s.description = "A scheduler process to replace cron, using a more flexible Ruby syntax running as a single long-running process. Inspired by rufus-scheduler and resque-scheduler." - s.email = ["adam@heroku.com", "tomykaira@gmail.com"] - s.extra_rdoc_files = [ - "README.md" - ] - s.homepage = "http://github.com/Rykian/clockwork" - s.summary = "A scheduler process to replace cron." + s.description = 'A scheduler process to replace cron, using a more flexible Ruby syntax running as a single long-running process. Inspired by rufus-scheduler and resque-scheduler.' + s.email = ['adam@heroku.com', 'tomykaira@gmail.com', 'rafael.audibert@leadsimple.com'] + s.extra_rdoc_files = ['README.md'] + s.homepage = 'http://github.com/LeadSimple/clockwork' + s.summary = 'A scheduler process to replace cron.' s.files = `git ls-files`.split($/) s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) } s.test_files = s.files.grep(%r{^(test|spec|features)/}) - s.require_paths = ["lib"] + s.require_paths = ['lib'] - s.add_dependency(%q) - s.add_dependency(%q) + s.add_dependency('activesupport') + s.add_dependency('tzinfo') - s.add_development_dependency "rake" - s.add_development_dependency "daemons" - s.add_development_dependency "minitest", "~> 5.8" - s.add_development_dependency "mocha" - s.add_development_dependency "test-unit" + s.add_development_dependency 'daemons' + s.add_development_dependency 'minitest', '~> 5.8' + s.add_development_dependency 'mocha' + s.add_development_dependency 'rake' + s.add_development_dependency 'test-unit' end diff --git a/example.rb b/example.rb index ad420ba..81cd65f 100644 --- a/example.rb +++ b/example.rb @@ -9,20 +9,20 @@ module Clockwork every(1.minute, 'run.me.every.minute') every(1.hour, 'run.me.every.hour') - every(1.day, 'run.me.at.midnight', :at => '00:00') + every(1.day, 'run.me.at.midnight', at: '00:00') - every(1.day, 'custom.event.handler', :at => '00:30') do + every(1.day, 'custom.event.handler', at: '00:30') do puts 'This event has its own handler' end - # note: callbacks that return nil or false will cause event to not run + # NOTE: callbacks that return nil or false will cause event to not run on(:before_tick) do - puts "tick" + puts 'tick' true end on(:after_tick) do - puts "tock" + puts 'tock' true end end diff --git a/lib/clockwork.rb b/lib/clockwork.rb index 1eb0e5e..5bdaaf3 100644 --- a/lib/clockwork.rb +++ b/lib/clockwork.rb @@ -9,7 +9,7 @@ module Clockwork class << self def included(klass) - klass.send "include", Methods + klass.send 'include', Methods klass.extend Methods end @@ -17,9 +17,7 @@ def manager @manager ||= Manager.new end - def manager=(manager) - @manager = manager - end + attr_writer :manager end module Methods @@ -35,11 +33,11 @@ def error_handler(&block) Clockwork.manager.error_handler(&block) end - def on(event, options={}, &block) + def on(event, options = {}, &block) Clockwork.manager.on(event, options, &block) end - def every(period, job, options={}, &block) + def every(period, job, options = {}, &block) Clockwork.manager.every(period, job, options, &block) end diff --git a/lib/clockwork/at.rb b/lib/clockwork/at.rb index 19ca4f9..6e770de 100644 --- a/lib/clockwork/at.rb +++ b/lib/clockwork/at.rb @@ -3,7 +3,9 @@ class At class FailedToParse < StandardError; end NOT_SPECIFIED = nil - WDAYS = %w[sunday monday tuesday wednesday thursday friday saturday].each.with_object({}).with_index do |(w, wdays), index| + WDAYS = %w[ + sunday monday tuesday wednesday thursday friday saturday + ].each.with_object({}).with_index do |(w, wdays), index| [w, w.capitalize, w[0...3], w[0...3].capitalize].each do |k| wdays[k] = index end @@ -11,21 +13,20 @@ class FailedToParse < StandardError; end def self.parse(at) return unless at + case at when /\A([[:alpha:]]+)\s(.*)\z/ - if wday = WDAYS[$1] - parsed_time = parse($2) - parsed_time.wday = wday - parsed_time - else - raise FailedToParse, at - end + raise FailedToParse, at unless (wday = WDAYS[Regexp.last_match(1)]) + + parsed_time = parse(Regexp.last_match(2)) + parsed_time.wday = wday + parsed_time when /\A(\d{1,2}):(\d\d)\z/ - new($2.to_i, $1.to_i) + new(Regexp.last_match(2).to_i, Regexp.last_match(1).to_i) when /\A\*{1,2}:(\d\d)\z/ - new($1.to_i) + new(Regexp.last_match(1).to_i) when /\A(\d{1,2}):\*\*\z/ - new(NOT_SPECIFIED, $1.to_i) + new(NOT_SPECIFIED, Regexp.last_match(1).to_i) else raise FailedToParse, at end @@ -35,28 +36,29 @@ def self.parse(at) attr_accessor :min, :hour, :wday - def initialize(min, hour=NOT_SPECIFIED, wday=NOT_SPECIFIED) + def initialize(min, hour = NOT_SPECIFIED, wday = NOT_SPECIFIED) @min = min @hour = hour @wday = wday raise ArgumentError unless valid? end - def ready?(t) - (@min == NOT_SPECIFIED or t.min == @min) and - (@hour == NOT_SPECIFIED or t.hour == @hour) and - (@wday == NOT_SPECIFIED or t.wday == @wday) + def ready?(time) + (@min == NOT_SPECIFIED or time.min == @min) and + (@hour == NOT_SPECIFIED or time.hour == @hour) and + (@wday == NOT_SPECIFIED or time.wday == @wday) end - def == other + def ==(other) @min == other.min && @hour == other.hour && @wday == other.wday end private + def valid? @min == NOT_SPECIFIED || (0..59).cover?(@min) && @hour == NOT_SPECIFIED || (0..23).cover?(@hour) && - @wday == NOT_SPECIFIED || (0..6).cover?(@wday) + @wday == NOT_SPECIFIED || (0..6).cover?(@wday) end end end diff --git a/lib/clockwork/database_events.rb b/lib/clockwork/database_events.rb index cc10caa..d5e687a 100644 --- a/lib/clockwork/database_events.rb +++ b/lib/clockwork/database_events.rb @@ -5,16 +5,15 @@ # TERMINOLOGY # -# For clarity, we have chosen to define terms as follows for better communication in the code, and when +# For clarity, we have chosen to define terms as follows for better communication in the code, and when # discussing the database event implementation. # # "Event": "Native" Clockwork events, whether Clockwork::Event or Clockwork::DatabaseEvents::Event # "Model": Database-backed model instances representing events to be created in Clockwork module Clockwork - module Methods - def sync_database_events(options={}, &block) + def sync_database_events(options = {}, &block) DatabaseEvents::Synchronizer.setup(options, &block) end end diff --git a/lib/clockwork/database_events/event.rb b/lib/clockwork/database_events/event.rb index 6f8d05f..942df0a 100644 --- a/lib/clockwork/database_events/event.rb +++ b/lib/clockwork/database_events/event.rb @@ -1,12 +1,9 @@ module Clockwork - module DatabaseEvents - class Event < Clockwork::Event - attr_accessor :event_store, :model_attributes - def initialize(manager, period, job, block, event_store, model_attributes, options={}) + def initialize(manager, period, job, block, event_store, model_attributes, options = {}) super(manager, period, job, block, options) @event_store = event_store @event_store.register(self, job) @@ -14,7 +11,7 @@ def initialize(manager, period, job, block, event_store, model_attributes, optio end def name - (job_has_name? && job.name) ? job.name : "#{job.class}:#{job.id}" + job_has_name? && job.name ? job.name : "#{job.class}:#{job.id}" end def job_has_name? @@ -29,6 +26,5 @@ def frequency @period end end - end end diff --git a/lib/clockwork/database_events/event_collection.rb b/lib/clockwork/database_events/event_collection.rb index 5b51b8a..7e1c6fd 100644 --- a/lib/clockwork/database_events/event_collection.rb +++ b/lib/clockwork/database_events/event_collection.rb @@ -1,8 +1,7 @@ module Clockwork module DatabaseEvents class EventCollection - - def initialize(manager=Clockwork.manager) + def initialize(manager = Clockwork.manager) @events = [] @manager = manager end @@ -17,15 +16,15 @@ def has_changed?(model) ignored_attributes = model.ignored_attributes if model.respond_to?(:ignored_attributes) ignored_attributes ||= [] - model_attributes = model.attributes.select do |k, _| - not ignored_attributes.include?(k.to_sym) + model_attributes = model.attributes.reject do |k, _| + ignored_attributes.include?(k.to_sym) end event.model_attributes != model_attributes end def unregister - events.each{|e| manager.unregister(e) } + events.each { |e| manager.unregister(e) } end private diff --git a/lib/clockwork/database_events/event_store.rb b/lib/clockwork/database_events/event_store.rb index 75b409d..77fae99 100644 --- a/lib/clockwork/database_events/event_store.rb +++ b/lib/clockwork/database_events/event_store.rb @@ -24,11 +24,8 @@ # - it creates a new DatabaseEvents::Event # - DatabaseEvents::Event#initialize registers it with the EventStore module Clockwork - module DatabaseEvents - class EventStore - def initialize(block_to_perform_on_event_trigger) @related_events = {} @block_to_perform_on_event_trigger = block_to_perform_on_event_trigger @@ -49,7 +46,7 @@ def update(current_model_objects) def unregister_all_except(model_objects) ids = model_objects.collect(&:id) - (@related_events.keys - ids).each{|id| unregister(id) } + (@related_events.keys - ids).each { |id| unregister(id) } end def update_registered_models(model_objects) @@ -84,11 +81,11 @@ def related_events_for(id) end def registered_models(model_objects) - model_objects.select{|m| registered?(m) } + model_objects.select { |m| registered?(m) } end def unregistered_models(model_objects) - model_objects.select{|m| !registered?(m) } + model_objects.reject { |m| registered?(m) } end def unregister(id) @@ -103,28 +100,31 @@ def unregister(id) # called, which creates a new DatabaseEvent::Event, which will be # registered with the EventStore on #initialize. def register_with_manager(model) - Clockwork.manager. - every(model.frequency, model, options(model), - &@block_to_perform_on_event_trigger) + Clockwork.manager.every( + model.frequency, + model, + options(model), + &@block_to_perform_on_event_trigger + ) end def options(model) options = { - :from_database => true, - :synchronizer => self, - :ignored_attributes => [], + from_database: true, + synchronizer: self, + ignored_attributes: [] } options[:at] = at_strings_for(model) if model.respond_to?(:at) - options[:if] = ->(time){ model.if?(time) } if model.respond_to?(:if?) + options[:if] = ->(time) { model.if?(time) } if model.respond_to?(:if?) options[:tz] = model.tz if model.respond_to?(:tz) options[:ignored_attributes] = model.ignored_attributes if model.respond_to?(:ignored_attributes) options[:skip_first_run] = model.skip_first_run if model.respond_to?(:skip_first_run) # store the state of the model at time of registering so we can # easily compare and determine if state has changed later - options[:model_attributes] = model.attributes.select do |k, v| - not options[:ignored_attributes].include?(k.to_sym) + options[:model_attributes] = model.attributes.reject do |k, _v| + options[:ignored_attributes].include?(k.to_sym) end options @@ -136,6 +136,5 @@ def at_strings_for(model) model.at.split(',').map(&:strip) end end - end end diff --git a/lib/clockwork/database_events/manager.rb b/lib/clockwork/database_events/manager.rb index 3231d6f..53806ff 100644 --- a/lib/clockwork/database_events/manager.rb +++ b/lib/clockwork/database_events/manager.rb @@ -1,23 +1,27 @@ module Clockwork - module DatabaseEvents - class Manager < Clockwork::Manager - def unregister(event) @events.delete(event) end def register(period, job, block, options) @events << if options[:from_database] - synchronizer = options.fetch(:synchronizer) - model_attributes = options.fetch(:model_attributes) + synchronizer = options.fetch(:synchronizer) + model_attributes = options.fetch(:model_attributes) - Clockwork::DatabaseEvents::Event. - new(self, period, job, (block || handler), synchronizer, model_attributes, options) - else - Clockwork::Event.new(self, period, job, block || handler, options) - end + Clockwork::DatabaseEvents::Event.new( + self, + period, + job, + (block || handler), + synchronizer, + model_attributes, + options + ) + else + Clockwork::Event.new(self, period, job, block || handler, options) + end end end end diff --git a/lib/clockwork/database_events/synchronizer.rb b/lib/clockwork/database_events/synchronizer.rb index 24c9645..aeb5c48 100644 --- a/lib/clockwork/database_events/synchronizer.rb +++ b/lib/clockwork/database_events/synchronizer.rb @@ -1,12 +1,9 @@ module Clockwork - module DatabaseEvents - class Synchronizer - - def self.setup(options={}, &block_to_perform_on_event_trigger) - model_class = options.fetch(:model) { raise KeyError, ":model must be set to the model class" } - every = options.fetch(:every) { raise KeyError, ":every must be set to the database sync frequency" } + def self.setup(options = {}, &block_to_perform_on_event_trigger) + model_class = options.fetch(:model) { raise KeyError, ':model must be set to the model class' } + every = options.fetch(:every) { raise KeyError, ':every must be set to the database sync frequency' } event_store = EventStore.new(block_to_perform_on_event_trigger) @@ -16,6 +13,5 @@ def self.setup(options={}, &block_to_perform_on_event_trigger) end end end - end end diff --git a/lib/clockwork/event.rb b/lib/clockwork/event.rb index 8e589f3..5da71c1 100644 --- a/lib/clockwork/event.rb +++ b/lib/clockwork/event.rb @@ -2,7 +2,7 @@ module Clockwork class Event attr_accessor :job, :last - def initialize(manager, period, job, block, options={}) + def initialize(manager, period, job, block, options = {}) validate_if_option(options[:if]) @manager = manager @period = period @@ -16,15 +16,16 @@ def initialize(manager, period, job, block, options={}) @last = @skip_first_run ? convert_timezone(Time.now) : nil end - def convert_timezone(t) - @timezone ? t.in_time_zone(@timezone) : t + def convert_timezone(time) + @timezone ? time.in_time_zone(@timezone) : time end - def run_now?(t) - t = convert_timezone(t) - return false unless elapsed_ready?(t) - return false unless run_at?(t) - return false unless run_if?(t) + def run_now?(time) + time = convert_timezone(time) + return false unless elapsed_ready?(time) + return false unless run_at?(time) + return false unless run_if?(time) + true end @@ -32,15 +33,14 @@ def thread? @thread end - def run(t) + def run(time) @manager.log "Triggering '#{self}'" - @last = convert_timezone(t) + @last = convert_timezone(time) + if thread? if @manager.thread_available? - t = Thread.new do - execute - end - t['creator'] = @manager + thread = Thread.new { execute } + thread['creator'] = @manager else @manager.log_error "Threads exhausted; skipping #{self}" end @@ -54,15 +54,16 @@ def to_s end private + def execute start = Process.clock_gettime(Process::CLOCK_MONOTONIC) error = nil @block.call(@job, @last) - rescue => e + rescue StandardError => e error = e @manager.log_error e - @manager.handle_error e + @manager.handle_error(@job, e) ensure finish = Process.clock_gettime(Process::CLOCK_MONOTONIC) duration = ((finish - start) * 1000).round # milliseconds @@ -70,22 +71,22 @@ def execute @manager.log "Finished '#{self}' duration_ms=#{duration} error=#{error.inspect}" end - def elapsed_ready?(t) - @last.nil? || (t - @last.to_i).to_i >= @period + def elapsed_ready?(time) + @last.nil? || (time - @last.to_i).to_i >= @period end - def run_at?(t) - @at.nil? || @at.ready?(t) + def run_at?(time) + @at.nil? || @at.ready?(time) end - def run_if?(t) - @if.nil? || @if.call(t) + def run_if?(time) + @if.nil? || @if.call(time) end def validate_if_option(if_option) - if if_option && !if_option.respond_to?(:call) - raise ArgumentError.new(':if expects a callable object, but #{if_option} does not respond to call') - end + return unless if_option && !if_option.respond_to?(:call) + + raise ArgumentError, ':if expects a callable object, but `if_option` does not respond to call' end end end diff --git a/lib/clockwork/manager.rb b/lib/clockwork/manager.rb index 78fc739..354060a 100644 --- a/lib/clockwork/manager.rb +++ b/lib/clockwork/manager.rb @@ -2,6 +2,8 @@ module Clockwork class Manager class NoHandlerDefined < RuntimeError; end + SUPPORTED_CALLBACKS = %i[before_tick after_tick before_run after_run].freeze + attr_reader :config def initialize @@ -20,36 +22,37 @@ def thread_available? def configure yield(config) - if config[:sleep_timeout] < 1 - config[:logger].warn 'sleep_timeout must be >= 1 second' - end + config[:logger].warn 'sleep_timeout must be >= 1 second' if config[:sleep_timeout] < 1 end def default_configuration - { :sleep_timeout => 1, :logger => Logger.new(STDOUT), :thread => false, :max_threads => 10 } + { sleep_timeout: 1, logger: Logger.new($stdout), thread: false, max_threads: 10 } end def handler(&block) @handler = block if block_given? raise NoHandlerDefined unless @handler + @handler end def error_handler(&block) @error_handler = block if block_given? - @error_handler if instance_variable_defined?("@error_handler") + @error_handler if instance_variable_defined?('@error_handler') end - def on(event, options={}, &block) - raise "Unsupported callback #{event}" unless [:before_tick, :after_tick, :before_run, :after_run].include?(event.to_sym) - (@callbacks[event.to_sym]||=[]) << block + def on(event, _options = {}, &block) + raise "Unsupported callback #{event}" unless SUPPORTED_CALLBACKS.include?(event.to_sym) + + (@callbacks[event.to_sym] ||= []) << block end - def every(period, job='unnamed', options={}, &block) - if job.is_a?(Hash) and options.empty? + def every(period, job = 'unnamed', options = {}, &block) + if job.is_a?(Hash) && options.empty? options = job - job = "unnamed" + job = 'unnamed' end + if options[:at].respond_to?(:each) every_with_multiple_times(period, job, options, &block) else @@ -74,7 +77,7 @@ def run run_tick_loop - while io = IO.select([sig_read]) + while (io = IO.select([sig_read])) sig = io.first[0].gets.chomp handle_signal(sig) end @@ -122,17 +125,17 @@ def run_tick_loop until @finish tick interval = config[:sleep_timeout] - Time.now.subsec + 0.001 - @condvar.wait(@mutex, interval) if interval > 0 + @condvar.wait(@mutex, interval) if interval.positive? end end end end - def tick(t=Time.now) - if (fire_callbacks(:before_tick)) + def tick(t = Time.now) + if fire_callbacks(:before_tick) events = events_to_run(t) events.each do |event| - if (fire_callbacks(:before_run, event, t)) + if fire_callbacks(:before_run, event, t) event.run(t) fire_callbacks(:after_run, event, t) end @@ -146,12 +149,12 @@ def logger config[:logger] end - def log_error(e) - config[:logger].error(e) + def log_error(error) + config[:logger].error(error) end - def handle_error(e) - error_handler.call(e) if error_handler + def handle_error(error) + error_handler&.call(error) end def log(msg) @@ -159,14 +162,13 @@ def log(msg) end private - def events_to_run(t) + + def events_to_run(time) @events.select do |event| - begin - event.run_now?(t) - rescue => e - log_error(e) - false - end + event.run_now?(time) + rescue StandardError => e + log_error(e) + false end end @@ -176,7 +178,7 @@ def register(period, job, block, options) event end - def every_with_multiple_times(period, job, options={}, &block) + def every_with_multiple_times(period, job, options = {}, &block) each_options = options.clone options[:at].each do |at| each_options[:at] = at