crono_trigger 0.5.4 → 0.6.2

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
  SHA256:
3
- metadata.gz: 414c7d5d5bc3a58298ef4cefaaf5dd23e12e6b2af43b2f269b77ddfa4460e3fa
4
- data.tar.gz: 8004dcf5504db3f36c6e0548197842eaf3665cb1957ed8400c39338fc94b5539
3
+ metadata.gz: 50a79e41f2762ce69d2667b8f9af205a30764986f8f7dab39c6ad26539e35e5d
4
+ data.tar.gz: 8d10f131f42691ee9e8b4f0479f8959d9ac7f8b3ff3b5e33b03d10c5933d49a0
5
5
  SHA512:
6
- metadata.gz: 7de0e41900387c9c2f66e1df342645080e98d1058a39a7d4553dc9bd38625c640d2f48ccb360052bc31d614022a0c93bd1a08976d4b766f70c6fbf1de3d03133
7
- data.tar.gz: e79b4255892a31cfe73bc673705605549b6ab687712e754d0aa790d45d4e09bc98006b86a52fac6901ad4e433951c15ae5f15ba71b88da60b6297884ee8db224
6
+ metadata.gz: bf08091433a57d26df8e5fc92098e9e9b19a2693394c7006f60cfb86202f3856d848c2781eb1164436e47ba98ac9bb9e07319ca508092668f94b390735f28fdb
7
+ data.tar.gz: 6d5cba81e1169e57258fbd410aeaf5ab27a12155c93ad4ca7ec1b64b1fce6f7eca9b0fa57aa0dbaa9bcfa68118c67e0fe0dc64b16e9541cbd6ffb7b36b78012f
@@ -0,0 +1,27 @@
1
+ name: RSpec
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ['2.7', '3.0', '3.1']
15
+
16
+ steps:
17
+ - uses: actions/checkout@v2
18
+ - name: Set up Ruby
19
+ # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
20
+ # change this to (see https://github.com/ruby/setup-ruby#versioning):
21
+ # uses: ruby/setup-ruby@v1
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: ${{ matrix.ruby-version }}
25
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
26
+ - name: Run tests
27
+ run: bundle exec rake
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CronoTrigger
2
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)
3
+ ![rspec](https://github.com/joker1007/crono_trigger/actions/workflows/rspec.yml/badge.svg)
4
4
  [![codecov](https://codecov.io/gh/joker1007/crono_trigger/branch/master/graph/badge.svg)](https://codecov.io/gh/joker1007/crono_trigger)
5
5
 
6
6
  Asynchronous Job Scheduler for Rails.
@@ -111,7 +111,7 @@ class MailNotification < ActiveRecord::Base
111
111
  send_mail
112
112
 
113
113
  throw :retry # break execution and retry task
114
- throw :abort # break execution and raise AbortExecution. AbortExecution is not retried
114
+ throw :abort # break execution
115
115
  throw :ok # break execution and handle task as success
116
116
  throw :ok_without_reset # break execution and handle task as success but without schedule reseting and unlocking
117
117
  end
@@ -137,10 +137,17 @@ mail.next_execute_at # => next 13:00 with Asia/Japan
137
137
 
138
138
  #### Run Worker
139
139
 
140
+ ues `crono_trigger` command.
141
+ `crono_trigger` command accepts model class names.
142
+
143
+ For example,
144
+
140
145
  ```
141
146
  $ crono_trigger MailNotification
142
147
  ```
143
148
 
149
+ And other options is following.
150
+
144
151
  ```
145
152
  $ crono_trigger --help
146
153
  Usage: crono_trigger [options] MODEL [MODEL..]
@@ -160,19 +167,20 @@ Usage: crono_trigger [options] MODEL [MODEL..]
160
167
 
161
168
  ### Columns
162
169
 
163
- |name |type |required|rename|description |
164
- |-----------------|--------|--------|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
165
- |cron |string |no |no |Recurring schedule formatted by cron style |
166
- |next_execute_at |datetime|yes |yes |Timestamp of next execution. Worker executes task if this column <= now |
167
- |last_executed_at |datetime|no |yes |Timestamp of last execution |
168
- |timezone |datetime|no |yes |Timezone name (Parsed by tzinfo) |
169
- |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|
170
- |started_at |datetime|no |yes |Timestamp of schedule activated |
171
- |finished_at |datetime|no |yes |Timestamp of schedule deactivated |
172
- |last_error_name |string |no |no |Class name of last error |
173
- |last_error_reason|string |no |no |Error message of last error |
174
- |last_error_time |datetime|no |no |Timestamp of last error occured |
175
- |retry_count |integer |no |no |Retry count. <br> If execution succeed retry_count is reset to 0 |
170
+ | name | type | required | rename | description |
171
+ | ----------------- | -------- | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
172
+ | cron | string | no | no | Recurring schedule formatted by cron style |
173
+ | next_execute_at | datetime | yes | yes | Timestamp of next execution. Worker executes task if this column <= now |
174
+ | last_executed_at | datetime | no | yes | Timestamp of last execution |
175
+ | timezone | datetime | no | yes | Timezone name (Parsed by tzinfo) |
176
+ | 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 |
177
+ | started_at | datetime | no | yes | Timestamp of schedule activated |
178
+ | finished_at | datetime | no | yes | Timestamp of schedule deactivated |
179
+ | last_error_name | string | no | no | Class name of last error |
180
+ | last_error_reason | string | no | no | Error message of last error |
181
+ | last_error_time | datetime | no | no | Timestamp of last error occured |
182
+ | retry_count | integer | no | no | Retry count. <br> If execution succeed retry_count is reset to 0 |
183
+ | current_cycle_id | string | no | yes | UUID that is updated when the schedule is resetted successfully |
176
184
 
177
185
  You can rename some columns.
178
186
  ex. `crono_trigger_options[:next_execute_at_column_name] = "next_time"`
@@ -31,7 +31,7 @@ opt_parser = OptionParser.new do |opts|
31
31
  end
32
32
 
33
33
  opts.on("-c", "--concurrency=SIZE", Integer, "Execute thread size (Default: 25)") do |i|
34
- options[:execute_thread] = i
34
+ options[:executor_thread] = i
35
35
  end
36
36
 
37
37
  opts.on("-l", "--log=LOGFILE", "Set log output destination (Default: STDOUT or ./crono_trigger.log if daemonize is true)") do |log|
@@ -67,7 +67,7 @@ end
67
67
 
68
68
  CronoTrigger.load_config(options[:config], options[:env]) if options[:config]
69
69
 
70
- %i(polling_thread polling_interval execute_thread).each do |name|
70
+ %i(worker_id polling_thread polling_interval executor_thread).each do |name|
71
71
  CronoTrigger.config[name] = options[name] if options[name]
72
72
  end
73
73
 
@@ -4,12 +4,25 @@ module CronoTrigger
4
4
  @schedulable = schedulable
5
5
  end
6
6
 
7
+ def self.track(schedulable, &pr)
8
+ new(schedulable).track(&pr)
9
+ end
10
+
7
11
  def track(&pr)
8
12
  if @schedulable.track_execution
9
13
  begin
10
14
  execution = @schedulable.crono_trigger_executions.create_with_timestamp!
11
- pr.call
12
- execution.complete!
15
+ result = pr.call
16
+ case result
17
+ when :ok
18
+ execution.complete!
19
+ when :retry
20
+ execution.retrying!
21
+ when :abort
22
+ execution.aborted!
23
+ else
24
+ execution.complete!
25
+ end
13
26
  rescue => e
14
27
  execution.error!(e)
15
28
  raise
@@ -0,0 +1,29 @@
1
+ module CronoTrigger
2
+ class GlobalExceptionHandler
3
+ def self.handle_global_exception(ex)
4
+ new.handle_global_exception(ex)
5
+ end
6
+
7
+ def handle_global_exception(ex)
8
+ handlers = CronoTrigger.config.global_error_handlers
9
+ handlers.each do |callable|
10
+ callable, arity = ensure_callable(callable)
11
+
12
+ args = [ex]
13
+ args = arity < 0 ? args : args.take(arity)
14
+ callable.call(*args)
15
+ end
16
+ rescue Exception => e
17
+ ActiveRecord::Base.logger.error("CronoTrigger error handler raises error")
18
+ ActiveRecord::Base.logger.error(e)
19
+ end
20
+
21
+ private
22
+
23
+ def ensure_callable(callable)
24
+ if callable.respond_to?(:call)
25
+ return callable, callable.arity
26
+ end
27
+ end
28
+ end
29
+ end
@@ -11,6 +11,8 @@ module CronoTrigger
11
11
  executing: "executing",
12
12
  completed: "completed",
13
13
  failed: "failed",
14
+ retrying: "retrying",
15
+ aborted: "aborted",
14
16
  }
15
17
 
16
18
  def self.create_with_timestamp!
@@ -22,8 +22,9 @@ module CronoTrigger
22
22
  poll(model)
23
23
  rescue ThreadError => e
24
24
  @logger.error(e) unless e.message == "queue empty"
25
- rescue => e
26
- @logger.error(e)
25
+ rescue => ex
26
+ @logger.error(ex)
27
+ CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
27
28
  ensure
28
29
  @model_queue << model_name if model_name
29
30
  end
@@ -63,14 +64,7 @@ module CronoTrigger
63
64
  @executor.post do
64
65
  @execution_counter.increment
65
66
  begin
66
- model.connection_pool.with_connection do
67
- @logger.info "(executor-thread-#{Thread.current.object_id}) Execute #{record.class}-#{record.id}"
68
- begin
69
- record.do_execute
70
- rescue Exception => e
71
- @logger.error(e)
72
- end
73
- end
67
+ process_record(record)
74
68
  ensure
75
69
  @execution_counter.decrement
76
70
  end
@@ -83,7 +77,19 @@ module CronoTrigger
83
77
  end while overflowed_record_ids.empty? && records.any?
84
78
  end
85
79
 
86
- private def unlock_overflowed_records(model, overflowed_record_ids)
80
+ private
81
+
82
+ def process_record(record)
83
+ record.class.connection_pool.with_connection do
84
+ @logger.info "(executor-thread-#{Thread.current.object_id}) Execute #{record.class}-#{record.id}"
85
+ record.do_execute
86
+ end
87
+ rescue Exception => ex
88
+ @logger.error(ex)
89
+ CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
90
+ end
91
+
92
+ def unlock_overflowed_records(model, overflowed_record_ids)
87
93
  model.connection_pool.with_connection do
88
94
  unless overflowed_record_ids.empty?
89
95
  model.where(id: overflowed_record_ids).crono_trigger_unlock_all!
@@ -2,12 +2,15 @@ require 'rollbar'
2
2
 
3
3
  module Rollbar
4
4
  class CronoTrigger
5
- def self.handle_exception(ex, record)
5
+ def self.handle_exception(ex, record = nil)
6
6
  scope = {
7
7
  framework: "CronoTrigger: #{::CronoTrigger::VERSION}",
8
- context: "#{record.class}/#{record.id}"
9
8
  }
10
9
 
10
+ if record
11
+ scope.merge!({context: "#{record.class}/#{record.id}"})
12
+ end
13
+
11
14
  Rollbar.scope(scope).error(ex, use_exception_level_filters: true)
12
15
  end
13
16
  end
@@ -18,8 +21,11 @@ Rollbar.plugins.define('crono_trigger') do
18
21
 
19
22
  execute! do
20
23
  CronoTrigger.config.error_handlers << proc do |ex, record|
21
- Rollbar.reset_notifier!
22
24
  Rollbar::CronoTrigger.handle_exception(ex, record)
23
25
  end
26
+
27
+ CronoTrigger.config.global_error_handlers << proc do |ex|
28
+ Rollbar::CronoTrigger.handle_exception(ex)
29
+ end
24
30
  end
25
31
  end
@@ -20,9 +20,6 @@ module CronoTrigger
20
20
  @included_by
21
21
  end
22
22
 
23
- class AbortExecution < StandardError; end
24
- class RetryExecution < StandardError; end
25
-
26
23
  extend ActiveSupport::Concern
27
24
  include ActiveSupport::Callbacks
28
25
 
@@ -59,6 +56,7 @@ module CronoTrigger
59
56
  rel
60
57
  end
61
58
 
59
+ before_create :set_current_cycle_id
62
60
  before_update :update_next_execute_at_if_update_cron
63
61
 
64
62
  validate :validate_cron_format
@@ -112,37 +110,34 @@ module CronoTrigger
112
110
  end
113
111
 
114
112
  def do_execute
115
- execution_tracker = ExecutionTracker.new(self)
116
- run_callbacks :execute do
117
- execution_tracker.track do
118
- catch(:ok_without_reset) do
119
- catch(:ok) do
120
- catch(:retry) do
121
- catch(:abort) do
122
- execute
123
- throw :ok
124
- end
125
- raise AbortExecution
126
- end
127
- retry!
128
- raise RetryExecution
129
- end
130
- reset!
131
- end
132
- end
113
+ ExecutionTracker.track(self) do
114
+ do_execute_with_catch
133
115
  end
134
- rescue AbortExecution => ex
135
- save_last_error_info(ex)
136
- reset!(false)
137
-
138
- raise
139
- rescue RetryExecution => ex
140
- save_last_error_info(ex)
141
116
  rescue Exception => ex
117
+ logger.error(ex) if logger
142
118
  save_last_error_info(ex)
143
119
  retry_or_reset!(ex)
120
+ end
144
121
 
145
- raise
122
+ private def do_execute_with_catch
123
+ catch(:ok_without_reset) do
124
+ catch(:ok) do
125
+ catch(:retry) do
126
+ catch(:abort) do
127
+ run_callbacks :execute do
128
+ execute
129
+ end
130
+ throw :ok
131
+ end
132
+ abort_execution!
133
+ return :abort
134
+ end
135
+ retry!
136
+ return :retry
137
+ end
138
+ reset!
139
+ return :ok
140
+ end
146
141
  end
147
142
 
148
143
  def activate_schedule!(at: Time.current)
@@ -213,10 +208,18 @@ module CronoTrigger
213
208
  attributes.merge!(retry_count: 0)
214
209
  end
215
210
 
211
+ if self.class.column_names.include?(crono_trigger_column_name(:current_cycle_id))
212
+ attributes.merge!(crono_trigger_column_name(:current_cycle_id) => SecureRandom.uuid)
213
+ end
214
+
216
215
  merge_updated_at_for_crono_trigger!(attributes, now)
217
216
  update_columns(attributes)
218
217
  end
219
218
 
219
+ def abort_execution!
220
+ reset!(false)
221
+ end
222
+
220
223
  def crono_trigger_lock!(**attributes)
221
224
  attributes = {
222
225
  crono_trigger_column_name(:execute_lock) => Time.current.to_i,
@@ -259,7 +262,7 @@ module CronoTrigger
259
262
  end
260
263
 
261
264
  def locking?(at: Time.now)
262
- self[crono_trigger_column_name(:execute_lock)] > 0 &&
265
+ self[crono_trigger_column_name(:execute_lock)] > 0 &&
263
266
  self[crono_trigger_column_name(:execute_lock)] >= at.to_f - self.class.execute_lock_timeout
264
267
  end
265
268
 
@@ -293,7 +296,19 @@ module CronoTrigger
293
296
  tz = self[crono_trigger_column_name(:timezone)].try { |zn| TZInfo::Timezone.get(zn) }
294
297
  base = [now, self[crono_trigger_column_name(:started_at)]].compact.max
295
298
  cron_now = tz ? base.in_time_zone(tz) : base
296
- Chrono::NextTime.new(now: cron_now, source: self[crono_trigger_column_name(:cron)]).to_time
299
+ calculated = Chrono::NextTime.new(now: cron_now, source: self[crono_trigger_column_name(:cron)]).to_time
300
+
301
+ return calculated unless self[crono_trigger_column_name(:finished_at)]
302
+ return if calculated > self[crono_trigger_column_name(:finished_at)]
303
+
304
+ calculated
305
+ end
306
+ end
307
+
308
+ def set_current_cycle_id
309
+ if self.class.column_names.include?(crono_trigger_column_name(:current_cycle_id)) &&
310
+ self[crono_trigger_column_name(:current_cycle_id)].nil?
311
+ self[crono_trigger_column_name(:current_cycle_id)] = SecureRandom.uuid
297
312
  end
298
313
  end
299
314
 
@@ -1,3 +1,3 @@
1
1
  module CronoTrigger
2
- VERSION = "0.5.4"
2
+ VERSION = "0.6.2"
3
3
  end
@@ -1,5 +1,7 @@
1
1
  require "active_support/core_ext/string"
2
2
 
3
+ require "crono_trigger/global_exception_handler"
4
+
3
5
  module CronoTrigger
4
6
  module Worker
5
7
  HEARTBEAT_INTERVAL = 60
@@ -100,9 +102,9 @@ module CronoTrigger
100
102
  worker_record.polling_model_names = @model_names
101
103
  worker_record.last_heartbeated_at = Time.current
102
104
  @logger.info("[worker_id:#{@crono_trigger_worker_id}] Send heartbeat to database")
103
- worker_record.save
105
+ worker_record.save!
104
106
  rescue => ex
105
- p ex
107
+ CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
106
108
  stop
107
109
  end
108
110
  end
@@ -128,6 +130,8 @@ module CronoTrigger
128
130
  CronoTrigger::Models::Worker.connection_pool.with_connection do
129
131
  CronoTrigger::Models::Worker.find_by(worker_id: @crono_trigger_worker_id)&.destroy
130
132
  end
133
+ rescue => ex
134
+ CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
131
135
  end
132
136
 
133
137
  def handle_signal_from_rdb
@@ -137,6 +141,8 @@ module CronoTrigger
137
141
  s.kill_me(to_supervisor: s.signal != "TSTP")
138
142
  end
139
143
  end
144
+ rescue => ex
145
+ CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
140
146
  end
141
147
  end
142
148
  end
data/lib/crono_trigger.rb CHANGED
@@ -19,6 +19,7 @@ module CronoTrigger
19
19
  executor_thread: 25,
20
20
  model_names: nil,
21
21
  error_handlers: [],
22
+ global_error_handlers: [],
22
23
  )
23
24
 
24
25
  def self.config
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.5.4
4
+ version: 0.6.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - joker1007
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-03-23 00:00:00.000000000 Z
11
+ date: 2022-07-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chrono
@@ -244,6 +244,7 @@ executables:
244
244
  extensions: []
245
245
  extra_rdoc_files: []
246
246
  files:
247
+ - ".github/workflows/rspec.yml"
247
248
  - ".gitignore"
248
249
  - ".rspec"
249
250
  - ".travis.yml"
@@ -263,6 +264,7 @@ files:
263
264
  - lib/crono_trigger/cli.rb
264
265
  - lib/crono_trigger/exception_handler.rb
265
266
  - lib/crono_trigger/execution_tracker.rb
267
+ - lib/crono_trigger/global_exception_handler.rb
266
268
  - lib/crono_trigger/models/execution.rb
267
269
  - lib/crono_trigger/models/signal.rb
268
270
  - lib/crono_trigger/models/worker.rb
@@ -323,7 +325,7 @@ homepage: https://github.com/joker1007/crono_trigger
323
325
  licenses:
324
326
  - MIT
325
327
  metadata: {}
326
- post_install_message:
328
+ post_install_message:
327
329
  rdoc_options: []
328
330
  require_paths:
329
331
  - lib
@@ -338,8 +340,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
338
340
  - !ruby/object:Gem::Version
339
341
  version: '0'
340
342
  requirements: []
341
- rubygems_version: 3.1.2
342
- signing_key:
343
+ rubygems_version: 3.3.3
344
+ signing_key:
343
345
  specification_version: 4
344
346
  summary: In Service Asynchronous Job Scheduler for Rails
345
347
  test_files: []