skiplock 1.0.14 → 1.0.15

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: 55c98833e2792a216b6389d399e992aae9660f23ea076fcf852fb164cbc32d55
4
- data.tar.gz: 43c8c2f90ee55ec423242042d1ab2986c0cb9f70fcc04db56aaf2c566bbb0382
3
+ metadata.gz: 5c965cac0fea118b7a8295bd82999bd33cd2807829dee639b6b782279a27697d
4
+ data.tar.gz: f5357c225546ef4fcec74a10b3180d07950c7f7f3308a39b2fe19c5668677a0f
5
5
  SHA512:
6
- metadata.gz: 873492b815e6b5fc653630fed3bc64c5a363339551bbad304ecbad9453ea3738c24f3f0b874f4d6fe47cd4cc0ea5d46fb61b4c0dc322814d08dbc11002c24443
7
- data.tar.gz: e84f7081e07ba0aff19834251de09e08df573d45ff159299fe08baf621b76845cd2fdf0461229da77b07cf707f151e7d8fc00d97b19a9bf30f1129376c2dd053
6
+ metadata.gz: d42f8d156e11c25d117a246f30cee23e7c487ecfa80e74e0a10866013f2a6145afd42bb55e3f057ff1687f686ee4e030f851d88fd3c2d3a99b33df745c4617f0
7
+ data.tar.gz: 981e7adad4bae9281e40bda3e049f68b32458078b98f192f5a9721ba31b40d82347c19a4da1876b210826a066f49f3ff0f2b8c966e9708b2d5b7d7bb890ff8b4
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  It only uses the `LISTEN/NOTIFY/SKIP LOCKED` features provided natively on PostgreSQL 9.5+ to efficiently and reliably dispatch jobs to worker processes and threads ensuring that each job can be completed successfully **only once**. No other polling or timer is needed.
6
6
 
7
- The library is quite small compared to other PostgreSQL job queues (eg. *delay_job*, *queue_classic*, *que*, *good_job*) with less than 400 lines of codes; and it still provides similar set of features and more...
7
+ The library is quite small compared to other PostgreSQL job queues (eg. *delay_job*, *queue_classic*, *que*, *good_job*) with less than 500 lines of codes; and it still provides similar set of features and more...
8
8
 
9
9
  #### Compatibility:
10
10
 
@@ -187,6 +187,37 @@ If the `retry_on` block is not defined, then the built-in retry system of `skipl
187
187
  Subscription.skiplock(wait_until: Date.tomorrow.noon).charge(amount: 100)
188
188
  ```
189
189
 
190
+ ## Fault tolerant
191
+ `Skiplock` ensures that jobs will be executed sucessfully only once even if database connection is lost during or after the job was dispatched. Successful jobs are marked as completed or removed (with `purge_completion` turned on), and failed or interrupted jobs are marked for retry; however, when the database connection is dropped for any reasons and the commit is lost, `Skiplock` will then save the commit data to local disk (as `tmp/skiplock/<job_id>`) and synchronize with the database when the connection resumes. This also protects in-progress jobs that were terminated abruptly during a graceful shutdown with timeout; they will be queued for retry.
192
+
193
+ ## Scalability
194
+ `Skiplock` can scale both vertically and horizontally. To scale vertically, simply increase the number of `Skiplock` workers per host. To scale horizontally, simply deploy `Skiplock` to multiple hosts sharing the same PostgreSQL database.
195
+
196
+ ## Statistics and counters
197
+ The `skiplock.workers` database table contains all the `Skiplock` workers running on all the hosts. Active worker will update its timestamp column (`updated_at`) every minute; and dispatched jobs would be associated with the running workers. At any given time, a list of active workers running a list of jobs can be determined using the database table.
198
+
199
+ The `skiplock.counters` database table contains all historical job dispatches, completions, expiries, failures and retries. The counters are recorded by dates; so it's possible to get statistical data for any given day or range of dates.
200
+
201
+ - **completions**: numbers of jobs completed successfully
202
+ - **dispatches**: number of jobs dispatched for the first time (**retries** are not counted here)
203
+ - **expiries**: number of jobs exceeded `max_retry` and still failed to complete
204
+ - **failures**: number of jobs interrupted by graceful shutdown or errors (exceptions)
205
+ - **retries**: number of jobs dispatched for retrying
206
+
207
+ Code examples of gathering counters information:
208
+ - get counter information for today
209
+ ```ruby
210
+ Skiplock::Counter.where(day: Date.today).first
211
+ ```
212
+ - get total number of successfully completed jobs within the past 30 days
213
+ ```ruby
214
+ Skiplock::Counter.where("day >= ?", 30.days.ago).sum(:completions)
215
+ ```
216
+ - get total number of expired jobs
217
+ ```ruby
218
+ Skiplock::Counter.sum(:expiries)
219
+ ```
220
+
190
221
  ## Contributing
