skiplock 1.0.12 → 1.0.13
Sign up to get free protection for your applications and to get access to all the features.
- 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
|