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 +4 -4
- data/README.md +4 -8
- data/lib/skiplock/manager.rb +6 -6
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +72 -67
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 69e771cf5508d4a5a648dab43eb33b980370e317a1bf370ceaff62ecee4592e3
|
4
|
+
data.tar.gz: d1df71a515d871073b8d039f1bcbc3fb00b398806ed9f3bd82dc0169737e2f52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
```
|
data/lib/skiplock/manager.rb
CHANGED
@@ -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
|
120
|
-
Airbrake.notify_sync(ex)
|
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
|
125
|
-
Bugsnag.notify(ex)
|
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
|
130
|
-
ExceptionNotifier.notify_exception(ex)
|
129
|
+
Skiplock.on_error do |ex|
|
130
|
+
ExceptionNotifier.notify_exception(ex)
|
131
131
|
end
|
132
132
|
else
|
133
133
|
@config[:notification] = 'custom'
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
@@ -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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
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.
|
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-
|
11
|
+
date: 2021-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|