crono_trigger 0.8.0 → 0.8.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: 734da2ad0480e79b440d8ef73f9b83fb88f13e6f6510513340c62b70c3750c39
4
- data.tar.gz: b338b9c86a9fdf01d0ba739fa9f53c9536393cec05d7fe5c8943fe05782ad7b4
3
+ metadata.gz: 94ee913ba2838c73020154bb6f85f49ff93a4d2d3ba39bed1d0be26255b7d06c
4
+ data.tar.gz: 4222b9af986177a6190c954b8dc9c639a06f5775568422929e3e33699443e5c5
5
5
  SHA512:
6
- metadata.gz: 6e73b97cece87a4b887e4cec1c5cbd153cc9ad51c3de1956c38ef2a5000fa74c0d3e52c07401c0aa033f0921852a2f6be85533aea050d425dc220c591681a210
7
- data.tar.gz: dfbec13afaa581a544b50481fa54509ace24de39f8ecb6d99788e508bd680da1e5afd89abeb0cde7be8ad8db3c7c4b56ae67dbdf0bd087775eaeed62b92d8cac
6
+ metadata.gz: 2f0afb27ab496e72e5754c7adb777526aefee00b7c9e76e31775395c1d40dadf5889dc072308193becc43fa2b2e2e87a92f5ef5a93942319869065aa590aeb4b
7
+ data.tar.gz: 21a0f3570da6a32302bd84ed72630afdcdf7c70f0b6e691fcb0bd21d6e3cdde87cf91129a74a09a2fd692f0a33830d7e904df6fc253ba9a63eb4709c225f2e3c
@@ -11,17 +11,21 @@ jobs:
11
11
  runs-on: ubuntu-latest
12
12
  strategy:
13
13
  matrix:
14
- ruby-version: ['2.7', '3.0', '3.1']
14
+ ruby-version: ['3.1', '3.2', '3.3']
15
15
  gemfile: ["Gemfile", "gemfiles/activerecord-61.gemfile", "gemfiles/activerecord-70.gemfile", "gemfiles/activerecord-71.gemfile"]
16
+ exclude:
17
+ - ruby-version: '3.2'
18
+ gemfile: "gemfiles/activerecord-61.gemfile"
19
+ - ruby-version: '3.3'
20
+ gemfile: "gemfiles/activerecord-61.gemfile"
16
21
  env:
17
22
  BUNDLE_GEMFILE: ${{ matrix.gemfile }}
18
23
 
19
24
  steps:
20
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v4
21
26
  - name: Set up Ruby
22
27
  # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
23
28
  # change this to (see https://github.com/ruby/setup-ruby#versioning):
24
- # uses: ruby/setup-ruby@v1
25
29
  uses: ruby/setup-ruby@v1
26
30
  with:
27
31
  ruby-version: ${{ matrix.ruby-version }}
data/README.md CHANGED
@@ -178,6 +178,57 @@ Usage: crono_trigger [options] MODEL [MODEL..]
178
178
  -h, --help Prints this help
