crono_trigger 0.5.4 → 0.6.2

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