191
222
 
192
223
  Bug reports and pull requests are welcome on GitHub at https://github.com/vtt/skiplock.
data/bin/skiplock CHANGED
@@ -5,7 +5,7 @@ begin
5
5
  op = OptionParser.new do |opts|
6
6
  opts.banner = "Usage: #{File.basename($0)} [options]"
7
7
  opts.on('-e', '--environment STRING', String, 'Rails environment')
8
- opts.on('-l', '--logfile STRING', String, 'Full path to logfile')
8
+ opts.on('-l', '--logfile STRING', String, 'Log filename')
9
9
  opts.on('-s', '--graceful-shutdown NUM', Integer, 'Number of seconds to wait for graceful shutdown')
10
10
  opts.on('-r', '--max-retries NUM', Integer, 'Number of maxixum retries')
11
11
  opts.on('-t', '--max-threads NUM', Integer, 'Number of maximum threads')
@@ -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
- Rails.application.config.skiplock.standalone(**options.merge(standalone: true))
28
+ Rails.application.config.skiplock.standalone(**options)
@@ -43,21 +43,20 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
43
43
  record = NEW;
44
44
  IF (TG_OP = 'DELETE') THEN
45
45
  record = OLD;
46
- IF (record.finished_at IS NOT NULL OR record.expired_at IS NOT NULL) THEN
47
- RETURN NULL;
46
+ IF (record.running = TRUE) THEN
47
+ INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
48
48
  END IF;
49
- INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
50
49
  ELSIF (record.running = TRUE) THEN
51
- IF (record.executions IS NULL) THEN
52
- INSERT INTO skiplock.counters (day,dispatches) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET dispatches = skiplock.counters.dispatches + 1;
53
- ELSE
50
+ IF (record.executions > 0) THEN
54
51
  INSERT INTO skiplock.counters (day,retries) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET retries = skiplock.counters.retries + 1;
52
+ ELSE
53
+ INSERT INTO skiplock.counters (day,dispatches) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET dispatches = skiplock.counters.dispatches + 1;
55
54
  END IF;
56
55
  ELSIF (record.finished_at IS NOT NULL) THEN
57
56
  INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
58
57
  ELSIF (record.expired_at IS NOT NULL) THEN
59
58
  INSERT INTO skiplock.counters (day,expiries) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET expiries = skiplock.counters.expiries + 1;
60
- ELSIF (record.executions IS NOT NULL AND record.scheduled_at IS NOT NULL) THEN
59
+ ELSIF (record.executions > 0) THEN
61
60
  INSERT INTO skiplock.counters (day,failures) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET failures = skiplock.counters.failures + 1;
62
61
  END IF;
63
62
  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));
@@ -1,4 +1,5 @@
1
1
  module Skiplock
2
2
  class Counter < ActiveRecord::Base
3
+ self.implicit_order_column = 'day'
3
4
  end
4
5
  end
data/lib/skiplock/cron.rb CHANGED
@@ -2,7 +2,6 @@ require 'cron_parser'
2
2
  module Skiplock
3
3
  class Cron
4
4
  def self.setup
5
- Rails.application.eager_load! if Rails.env.development?
6
5
  cronjobs = []
7
6
  ActiveJob::Base.descendants.each do |j|
8
7
  next unless j.const_defined?('CRON')
