skiplock 1.0.20 → 1.0.21

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: 982ed70ce969943c421cad0923a280c87c4c26ea90da5a3451ce73becd5db42e
4
- data.tar.gz: 9f68becb9ab21e00816315d1e2ff20058d2f5942a3688ed78959018e3ccf940f
3
+ metadata.gz: 69e771cf5508d4a5a648dab43eb33b980370e317a1bf370ceaff62ecee4592e3
4
+ data.tar.gz: d1df71a515d871073b8d039f1bcbc3fb00b398806ed9f3bd82dc0169737e2f52
5
5
  SHA512:
6
- metadata.gz: 0fb583418cf8c0a190f9b651ecd95effba2d4b415f91bf6fbd452d2271f02534e7c5823e360f1e7a258cdd0a22b14cc99d44915007e9c32a186371748c086144
7
- data.tar.gz: 6aa546ad0a538213ae9587e667543027fb2ca080e7623b3f409fe6303c005085f39fd27c310a690a2da9fbb50321d97519ed9aee7bb4922a126c0a280f6ad5fe
6
+ metadata.gz: cca4a86fc4c31e4054a920b32740f294e3f54e9de65fe3a0c31b43258fee6e6f1a790ae1a95f79d9b719119936fedac953afd695ef5e7ce9e990b7f29fa1c397
7
+ data.tar.gz: 6bdc5a38218b052a825ab2b5f5ef2c73a2c9a6a2d981d8aed7cc61244f8806fdad06876278aa320bafb956c9dc2f15b705d87a2f171cbb2bc3805bfada640ce4
data/README.md CHANGED
@@ -172,14 +172,10 @@ If the `retry_on` block is not defined, then the built-in retry system of `Skipl
172
172
  `Skiplock` can use existing exception notification library to notify errors and exceptions. It supports `airbrake`, `bugsnag`, and `exception_notification`. Custom notification can also be called whenever an exception occurs; it can be configured in an initializer like below:
173
173
  ```ruby
174
174
  # config/initializers/skiplock.rb
175
- Skiplock.on_error do |ex, previous|
176
- if ex.backtrace != previous.try(:backtrace)
177
- # sends text message using Amazon SNS on new exceptions only
178
- # the same repeated exceptions will only be sent once to avoid SPAM
179
- # NOTE: exceptions generated from Job executions will not provide 'previous' exceptions
180
- sms = Aws::SNS::Client.new(region: 'us-west-2', access_key_id: Rails.application.credentials[:aws][:access_key_id], secret_access_key: Rails.application.credentials[:aws][:secret_access_key])
181
- sms.publish({ phone_number: '+122233334444', message: "Exception: #{ex.message}"[0..130] })
182
- end
175
+ Skiplock.on_error do |ex|
176
+ # sends text message using Amazon SNS
177
+ sms = Aws::SNS::Client.new(region: 'us-west-2', access_key_id: Rails.application.credentials[:aws][:access_key_id], secret_access_key: Rails.application.credentials[:aws][:secret_access_key])
178
+ sms.publish(phone_number: '+12223334444', message: "Exception: #{ex.message}"[0..130])
183
179
  end
184
180
  # supports multiple 'on_error' event callbacks
