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 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