data/lib/skiplock/job.rb CHANGED
@@ -1,15 +1,32 @@
1
1
  module Skiplock
2
2
  class Job < ActiveRecord::Base
3
3
  self.implicit_order_column = 'created_at'
4
+ attr_accessor :activejob_retry
5
+ belongs_to :worker, inverse_of: :jobs, required: false
4
6
 
5
- def self.dispatch(worker_id: nil, purge_completion: true, max_retries: 20)
7
+ # resynchronize jobs that could not commit to database and retry any abandoned jobs
8
+ def self.cleanup(purge_completion: true, max_retries: 20)
9
+ Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
10
+ Dir.glob('tmp/skiplock/*').each do |f|
11
+ job_from_db = self.find_by(id: File.basename(f), running: true)
12
+ disposed = true
13
+ if job_from_db
14
+ job, ex = YAML.load_file(f) rescue nil
15
+ disposed = job.dispose(ex, purge_completion: purge_completion, max_retries: max_retries) if job
16
+ end
17
+ (File.delete(f) rescue nil) if disposed
18
+ end
19
+ self.where(running: true).where.not(worker_id: Worker.ids).update_all(running: false, worker_id: nil)
20
+ end
21
+
22
+ def self.dispatch(purge_completion: true, max_retries: 20)
6
23
  job = nil
7
24
  self.connection.transaction do
8
25
  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
26
  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
27
+ job = self.find_by_sql("UPDATE skiplock.jobs SET running = TRUE, updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
11
28
  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)
29
+ self.dispatch(purge_completion: purge_completion, max_retries: max_retries) if job.execute(purge_completion: purge_completion, max_retries: max_retries)
13
30
  end
14
31
 
15
32
  def self.enqueue(activejob)
@@ -19,8 +36,9 @@ module Skiplock
19
36
  def self.enqueue_at(activejob, timestamp)
20
37
  timestamp = Time.at(timestamp) if timestamp
21
38
  if Thread.current[:skiplock_dispatch_job].try(:id) == activejob.job_id
22
- Thread.current[:skiplock_dispatch_job].exception_executions = activejob.exception_executions.merge('activejob_retry' => true)
39
+ Thread.current[:skiplock_dispatch_job].activejob_retry = true
23
40
  Thread.current[:skiplock_dispatch_job].executions = activejob.executions
41
+ Thread.current[:skiplock_dispatch_job].exception_executions = activejob.exception_executions
24
42
  Thread.current[:skiplock_dispatch_job].scheduled_at = timestamp
25
43
  Thread.current[:skiplock_dispatch_job]
26
44
  else
@@ -35,48 +53,45 @@ module Skiplock
35
53
 
36
54
  def dispose(ex, purge_completion: true, max_retries: 20)
37
55
  yaml = [self, ex].to_yaml
56
+ purging = false
38
57
  self.running = false
39
58
  self.worker_id = nil
40
- self.updated_at = (Time.now > self.updated_at ? Time.now : self.updated_at + 1)
59
+ self.updated_at = Time.now > self.updated_at ? Time.now : self.updated_at + 1 # in case of clock drifting
41
60
  if ex
42
- self.exception_executions["[#{ex.class.name}]"] = (self.exception_executions["[#{ex.class.name}]"] || 0) + 1 unless self.exception_executions.key?('activejob_retry')
43
- if self.executions >= max_retries || self.exception_executions.key?('activejob_retry')
61
+ self.exception_executions ||= {}
62
+ self.exception_executions["[#{ex.class.name}]"] = self.exception_executions["[#{ex.class.name}]"].to_i + 1 unless self.activejob_retry
63
+ if self.executions.to_i >= max_retries || self.activejob_retry
44
64
  self.expired_at = Time.now
45
65
  else
46
- self.scheduled_at = Time.now + (5 * 2**self.executions)
66
+ self.scheduled_at = Time.now + (5 * 2**self.executions.to_i)
47
67
  end
48
- self.save!
49
68
  Skiplock.on_errors.each { |p| p.call(ex) }
