skiplock 1.0.12 → 1.0.13
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/bin/skiplock +1 -1
- data/lib/active_job/queue_adapters/skiplock_adapter.rb +1 -1
- data/lib/generators/skiplock/templates/migration.rb.erb +1 -1
- data/lib/skiplock/job.rb +36 -37
- data/lib/skiplock/manager.rb +58 -64
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +112 -0
- data/lib/skiplock.rb +0 -1
- metadata +2 -3
- data/lib/skiplock/dispatcher.rb +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 89240e4318ca72cf30a3426948d9aa8479f59f85b82e4dca4ef5ee3b449b9894
|
4
|
+
data.tar.gz: b8f6799ccf4ba566379548dc4e9c90b6a1202842139b0ac0a3e0247875be00d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d8ec5ec2c3b0612fd3077fac5d6ff0c6e1b6826ed74b25b640ccc966874461b470f633c582af4f408f547586e30e1fdc5c22f158ca6fdf5c91000ec33c609a2
|
7
|
+
data.tar.gz: 27b6f4746f255777fdb6b3733d08a0592969a45d6d0f315d76231269255e532b51834fa79e1be89a0c18ac62509fe342266c9a8a5c7c038cc2b9e52b120e2469
|
data/bin/skiplock
CHANGED
@@ -25,4 +25,4 @@ options.transform_keys! { |k| k.to_s.gsub('-', '_').to_sym }
|
|
25
25
|
env = options.delete(:environment)
|
26
26
|
ENV['RAILS_ENV'] = env if env
|
27
27
|
require File.expand_path("config/environment.rb")
|
28
|
-
|
28
|
+
Rails.application.config.skiplock.standalone(**options.merge(standalone: true))
|
@@ -2,7 +2,7 @@ module ActiveJob
|
|
2
2
|
module QueueAdapters
|
3
3
|
class SkiplockAdapter
|
4
4
|
def initialize
|
5
|
-
Rails.application.config.after_initialize { Skiplock::Manager.new }
|
5
|
+
Rails.application.config.after_initialize { Rails.application.config.skiplock = Skiplock::Manager.new }
|
6
6
|
end
|
7
7
|
|
8
8
|
def enqueue(job)
|
@@ -60,7 +60,7 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
|
|
60
60
|
ELSIF (record.executions IS NOT NULL AND record.scheduled_at IS NOT NULL) THEN
|
61
61
|
INSERT INTO skiplock.counters (day,failures) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET failures = skiplock.counters.failures + 1;
|
62
62
|
END IF;
|
63
|
-
PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.queue_name,',',record.running::TEXT,',',CAST(EXTRACT(EPOCH FROM record.expired_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.finished_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM CASE WHEN record.scheduled_at IS NULL THEN record.updated_at ELSE record.scheduled_at END) AS FLOAT)::TEXT));
|
63
|
+
PERFORM pg_notify('skiplock::jobs', CONCAT(TG_OP,',',record.id::TEXT,',',record.worker_id::TEXT,',',record.job_class,',',record.queue_name,',',record.running::TEXT,',',CAST(EXTRACT(EPOCH FROM record.expired_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM record.finished_at) AS FLOAT)::TEXT,',',CAST(EXTRACT(EPOCH FROM CASE WHEN record.scheduled_at IS NULL THEN record.updated_at ELSE record.scheduled_at END) AS FLOAT)::TEXT));
|
64
64
|
RETURN NULL;
|
65
65
|
END;
|
66
66
|
$$ LANGUAGE plpgsql
|
data/lib/skiplock/job.rb
CHANGED
@@ -2,39 +2,6 @@ module Skiplock
|
|
2
2
|
class Job < ActiveRecord::Base
|
3
3
|
self.implicit_order_column = 'created_at'
|
4
4
|
|
5
|
-
def self.dispatch(queues_order_query: nil, worker_id: nil, purge_completion: true, max_retries: 20)
|
6
|
-
job = nil
|
7
|
-
self.transaction do
|
8
|
-
job = self.find_by_sql("SELECT id, scheduled_at FROM #{self.table_name} 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
|
9
|
-
return (job ? job.scheduled_at.to_f : Float::INFINITY) if job.nil? || job.scheduled_at.to_f > Time.now.to_f
|
10
|
-
job = Skiplock::Job.find_by_sql("UPDATE #{self.table_name} SET running = TRUE, worker_id = #{self.connection.quote(worker_id)}, updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
|
11
|
-
end
|
12
|
-
job.data ||= {}
|
13
|
-
job.exception_executions ||= {}
|
14
|
-
job_data = job.attributes.slice('job_class', 'queue_name', 'locale', 'timezone', 'priority', 'executions', 'exception_executions').merge('job_id' => job.id, 'enqueued_at' => job.updated_at, 'arguments' => (job.data['arguments'] || []))
|
15
|
-
job.executions = (job.executions || 0) + 1
|
16
|
-
Skiplock.logger.info "[Skiplock] Performing #{job.job_class} (#{job.id}) from queue '#{job.queue_name || 'default'}'..."
|
17
|
-
Thread.current[:skiplock_dispatch_job] = job
|
18
|
-
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
19
|
-
begin
|
20
|
-
ActiveJob::Base.execute(job_data)
|
21
|
-
rescue Exception => ex
|
22
|
-
Skiplock.logger.error(ex)
|
23
|
-
end
|
24
|
-
unless ex
|
25
|
-
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
26
|
-
job_name = job.job_class
|
27
|
-
if job.job_class == 'Skiplock::Extension::ProxyJob'
|
28
|
-
target, method_name = ::YAML.load(job.data['arguments'].first)
|
29
|
-
job_name = "'#{target.name}.#{method_name}'"
|
30
|
-
end
|
31
|
-
Skiplock.logger.info "[Skiplock] Performed #{job_name} (#{job.id}) from queue '#{job.queue_name || 'default'}' in #{end_time - start_time} seconds"
|
32
|
-
end
|
33
|
-
job.dispose(ex, purge_completion: purge_completion, max_retries: max_retries)
|
34
|
-
ensure
|
35
|
-
Thread.current[:skiplock_dispatch_job] = nil
|
36
|
-
end
|
37
|
-
|
38
5
|
def self.enqueue(activejob)
|
39
6
|
self.enqueue_at(activejob, nil)
|
40
7
|
end
|
@@ -48,7 +15,7 @@ module Skiplock
|
|
48
15
|
Thread.current[:skiplock_dispatch_job]
|
49
16
|
else
|
50
17
|
serialize = activejob.serialize
|
51
|
-
|
18
|
+
self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'] }, 'scheduled_at' => timestamp))
|
52
19
|
end
|
53
20
|
end
|
54
21
|
|
@@ -57,7 +24,7 @@ module Skiplock
|
|
57
24
|
end
|
58
25
|
|
59
26
|
def dispose(ex, purge_completion: true, max_retries: 20)
|
60
|
-
|
27
|
+
yaml = [self, ex].to_yaml
|
61
28
|
self.running = false
|
62
29
|
self.worker_id = nil
|
63
30
|
self.updated_at = (Time.now > self.updated_at ? Time.now : self.updated_at + 1)
|
@@ -95,9 +62,41 @@ module Skiplock
|
|
95
62
|
end
|
96
63
|
self
|
97
64
|
rescue Exception => e
|
98
|
-
Skiplock.logger.error(e)
|
99
|
-
|
65
|
+
Skiplock.logger.error(e.name)
|
66
|
+
Skiplock.logger.error(e.backtrace.join("\n"))
|
67
|
+
File.write("tmp/skiplock/#{self.id}", yaml)
|
100
68
|
nil
|
101
69
|
end
|
70
|
+
|
71
|
+
def execute(purge_completion: true, max_retries: 20)
|
72
|
+
Skiplock.logger.info "[Skiplock] Performing #{self.job_class} (#{self.id}) from queue '#{self.queue_name || 'default'}'..."
|
73
|
+
self.data ||= {}
|
74
|
+
self.exception_executions ||= {}
|
75
|
+
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'] || []))
|
76
|
+
self.executions = (self.executions || 0) + 1
|
77
|
+
Thread.current[:skiplock_dispatch_job] = self
|
78
|
+
activejob = ActiveJob::Base.deserialize(job_data)
|
79
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
80
|
+
begin
|
81
|
+
activejob.perform_now
|
82
|
+
rescue Exception => ex
|
83
|
+
end
|
84
|
+
if ex || self.exception_executions.key?('activejob_retry')
|
85
|
+
Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.exception_executions.key?('activejob_retry') }")
|
86
|
+
if ex
|
87
|
+
Skiplock.logger.error(ex)
|
88
|
+
Skiplock.logger.error(ex.backtrace.join("\n"))
|
89
|
+
end
|
90
|
+
else
|
91
|
+
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
92
|
+
job_name = self.job_class
|
93
|
+
if self.job_class == 'Skiplock::Extension::ProxyJob'
|
94
|
+
target, method_name = ::YAML.load(self.data['arguments'].first)
|
95
|
+
job_name = "'#{target.name}.#{method_name}'"
|
96
|
+
end
|
97
|
+
Skiplock.logger.info "[Skiplock] Performed #{job_name} (#{self.id}) from queue '#{self.queue_name || 'default'}' in #{end_time - start_time} seconds"
|
98
|
+
end
|
99
|
+
self.dispose(ex, purge_completion: purge_completion, max_retries: max_retries)
|
100
|
+
end
|
102
101
|
end
|
103
102
|
end
|
data/lib/skiplock/manager.rb
CHANGED
@@ -6,30 +6,65 @@ module Skiplock
|
|
6
6
|
@config.symbolize_keys!
|
7
7
|
@config.transform_values! {|v| v.is_a?(String) ? v.downcase : v}
|
8
8
|
@config.merge!(config)
|
9
|
-
Module.__send__(:include, Skiplock::Extension) if @config[:extensions] == true
|
10
|
-
return unless @config[:standalone] || (caller.any?{ |l| l =~ %r{/rack/} } && (@config[:workers] == 0 || Rails.env.development?))
|
11
9
|
@config[:hostname] = `hostname -f`.strip
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
standalone
|
19
|
-
else
|
20
|
-
dispatcher = Dispatcher.new(worker: @worker, **@config)
|
21
|
-
thread = dispatcher.run
|
10
|
+
configure
|
11
|
+
Module.__send__(:include, Skiplock::Extension) if @config[:extensions] == true
|
12
|
+
if (caller.any?{ |l| l =~ %r{/rack/} } && (@config[:workers] == 0 || Rails.env.development?))
|
13
|
+
cleanup_workers
|
14
|
+
@worker = create_worker
|
15
|
+
@thread = @worker.run(**@config)
|
22
16
|
at_exit do
|
23
|
-
|
24
|
-
thread.join(@config[:graceful_shutdown])
|
17
|
+
@worker.shutdown
|
18
|
+
@thread.join(@config[:graceful_shutdown])
|
25
19
|
@worker.delete
|
26
20
|
end
|
27
21
|
end
|
28
22
|
rescue Exception => ex
|
29
|
-
@logger.error(ex)
|
23
|
+
@logger.error(ex.name)
|
24
|
+
@logger.error(ex.backtrace.join("\n"))
|
25
|
+
end
|
26
|
+
|
27
|
+
def standalone(**options)
|
28
|
+
@config.merge!(options)
|
29
|
+
Rails.logger.reopen('/dev/null')
|
30
|
+
Rails.logger.extend(ActiveSupport::Logger.broadcast(@logger))
|
31
|
+
@config[:workers] = 1 if @config[:workers] <= 0
|
32
|
+
banner
|
33
|
+
cleanup_workers
|
34
|
+
@worker = create_worker
|
35
|
+
@parent_id = Process.pid
|
36
|
+
@shutdown = false
|
37
|
+
Signal.trap("INT") { @shutdown = true }
|
38
|
+
Signal.trap("TERM") { @shutdown = true }
|
39
|
+
(@config[:workers] - 1).times do |n|
|
40
|
+
fork do
|
41
|
+
sleep 1
|
42
|
+
worker = create_worker(master: false)
|
43
|
+
thread = worker.run(worker_num: n + 1, **@config)
|
44
|
+
loop do
|
45
|
+
sleep 0.5
|
46
|
+
break if @shutdown || Process.ppid != @parent_id
|
47
|
+
end
|
48
|
+
worker.shutdown
|
49
|
+
thread.join(@config[:graceful_shutdown])
|
50
|
+
worker.delete
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
end
|
54
|
+
@thread = @worker.run(**@config)
|
55
|
+
loop do
|
56
|
+
sleep 0.5
|
57
|
+
break if @shutdown
|
58
|
+
end
|
59
|
+
@logger.info "[Skiplock] Terminating signal... Waiting for jobs to finish (up to #{@config[:graceful_shutdown]} seconds)..." if @config[:graceful_shutdown]
|
60
|
+
Process.waitall
|
61
|
+
@worker.shutdown
|
62
|
+
@thread.join(@config[:graceful_shutdown])
|
63
|
+
@worker.delete
|
64
|
+
@logger.info "[Skiplock] Shutdown completed."
|
30
65
|
end
|
31
66
|
|
32
|
-
|
67
|
+
private
|
33
68
|
|
34
69
|
def banner
|
35
70
|
title = "Skiplock #{Skiplock::VERSION} (Rails #{Rails::VERSION::STRING} | Ruby #{RUBY_VERSION}-p#{RUBY_PATCHLEVEL})"
|
@@ -58,19 +93,17 @@ module Skiplock
|
|
58
93
|
sid = Process.getsid(worker.pid) rescue nil
|
59
94
|
delete_ids << worker.id if worker.sid != sid || worker.updated_at < 30.minutes.ago
|
60
95
|
end
|
61
|
-
if delete_ids.count > 0
|
62
|
-
|
63
|
-
Worker.where(id: delete_ids).delete_all
|
64
|
-
end
|
96
|
+
Worker.where(id: delete_ids).delete_all if delete_ids.count > 0
|
97
|
+
Job.where(running: true).where.not(worker_id: Worker.ids).update_all(running: false, worker_id: nil)
|
65
98
|
end
|
66
99
|
|
67
|
-
def create_worker(
|
68
|
-
|
100
|
+
def create_worker(master: true)
|
101
|
+
Worker.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: @config[:hostname], capacity: @config[:max_threads])
|
69
102
|
rescue
|
70
|
-
|
103
|
+
Worker.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: @config[:hostname], capacity: @config[:max_threads])
|
71
104
|
end
|
72
105
|
|
73
|
-
def
|
106
|
+
def configure
|
74
107
|
@config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s)
|
75
108
|
@config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
|
76
109
|
@config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
|
@@ -80,7 +113,6 @@ module Skiplock
|
|
80
113
|
@config[:max_threads] = 20 if @config[:max_threads] > 20
|
81
114
|
@config[:min_threads] = 0 if @config[:min_threads] < 0
|
82
115
|
@config[:workers] = 0 if @config[:workers] < 0
|
83
|
-
@config[:workers] = 1 if @config[:standalone] && @config[:workers] <= 0
|
84
116
|
@logger = ActiveSupport::Logger.new(STDOUT)
|
85
117
|
@logger.level = @config[:loglevel].to_sym
|
86
118
|
Skiplock.logger = @logger
|
@@ -88,10 +120,7 @@ module Skiplock
|
|
88
120
|
@config[:logfile] = nil if @config[:logfile].to_s.length == 0
|
89
121
|
if @config[:logfile]
|
90
122
|
@logger.extend(ActiveSupport::Logger.broadcast(::Logger.new(@config[:logfile])))
|
91
|
-
|
92
|
-
Rails.logger.reopen('/dev/null')
|
93
|
-
Rails.logger.extend(ActiveSupport::Logger.broadcast(@logger))
|
94
|
-
end
|
123
|
+
ActiveJob::Base.logger = nil
|
95
124
|
end
|
96
125
|
@config[:queues].values.each { |v| raise 'Queue value must be an integer' unless v.is_a?(Integer) } if @config[:queues].is_a?(Hash)
|
97
126
|
if @config[:notification] == 'auto'
|
@@ -126,40 +155,5 @@ module Skiplock
|
|
126
155
|
end
|
127
156
|
Skiplock.on_errors.freeze unless Skiplock.on_errors.frozen?
|
128
157
|
end
|
129
|
-
|
130
|
-
def standalone
|
131
|
-
parent_id = Process.pid
|
132
|
-
shutdown = false
|
133
|
-
Signal.trap("INT") { shutdown = true }
|
134
|
-
Signal.trap("TERM") { shutdown = true }
|
135
|
-
(@config[:workers] - 1).times do |n|
|
136
|
-
fork do
|
137
|
-
sleep 1
|
138
|
-
worker = create_worker(master: false)
|
139
|
-
dispatcher = Dispatcher.new(worker: worker, worker_num: n + 1, **@config)
|
140
|
-
thread = dispatcher.run
|
141
|
-
loop do
|
142
|
-
sleep 0.5
|
143
|
-
break if shutdown || Process.ppid != parent_id
|
144
|
-
end
|
145
|
-
dispatcher.shutdown
|
146
|
-
thread.join(@config[:graceful_shutdown])
|
147
|
-
worker.delete
|
148
|
-
exit
|
149
|
-
end
|
150
|
-
end
|
151
|
-
dispatcher = Dispatcher.new(worker: @worker, **@config)
|
152
|
-
thread = dispatcher.run
|
153
|
-
loop do
|
154
|
-
sleep 0.5
|
155
|
-
break if shutdown
|
156
|
-
end
|
157
|
-
@logger.info "[Skiplock] Terminating signal... Waiting for jobs to finish (up to #{@config[:graceful_shutdown]} seconds)..." if @config[:graceful_shutdown]
|
158
|
-
Process.waitall
|
159
|
-
dispatcher.shutdown
|
160
|
-
thread.join(@config[:graceful_shutdown])
|
161
|
-
@worker.delete
|
162
|
-
@logger.info "[Skiplock] Shutdown completed."
|
163
|
-
end
|
164
158
|
end
|
165
159
|
end
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
@@ -1,5 +1,117 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Worker < ActiveRecord::Base
|
3
3
|
self.implicit_order_column = 'created_at'
|
4
|
+
|
5
|
+
def run(worker_num: 0, **config)
|
6
|
+
@config = config
|
7
|
+
@worker_num = worker_num
|
8
|
+
@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
|
9
|
+
@next_schedule_at = Time.now.to_f
|
10
|
+
@executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads], max_threads: @config[:max_threads], max_queue: @config[:max_threads], idletime: 60, auto_terminate: true, fallback_policy: :discard)
|
11
|
+
@running = true
|
12
|
+
Process.setproctitle("skiplock-#{self.master ? 'master[0]' : 'worker[' + @worker_num.to_s + ']'}") if @config[:standalone]
|
13
|
+
Thread.new do
|
14
|
+
@connection = self.class.connection
|
15
|
+
@connection.exec_query('LISTEN "skiplock::jobs"')
|
16
|
+
if self.master
|
17
|
+
Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
|
18
|
+
check_sync_errors
|
19
|
+
Cron.setup
|
20
|
+
end
|
21
|
+
error = false
|
22
|
+
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
23
|
+
while @running
|
24
|
+
Rails.application.reloader.wrap do
|
25
|
+
begin
|
26
|
+
if error
|
27
|
+
unless @connection.active?
|
28
|
+
@connection.reconnect!
|
29
|
+
sleep(0.5)
|
30
|
+
@connection.exec_query('LISTEN "skiplock::jobs"')
|
31
|
+
@next_schedule_at = Time.now.to_f
|
32
|
+
end
|
33
|
+
check_sync_errors if self.master
|
34
|
+
error = false
|
35
|
+
end
|
36
|
+
job_notifications = []
|
37
|
+
@connection.raw_connection.wait_for_notify(0.1) do |channel, pid, payload|
|
38
|
+
job_notifications << payload if payload
|
39
|
+
loop do
|
40
|
+
payload = @connection.raw_connection.notifies
|
41
|
+
break unless @running && payload
|
42
|
+
job_notifications << payload[:extra]
|
43
|
+
end
|
44
|
+
job_notifications.each do |n|
|
45
|
+
op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
|
46
|
+
next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
|
47
|
+
if scheduled_at.to_f <= Time.now.to_f
|
48
|
+
@next_schedule_at = Time.now.to_f
|
49
|
+
elsif scheduled_at.to_f < @next_schedule_at
|
50
|
+
@next_schedule_at = scheduled_at.to_f
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
|
55
|
+
job = dispatch_job
|
56
|
+
if job.is_a?(Job)
|
57
|
+
@executor.post(job, @config[:purge_completion], @config[:max_retries]) do |job, purge_completion, max_retries|
|
58
|
+
job.execute(purge_completion: purge_completion, max_retries: max_retries)
|
59
|
+
end
|
60
|
+
else
|
61
|
+
@next_schedule_at = job
|
62
|
+
end
|
63
|
+
end
|
64
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
|
65
|
+
self.touch
|
66
|
+
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
67
|
+
end
|
68
|
+
rescue Exception => ex
|
69
|
+
# most likely error with database connection
|
70
|
+
Skiplock.logger.error(ex.name)
|
71
|
+
Skiplock.logger.error(ex.backtrace.join("\n"))
|
72
|
+
Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
|
73
|
+
error = true
|
74
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
75
|
+
while @running
|
76
|
+
sleep(0.5)
|
77
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - t > 5
|
78
|
+
end
|
79
|
+
@last_exception = ex
|
80
|
+
end
|
81
|
+
sleep(0.2)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
@connection.exec_query('UNLISTEN *')
|
85
|
+
@executor.shutdown
|
86
|
+
@executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def shutdown
|
91
|
+
@running = false
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def check_sync_errors
|
97
|
+
# get performed jobs that could not sync with database
|
98
|
+
Dir.glob('tmp/skiplock/*').each do |f|
|
99
|
+
job_from_db = Job.find_by(id: File.basename(f), running: true)
|
100
|
+
disposed = true
|
101
|
+
if job_from_db
|
102
|
+
job, ex = YAML.load_file(f) rescue nil
|
103
|
+
disposed = job.dispose(ex, purge_completion: @config[:purge_completion], max_retries: @config[:max_retries])
|
104
|
+
end
|
105
|
+
File.delete(f) if disposed
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def dispatch_job
|
110
|
+
@connection.transaction do
|
111
|
+
job = Job.find_by_sql("SELECT id, 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
|
112
|
+
return (job ? job.scheduled_at.to_f : Float::INFINITY) if job.nil? || job.scheduled_at.to_f > Time.now.to_f
|
113
|
+
Job.find_by_sql("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
|
114
|
+
end
|
115
|
+
end
|
4
116
|
end
|
5
117
|
end
|
data/lib/skiplock.rb
CHANGED
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.13
|
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-
|
11
|
+
date: 2021-09-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -84,7 +84,6 @@ files:
|
|
84
84
|
- lib/skiplock.rb
|
85
85
|
- lib/skiplock/counter.rb
|
86
86
|
- lib/skiplock/cron.rb
|
87
|
-
- lib/skiplock/dispatcher.rb
|
88
87
|
- lib/skiplock/extension.rb
|
89
88
|
- lib/skiplock/job.rb
|
90
89
|
- lib/skiplock/manager.rb
|
data/lib/skiplock/dispatcher.rb
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
module Skiplock
|
2
|
-
class Dispatcher
|
3
|
-
def initialize(worker:, worker_num: nil, **config)
|
4
|
-
@config = config
|
5
|
-
@worker = worker
|
6
|
-
@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
|
7
|
-
@executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads], max_threads: @config[:max_threads], max_queue: @config[:max_threads], idletime: 60, auto_terminate: true, fallback_policy: :discard)
|
8
|
-
@last_dispatch_at = 0
|
9
|
-
@next_schedule_at = Time.now.to_f
|
10
|
-
Process.setproctitle("skiplock-#{@worker.master ? 'master[0]' : 'worker[' + worker_num.to_s + ']'}") if @config[:standalone]
|
11
|
-
end
|
12
|
-
|
13
|
-
def run
|
14
|
-
@running = true
|
15
|
-
Thread.new do
|
16
|
-
ActiveRecord::Base.connection_pool.with_connection do |connection|
|
17
|
-
connection.exec_query('LISTEN "skiplock::jobs"')
|
18
|
-
if @worker.master
|
19
|
-
Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
|
20
|
-
check_sync_errors
|
21
|
-
Cron.setup
|
22
|
-
end
|
23
|
-
error = false
|
24
|
-
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
25
|
-
while @running
|
26
|
-
begin
|
27
|
-
if error
|
28
|
-
unless connection.active?
|
29
|
-
connection.reconnect!
|
30
|
-
sleep(0.5)
|
31
|
-
connection.exec_query('LISTEN "skiplock::jobs"')
|
32
|
-
@next_schedule_at = Time.now.to_f
|
33
|
-
end
|
34
|
-
check_sync_errors
|
35
|
-
error = false
|
36
|
-
end
|
37
|
-
job_notifications = []
|
38
|
-
connection.raw_connection.wait_for_notify(0.1) do |channel, pid, payload|
|
39
|
-
job_notifications << payload if payload
|
40
|
-
loop do
|
41
|
-
payload = connection.raw_connection.notifies
|
42
|
-
break unless @running && payload
|
43
|
-
job_notifications << payload[:extra]
|
44
|
-
end
|
45
|
-
job_notifications.each do |n|
|
46
|
-
op, id, worker_id, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
|
47
|
-
next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0 || scheduled_at.to_f < @last_dispatch_at
|
48
|
-
if scheduled_at.to_f <= Time.now.to_f
|
49
|
-
@next_schedule_at = Time.now.to_f
|
50
|
-
elsif scheduled_at.to_f < @next_schedule_at
|
51
|
-
@next_schedule_at = scheduled_at.to_f
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
|
56
|
-
@executor.post { do_work }
|
57
|
-
end
|
58
|
-
if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
|
59
|
-
@worker.touch
|
60
|
-
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
61
|
-
end
|
62
|
-
rescue Exception => ex
|
63
|
-
# most likely error with database connection
|
64
|
-
Skiplock.logger.error(ex)
|
65
|
-
Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
|
66
|
-
error = true
|
67
|
-
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
68
|
-
while @running
|
69
|
-
sleep(0.5)
|
70
|
-
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - t > 5
|
71
|
-
end
|
72
|
-
@last_exception = ex
|
73
|
-
end
|
74
|
-
sleep(0.2)
|
75
|
-
end
|
76
|
-
connection.exec_query('UNLISTEN *')
|
77
|
-
@executor.shutdown
|
78
|
-
@executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
|
79
|
-
end
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
def shutdown
|
84
|
-
@running = false
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
def check_sync_errors
|
90
|
-
# get performed jobs that could not sync with database
|
91
|
-
Dir.glob('tmp/skiplock/*').each do |f|
|
92
|
-
job_from_db = Job.find_by(id: File.basename(f), running: true)
|
93
|
-
disposed = true
|
94
|
-
if job_from_db
|
95
|
-
job, ex = YAML.load_file(f) rescue nil
|
96
|
-
disposed = job.dispose(ex, purge_completion: @config[:purge_completion], max_retries: @config[:max_retries])
|
97
|
-
end
|
98
|
-
File.delete(f) if disposed
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
|
-
def do_work
|
103
|
-
while @running
|
104
|
-
@last_dispatch_at = Time.now.to_f - 1 # 1 second allowance for time drift
|
105
|
-
result = Job.dispatch(queues_order_query: @queues_order_query, worker_id: @worker.id, purge_completion: @config[:purge_completion], max_retries: @config[:max_retries])
|
106
|
-
next if result.is_a?(Job) && Time.now.to_f >= @next_schedule_at
|
107
|
-
@next_schedule_at = result if result.is_a?(Float)
|
108
|
-
break
|
109
|
-
end
|
110
|
-
rescue Exception => ex
|
111
|
-
Skiplock.logger.error(ex)
|
112
|
-
Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
|
113
|
-
@last_exception = ex
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|