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 +4 -4
- data/.github/workflows/rspec.yml +27 -0
- data/README.md +23 -15
- data/lib/crono_trigger/cli.rb +2 -2
- data/lib/crono_trigger/execution_tracker.rb +15 -2
- data/lib/crono_trigger/global_exception_handler.rb +29 -0
- data/lib/crono_trigger/models/execution.rb +2 -0
- data/lib/crono_trigger/polling_thread.rb +17 -11
- data/lib/crono_trigger/rollbar.rb +9 -3
- data/lib/crono_trigger/schedulable.rb +46 -31
- data/lib/crono_trigger/version.rb +1 -1
- data/lib/crono_trigger/worker.rb +8 -2
- data/lib/crono_trigger.rb +1 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50a79e41f2762ce69d2667b8f9af205a30764986f8f7dab39c6ad26539e35e5d
|
4
|
+
data.tar.gz: 8d10f131f42691ee9e8b4f0479f8959d9ac7f8b3ff3b5e33b03d10c5933d49a0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
[](https://badge.fury.io/rb/crono_trigger)
|
3
|
-
|
3
|
+

|
4
4
|
[](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
|
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
|
164
|
-
|
165
|
-
|cron
|
166
|
-
|next_execute_at
|
167
|
-
|last_executed_at
|
168
|
-
|timezone
|
169
|
-
|execute_lock
|
170
|
-
|started_at
|
171
|
-
|finished_at
|
172
|
-
|last_error_name
|
173
|
-
|last_error_reason|string
|
174
|
-
|last_error_time
|
175
|
-
|retry_count
|
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"`
|
data/lib/crono_trigger/cli.rb
CHANGED
@@ -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[:
|
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
|
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
|
-
|
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
|
@@ -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 =>
|
26
|
-
@logger.error(
|
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
|
-
|
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
|
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
|
-
|
116
|
-
|
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
|
-
|
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
|
|
data/lib/crono_trigger/worker.rb
CHANGED
@@ -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
|
-
|
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
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.
|
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:
|
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.
|
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: []
|