50
- elsif self.exception_executions.try(:key?, 'activejob_retry')
51
- self.save!
52
- elsif self.cron
53
- self.data ||= {}
54
- self.data['crons'] = (self.data['crons'] || 0) + 1
55
- self.data['last_cron_at'] = Time.now.utc.to_s
56
- next_cron_at = Cron.next_schedule_at(self.cron)
57
- if next_cron_at
58
- self.executions = nil
59
- self.exception_executions = nil
60
- self.scheduled_at = Time.at(next_cron_at)
61
- self.save!
62
- else
63
- Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
64
- self.delete
69
+ elsif self.finished_at
70
+ if self.cron
71
+ self.data ||= {}
72
+ self.data['crons'] = (self.data['crons'] || 0) + 1
73
+ self.data['last_cron_at'] = Time.now.utc.to_s
74
+ next_cron_at = Cron.next_schedule_at(self.cron)
75
+ if next_cron_at
76
+ self.executions = nil
77
+ self.exception_executions = nil
78
+ self.scheduled_at = Time.at(next_cron_at)
79
+ else
80
+ Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
81
+ purging = true
82
+ end
83
+ elsif purge_completion
84
+ purging = true
65
85
  end
66
- elsif purge_completion
67
- self.delete
68
- else
69
- self.finished_at = Time.now
70
- self.exception_executions = nil
71
- self.save!
72
86
  end
73
- self
87
+ purging ? self.delete : self.update_columns(self.attributes.slice(*self.changes.keys))
74
88
  rescue Exception => e
89
+ File.write("tmp/skiplock/#{self.id}", yaml) rescue nil
75
90
  if Skiplock.logger
76
91
  Skiplock.logger.error(e.to_s)
77
92
  Skiplock.logger.error(e.backtrace.join("\n"))
78
- File.write("tmp/skiplock/#{self.id}", yaml)
79
93
  end
94
+ Skiplock.on_errors.each { |p| p.call(e) }
80
95
  nil
81
96
  end
82
97
 
@@ -84,18 +99,19 @@ module Skiplock
84
99
  Skiplock.logger.info("[Skiplock] Performing #{self.job_class} (#{self.id}) from queue '#{self.queue_name || 'default'}'...") if Skiplock.logger
85
100
  self.data ||= {}
86
101
  self.exception_executions ||= {}
102
+ self.activejob_retry = false
87
103
  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'] || []))
88
- self.executions = (self.executions || 0) + 1
104
+ self.executions = self.executions.to_i + 1
89
105
  Thread.current[:skiplock_dispatch_job] = self
90
- activejob = ActiveJob::Base.deserialize(job_data)
91
106
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
92
107
  begin
93
- activejob.perform_now
108
+ ActiveJob::Base.execute(job_data)
109
+ self.finished_at = Time.now unless self.activejob_retry
94
110
  rescue Exception => ex
95
111
  end
96
112
  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') }")
113
+ if ex || self.activejob_retry
114
+ Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.activejob_retry }")
99
115
  if ex
100
116
  Skiplock.logger.error(ex.to_s)
101
117
  Skiplock.logger.error(ex.backtrace.join("\n"))
@@ -110,6 +126,7 @@ module Skiplock
110
126
  Skiplock.logger.info "[Skiplock] Performed #{job_name} (#{self.id}) from queue '#{self.queue_name || 'default'}' in #{end_time - start_time} seconds"
111
127
  end
112
128
  end
129
+ ensure
113
130
  self.dispose(ex, purge_completion: purge_completion, max_retries: max_retries)
114
131
  end
115
132
  end
@@ -8,6 +8,7 @@ module Skiplock
8
8
  @config.merge!(config)
9
9
  @config[:hostname] = `hostname -f`.strip
10
10
  configure
11
+ setup_logger
11
12
  Module.__send__(:include, Skiplock::Extension) if @config[:extensions] == true
12
13
  if (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0)
13
14
  cleanup_workers
@@ -25,13 +26,15 @@ module Skiplock
25
26
  Rails.logger.reopen('/dev/null')
26
27
  Rails.logger.extend(ActiveSupport::Logger.broadcast(@logger))
