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 +4 -4
- data/README.md +32 -1
- data/bin/skiplock +2 -2
- data/lib/generators/skiplock/templates/migration.rb.erb +6 -7
- data/lib/skiplock/counter.rb +1 -0
- data/lib/skiplock/cron.rb +0 -1
- data/lib/skiplock/job.rb +54 -37
- data/lib/skiplock/manager.rb +22 -14
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +23 -34
- data/lib/skiplock.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5c965cac0fea118b7a8295bd82999bd33cd2807829dee639b6b782279a27697d
|
4
|
+
data.tar.gz: f5357c225546ef4fcec74a10b3180d07950c7f7f3308a39b2fe19c5668677a0f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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, '
|
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
|
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.
|
47
|
-
|
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
|
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
|
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));
|
data/lib/skiplock/counter.rb
CHANGED
data/lib/skiplock/cron.rb
CHANGED
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
|
-
|
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,
|
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(
|
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].
|
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 =
|
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
|
43
|
-
|
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.
|
51
|
-
self.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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 =
|
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
|
-
|
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.
|
98
|
-
Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.
|
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
|
data/lib/skiplock/manager.rb
CHANGED
@@ -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(
|
34
|
-
Signal.trap(
|
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 <
|
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
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
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
|
-
|
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' => '
|
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.
|
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-
|
11
|
+
date: 2021-09-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|