skiplock 1.0.20 → 1.0.24
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 +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
|