skiplock 1.0.20 → 1.0.21

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