skiplock 1.0.13 → 1.0.14

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: 89240e4318ca72cf30a3426948d9aa8479f59f85b82e4dca4ef5ee3b449b9894
4
- data.tar.gz: b8f6799ccf4ba566379548dc4e9c90b6a1202842139b0ac0a3e0247875be00d9
3
+ metadata.gz: 55c98833e2792a216b6389d399e992aae9660f23ea076fcf852fb164cbc32d55
4
+ data.tar.gz: 43c8c2f90ee55ec423242042d1ab2986c0cb9f70fcc04db56aaf2c566bbb0382
5
5
  SHA512:
6
- metadata.gz: 3d8ec5ec2c3b0612fd3077fac5d6ff0c6e1b6826ed74b25b640ccc966874461b470f633c582af4f408f547586e30e1fdc5c22f158ca6fdf5c91000ec33c609a2
7
- data.tar.gz: 27b6f4746f255777fdb6b3733d08a0592969a45d6d0f315d76231269255e532b51834fa79e1be89a0c18ac62509fe342266c9a8a5c7c038cc2b9e52b120e2469
6
+ metadata.gz: 873492b815e6b5fc653630fed3bc64c5a363339551bbad304ecbad9453ea3738c24f3f0b874f4d6fe47cd4cc0ea5d46fb61b4c0dc322814d08dbc11002c24443
7
+ data.tar.gz: e84f7081e07ba0aff19834251de09e08df573d45ff159299fe08baf621b76845cd2fdf0461229da77b07cf707f151e7d8fc00d97b19a9bf30f1129376c2dd053
data/lib/skiplock/job.rb CHANGED
@@ -2,6 +2,16 @@ module Skiplock
2
2
  class Job < ActiveRecord::Base
3
3
  self.implicit_order_column = 'created_at'
4
4
 
