crono_trigger 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8410e0b2dd3dc398df839baba03aaec050d3e31
4
- data.tar.gz: 945ece057b38511d03324505c4fe960420b9f581
3
+ metadata.gz: 1f57e39321ff772c9fc7a02d4d63d19e991be0eb
4
+ data.tar.gz: 72949a4a2b1984e90ff27c0acc0f5fc74818a947
5
5
  SHA512:
6
- metadata.gz: 76e9b4270e9d9ea439106e88e5e031a2489212f4caaf8dedd0e39c0ba3d64189b0a3c7ecc6e67d9dfbdaffcfcfb27cbe8839a4860268c00fcede1f8c5a4f5fd5
7
- data.tar.gz: 5adc3071703de67b4d9a894b32e4ede1677dcc119fdb8fdc021fdae4f232e56d297984363eaf5d4d1eec586226841666bef9ffa6d2713e166ff524fc4c583a74
6
+ metadata.gz: f537b832ef60d83e107946134f6d112d104e984cb6432ed3fcd9da2f95c38ca800c100bef471d8473f398fe18fab3cf2f21db000f921c0d7f9928a72dfa87a09
7
+ data.tar.gz: f3268eabf16acfeb54aa39246d0315cc1c01f0668f7674d2f3b6e03607f41984a37d0ba0bd0dc51c33cd5d41b4dc3756df891e6179bd5604ff9243b3dcb944b5
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
1
  # CronoTrigger