185
181
  ```
@@ -116,18 +116,18 @@ module Skiplock
116
116
  case @config[:notification]
117
117
  when 'airbrake'
118
118
  raise 'airbrake gem not found' unless defined?(Airbrake)
119
- Skiplock.on_error do |ex, previous|
120
- Airbrake.notify_sync(ex) unless ex.backtrace == previous.try(:backtrace)
119
+ Skiplock.on_error do |ex|
120
+ Airbrake.notify_sync(ex)
121
121
  end
122
122
  when 'bugsnag'
123
123
  raise 'bugsnag gem not found' unless defined?(Bugsnag)
124
- Skiplock.on_error do |ex, previous|
125
- Bugsnag.notify(ex) unless ex.backtrace == previous.try(:backtrace)
124
+ Skiplock.on_error do |ex|
125
+ Bugsnag.notify(ex)
126
126
  end
127
127
  when 'exception_notification'
128
128
  raise 'exception_notification gem not found' unless defined?(ExceptionNotifier)
129
- Skiplock.on_error do |ex, previous|
130
- ExceptionNotifier.notify_exception(ex) unless ex.backtrace == previous.try(:backtrace)
129
+ Skiplock.on_error do |ex|
130
+ ExceptionNotifier.notify_exception(ex)
131
131
  end
132
132
  else
133
133
  @config[:notification] = 'custom'
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.20'
2
+ VERSION = Version = '1.0.21'
3
3
  end
4
4
 
@@ -18,95 +18,100 @@ module Skiplock
18
18
  self.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: hostname, capacity: capacity)
19
19
  end
20
20
 
21
+ def shutdown
22
+ @running = false
23
+ @executor.shutdown
24
+ @executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
25
+ self.delete
26
+ Skiplock.logger.info "[Skiplock] Shutdown of #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2} (PID: #{self.pid}) was completed."
27
+ end
28
+
21
29
  def start(worker_num: 0, **config)
22
- if self.master
23
- Job.flush
24
- Cron.setup
25
- end
26
30
  @num = worker_num
27
31
  @config = config
28
32
  @queues_order_query = @config[:queues].map { |q,v| "WHEN queue_name = '#{q}' THEN #{v}" }.join(' ') if @config[:queues].is_a?(Hash) && @config[:queues].count > 0
29
33
  @running = true
30
- @executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads], idletime: 60, auto_terminate: true, fallback_policy: :discard)
34
+ @executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads] + 1, idletime: 60, auto_terminate: true, fallback_policy: :discard)
35
+ if self.master
36
+ Job.flush
37
+ Cron.setup
38
+ end
31
39
  @executor.post { run }
32
40
  Process.setproctitle("skiplock: #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2} [#{Rails.application.class.name.deconstantize.downcase}:#{Rails.env}]") if @config[:standalone]
33
41
  end
34
42
 
35
- def shutdown
36
- @running = false
37
- @executor.shutdown
38
- @executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
39
- self.delete
40
- Skiplock.logger.info "[Skiplock] Shutdown of #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2} (PID: #{self.pid}) was completed."
41
- end
42
-
43
43
  private
44
44
 
45
+ def reloader_post
46
+ Rails.application.reloader.wrap { @executor.post { Rails.application.executor.wrap { yield } } } if block_given?
47
+ end
48
+
45
49
  def run
46
50
  sleep 3
51
+ Skiplock.logger.info "[Skiplock] Starting in #{@config[:standalone] ? 'standalone' : 'async'} mode (PID: #{self.pid}) with #{@config[:max_threads]} max threads as #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2}..."
52
+ error = false
53
+ next_schedule_at = Time.now.to_f
54
+ pg_exception_timestamp = nil
55
+ timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
47
56
  ActiveRecord::Base.connection_pool.with_connection do |connection|
48
- Skiplock.logger.info "[Skiplock] Starting in #{@config[:standalone] ? 'standalone' : 'async'} mode (PID: #{self.pid}) with #{@config[:max_threads]} max threads as #{self.master ? 'master' : 'cluster'} worker#{(' ' + @num.to_s) if @num > 0 && @config[:workers] > 2}..."
49
57
  connection.exec_query('LISTEN "skiplock::jobs"')
50
- error = false
51
- next_schedule_at = Time.now.to_f
52
- timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
53
58
  while @running
54
- Rails.application.reloader.wrap do
55
- begin
56
- if error
57
- unless connection.active?
58
- connection.reconnect!
59
- sleep(0.5)
60
- connection.exec_query('LISTEN "skiplock::jobs"')
61
- next_schedule_at = Time.now.to_f
62
- end
63
- Job.flush if self.master
64
- error = false
59
+ begin
60
+ if error
61
+ unless connection.active?
62
+ connection.reconnect!
63
+ sleep(0.5)
64
+ connection.exec_query('LISTEN "skiplock::jobs"')
65
+ reloader_post { Job.flush } if self.master
66
+ pg_exception_timestamp = nil
67
+ next_schedule_at = Time.now.to_f
65
68
  end
66
- if Time.now.to_f >= next_schedule_at && @executor.remaining_capacity > 0
67
- job = nil
68
- connection.transaction do
69
- result = connection.select_all("SELECT id, running, scheduled_at FROM skiplock.jobs WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL ORDER BY scheduled_at ASC NULLS FIRST,#{@queues_order_query ? ' CASE ' + @queues_order_query + ' ELSE NULL END ASC NULLS LAST,' : ''} priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
70
- result = connection.select_all("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{result['id']}' RETURNING *").first if result && result['scheduled_at'].to_f <= Time.now.to_f
71
- job = Job.instantiate(result) if result
72
- end
73
- if job.try(:running)
74
- @executor.post do
75
- Rails.application.executor.wrap { job.execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) }
76
- end
77
- else
78
- next_schedule_at = (job ? job.scheduled_at.to_f : Float::INFINITY)
79
- end
69
+ error = false
70
+ end
71
+ if Time.now.to_f >= next_schedule_at && @executor.remaining_capacity > 1 # reserves 1 slot in queue for Job.flush in case of pg_connection error
72
+ result = nil
73
+ connection.transaction do
74
+ result = connection.select_all("SELECT id, running, scheduled_at FROM skiplock.jobs WHERE running = FALSE AND expired_at IS NULL AND finished_at IS NULL ORDER BY scheduled_at ASC NULLS FIRST,#{@queues_order_query ? ' CASE ' + @queues_order_query + ' ELSE NULL END ASC NULLS LAST,' : ''} priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
75
+ result = connection.select_all("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{result['id']}' RETURNING *").first if result && result['scheduled_at'].to_f <= Time.now.to_f
80
76
  end
81
- job_notifications = []
82
- connection.raw_connection.wait_for_notify(0.2) do |channel, pid, payload|
83
- job_notifications << payload if payload
84
- loop do
85
- payload = connection.raw_connection.notifies
86
- break unless @running && payload
87
- job_notifications << payload[:extra]
88
- end
89
- job_notifications.each do |n|
90
- op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
91
- next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
92
- next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
93
- end
77
+ if result && result['running']
78
+ reloader_post { Job.instantiate(result).execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) }
79
+ else
80
+ next_schedule_at = (result ? result['scheduled_at'].to_f : Float::INFINITY)
94
81
  end
95
- if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
96
- self.touch
97
- timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
82
+ end
83
+ job_notifications = []
84
+ connection.raw_connection.wait_for_notify(0.2) do |channel, pid, payload|
85
+ job_notifications << payload if payload
86
+ loop do
87
+ payload = connection.raw_connection.notifies
88
+ break unless @running && payload
89
+ job_notifications << payload[:extra]
98
90
  end
99
- rescue Exception => ex
100
- # most likely error with database connection
101
- Skiplock.logger.error(ex.to_s)
102
- Skiplock.logger.error(ex.backtrace.join("\n"))
103
- Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
104
- error = true
105
- wait(5)
106
- @last_exception = ex
91
+ job_notifications.each do |n|
92
+ op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
93
+ next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
94
+ next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
95
+ end
96
+ end
97
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
98
+ connection.exec_query("UPDATE skiplock.workers SET updated_at = NOW() WHERE id = '#{self.id}'")
99
+ timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
100
+ end
101
+ rescue Exception => ex
102
+ Skiplock.logger.error(ex.to_s)
103
+ Skiplock.logger.error(ex.backtrace.join("\n"))
104
+ report_exception = true
105
+ # if error is with database connection then only report if it persists longer than 1 minute
106
+ if ex.is_a?(::PG::ConnectionBad)
107
+ report_exception = false if pg_exception_timestamp.nil? || Process.clock_gettime(Process::CLOCK_MONOTONIC) - pg_exception_timestamp <= 60
108
+ pg_exception_timestamp ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
107
109
  end
108
- sleep(0.3)
110
+ Skiplock.on_errors.each { |p| p.call(ex) } if report_exception
111
+ error = true
112
+ wait(5)
109
113
  end
114
+ sleep(0.3)
110
115
  end
111
116
  connection.exec_query('UNLISTEN *')
112
117
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skiplock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.20
4
+ version: 1.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tin Vo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-14 00:00:00.000000000 Z
11
+ date: 2021-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob