skiplock 1.0.20 → 1.0.24
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +14 -8
- data/bin/skiplock +1 -0
- data/lib/skiplock/cron.rb +1 -0
- data/lib/skiplock/job.rb +24 -21
- data/lib/skiplock/manager.rb +47 -38
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +96 -73
- data/lib/skiplock.rb +2 -3
- 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: a43b01cedd94ea395b0b173e2da3c5c7ab0baf6ccbd35e599127544084ca5589
|
4
|
+
data.tar.gz: a908c4eea5b2b75727a70fe5efc248f0b0183e9fe97a37c068b185b9f259a4ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5ae8b9bd79e37426710d707fadd2e4d658e541ffdd109a760687f2c14dacfa9d61614130d25be801264ecee86ea63d54ce4d6604c4cc80c7a47ac31752618f6
|
7
|
+
data.tar.gz: 42ba6d9047ac028c24389d1915f686f5d553751244bd557a98a8b28af311a53702d6c69078abafcb6696d5d00e441fd8eca43d2e18b11530945fa10a42d15cea
|
data/README.md
CHANGED
@@ -50,12 +50,14 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
50
50
|
```yaml
|
51
51
|
# config/skiplock.yml (default settings)
|
52
52
|
---
|
53
|
+
graceful_shutdown: 15
|
53
54
|
min_threads: 1
|
54
55
|
max_threads: 10
|
55
56
|
max_retries: 20
|
56
57
|
logfile: skiplock.log
|
57
58
|
loglevel: info
|
58
59
|
notification: custom
|
60
|
+
actioncable: false
|
59
61
|
extensions: false
|
60
62
|
purge_completion: true
|
61
63
|
queues:
|
@@ -64,12 +66,14 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
64
66
|
workers: 0
|
65
67
|
```
|
66
68
|
Available configuration options are:
|
69
|
+
- **graceful_shutdown** (*integer*): sets the number of seconds to wait for jobs to finish before being killed during shutdown
|
67
70
|
- **min_threads** (*integer*): sets minimum number of threads staying idle
|
68
71
|
- **max_threads** (*integer*): sets the maximum number of threads allowed to run jobs
|
69
72
|
- **max_retries** (*integer*): sets the maximum attempt a job will be retrying before it is marked expired. See `Retry system` for more details
|
70
73
|
- **logfile** (*string*): filename for skiplock logs; empty logfile will disable logging
|
71
74
|
- **loglevel** (*string*): sets logging level (`debug, info, warn, error, fatal, unknown`)
|
72
75
|
- **notification** (*string*): sets the library to be used for notifying errors and exceptions (`auto, airbrake, bugsnag, exception_notification, custom`); using `auto` will detect library if available. See `Notification system` for more details
|
76
|
+
- **actioncable** (*boolean*): enable or disable usage of ActionCable notification
|
73
77
|
- **extensions** (*multi*): enable or disable the class method extension. See `ClassMethod extension` for more details
|
74
78
|
- **purge_completion** (*boolean*): when set to **true** will delete jobs after they were completed successfully; if set to **false** then the completed jobs should be purged periodically to maximize performance (eg. clean up old jobs after 3 months); queued jobs can manually override using `purge` option
|
75
79
|
- **queues** (*hash*): defines the set of queues with priorities; lower priority takes precedence
|
@@ -83,8 +87,10 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
83
87
|
```
|
84
88
|
$ bundle exec skiplock -h
|
85
89
|
Usage: skiplock [options]
|
90
|
+
-a, --actioncable YESNO Actioncable notification
|
86
91
|
-e, --environment STRING Rails environment
|
87
92
|
-l, --logfile STRING Log filename
|
93
|
+
-L, --loglevel STRING Log level (debug, info, warn, error, fatal, unknown)
|
88
94
|
-s, --graceful-shutdown NUM Number of seconds to wait for graceful shutdown
|
89
95
|
-r, --max-retries NUM Number of maxixum retries
|
90
96
|
-t, --max-threads NUM Number of maximum threads
|
@@ -172,14 +178,10 @@ If the `retry_on` block is not defined, then the built-in retry system of `Skipl
|
|
172
178
|
`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
179
|
```ruby
|
174
180
|
# 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
|
181
|
+
Skiplock.on_error do |ex|
|
182
|
+
# sends text message using Amazon SNS
|
183
|
+
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])
|
184
|
+
sms.publish(phone_number: '+12223334444', message: "Exception: #{ex.message}"[0..130])
|
183
185
|
end
|
184
186
|
# supports multiple 'on_error' event callbacks
|
185
187
|
```
|
@@ -241,6 +243,10 @@ Code examples of gathering counters information:
|
|
241
243
|
```ruby
|
242
244
|
Skiplock::Counter.sum(:expiries)
|
243
245
|
```
|
246
|
+
- get all information in one query
|
247
|
+
```ruby
|
248
|
+
Skiplock::Counter.pluck("sum(completions), sum(dispatches), sum(expiries), sum(failures), sum(retries)").first
|
249
|
+
```
|
244
250
|
|
245
251
|
## Contributing
|
246
252
|
|
data/bin/skiplock
CHANGED
@@ -5,6 +5,7 @@ options = {}
|
|
5
5
|
begin
|
6
6
|
op = OptionParser.new do |opts|
|
7
7
|
opts.banner = "Usage: #{File.basename($0)} [options]"
|
8
|
+
opts.on('-a', '--actioncable YESNO', TrueClass, 'Actioncable notification')
|
8
9
|
opts.on('-e', '--environment STRING', String, 'Rails environment')
|
9
10
|
opts.on('-l', '--logfile STRING', String, 'Log filename')
|
10
11
|
opts.on('-L', '--loglevel STRING', String, 'Log level (debug, info, warn, error, fatal, unknown)')
|
data/lib/skiplock/cron.rb
CHANGED
data/lib/skiplock/job.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Job < ActiveRecord::Base
|
3
3
|
self.implicit_order_column = 'updated_at'
|
4
|
-
|
4
|
+
attribute :activejob_error
|
5
|
+
attribute :exception
|
6
|
+
attribute :max_retries, :integer
|
7
|
+
attribute :purge, :boolean
|
5
8
|
belongs_to :worker, inverse_of: :jobs, required: false
|
6
9
|
|
7
10
|
def self.dispatch(purge_completion: true, max_retries: 20)
|
@@ -40,7 +43,7 @@ module Skiplock
|
|
40
43
|
Dir.glob('tmp/skiplock/*').each do |f|
|
41
44
|
disposed = true
|
42
45
|
if self.exists?(id: File.basename(f), running: true)
|
43
|
-
job =
|
46
|
+
job = YAML.load_file(f) rescue nil
|
44
47
|
disposed = job.dispose if job.is_a?(Skiplock::Job)
|
45
48
|
end
|
46
49
|
(File.delete(f) rescue nil) if disposed
|
@@ -54,15 +57,15 @@ module Skiplock
|
|
54
57
|
end
|
55
58
|
|
56
59
|
def dispose
|
57
|
-
return unless
|
58
|
-
|
60
|
+
return unless self.max_retries
|
61
|
+
yaml = self.to_yaml
|
59
62
|
purging = false
|
60
63
|
self.running = false
|
61
64
|
self.worker_id = nil
|
62
65
|
self.updated_at = Time.now > self.updated_at ? Time.now : self.updated_at + 1 # in case of clock drifting
|
63
|
-
if
|
64
|
-
self.exception_executions["[#{
|
65
|
-
if (self.executions.to_i >=
|
66
|
+
if self.exception
|
67
|
+
self.exception_executions["[#{self.exception.class.name}]"] = self.exception_executions["[#{self.exception.class.name}]"].to_i + 1 unless self.data.key?('activejob_error')
|
68
|
+
if (self.executions.to_i >= self.max_retries + 1) || self.data.key?('activejob_error') || self.exception.is_a?(Skiplock::Extension::ProxyError)
|
66
69
|
self.expired_at = Time.now
|
67
70
|
else
|
68
71
|
self.scheduled_at = Time.now + (5 * 2**self.executions.to_i)
|
@@ -76,7 +79,7 @@ module Skiplock
|
|
76
79
|
next_cron_at = Cron.next_schedule_at(self.cron)
|
77
80
|
if next_cron_at
|
78
81
|
# update job to record completions counter before resetting finished_at to nil
|
79
|
-
self.update_columns(self.attributes.slice(*self.changes.keys))
|
82
|
+
self.update_columns(self.attributes.slice(*(self.changes.keys & self.class.column_names)))
|
80
83
|
self.finished_at = nil
|
81
84
|
self.executions = nil
|
82
85
|
self.exception_executions = nil
|
@@ -86,13 +89,13 @@ module Skiplock
|
|
86
89
|
Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
|
87
90
|
purging = true
|
88
91
|
end
|
89
|
-
elsif
|
92
|
+
elsif self.purge == true
|
90
93
|
purging = true
|
91
94
|
end
|
92
95
|
end
|
93
|
-
purging ? self.delete : self.update_columns(self.attributes.slice(*self.changes.keys))
|
96
|
+
purging ? self.delete : self.update_columns(self.attributes.slice(*(self.changes.keys & self.class.column_names)))
|
94
97
|
rescue Exception => e
|
95
|
-
File.
|
98
|
+
File.write("tmp/skiplock/#{self.id}", yaml) rescue nil
|
96
99
|
if Skiplock.logger
|
97
100
|
Skiplock.logger.error(e.to_s)
|
98
101
|
Skiplock.logger.error(e.backtrace.join("\n"))
|
@@ -109,9 +112,9 @@ module Skiplock
|
|
109
112
|
self.data.delete('result')
|
110
113
|
self.exception_executions ||= {}
|
111
114
|
self.activejob_error = nil
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
+
self.max_retries = (self.data['options'].key?('max_retries') ? self.data['options']['max_retries'].to_i : max_retries) rescue max_retries
|
116
|
+
self.max_retries = 20 if self.max_retries < 0 || self.max_retries > 20
|
117
|
+
self.purge = (self.data['options'].key?('purge') ? self.data['options']['purge'] : purge_completion) rescue purge_completion
|
115
118
|
job_data = self.attributes.slice('job_class', 'queue_name', 'locale', 'timezone', 'priority', 'executions', 'exception_executions').merge('job_id' => self.id, 'enqueued_at' => self.updated_at, 'arguments' => (self.data['arguments'] || []))
|
116
119
|
self.executions = self.executions.to_i + 1
|
117
120
|
Thread.current[:skiplock_job] = self
|
@@ -119,15 +122,15 @@ module Skiplock
|
|
119
122
|
begin
|
120
123
|
self.data['result'] = ActiveJob::Base.execute(job_data)
|
121
124
|
rescue Exception => ex
|
122
|
-
|
123
|
-
Skiplock.on_errors.each { |p| p.call(
|
125
|
+
self.exception = ex
|
126
|
+
Skiplock.on_errors.each { |p| p.call(ex) }
|
124
127
|
end
|
125
128
|
if Skiplock.logger
|
126
|
-
if
|
129
|
+
if self.exception || self.activejob_error
|
127
130
|
Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.activejob_error }")
|
128
|
-
if
|
129
|
-
Skiplock.logger.error(
|
130
|
-
Skiplock.logger.error(
|
131
|
+
if self.exception
|
132
|
+
Skiplock.logger.error(self.exception.to_s)
|
133
|
+
Skiplock.logger.error(self.exception.backtrace.join("\n"))
|
131
134
|
end
|
132
135
|
else
|
133
136
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -139,7 +142,7 @@ module Skiplock
|
|
139
142
|
Skiplock.logger.info "[Skiplock] Performed #{job_name} (#{self.id}) from queue '#{self.queue_name || 'default'}' in #{end_time - start_time} seconds"
|
140
143
|
end
|
141
144
|
end
|
142
|
-
|
145
|
+
self.exception || self.activejob_error || self.data['result']
|
143
146
|
ensure
|
144
147
|
self.finished_at ||= Time.now if self.data.key?('result') && !self.activejob_error
|
145
148
|
self.dispose
|
data/lib/skiplock/manager.rb
CHANGED
@@ -4,14 +4,22 @@ module Skiplock
|
|
4
4
|
@config = Skiplock::DEFAULT_CONFIG.dup
|
5
5
|
@config.merge!(YAML.load_file('config/skiplock.yml')) rescue nil
|
6
6
|
@config.symbolize_keys!
|
7
|
-
|
7
|
+
Rails.application.eager_load! if Rails.env.development?
|
8
|
+
if @config[:extensions] == true
|
9
|
+
Module.__send__(:include, Skiplock::Extension)
|
10
|
+
elsif @config[:extensions].is_a?(Array)
|
11
|
+
@config[:extensions].each { |n| n.constantize.__send__(:extend, Skiplock::Extension) if n.safe_constantize }
|
12
|
+
end
|
13
|
+
ActiveJob::Base.__send__(:include, Skiplock::Patch)
|
14
|
+
(caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0) ? async : Cron.setup
|
8
15
|
end
|
9
16
|
|
10
17
|
def async
|
11
|
-
configure
|
12
18
|
setup_logger
|
19
|
+
configure
|
13
20
|
Worker.cleanup(@hostname)
|
14
|
-
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
21
|
+
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, actioncable: @config[:actioncable])
|
22
|
+
Cron.setup if @worker.master
|
15
23
|
@worker.start(**@config)
|
16
24
|
at_exit { @worker.shutdown }
|
17
25
|
rescue Exception => ex
|
@@ -21,25 +29,24 @@ module Skiplock
|
|
21
29
|
|
22
30
|
def standalone(**options)
|
23
31
|
@config.merge!(options)
|
24
|
-
configure
|
25
|
-
setup_logger
|
26
|
-
Rails.logger.reopen('/dev/null') rescue Rails.logger.reopen('NUL') # supports Windows NUL device
|
27
|
-
Rails.logger.extend(ActiveSupport::Logger.broadcast(@logger))
|
28
|
-
@config[:workers] = 1 if @config[:workers] <= 0
|
29
32
|
@config[:standalone] = true
|
33
|
+
@config[:workers] = 1 if @config[:workers] <= 0
|
34
|
+
setup_logger
|
35
|
+
configure
|
30
36
|
banner
|
31
|
-
Worker.cleanup(@hostname)
|
32
|
-
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
33
37
|
@parent_id = Process.pid
|
34
38
|
@shutdown = false
|
35
39
|
Signal.trap('INT') { @shutdown = true }
|
36
40
|
Signal.trap('TERM') { @shutdown = true }
|
37
41
|
Signal.trap('HUP') { setup_logger }
|
42
|
+
Worker.cleanup(@hostname)
|
43
|
+
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, actioncable: @config[:actioncable])
|
44
|
+
ActiveRecord::Base.connection.disconnect! if @config[:workers] > 1
|
38
45
|
(@config[:workers] - 1).times do |n|
|
39
|
-
sleep 0.2
|
40
46
|
fork do
|
41
|
-
sleep 1
|
42
|
-
|
47
|
+
sleep(0.25*n + 1)
|
48
|
+
ActiveRecord::Base.establish_connection
|
49
|
+
worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false, actioncable: @config[:actioncable])
|
43
50
|
worker.start(worker_num: n + 1, **@config)
|
44
51
|
loop do
|
45
52
|
sleep 0.5
|
@@ -48,6 +55,7 @@ module Skiplock
|
|
48
55
|
worker.shutdown
|
49
56
|
end
|
50
57
|
end
|
58
|
+
ActiveRecord::Base.establish_connection if @config[:workers] > 1
|
51
59
|
@worker.start(**@config)
|
52
60
|
loop do
|
53
61
|
sleep 0.5
|
@@ -68,24 +76,25 @@ module Skiplock
|
|
68
76
|
@logger.info "-"*(title.length)
|
69
77
|
@logger.info title
|
70
78
|
@logger.info "-"*(title.length)
|
71
|
-
@logger.info "
|
72
|
-
@logger.info "
|
73
|
-
@logger.info "
|
74
|
-
@logger.info "
|
75
|
-
@logger.info "
|
76
|
-
@logger.info "
|
77
|
-
@logger.info "
|
78
|
-
@logger.info "
|
79
|
-
@logger.info "
|
80
|
-
@logger.info "
|
81
|
-
@logger.info "
|
82
|
-
@logger.info "
|
79
|
+
@logger.info "ActionCable notification: #{@config[:actioncable]}#{' (not available)' unless defined?(ActionCable)}"
|
80
|
+
@logger.info " ClassMethod extensions: #{@config[:extensions]}"
|
81
|
+
@logger.info " Purge completion: #{@config[:purge_completion]}"
|
82
|
+
@logger.info " Notification: #{@config[:notification]}"
|
83
|
+
@logger.info " Max retries: #{@config[:max_retries]}"
|
84
|
+
@logger.info " Min threads: #{@config[:min_threads]}"
|
85
|
+
@logger.info " Max threads: #{@config[:max_threads]}"
|
86
|
+
@logger.info " Environment: #{Rails.env}"
|
87
|
+
@logger.info " Loglevel: #{@config[:loglevel]}"
|
88
|
+
@logger.info " Logfile: #{@config[:logfile] || '(disabled)'}"
|
89
|
+
@logger.info " Workers: #{@config[:workers]}"
|
90
|
+
@logger.info " Queues: #{@config[:queues].map {|k,v| k + '(' + v.to_s + ')'}.join(', ')}" if @config[:queues].is_a?(Hash)
|
91
|
+
@logger.info " PID: #{Process.pid}"
|
83
92
|
@logger.info "-"*(title.length)
|
84
93
|
@logger.warn "[Skiplock] Custom notification has no registered 'on_error' callback" if Skiplock.on_errors.count == 0
|
85
94
|
end
|
86
95
|
|
87
96
|
def configure
|
88
|
-
@hostname = Socket.
|
97
|
+
@hostname = "#{`hostname -f`.strip}|#{Socket.ip_address_list.reject(&:ipv4_loopback?).reject(&:ipv6?).map(&:ip_address).join('|')}"
|
89
98
|
@config.transform_values! {|v| v.is_a?(String) ? v.downcase : v}
|
90
99
|
@config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
|
91
100
|
@config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
|
@@ -107,31 +116,26 @@ module Skiplock
|
|
107
116
|
raise "Unable to detect any known exception notification library. Please define custom 'on_error' event callbacks and change to 'custom' notification in 'config/skiplock.yml'"
|
108
117
|
end
|
109
118
|
end
|
110
|
-
Rails.application.eager_load! if Rails.env.development?
|
111
|
-
if @config[:extensions] == true
|
112
|
-
Module.__send__(:include, Skiplock::Extension)
|
113
|
-
elsif @config[:extensions].is_a?(Array)
|
114
|
-
@config[:extensions].each { |n| n.constantize.__send__(:extend, Skiplock::Extension) if n.safe_constantize }
|
115
|
-
end
|
116
119
|
case @config[:notification]
|
117
120
|
when 'airbrake'
|
118
121
|
raise 'airbrake gem not found' unless defined?(Airbrake)
|
119
|
-
Skiplock.on_error do |ex
|
120
|
-
Airbrake.notify_sync(ex)
|
122
|
+
Skiplock.on_error do |ex|
|
123
|
+
Airbrake.notify_sync(ex)
|
121
124
|
end
|
122
125
|
when 'bugsnag'
|
123
126
|
raise 'bugsnag gem not found' unless defined?(Bugsnag)
|
124
|
-
Skiplock.on_error do |ex
|
125
|
-
Bugsnag.notify(ex)
|
127
|
+
Skiplock.on_error do |ex|
|
128
|
+
Bugsnag.notify(ex)
|
126
129
|
end
|
127
130
|
when 'exception_notification'
|
128
131
|
raise 'exception_notification gem not found' unless defined?(ExceptionNotifier)
|
129
|
-
Skiplock.on_error do |ex
|
130
|
-
ExceptionNotifier.notify_exception(ex)
|
132
|
+
Skiplock.on_error do |ex|
|
133
|
+
ExceptionNotifier.notify_exception(ex)
|
131
134
|
end
|
132
135
|
else
|
133
136
|
@config[:notification] = 'custom'
|
134
137
|
end
|
138
|
+
@logger.error 'ActionCable is not found!' if @config[:actioncable] && !defined?(ActionCable)
|
135
139
|
Skiplock.on_errors.freeze
|
136
140
|
end
|
137
141
|
|
@@ -144,6 +148,11 @@ module Skiplock
|
|
144
148
|
@logger.extend(ActiveSupport::Logger.broadcast(::Logger.new(File.join(Rails.root, 'log', @config[:logfile].to_s), 'daily')))
|
145
149
|
ActiveJob::Base.logger = nil
|
146
150
|
end
|
151
|
+
if @config[:standalone]
|
152
|
+
Rails.logger.reopen('/dev/null') rescue Rails.logger.reopen('NUL') # supports Windows NUL device
|
153
|
+
Rails.logger.level = @logger.level
|
154
|
+
Rails.logger.extend(ActiveSupport::Logger.broadcast(@logger))
|
155
|
+
end
|
147
156
|
rescue Exception => ex
|
148
157
|
@logger.error "Exception with logger: #{ex.to_s}"
|
149
158
|
@logger.error ex.backtrace.join("\n")
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
@@ -12,107 +12,130 @@ module Skiplock
|
|
12
12
|
self.where(id: delete_ids).delete_all if delete_ids.count > 0
|
13
13
|
end
|
14
14
|
|
15
|
-
def self.generate(capacity:, hostname:, master: true)
|
16
|
-
self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity)
|
15
|
+
def self.generate(capacity:, hostname:, master: true, actioncable: false)
|
16
|
+
worker = self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity)
|
17
17
|
rescue
|
18
|
-
self.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: hostname, capacity: capacity)
|
19
|
-
|
20
|
-
|
21
|
-
def start(worker_num: 0, **config)
|
22
|
-
if self.master
|
23
|
-
Job.flush
|
24
|
-
Cron.setup
|
25
|
-
end
|
26
|
-
@num = worker_num
|
27
|
-
@config = config
|
28
|
-
@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
|
-
@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)
|
31
|
-
@executor.post { run }
|
32
|
-
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]
|
18
|
+
worker = self.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: hostname, capacity: capacity)
|
19
|
+
ensure
|
20
|
+
ActionCable.server.broadcast('skiplock', { worker: { op: 'CREATE', id: worker.id, hostname: worker.hostname, master: worker.master, capacity: worker.capacity, pid: worker.pid, sid: worker.sid, created_at: worker.created_at.to_f, updated_at: worker.updated_at.to_f } }) if actioncable && defined?(ActionCable)
|
33
21
|
end
|
34
22
|
|
35
23
|
def shutdown
|
36
24
|
@running = false
|
37
25
|
@executor.shutdown
|
38
26
|
@executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
|
27
|
+
ActionCable.server.broadcast('skiplock', { worker: { op: 'DELETE', id: self.id, hostname: self.hostname, master: self.master, capacity: self.capacity, pid: self.pid, sid: self.sid, created_at: self.created_at.to_f, updated_at: self.updated_at.to_f } }) if @config[:actioncable] && defined?(ActionCable)
|
39
28
|
self.delete
|
40
29
|
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
30
|
end
|
42
31
|
|
32
|
+
def start(worker_num: 0, **config)
|
33
|
+
@num = worker_num
|
34
|
+
@config = config
|
35
|
+
@pg_config = ActiveRecord::Base.connection.raw_connection.conninfo_hash.compact
|
36
|
+
@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
|
37
|
+
@running = true
|
38
|
+
@map = ::PG::TypeMapByOid.new
|
39
|
+
@map.add_coder(::PG::TextDecoder::Boolean.new(oid: 16, name: 'bool'))
|
40
|
+
@map.add_coder(::PG::TextDecoder::Integer.new(oid: 20, name: 'int8'))
|
41
|
+
@map.add_coder(::PG::TextDecoder::Integer.new(oid: 21, name: 'int2'))
|
42
|
+
@map.add_coder(::PG::TextDecoder::Integer.new(oid: 23, name: 'int4'))
|
43
|
+
@map.add_coder(::PG::TextDecoder::TimestampUtc.new(oid: 1114, name: 'timestamp'))
|
44
|
+
@map.add_coder(::PG::TextDecoder::String.new(oid: 2950, name: 'uuid'))
|
45
|
+
@map.add_coder(::PG::TextDecoder::JSON.new(oid: 3802, name: 'jsonb'))
|
46
|
+
@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: false, fallback_policy: :abort)
|
47
|
+
@executor.post { run }
|
48
|
+
if @config[:standalone]
|
49
|
+
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}]")
|
50
|
+
ActiveRecord::Base.connection.throw_away!
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
43
54
|
private
|
44
55
|
|
56
|
+
def establish_connection
|
57
|
+
@connection = ::PG.connect(@pg_config)
|
58
|
+
@connection.type_map_for_results = @map
|
59
|
+
@connection.exec('LISTEN "skiplock::jobs"').clear
|
60
|
+
@connection.exec('LISTEN "skiplock::workers"').clear
|
61
|
+
end
|
62
|
+
|
45
63
|
def run
|
46
64
|
sleep 3
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
if
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
65
|
+
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}..."
|
66
|
+
next_schedule_at = Time.now.to_f
|
67
|
+
pg_exception_timestamp = nil
|
68
|
+
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
69
|
+
while @running
|
70
|
+
Rails.application.reloader.wrap do
|
71
|
+
begin
|
72
|
+
if @connection.nil? || @connection.status != ::PG::CONNECTION_OK
|
73
|
+
establish_connection
|
74
|
+
@executor.post { Rails.application.executor.wrap { Job.flush } } if self.master
|
75
|
+
pg_exception_timestamp = nil
|
76
|
+
next_schedule_at = Time.now.to_f
|
77
|
+
end
|
78
|
+
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
|
79
|
+
result = nil
|
80
|
+
@connection.transaction do |conn|
|
81
|
+
conn.exec("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") do |r|
|
82
|
+
result = r.first
|
83
|
+
conn.exec("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{result['id']}' RETURNING *") { |r| result = r.first } if result && result['scheduled_at'].to_f <= Time.now.to_f
|
62
84
|
end
|
63
|
-
Job.flush if self.master
|
64
|
-
error = false
|
65
85
|
end
|
66
|
-
if
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
86
|
+
if result && result['running']
|
87
|
+
@executor.post { Rails.application.executor.wrap { Job.instantiate(result).execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
|
88
|
+
else
|
89
|
+
next_schedule_at = (result ? result['scheduled_at'].to_f : Float::INFINITY)
|
80
90
|
end
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
91
|
+
end
|
92
|
+
notifications = { 'skiplock::jobs' => [], 'skiplock::workers' => [] }
|
93
|
+
@connection.wait_for_notify(0.2) do |channel, pid, payload|
|
94
|
+
notifications[channel] << payload if payload
|
95
|
+
loop do
|
96
|
+
payload = @connection.notifies
|
97
|
+
break unless @running && payload
|
98
|
+
notifications[payload[:relname]] << payload[:extra]
|
99
|
+
end
|
100
|
+
notifications['skiplock::jobs'].each do |n|
|
101
|
+
op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
|
102
|
+
ActionCable.server.broadcast('skiplock', { job: { op: op, id: id, worker_id: worker_id, job_class: job_class, queue_name: queue_name, running: (running == 'true'), expired_at: expired_at.to_f, finished_at: finished_at.to_f, scheduled_at: scheduled_at.to_f } }) if self.master && @config[:actioncable] && defined?(ActionCable)
|
103
|
+
next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
|
104
|
+
next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
|
94
105
|
end
|
95
|
-
if
|
96
|
-
|
97
|
-
|
106
|
+
if self.master && @config[:actioncable] && defined?(ActionCable)
|
107
|
+
notifications['skiplock::workers'].each do |w|
|
108
|
+
op, id, hostname, master, capacity, pid, sid, created_at, updated_at = w.split(',')
|
109
|
+
ActionCable.server.broadcast('skiplock', { worker: { op: op, id: id, hostname: hostname, master: (master == 'true'), capacity: capacity.to_i, pid: pid.to_i, sid: sid.to_i, created_at: created_at.to_f, updated_at: updated_at.to_f } })
|
110
|
+
end
|
98
111
|
end
|
99
|
-
|
100
|
-
|
112
|
+
end
|
113
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
|
114
|
+
@connection.exec("UPDATE skiplock.workers SET updated_at = NOW() WHERE id = '#{self.id}'").clear
|
115
|
+
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
116
|
+
end
|
117
|
+
rescue Exception => ex
|
118
|
+
report_exception = true
|
119
|
+
# if error is with database connection then only report if it persists longer than 1 minute
|
120
|
+
if @connection.nil? || @connection.status != ::PG::CONNECTION_OK
|
121
|
+
report_exception = false if pg_exception_timestamp.nil? || Process.clock_gettime(Process::CLOCK_MONOTONIC) - pg_exception_timestamp <= 60
|
122
|
+
pg_exception_timestamp ||= Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
123
|
+
end
|
124
|
+
if report_exception
|
101
125
|
Skiplock.logger.error(ex.to_s)
|
102
126
|
Skiplock.logger.error(ex.backtrace.join("\n"))
|
103
|
-
Skiplock.on_errors.each { |p| p.call(ex
|
104
|
-
error = true
|
105
|
-
wait(5)
|
106
|
-
@last_exception = ex
|
127
|
+
Skiplock.on_errors.each { |p| p.call(ex) }
|
107
128
|
end
|
108
|
-
|
129
|
+
wait(5)
|
109
130
|
end
|
131
|
+
sleep(0.3)
|
110
132
|
end
|
111
|
-
connection.exec_query('UNLISTEN *')
|
112
133
|
end
|
134
|
+
ensure
|
135
|
+
@connection.close if @connection && !@connection.finished?
|
113
136
|
end
|
114
137
|
|
115
|
-
def wait(timeout)
|
138
|
+
def wait(timeout = 1)
|
116
139
|
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
117
140
|
while @running
|
118
141
|
sleep(0.5)
|
data/lib/skiplock.rb
CHANGED
@@ -11,7 +11,7 @@ require 'skiplock/worker'
|
|
11
11
|
require 'skiplock/version'
|
12
12
|
|
13
13
|
module Skiplock
|
14
|
-
DEFAULT_CONFIG = { '
|
14
|
+
DEFAULT_CONFIG = { 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 10, 'max_retries' => 20, 'logfile' => 'skiplock.log', 'loglevel' => 'info', 'notification' => 'custom', 'actioncable' => false, 'extensions' => false, 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
|
15
15
|
|
16
16
|
def self.logger=(l)
|
17
17
|
@logger = l
|
@@ -34,5 +34,4 @@ module Skiplock
|
|
34
34
|
def self.table_name_prefix
|
35
35
|
'skiplock.'
|
36
36
|
end
|
37
|
-
end
|
38
|
-
ActiveJob::Base.__send__(:include, Skiplock::Patch)
|
37
|
+
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.24
|
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-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|