179
179
  ```
180
180
 
181
+ #### Handle errors
182
+
183
+ This gem provides the following options to handle errors:
184
+
185
+ ```ruby
186
+ CronoTrigger.configure do |config|
187
+ # These handlers are called when the execute method fails even after retries.
188
+ config.error_handlers << proc do |exception, record|
189
+ ActiveRecord::Base.logger.error("Failed to process #{record.class}##{record.id}: #{exception} (#{exception.class})")
190
+ end
191
+
192
+ # These handlers are called when an exception occurs outside of the execute method.
193
+ config.global_error_handlers << proc do |exception|
194
+ ActiveRecord::Base.logger.error("#{exception} (#{exception.class})\n#{exception.backtrace.join("\n")}")
195
+ end
196
+
197
+ # db_error_retriable_options are passed to `Retriable.retriable` used in the idempotent code that accesses the database
198
+ # except for the code processing each record.
199
+ # Here is the default value.
200
+ config.db_error_retriable_options = {
201
+ on: {
202
+ ActiveRecord::ConnectionNotEstablished => nil,
203
+ },
204
+ }
205
+ end
206
+ ```
207
+
208
+ For example, if you would like to reconnect to the database before retry for some reason, you can do so using `on_retry` option as follows:
209
+
210
+ ```ruby
211
+ CronoTrigger.configure do |config|
212
+ config.db_error_retriable_options = {
213
+ on: {
214
+ ActiveRecord::ConnectionNotEstablished => nil,
215
+ Mysql2::Error::ConnectionError => nil,
216
+ ActiveRecord::StatementInvalid => /MySQL server is running with the --read-only option/,
217
+ },
218
+ on_retry: proc do |exception, try, elapsed_time, interval|
219
+ ActiveRecord::Base.logger.info("#{try}th: Retry on #{exception.class}: #{exception}")
220
+ next unless exception.is_a?(ActiveRecord::StatementInvalid)
221
+
222
+ ActiveRecord::Base.logger.info("#{try}th: Reconnect to MySQL on #{exception.class}: #{exception}")
223
+ # NOTE: The connection acquired here might be different from the one used in the code that raised the error,
224
+ # but we don't have a way to get the latter connection.
225
+ ActiveRecord::Base.connection_pool.with_connection(&:reconnect!)
226
+ end,
227
+ }
228
+ end
229
+ ```
230
+
231
+
181
232
  ## Specification
182
233
 
183
234
  ### Columns
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_dependency "rack-contrib"
31
31
  spec.add_dependency "oj"
32
32
  spec.add_dependency "activerecord", ">= 4.2"
33
+ spec.add_dependency "retriable"
33
34
 
34
35
  spec.add_development_dependency "sqlite3", "~> 1.3"
35
36
  spec.add_development_dependency "mysql2"
@@ -38,5 +39,6 @@ Gem::Specification.new do |spec|
38
39
  spec.add_development_dependency "bundler", "~> 2.0"
39
40
  spec.add_development_dependency "rake"
40
41
  spec.add_development_dependency "rspec"
41
- spec.add_development_dependency "codecov"
42
+ spec.add_development_dependency "simplecov"
43
+ spec.add_development_dependency "simplecov-cobertura"
42
44
  end
@@ -58,8 +58,10 @@ module CronoTrigger
58
58
 
59
59
  maybe_has_next = true
60
60
  while maybe_has_next && !@stop_flag.set?
61
- records, maybe_has_next = model.connection_pool.with_connection do
62
- model.executables_with_lock(limit: CronoTrigger.config.fetch_records || CronoTrigger.config.executor_thread * 3, worker_count: @worker_count)
61
+ records, maybe_has_next = CronoTrigger.retry_on_db_errors do
62
+ model.connection_pool.with_connection do
63
+ model.executables_with_lock(limit: CronoTrigger.config.fetch_records || CronoTrigger.config.executor_thread * 3, worker_count: @worker_count)
64
+ end
63
65
  end
64
66
 
65
67
  records.each do |record|
@@ -81,6 +81,12 @@ module CronoTrigger
81
81
 
82
82
  return [records, maybe_has_next] if records.size == limit
83
83
  end
84
+
85
+ [records, maybe_has_next]
86
+ rescue => e
87
+ raise if records.empty?
88
+
89
+ logger&.warn("Failed to fetching some records but continue processing records: #{e} (#{e.class})")
84
90
  [records, maybe_has_next]
85
91
  end
86
92
 
@@ -1,3 +1,3 @@
1
1
  module CronoTrigger
2
- VERSION = "0.8.0"
2
+ VERSION = "0.8.2"
3
3
  end
@@ -38,7 +38,6 @@ module CronoTrigger
38
38
  @heartbeat_thread = run_heartbeat_thread
39
39
  @signal_fetcn_thread = run_signal_fetch_thread
40
40
  @monitor_thread = run_monitor_thread
41
- @worker_count_updater_thread = run_worker_count_updater_thread
42
41
 
43
42
  polling_thread_count = CronoTrigger.config.polling_thread || [@model_names.size, Concurrent.processor_count].min
44
43
  # Assign local variable for Signal handling
@@ -46,6 +45,8 @@ module CronoTrigger
46
45
  @polling_threads = polling_threads
47
46
  @polling_threads.each(&:run)
48
47
 
48
+ @worker_count_updater_thread = run_worker_count_updater_thread
49
+
49
50
  ServerEngine::SignalThread.new do |st|
50
51
  st.trap(:TSTP) do
51
52
  @logger.info("[worker_id:#{@crono_trigger_worker_id}] Transit to quiet mode")
@@ -117,8 +118,8 @@ module CronoTrigger
117
118
  end
118
119
 
119
120
  def heartbeat
120
- CronoTrigger::Models::Worker.connection_pool.with_connection do
121
- begin
121
+ CronoTrigger.retry_on_db_errors do
122
+ CronoTrigger::Models::Worker.connection_pool.with_connection do
122
123
  worker_record = CronoTrigger::Models::Worker.find_or_initialize_by(worker_id: @crono_trigger_worker_id)
123
124
  worker_record.max_thread_size = @executor.max_length
124
125
  worker_record.current_executing_size = @execution_counter.value
@@ -128,11 +129,11 @@ module CronoTrigger
128
129
  worker_record.last_heartbeated_at = Time.current
129
130
  @logger.info("[worker_id:#{@crono_trigger_worker_id}] Send heartbeat to database")
130
131
  worker_record.save!
131
- rescue => ex
132
- CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
133
- stop
134
132
  end
135
133
  end
134
+ rescue => ex
135
+ CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
136
+ stop
136
137
  end
137
138
 
138
139
  def executor_status
@@ -152,18 +153,22 @@ module CronoTrigger
152
153
 
153
154
  def unregister
154
155
  @logger.info("[worker_id:#{@crono_trigger_worker_id}] Unregister worker from database")
155
- CronoTrigger::Models::Worker.connection_pool.with_connection do
156
- CronoTrigger::Models::Worker.find_by(worker_id: @crono_trigger_worker_id)&.destroy
156
+ CronoTrigger.retry_on_db_errors do
157
+ CronoTrigger::Models::Worker.connection_pool.with_connection do
158
+ CronoTrigger::Models::Worker.find_by(worker_id: @crono_trigger_worker_id)&.destroy
159
+ end
157
160
  end
158
161
  rescue => ex
159
162
  CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
160
163
  end
161
164
 
162
165
  def handle_signal_from_rdb
163
- CronoTrigger::Models::Signal.connection_pool.with_connection do
164
- CronoTrigger::Models::Signal.sent_to_me.take(1)[0]&.tap do |s|
165
- @logger.info("[worker_id:#{@crono_trigger_worker_id}] Receive Signal #{s.signal} from database")
166
- s.kill_me(to_supervisor: s.signal != "TSTP")
166
+ CronoTrigger.retry_on_db_errors do
167
+ CronoTrigger::Models::Signal.connection_pool.with_connection do
168
+ CronoTrigger::Models::Signal.sent_to_me.take(1)[0]&.tap do |s|
169
+ @logger.info("[worker_id:#{@crono_trigger_worker_id}] Receive Signal #{s.signal} from database")
170
+ s.kill_me(to_supervisor: s.signal != "TSTP")
171
+ end
167
172
  end
168
173
  end
169
174
  rescue => ex
@@ -173,29 +178,31 @@ module CronoTrigger
173
178
  def monitor
174
179
  return unless ActiveSupport::Notifications.notifier.listening?(CronoTrigger::Events::MONITOR)
175
180
 
176
- CronoTrigger::Models::Worker.connection_pool.with_connection do
177
- if workers_processing_same_models.order(:worker_id).limit(1).pluck(:worker_id).first != @crono_trigger_worker_id
178
- # Return immediately to avoid redundant instruments
179
- return
180
- end
181
-
182
- @model_names.each do |model_name|
183
- model = model_name.classify.constantize
184
- executable_count = model.executables.limit(nil).count
185
-
186
- execute_lock_column = model.crono_trigger_column_name(:execute_lock)
187
- oldest_execute_lock = model.executables(including_locked: true).where.not(execute_lock_column => 0).order(execute_lock_column).limit(1).pluck(execute_lock_column).first
188
-
189
- next_execute_at_column = model.crono_trigger_column_name(:next_execute_at)
190
- oldest_next_execute_at = model.executables.order(next_execute_at_column).limit(1).pluck(next_execute_at_column).first
191
-
192
- now = Time.now
193
- ActiveSupport::Notifications.instrument(CronoTrigger::Events::MONITOR, {
194
- model_name: model_name,
195
- executable_count: executable_count,
196
- max_lock_duration_sec: oldest_execute_lock.nil? ? 0 : now.to_i - oldest_execute_lock,
197
- max_latency_sec: oldest_next_execute_at.nil? ? 0 : now - oldest_next_execute_at,
198
- })
181
+ CronoTrigger.retry_on_db_errors do
182
+ CronoTrigger::Models::Worker.connection_pool.with_connection do
183
+ if workers_processing_same_models.order(:worker_id).limit(1).pluck(:worker_id).first != @crono_trigger_worker_id
184
+ # Return immediately to avoid redundant instruments
185
+ return
186
+ end
187
+
188
+ @model_names.each do |model_name|
189
+ model = model_name.classify.constantize
190
+ executable_count = model.executables.limit(nil).count
191
+
192
+ execute_lock_column = model.crono_trigger_column_name(:execute_lock)
193
+ oldest_execute_lock = model.executables(including_locked: true).where.not(execute_lock_column => 0).order(execute_lock_column).limit(1).pluck(execute_lock_column).first
194
+
195
+ next_execute_at_column = model.crono_trigger_column_name(:next_execute_at)
196
+ oldest_next_execute_at = model.executables.order(next_execute_at_column).limit(1).pluck(next_execute_at_column).first
197
+
198
+ now = Time.now
199
+ ActiveSupport::Notifications.instrument(CronoTrigger::Events::MONITOR, {
200
+ model_name: model_name,
201
+ executable_count: executable_count,
202
+ max_lock_duration_sec: oldest_execute_lock.nil? ? 0 : now.to_i - oldest_execute_lock,
203
+ max_latency_sec: oldest_next_execute_at.nil? ? 0 : now - oldest_next_execute_at,
204
+ })
205
+ end
199
206
  end
200
207
  end
201
208
  rescue => ex
@@ -203,10 +210,12 @@ module CronoTrigger
203
210
  end
204
211
 
205
212
  def update_worker_count
206
- CronoTrigger::Models::Worker.connection_pool.with_connection do
207
- worker_count = workers_processing_same_models.count
208
- return if worker_count.zero?
209
- @polling_threads.each { |th| th.worker_count = worker_count }
213
+ CronoTrigger.retry_on_db_errors do
214
+ CronoTrigger::Models::Worker.connection_pool.with_connection do
215
+ worker_count = workers_processing_same_models.count
216
+ return if worker_count.zero?
217
+ @polling_threads.each { |th| th.worker_count = worker_count }
218
+ end
210
219
  end
211
220
  rescue => ex
212
221
  CronoTrigger::GlobalExceptionHandler.handle_global_exception(ex)
data/lib/crono_trigger.rb CHANGED
@@ -4,6 +4,7 @@ require "ostruct"
4
4
  require "socket"
5
5
  require "active_record"
6
6
  require "concurrent"
7
+ require "retriable"
7
8
  require "crono_trigger/events"
8
9
  require "crono_trigger/models/worker"
9
10
  require "crono_trigger/models/signal"
@@ -22,6 +23,11 @@ module CronoTrigger
22
23
  model_names: nil,
23
24
  error_handlers: [],
24
25
  global_error_handlers: [],
26
+ db_error_retriable_options: {
27
+ on: {
28
+ ActiveRecord::ConnectionNotEstablished => nil,
29
+ },
30
+ }
25
31
  )
26
32
 
27
33
  def self.config
@@ -52,6 +58,12 @@ module CronoTrigger
52
58
  def self.workers
53
59
  CronoTrigger::Models::Worker.alive_workers
54
60
  end
61
+
62
+ def self.retry_on_db_errors
63
+ Retriable.retriable(CronoTrigger.config.db_error_retriable_options) do
64
+ yield
65
+ end
66
+ end
55
67
  end
56
68
 
57
69
  if defined?(Rails)
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.8.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - joker1007
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-03-21 00:00:00.000000000 Z
11
+ date: 2024-05-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chrono
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '4.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: retriable
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
125
139
  - !ruby/object:Gem::Dependency
126
140
  name: sqlite3
127
141
  requirement: !ruby/object:Gem::Requirement
@@ -221,7 +235,21 @@ dependencies:
221
235
  - !ruby/object:Gem::Version
222
236
  version: '0'
223
237
  - !ruby/object:Gem::Dependency
224
- name: codecov
238
+ name: simplecov
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - ">="
242
+ - !ruby/object:Gem::Version
243
+ version: '0'
244
+ type: :development
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - ">="
249
+ - !ruby/object:Gem::Version
250
+ version: '0'
251
+ - !ruby/object:Gem::Dependency
252
+ name: simplecov-cobertura
225
253
  requirement: !ruby/object:Gem::Requirement
226
254
  requirements:
227
255
  - - ">="
@@ -340,7 +368,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
340
368
  - !ruby/object:Gem::Version
341
369
  version: '0'
342
370
  requirements: []
343
- rubygems_version: 3.5.3
371
+ rubygems_version: 3.5.9
344
372
  signing_key:
345
373
  specification_version: 4
346
374
  summary: In Service Asynchronous Job Scheduler for Rails