2
+ [![Gem Version](https://badge.fury.io/rb/crono_trigger.svg)](https://badge.fury.io/rb/crono_trigger)
3
+ [![Build Status](https://travis-ci.org/joker1007/crono_trigger.svg?branch=master)](https://travis-ci.org/joker1007/crono_trigger)
4
+ [![codecov](https://codecov.io/gh/joker1007/crono_trigger/branch/master/graph/badge.svg)](https://codecov.io/gh/joker1007/crono_trigger)
2
5
 
3
6
  Asynchronous Job Scheduler for Rails.
4
7
 
@@ -87,6 +90,7 @@ class MailNotification < ActiveRecord::Base
87
90
  throw :retry # break execution and retry task
88
91
  throw :abort # break execution and raise AbortExecution. AbortExecution is not retried
89
92
  throw :ok # break execution and handle task as success
93
+ throw :ok_without_reset # break execution and handle task as success but without schedule reseting and unlocking
90
94
  end
91
95
  end
92
96
  ```
@@ -116,18 +120,22 @@ Usage: crono_trigger [options] MODEL [MODEL..]
116
120
 
117
121
  ### Columns
118
122
 
119
- |name |type |required|description |
120
- |-----------------|--------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
121
- |cron |string |no |Recurring schedule formatted by cron style |
122
- |next_execute_at |datetime|yes |Timestamp of next execution. Worker executes task if this column <= now |
123
- |last_executed_at |datetime|no |Timestamp of last execution |
124
- |execute_lock |integer |yes |Timestamp of fetching record in order to hide record from other transaction during execute lock timeout. <br> when execution complete this column is reset to 0|
125
- |started_at |datetime|no |Timestamp of schedule activated |
126
- |finished_at |datetime|no |Timestamp of schedule deactivated |
127
- |last_error_name |string |no |Class name of last error |
128
- |last_error_reason|string |no |Error message of last error |
129
- |last_error_time |datetime|no |Timestamp of last error occured |
130
- |retry_count |integer |no |Retry count. <br> If execution succeed retry_count is reset to 0 |
123
+ |name |type |required|rename|description |
124
+ |-----------------|--------|--------|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
125
+ |cron |string |no |no |Recurring schedule formatted by cron style |
126
+ |next_execute_at |datetime|yes |yes |Timestamp of next execution. Worker executes task if this column <= now |
127
+ |last_executed_at |datetime|no |yes |Timestamp of last execution |
128
+ |timezone |datetime|no |yes |Timezone name (Parsed by tzinfo) |
129
+ |execute_lock |integer |yes |yes |Timestamp of fetching record in order to hide record from other transaction during execute lock timeout. <br> when execution complete this column is reset to 0|
130
+ |started_at |datetime|no |yes |Timestamp of schedule activated |
131
+ |finished_at |datetime|no |yes |Timestamp of schedule deactivated |
132
+ |last_error_name |string |no |no |Class name of last error |
133
+ |last_error_reason|string |no |no |Error message of last error |
134
+ |last_error_time |datetime|no |no |Timestamp of last error occured |
135
+ |retry_count |integer |no |no |Retry count. <br> If execution succeed retry_count is reset to 0 |
136
+
137
+ You can rename some columns.
138
+ ex. `crono_trigger_options[:next_execute_at_column_name] = "next_time"`
131
139
 
132
140
  ## Development
133
141
 
@@ -25,12 +25,14 @@ Gem::Specification.new do |spec|
25
25
  spec.add_dependency "chrono"
26
26
  spec.add_dependency "serverengine"
27
27
  spec.add_dependency "concurrent-ruby"
28
+ spec.add_dependency "tzinfo"
28
29
  spec.add_dependency "activerecord", ">= 4.2"
29
30
 
30
31
  spec.add_development_dependency "sqlite3"
31
- spec.add_development_dependency "database_rewinder"
32
32
  spec.add_development_dependency "timecop"
33
+ spec.add_development_dependency "rollbar"
33
34
  spec.add_development_dependency "bundler", "~> 1.14"
34
- spec.add_development_dependency "rake", "~> 10.0"
35
- spec.add_development_dependency "rspec", "~> 3.0"
35
+ spec.add_development_dependency "rake"
36
+ spec.add_development_dependency "rspec"
37
+ spec.add_development_dependency "codecov"
36
38
  end
data/lib/crono_trigger.rb CHANGED
@@ -13,6 +13,7 @@ module CronoTrigger
13
13
  polling_interval: 5,
14
14
  executor_thread: 25,
15
15
  model_names: [],
16
+ error_handlers: [],
16
17
  )
17
18
 
18
19
  def self.config
@@ -0,0 +1,34 @@
1
+ module CronoTrigger
2
+ class ExceptionHandler
3
+ def self.handle_exception(record, ex)
4
+ new(record).handle_exception(ex)
5
+ end
6
+
7
+ def initialize(record)
8
+ @record = record
9
+ end
10
+
11
+ def handle_exception(ex)
12
+ handlers = CronoTrigger.config.error_handlers + Array(@record.crono_trigger_options[:error_handlers])
13
+ handlers.each do |callable|
14
+ callable, arity = ensure_callable(callable)
15
+ args = [ex, @record]
16
+ args = arity < 0 ? args : args.take(arity)
17
+ callable.call(*args)
18
+ end
19
+ rescue Exception => e
20
+ @record.logger.error("CronoTrigger error handler raises error")
21
+ @record.logger.error(e)
22
+ end
23
+
24
+ private
25
+
26
+ def ensure_callable(callable)
27
+ if callable.respond_to?(:call)
28
+ return callable, callable.arity
29
+ elsif callable.is_a?(Symbol)
30
+ return @record.method(callable), 1
31
+ end
32
+ end
33
+ end
34
+ end
@@ -15,9 +15,9 @@ module CronoTrigger
15
15
  model = @model_queue.pop(true)
16
16
  poll(model)
17
17
  rescue ThreadError => e
18
- logger.error(e) unless e.message == "queue empty"
18
+ @logger.error(e) unless e.message == "queue empty"
19
19
  rescue => e
20
- logger.error(e)
20
+ @logger.error(e)
21
21
  ensure
22
22
  @model_queue << model if model
23
23
  end
@@ -0,0 +1,25 @@
1
+ require 'rollbar'
2
+
3
+ module Rollbar
4
+ class CronoTrigger
5
+ def self.handle_exception(ex, record)
6
+ scope = {
7
+ framework: "CronoTrigger: #{::CronoTrigger::VERSION}",
8
+ context: "#{record.class}/#{record.id}"
9
+ }
10
+
11
+ Rollbar.scope(scope).error(ex, use_exception_level_filters: true)
12
+ end
13
+ end
14
+ end
15
+
16
+ Rollbar.plugins.define('crono_trigger') do
17
+ require_dependency('crono_trigger')
18
+
19
+ execute! do
20
+ CronoTrigger.config.error_handlers << proc do |ex, record|
21
+ Rollbar.reset_notifier!
22
+ Rollbar::CronoTrigger.handle_exception(ex, record)
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,9 @@
1
1
  require "active_support/concern"
2
+ require "active_support/core_ext/object"
2
3
  require "chrono"
4
+ require "tzinfo"
5
+
6
+ require "crono_trigger/exception_handler"
3
7
 
4
8
  module CronoTrigger
5
9
  module Schedulable
@@ -13,23 +17,32 @@ module CronoTrigger
13
17
  include ActiveSupport::Callbacks
14
18
 
15
19
  included do
16
- class_attribute :crono_trigger_options
17
- self.crono_trigger_options = {}
20
+ class_attribute :crono_trigger_options, :executable_conditions
21
+ self.crono_trigger_options ||= {}
22
+ self.executable_conditions ||= []
18
23
 
19
24
  define_model_callbacks :execute
20
25
 
21
26
  scope :executables, ->(from: Time.current, primary_key_offset: nil, limit: 1000) do
22
27
  t = arel_table
23
28
 
24
- rel = where(t[:next_execute_at].lteq(from))
25
- .where(t[:execute_lock].lteq(from.to_i - (crono_trigger_options[:execute_lock_timeout] || DEFAULT_EXECUTE_LOCK_TIMEOUT)))
29
+ rel = where(t[crono_trigger_column_name(:next_execute_at)].lteq(from))
30
+ .where(t[crono_trigger_column_name(:execute_lock)].lteq(from.to_i - execute_lock_timeout))
26
31
 
27
- rel = rel.where(t[:started_at].lteq(from)) if column_names.include?("started_at")
28
- rel = rel.where(t[:finished_at].gt(from).or(t[:finished_at].eq(nil))) if column_names.include?("finished_at")
32
+ rel = rel.where(t[crono_trigger_column_name(:started_at)].lteq(from)) if column_names.include?(crono_trigger_column_name(:started_at))
33
+ rel = rel.where(t[crono_trigger_column_name(:finished_at)].gt(from).or(t[crono_trigger_column_name(:finished_at)].eq(nil))) if column_names.include?(crono_trigger_column_name(:finished_at))
29
34
  rel = rel.where(t[primary_key].gt(primary_key_offset)) if primary_key_offset
30
35
 
31
36
  rel = rel.order("#{quoted_table_name}.#{quoted_primary_key} ASC").limit(limit)
32
37
 
38
+ rel = executable_conditions.reduce(rel) do |merged, pr|
39
+ if pr.arity == 0
40
+ merged.merge(instance_exec(&pr))
41
+ else
42
+ merged.merge(instance_exec(from, &pr))
43
+ end
44
+ end
45
+
33
46
  rel
34
47
  end
35
48
 
@@ -42,36 +55,56 @@ module CronoTrigger
42
55
  transaction do
43
56
  records = executables(primary_key_offset: primary_key_offset, limit: limit).lock.to_a
44
57
  unless records.empty?
45
- where(id: records).update_all(execute_lock: Time.current.to_i)
58
+ where(id: records).update_all(crono_trigger_column_name(:execute_lock) => Time.current.to_i)
46
59
  end
47
60
  records
48
61
  end
49
62
  end
63
+
64
+ def crono_trigger_column_name(name)
65
+ crono_trigger_options["#{name}_column_name".to_sym].try(:to_s) || name.to_s
66
+ end
67
+
68
+ def execute_lock_timeout
69
+ (crono_trigger_options[:execute_lock_timeout] || DEFAULT_EXECUTE_LOCK_TIMEOUT)
70
+ end
71
+
72
+ private
73
+
74
+ def add_executable_conditions(pr)
75
+ self.executable_conditions << pr
76
+ end
77
+
78
+ def clear_executable_conditions
79
+ self.executable_conditions.clear
80
+ end
50
81
  end
51
82
 
52
83
  def do_execute
53
84
  run_callbacks :execute do
54
- catch(:ok) do
55
- catch(:retry) do
56
- catch(:abort) do
57
- execute
58
- throw :ok
85
+ catch(:ok_without_reset) do
86
+ catch(:ok) do
87
+ catch(:retry) do
88
+ catch(:abort) do
89
+ execute
90
+ throw :ok
91
+ end
92
+ raise AbortExecution
59
93
  end
60
- raise AbortExecution
94
+ retry!
95
+ return
61
96
  end
62
- retry!
63
- return
97
+ reset!
64
98
  end
65
- reset!(true)
66
99
  end
67
100
  rescue AbortExecution => ex
68
101
  save_last_error_info(ex)
69
- reset!
102
+ reset!(false)
70
103
 
71
104
  raise
72
105
  rescue Exception => ex
73
106
  save_last_error_info(ex)
74
- retry_or_reset!
107
+ retry_or_reset!(ex)
75
108
 
76
109
  raise
77
110
  end
@@ -81,7 +114,7 @@ module CronoTrigger
81
114
 
82
115
  now = Time.current
83
116
  wait = crono_trigger_options[:exponential_backoff] ? retry_interval * [2 * (retry_count - 1), 1].max : retry_interval
84
- attributes = {next_execute_at: now + wait, execute_lock: 0}
117
+ attributes = {crono_trigger_column_name(:next_execute_at) => now + wait, crono_trigger_column_name(:execute_lock) => 0}
85
118
 
86
119
  if self.class.column_names.include?("retry_count")
87
120
  attributes.merge!(retry_count: retry_count.to_i + 1)
@@ -90,13 +123,13 @@ module CronoTrigger
90
123
  update_columns(attributes)
91
124
  end
92
125
 
93
- def reset!(update_last_executed_at = false)
126
+ def reset!(update_last_executed_at = true)
94
127
  logger.info "Reset execution schedule #{self.class}-#{id}" if logger
95
128
 
96
- attributes = {next_execute_at: calculate_next_execute_at, execute_lock: 0}
129
+ attributes = {crono_trigger_column_name(:next_execute_at) => calculate_next_execute_at, crono_trigger_column_name(:execute_lock) => 0}
97
130
 
98
- if update_last_executed_at && self.class.column_names.include?("last_executed_at")
99
- attributes.merge!(last_executed_at: Time.current)
131
+ if update_last_executed_at && self.class.column_names.include?(crono_trigger_column_name(:last_executed_at))
132
+ attributes.merge!(crono_trigger_column_name(:last_executed_at) => Time.current)
100
133
  end
101
134
 
102
135
  if self.class.column_names.include?("retry_count")
@@ -106,25 +139,45 @@ module CronoTrigger
106
139
  update_columns(attributes)
107
140
  end
108
141
 
142
+ def assume_executing?
143
+ execute_lock_timeout = self.class.execute_lock_timeout
144
+ locking? &&
145
+ self[crono_trigger_column_name(:execute_lock)] + execute_lock_timeout >= Time.now.to_i
146
+ end
147
+
148
+ def locking?
149
+ self[crono_trigger_column_name(:execute_lock)] > 0
150
+ end
151
+
152
+ def idling?
153
+ !locking?
154
+ end
155
+
156
+ def crono_trigger_column_name(name)
157
+ self.class.crono_trigger_column_name(name)
158
+ end
159
+
109
160
  private
110
161
 
111
- def retry_or_reset!
112
- if respond_to?(:retry_count) && retry_count.to_i <= retry_limit
162
+ def retry_or_reset!(ex)
163
+ if respond_to?(:retry_count) && retry_count.to_i < retry_limit
113
164
  retry!
114
165
  else
115
- reset!
166
+ CronoTrigger::ExceptionHandler.handle_exception(self, ex)
167
+ reset!(false)
116
168
  end
117
169
  end
118
170
 
119
- def calculate_next_execute_at
120
- if respond_to?(:cron) && cron
121
- it = Chrono::Iterator.new(cron)
122
- it.next
171
+ def calculate_next_execute_at(now = Time.current)
172
+ if self[crono_trigger_column_name(:cron)]
173
+ tz = self[crono_trigger_column_name(:timezone)].try { |zn| TZInfo::Timezone.get(zn) }
174
+ now = tz ? now.in_time_zone(tz) : now
175
+ Chrono::NextTime.new(now: now, source: self[crono_trigger_column_name(:cron)]).to_time
123
176
  end
124
177
  end
125
178
 
126
179
  def ensure_next_execute_at
127
- self.next_execute_at ||= calculate_next_execute_at || Time.current
180
+ self[crono_trigger_column_name(:next_execute_at)] ||= calculate_next_execute_at || Time.current
128
181
  end
129
182
 
130
183
  def retry_limit
@@ -152,7 +205,7 @@ module CronoTrigger
152
205
  attributes.merge!(last_error_time: now)
153
206
  end
154
207
 
155
- update_columns(attributes)
208
+ update_columns(attributes) unless attributes.empty?
156
209
  end
157
210
  end
158
211
  end
@@ -1,3 +1,3 @@
1
1
  module CronoTrigger
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -15,6 +15,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::M
15
15
  t.string :cron
16
16
  t.datetime :next_execute_at
17
17
  t.datetime :last_executed_at
18
+ t.string :timezone
18
19
  t.integer :execute_lock, limit: 8, default: 0, null: false
19
20
  t.datetime :started_at, null: false
20
21
  t.datetime :finished_at
@@ -4,6 +4,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= Rails::VERSION::M
4
4
  add_column :<%= table_name %>, :cron, :string
5
5
  add_column :<%= table_name %>, :next_execute_at, :datetime
6
6
  add_column :<%= table_name %>, :last_executed_at, :datetime
7
+ add_column :<%= table_name %>, :timezone, :string
7
8
  add_column :<%= table_name %>, :execute_lock, :integer, limit: 8, default: 0, null: false
8
9
  add_column :<%= table_name %>, :started_at, :datetime, null: false
9
10
  add_column :<%= table_name %>, :finished_at, :datetime
@@ -0,0 +1,7 @@
1
+ <% module_namespacing do -%>
2
+ module <%= class_path.map(&:camelize).join('::') %>
3
+ def self.table_name_prefix
4
+ '<%= namespaced? ? namespaced_class_path.join('_') : class_path.join('_') %>_'
5
+ end
6
+ end
7
+ <% end -%>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crono_trigger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - joker1007
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-06-19 00:00:00.000000000 Z
11
+ date: 2017-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chrono
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: tzinfo
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: activerecord
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +95,7 @@ dependencies:
81
95
  - !ruby/object:Gem::Version
82
96
  version: '0'
83
97
  - !ruby/object:Gem::Dependency
84
- name: database_rewinder
98
+ name: timecop
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - ">="
@@ -95,7 +109,7 @@ dependencies:
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0'
97
111
  - !ruby/object:Gem::Dependency
98
- name: timecop
112
+ name: rollbar
99
113
  requirement: !ruby/object:Gem::Requirement
100
114
  requirements:
101
115
  - - ">="
@@ -126,30 +140,44 @@ dependencies:
126
140
  name: rake
127
141
  requirement: !ruby/object:Gem::Requirement
128
142
  requirements:
129
- - - "~>"
143
+ - - ">="
130
144
  - !ruby/object:Gem::Version
131
- version: '10.0'
145
+ version: '0'
132
146
  type: :development
133
147
  prerelease: false
134
148
  version_requirements: !ruby/object:Gem::Requirement
135
149
  requirements:
136
- - - "~>"
150
+ - - ">="
137
151
  - !ruby/object:Gem::Version
138
- version: '10.0'
152
+ version: '0'
139
153
  - !ruby/object:Gem::Dependency
140
154
  name: rspec
141
155
  requirement: !ruby/object:Gem::Requirement
142
156
  requirements:
143
- - - "~>"
157
+ - - ">="
144
158
  - !ruby/object:Gem::Version
145
- version: '3.0'
159
+ version: '0'
146
160
  type: :development
147
161
  prerelease: false
148
162
  version_requirements: !ruby/object:Gem::Requirement
149
163
  requirements:
150
- - - "~>"
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: codecov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
151
179
  - !ruby/object:Gem::Version
152
- version: '3.0'
180
+ version: '0'
153
181
  description: In Service Asynchronous Job Scheduler for Rails. This gem handles ActiveRecord
154
182
  model as schedule definition.
155
183
  email:
@@ -175,8 +203,10 @@ files:
175
203
  - gemfiles/activerecord-51.gemfile
176
204
  - lib/crono_trigger.rb
177
205
  - lib/crono_trigger/cli.rb
206
+ - lib/crono_trigger/exception_handler.rb
178
207
  - lib/crono_trigger/polling_thread.rb
179
208
  - lib/crono_trigger/railtie.rb
209
+ - lib/crono_trigger/rollbar.rb
180
210
  - lib/crono_trigger/schedulable.rb
181
211
  - lib/crono_trigger/version.rb
182
212
  - lib/crono_trigger/worker.rb
@@ -185,6 +215,7 @@ files:
185
215
  - lib/generators/crono_trigger/migration/templates/migration.rb
186
216
  - lib/generators/crono_trigger/model/model_generator.rb
187
217
  - lib/generators/crono_trigger/model/templates/model.rb
218
+ - lib/generators/crono_trigger/model/templates/module.rb
188
219
  homepage: https://github.com/joker1007/crono_trigger
189
220
  licenses:
190
221
  - MIT