27
28
  @config[:workers] = 1 if @config[:workers] <= 0
29
+ @config[:standalone] = true
28
30
  banner
29
31
  cleanup_workers
30
32
  @worker = create_worker
31
33
  @parent_id = Process.pid
32
34
  @shutdown = false
33
- Signal.trap("INT") { @shutdown = true }
34
- Signal.trap("TERM") { @shutdown = true }
35
+ Signal.trap('INT') { @shutdown = true }
36
+ Signal.trap('TERM') { @shutdown = true }
37
+ Signal.trap('HUP') { setup_logger }
35
38
  (@config[:workers] - 1).times do |n|
36
39
  fork do
37
40
  sleep 1
@@ -79,13 +82,13 @@ module Skiplock
79
82
  end
80
83
 
81
84
  def cleanup_workers
85
+ Rails.application.eager_load! if Rails.env.development?
82
86
  delete_ids = []
83
87
  Worker.where(hostname: @config[:hostname]).each do |worker|
84
88
  sid = Process.getsid(worker.pid) rescue nil
85
- delete_ids << worker.id if worker.sid != sid || worker.updated_at < 30.minutes.ago
89
+ delete_ids << worker.id if worker.sid != sid || worker.updated_at < 10.minutes.ago
86
90
  end
87
91
  Worker.where(id: delete_ids).delete_all if delete_ids.count > 0
88
- Job.where(running: true).where.not(worker_id: Worker.ids).update_all(running: false, worker_id: nil)
89
92
  end
90
93
 
91
94
  def create_worker(master: true)
@@ -95,7 +98,6 @@ module Skiplock
95
98
  end
96
99
 
97
100
  def configure
98
- @config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s)
99
101
  @config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
100
102
  @config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
101
103
  @config[:max_retries] = 20 if @config[:max_retries] > 20
@@ -104,15 +106,6 @@ module Skiplock
104
106
  @config[:max_threads] = 20 if @config[:max_threads] > 20
105
107
  @config[:min_threads] = 0 if @config[:min_threads] < 0
106
108
  @config[:workers] = 0 if @config[:workers] < 0
107
- @logger = ActiveSupport::Logger.new(STDOUT)
108
- @logger.level = @config[:loglevel].to_sym
109
- Skiplock.logger = @logger
110
- raise "Cannot create logfile '#{@config[:logfile]}'" if @config[:logfile] && !File.writable?(File.dirname(@config[:logfile]))
111
- @config[:logfile] = nil if @config[:logfile].to_s.length == 0
112
- if @config[:logfile]
113
- @logger.extend(ActiveSupport::Logger.broadcast(::Logger.new(@config[:logfile])))
114
- ActiveJob::Base.logger = nil
115
- end
116
109
  @config[:queues].values.each { |v| raise 'Queue value must be an integer' unless v.is_a?(Integer) } if @config[:queues].is_a?(Hash)
117
110
  if @config[:notification] == 'auto'
118
111
  if defined?(Airbrake)
@@ -146,5 +139,20 @@ module Skiplock
146
139
  end
147
140
  Skiplock.on_errors.freeze unless Skiplock.on_errors.frozen?
148
141
  end
142
+
143
+ def setup_logger
144
+ @config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s)
145
+ @logger = ActiveSupport::Logger.new(STDOUT)
146
+ @logger.level = @config[:loglevel].to_sym
147
+ Skiplock.logger = @logger
148
+ if @config[:logfile].to_s.length > 0
149
+ @logger.extend(ActiveSupport::Logger.broadcast(::Logger.new(File.join(Rails.root, 'log', @config[:logfile].to_s), 'daily')))
150
+ ActiveJob::Base.logger = nil
151
+ end
152
+ rescue Exception => ex
153
+ puts "Exception with logger: #{ex.to_s}"
154
+ puts ex.backtrace.join("\n")
155
+ Skiplock.on_errors.each { |p| p.call(ex) }
156
+ end
149
157
  end
150
158
  end
@@ -1,4 +1,4 @@
1
1
  module Skiplock
2
- VERSION = Version = '1.0.14'
2
+ VERSION = Version = '1.0.15'
3
3
  end
