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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1acb8564f7930a8acbd316ae7ab85064aa3edb29232802d495f6c8261a2dfb72
4
- data.tar.gz: c7de58da66f483450fb34680b5e83d37865ce02b9f7e500de386a2ce30c17edb
3
+ metadata.gz: 89240e4318ca72cf30a3426948d9aa8479f59f85b82e4dca4ef5ee3b449b9894
4
+ data.tar.gz: b8f6799ccf4ba566379548dc4e9c90b6a1202842139b0ac0a3e0247875be00d9
5
5
  SHA512:
6
- metadata.gz: 69f54b48dd37b8814ef1bfb046945f4aaf13c996f35f9b297823f980804958d13a897dd1e8f5e28d6514d76867a4bac3a401aee2e682334cbabc2b52d4a36184
7
- data.tar.gz: f2fda19cff5a11f6d51c7d2223de5a1251bf6744b86364c82343dafe02807ad59f02b0ad6a4324002e49393f0f87de1356686cda347172e6c947e523fcfe8c92
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
- Skiplock::Manager.new(**options.merge(standalone: true))
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
- Job.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'] }, 'scheduled_at' => timestamp))
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
- dup = self.dup
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
- File.write("tmp/skiplock/#{self.id}", [dup, ex].to_yaml)
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
@@ -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
- do_config
13
- banner if @config[:standalone]
14
- cleanup_workers
15
- create_worker
16
- ActiveJob::Base.logger = nil
17
- if @config[:standalone]
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
- dispatcher.shutdown
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
- private
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
- Job.where(running: true, worker_id: delete_ids).update_all(running: false, worker_id: nil)
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(pid: Process.pid, sid: Process.getsid(), master: true)
68
- @worker = Worker.create!(pid: pid, sid: sid, master: master, hostname: @config[:hostname], capacity: @config[:max_threads])
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
- @worker = Worker.create!(pid: pid, sid: sid, master: false, hostname: @config[:hostname], capacity: @config[:max_threads])
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 do_config
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
- if @config[:standalone]
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
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.12'
2
+ VERSION = Version = '1.0.13'
3
3
  end
4
4
 
@@ -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
@@ -3,7 +3,6 @@ require 'active_job/queue_adapters/skiplock_adapter'
3
3
  require 'active_record'
4
4
  require 'skiplock/counter'
5
5
  require 'skiplock/cron'
6
- require 'skiplock/dispatcher'
7
6
  require 'skiplock/extension'
8
7
  require 'skiplock/job'
9
8
  require 'skiplock/manager'
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.12
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-08-30 00:00:00.000000000 Z
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
@@ -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