5
+ def self.dispatch(worker_id: nil, purge_completion: true, max_retries: 20)
6
+ job = nil
7
+ self.connection.transaction do
8
+ job = self.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, priority ASC NULLS LAST, created_at ASC FOR UPDATE SKIP LOCKED LIMIT 1").first
9
+ return if job.nil? || job.scheduled_at.to_f > Time.now.to_f
10
+ job = self.find_by_sql("UPDATE skiplock.jobs SET running = TRUE, worker_id = #{self.connection.quote(worker_id)}, updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
11
+ end
12
+ self.dispatch(worker_id: worker_id, purge_completion: purge_completion, max_retries: max_retries) if job.execute(purge_completion: purge_completion, max_retries: max_retries)
13
+ end
14
+
5
15
  def self.enqueue(activejob)
6
16
  self.enqueue_at(activejob, nil)
7
17
  end
@@ -50,7 +60,7 @@ module Skiplock
50
60
  self.scheduled_at = Time.at(next_cron_at)
51
61
  self.save!
52
62
  else
53
- Skiplock.logger.error "[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}"
63
+ Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
54
64
  self.delete
55
65
  end
56
66
  elsif purge_completion
@@ -62,14 +72,16 @@ module Skiplock
62
72
  end
63
73
  self
64
74
  rescue Exception => e
65
- Skiplock.logger.error(e.name)
66
- Skiplock.logger.error(e.backtrace.join("\n"))
67
- File.write("tmp/skiplock/#{self.id}", yaml)
75
+ if Skiplock.logger
76
+ Skiplock.logger.error(e.to_s)
77
+ Skiplock.logger.error(e.backtrace.join("\n"))
78
+ File.write("tmp/skiplock/#{self.id}", yaml)
79
+ end
68
80
  nil
69
81
  end
70
82
 
71
83
  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'}'..."
84
+ Skiplock.logger.info("[Skiplock] Performing #{self.job_class} (#{self.id}) from queue '#{self.queue_name || 'default'}'...") if Skiplock.logger
73
85
  self.data ||= {}
74
86
  self.exception_executions ||= {}
75
87
  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'] || []))
@@ -81,20 +93,22 @@ module Skiplock
81
93
  activejob.perform_now
82
94
  rescue Exception => ex
83
95
  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
+ if Skiplock.logger
97
+ if ex || self.exception_executions.key?('activejob_retry')
98
+ 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') }")
99
+ if ex
100
+ Skiplock.logger.error(ex.to_s)
101
+ Skiplock.logger.error(ex.backtrace.join("\n"))
102
+ end
103
+ else
104
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
105
+ job_name = self.job_class
106
+ if self.job_class == 'Skiplock::Extension::ProxyJob'
107
+ target, method_name = ::YAML.load(self.data['arguments'].first)
108
+ job_name = "'#{target.name}.#{method_name}'"
109
+ end
110
+ Skiplock.logger.info "[Skiplock] Performed #{job_name} (#{self.id}) from queue '#{self.queue_name || 'default'}' in #{end_time - start_time} seconds"
96
111
  end
97
- Skiplock.logger.info "[Skiplock] Performed #{job_name} (#{self.id}) from queue '#{self.queue_name || 'default'}' in #{end_time - start_time} seconds"
98
112
  end
99
113
  self.dispose(ex, purge_completion: purge_completion, max_retries: max_retries)
100
114
  end
@@ -9,18 +9,14 @@ module Skiplock
9
9
  @config[:hostname] = `hostname -f`.strip
10
10
  configure
11
11
  Module.__send__(:include, Skiplock::Extension) if @config[:extensions] == true
12
- if (caller.any?{ |l| l =~ %r{/rack/} } && (@config[:workers] == 0 || Rails.env.development?))
12
+ if (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0)
13
13
  cleanup_workers
14
14
  @worker = create_worker
15
- @thread = @worker.run(**@config)
16
- at_exit do
17
- @worker.shutdown
18
- @thread.join(@config[:graceful_shutdown])
19
- @worker.delete
20
- end
15
+ @worker.start(**@config)
16
+ at_exit { @worker.shutdown }
21
17
  end
22
18
  rescue Exception => ex
23
- @logger.error(ex.name)
19
+ @logger.error(ex.to_s)
24
20
  @logger.error(ex.backtrace.join("\n"))
25
21
  end
26
22
 
@@ -40,18 +36,15 @@ module Skiplock
40
36
  fork do
41
37
  sleep 1
42
38
  worker = create_worker(master: false)
43
- thread = worker.run(worker_num: n + 1, **@config)
39
+ worker.start(worker_num: n + 1, **@config)
44
40
  loop do
45
41
  sleep 0.5
46
42
  break if @shutdown || Process.ppid != @parent_id
47
43
  end
48
44
  worker.shutdown
49
- thread.join(@config[:graceful_shutdown])
50
- worker.delete
51
- exit
52
45
  end
53
46
  end
54
- @thread = @worker.run(**@config)
47
+ @worker.start(**@config)
55
48
  loop do
56
49
  sleep 0.5
57
50
  break if @shutdown
@@ -59,8 +52,6 @@ module Skiplock
59
52
  @logger.info "[Skiplock] Terminating signal... Waiting for jobs to finish (up to #{@config[:graceful_shutdown]} seconds)..." if @config[:graceful_shutdown]
60
53
  Process.waitall
61
54
  @worker.shutdown
62
- @thread.join(@config[:graceful_shutdown])
63
- @worker.delete
64
55
  @logger.info "[Skiplock] Shutdown completed."
65
56
  end
66
57
 
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.13'
2
+ VERSION = Version = '1.0.14'
3
3
  end
4
4
 
@@ -2,116 +2,120 @@ module Skiplock
2
2
  class Worker < ActiveRecord::Base
3
3
  self.implicit_order_column = 'created_at'
4
4
 
5
- def run(worker_num: 0, **config)
5
+ def start(worker_num: 0, **config)
6
6
  @config = config
7
- @worker_num = worker_num
8
7
  @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
8
  @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)
9
+ @executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads], idletime: 60, auto_terminate: true, fallback_policy: :discard)
11
10
  @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])