4
4
 
@@ -1,17 +1,20 @@
1
1
  module Skiplock
2
2
  class Worker < ActiveRecord::Base
3
3
  self.implicit_order_column = 'created_at'
4
+ has_many :jobs, inverse_of: :worker
4
5
 
5
6
  def start(worker_num: 0, **config)
6
7
  @config = config
7
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
8
9
  @next_schedule_at = Time.now.to_f
9
10
  @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
+ if self.master
12
+ Job.cleanup(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries])
13
+ Cron.setup
14
+ end
10
15
  @running = true
11
16
  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 }
14
- end
17
+ @executor.post { run }
15
18
  end
16
19
 
17
20
  def shutdown
@@ -23,19 +26,6 @@ module Skiplock
23
26
 
24
27
  private
25
28
 
26
- def check_sync_errors
27
- # get executed jobs that could not sync with database
28
- Dir.glob('tmp/skiplock/*').each do |f|
29
- job_from_db = Job.find_by(id: File.basename(f), running: true)
30
- disposed = true
31
- if job_from_db
32
- job, ex = YAML.load_file(f) rescue nil
33
- disposed = job.dispose(ex, purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) if job
34
- end
35
- File.delete(f) if disposed
36
- end
37
- end
38
-
39
29
  def get_next_available_job
40
30
  @connection.transaction do
41
31
  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
@@ -47,18 +37,17 @@ module Skiplock
47
37
  end
48
38
 
49
39
  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
40
  error = false
41
+ listen = false
58
42
  timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
59
43
  while @running
60
44
  Rails.application.reloader.wrap do
61
45
  begin
46
+ unless listen
47
+ @connection = self.class.connection
48
+ @connection.exec_query('LISTEN "skiplock::jobs"')
49
+ listen = true
50
+ end
62
51
  if error
63
52
  unless @connection.active?
64
53
  @connection.reconnect!
@@ -66,17 +55,13 @@ module Skiplock
66
55
  @connection.exec_query('LISTEN "skiplock::jobs"')
67
56
  @next_schedule_at = Time.now.to_f
68
57
  end
69
- check_sync_errors if self.master
58
+ Job.cleanup if self.master
70
59
  error = false
71
60
  end
72
61
  if Time.now.to_f >= @next_schedule_at && @executor.remaining_capacity > 0
73
62
  job = get_next_available_job
74
63
  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
64
+ @executor.post { Rails.application.reloader.wrap { job.execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
80
65
  else
81
66
  @next_schedule_at = (job ? job.scheduled_at.to_f : Float::INFINITY)
82
67
  end
@@ -105,11 +90,7 @@ module Skiplock
105
90
  Skiplock.logger.error(ex.backtrace.join("\n"))
106
91
  Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
107
92
  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
93
+ wait(5)
113
94
  @last_exception = ex
114
95
  end
115
96
  sleep(0.3)
@@ -117,5 +98,13 @@ module Skiplock
117
98
  end
118
99
  @connection.exec_query('UNLISTEN *')
119
100
  end
101
+
102
+ def wait(timeout)
103
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
104
+ while @running
105
+ sleep(0.5)
106
+ break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - t > timeout
107
+ end
108
+ end
120
109
  end
121
110
  end
data/lib/skiplock.rb CHANGED
@@ -10,7 +10,7 @@ require 'skiplock/worker'
10
10
  require 'skiplock/version'
11
11
 
12
12
  module Skiplock
13
- DEFAULT_CONFIG = { 'extensions' => false, 'logfile' => 'log/skiplock.log', 'loglevel' => 'info', 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 5, 'max_retries' => 20, 'notification' => 'custom', 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
13
+ DEFAULT_CONFIG = { 'extensions' => false, 'logfile' => 'skiplock.log', 'loglevel' => 'info', 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 5, 'max_retries' => 20, 'notification' => 'custom', 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
14
14
 
15
15
  def self.logger=(l)
16
16
  @logger = l
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.14
4
+ version: 1.0.15
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-07 00:00:00.000000000 Z
11
+ date: 2021-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activejob