11
+ Process.setproctitle("skiplock-#{self.master ? 'master[0]' : 'worker[' + worker_num.to_s + ']'}") if @config[:standalone]
12
+ @executor.post do
13
+ Rails.application.reloader.wrap { run }
87
14
  end
88
15
  end
89
16
 
90
17
  def shutdown
91
18
  @running = false
19
+ @executor.shutdown
20
+ @executor.kill unless @executor.wait_for_termination(@config[:graceful_shutdown])
21
+ self.delete
92
22
  end
93
23
 
94
24
  private
95
25
 
96
26
  def check_sync_errors
97
- # get performed jobs that could not sync with database
27
+ # get executed jobs that could not sync with database
98
28
  Dir.glob('tmp/skiplock/*').each do |f|
99
29
  job_from_db = Job.find_by(id: File.basename(f), running: true)
100
30
  disposed = true
101
31
  if job_from_db
102
32
  job, ex = YAML.load_file(f) rescue nil
103
- disposed = job.dispose(ex, purge_completion: @config[:purge_completion], max_retries: @config[:max_retries])
33
+ disposed = job.dispose(ex, purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) if job
104
34
  end
105
35
  File.delete(f) if disposed
106
36
  end
107
37
  end
108
38
 
109
- def dispatch_job
39
+ def get_next_available_job
110
40
  @connection.transaction do
111
41
  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
42
+ if job && job.scheduled_at.to_f <= Time.now.to_f
43
+ job = Job.find_by_sql("UPDATE skiplock.jobs SET running = TRUE, worker_id = '#{self.id}', updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
44
+ end
45
+ job
46
+ end
47
+ end
48
+
49
+ def run
50
+ @connection = self.class.connection
51
+ @connection.exec_query('LISTEN "skiplock::jobs"')
52
+ if self.master
53
+ Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
54
+ check_sync_errors
55
+ Cron.setup
56
+ end
57
+ error = false
58
+ timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
+ while @running
60
+ Rails.application.reloader.wrap do
61
+ begin
62
+ if error
63
+ unless @connection.active?
64
+ @connection.reconnect!
65
+ sleep(0.5)
66
+ @connection.exec_query('LISTEN "skiplock::jobs"')
67
+ @next_schedule_at = Time.now.to_f
68
+ end
69
+ check_sync_errors if self.master
70
+ error = false
71
+ end
72
+ if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
73
+ job = get_next_available_job
74
+ if job.try(:running)
75
+ @executor.post do
76
+ Rails.application.executor.wrap do
77
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads { job.execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) }
78
+ end
79
+ end
80
+ else
81
+ @next_schedule_at = (job ? job.scheduled_at.to_f : Float::INFINITY)
82
+ end
83
+ end
84
+ job_notifications = []
85
+ @connection.raw_connection.wait_for_notify(0.4) do |channel, pid, payload|
86
+ job_notifications << payload if payload
87
+ loop do
88
+ payload = @connection.raw_connection.notifies
89
+ break unless @running && payload
90
+ job_notifications << payload[:extra]
91
+ end
92
+ job_notifications.each do |n|
93
+ op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
94
+ next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
95
+ @next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < @next_schedule_at
96
+ end
97
+ end
98
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
99
+ self.touch
100
+ timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
101
+ end
102
+ rescue Exception => ex
103
+ # most likely error with database connection
104
+ Skiplock.logger.error(ex.to_s)
105
+ Skiplock.logger.error(ex.backtrace.join("\n"))
106
+ Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
107
+ error = true
108
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
109
+ while @running
110
+ sleep(0.5)
111
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - t > 5
112
+ end
113
+ @last_exception = ex
114
+ end
115
+ sleep(0.3)
116
+ end
114
117
  end
118
+ @connection.exec_query('UNLISTEN *')
115
119
  end
116
120
  end
117
121
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skiplock
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.13
4
+ version: 1.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tin Vo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-09-04 00:00:00.000000000 Z
11
+ date: 2021-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob