skiplock 1.0.14 → 1.0.18
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 +4 -4
- data/README.md +69 -14
- data/bin/skiplock +7 -2
- data/lib/generators/skiplock/templates/migration.rb.erb +6 -7
- data/lib/skiplock/counter.rb +1 -0
- data/lib/skiplock/cron.rb +2 -3
- data/lib/skiplock/extension.rb +4 -1
- data/lib/skiplock/job.rb +83 -53
- data/lib/skiplock/manager.rb +34 -38
- data/lib/skiplock/patch.rb +8 -0
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +46 -42
- data/lib/skiplock.rb +4 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8112589dcbdf58a743529ec9bf5ddd8371791e83e79c0d455ed42cbca05ee5eb
|
4
|
+
data.tar.gz: 149f826e93ecd8ac6165a8db46a8e7e234636a4481e68bf09d208b04a1dea1ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 437f622f2398a0a6c965ea1720f77f11b9241373f277e249e4f3d39afbd3bccc7d8ad2b919abd141ebc5bc6a2543408a7940e53f2968c3cda9fc0519ac387910
|
7
|
+
data.tar.gz: '0473939dc2497842ff66fb02e2f74371f7281d58cc5ac82f1999b669f5b45e526d5d75054a3cc95594e26fc5f4e4aafc42e15d2f7336b88223256d6f7ac46d4e'
|
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
|
|
@@ -51,9 +51,9 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
51
51
|
# config/skiplock.yml (default settings)
|
52
52
|
---
|
53
53
|
min_threads: 1
|
54
|
-
max_threads:
|
54
|
+
max_threads: 10
|
55
55
|
max_retries: 20
|
56
|
-
logfile:
|
56
|
+
logfile: skiplock.log
|
57
57
|
loglevel: info
|
58
58
|
notification: custom
|
59
59
|
extensions: false
|
@@ -67,11 +67,11 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
67
67
|
- **min_threads** (*integer*): sets minimum number of threads staying idle
|
68
68
|
- **max_threads** (*integer*): sets the maximum number of threads allowed to run jobs
|
69
69
|
- **max_retries** (*integer*): sets the maximum attempt a job will be retrying before it is marked expired. See `Retry system` for more details
|
70
|
-
- **logfile** (*string*):
|
70
|
+
- **logfile** (*string*): filename for skiplock logs; empty logfile will disable logging
|
71
71
|
- **loglevel** (*string*): sets logging level (`debug, info, warn, error, fatal, unknown`)
|
72
72
|
- **notification** (*string*): sets the library to be used for notifying errors and exceptions (`auto, airbrake, bugsnag, exception_notification, custom`); using `auto` will detect library if available. See `Notification system` for more details
|
73
|
-
- **extensions** (*
|
74
|
-
- **purge_completion** (*boolean*): when set to **true** will delete jobs after they were completed successfully; if set to **false** then the completed jobs should be purged periodically to maximize performance (eg. clean up old jobs after 3 months)
|
73
|
+
- **extensions** (*multi*): enable or disable the class method extension. See `ClassMethod extension` for more details
|
74
|
+
- **purge_completion** (*boolean*): when set to **true** will delete jobs after they were completed successfully; if set to **false** then the completed jobs should be purged periodically to maximize performance (eg. clean up old jobs after 3 months); queued jobs can manually override using `purge` option
|
75
75
|
- **queues** (*hash*): defines the set of queues with priorities; lower priority takes precedence
|
76
76
|
- **workers** (*integer*) sets the maximum number of processes when running in standalone mode using the `skiplock` executable; setting this to **0** will enable **async mode**
|
77
77
|
|
@@ -84,11 +84,12 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
84
84
|
$ bundle exec skiplock -h
|
85
85
|
Usage: skiplock [options]
|
86
86
|
-e, --environment STRING Rails environment
|
87
|
-
-l, --logfile STRING
|
87
|
+
-l, --logfile STRING Log filename
|
88
88
|
-s, --graceful-shutdown NUM Number of seconds to wait for graceful shutdown
|
89
89
|
-r, --max-retries NUM Number of maxixum retries
|
90
90
|
-t, --max-threads NUM Number of maximum threads
|
91
91
|
-T, --min-threads NUM Number of minimum threads
|
92
|
+
-v, --version Show version information
|
92
93
|
-w, --workers NUM Number of workers
|
93
94
|
-h, --help Show this message
|
94
95
|
```
|
@@ -99,19 +100,27 @@ Inside the Rails application:
|
|
99
100
|
```ruby
|
100
101
|
MyJob.perform_later
|
101
102
|
```
|
102
|
-
- Skiplock supports all ActiveJob features
|
103
|
+
- Skiplock supports all ActiveJob features and options
|
103
104
|
```ruby
|
104
105
|
MyJob.set(queue: 'my_queue', wait: 5.minutes, priority: 10).perform_later(1,2,3)
|
106
|
+
MyJob.set(wait_until: Day.tomorrow.noon).perform_later(1,2,3)
|
107
|
+
```
|
108
|
+
- Skiplock supports custom options which override the global `Skiplock` configuration options for specified jobs
|
109
|
+
- **purge** (*boolean*): whether to remove this job after it has completed successfully
|
110
|
+
- **max_retries** (*integer*): set maximum retry attempt for this job
|
111
|
+
```ruby
|
112
|
+
MyJob.set(purge: false, max_retries: 5).perform_later(1,2,3)
|
105
113
|
```
|
106
114
|
Outside the Rails application:
|
107
115
|
- queue the jobs by inserting the job records directly to the database table
|
108
116
|
```sql
|
109
117
|
INSERT INTO skiplock.jobs(job_class) VALUES ('MyJob');
|
110
118
|
```
|
111
|
-
- with scheduling, priority, queue and
|
119
|
+
- with scheduling, priority, queue, arguments and custom options
|
112
120
|
```sql
|
113
121
|
INSERT INTO skiplock.jobs(job_class, queue_name, priority, scheduled_at, data)
|
114
|
-
VALUES ('MyJob', 'my_queue', 10, NOW() + INTERVAL '5 min',
|
122
|
+
VALUES ('MyJob', 'my_queue', 10, NOW() + INTERVAL '5 min',
|
123
|
+
'{"arguments":[1,2,3],"options":{"purge":false,"max_retries":5}}');
|
115
124
|
```
|
116
125
|
## Queue priority vs Job priority
|
117
126
|
*Why do queues use priorities when jobs already have priorities?*
|
@@ -155,9 +164,9 @@ Outside the Rails application:
|
|
155
164
|
# ...
|
156
165
|
end
|
157
166
|
```
|
158
|
-
If the retry attempt limit configured in ActiveJob has been reached, then the control will be passed back to `
|
167
|
+
If the retry attempt limit configured in ActiveJob has been reached, then the control will be passed back to `Skiplock` to be marked as an expired job.
|
159
168
|
|
160
|
-
If the `retry_on` block is not defined, then the built-in retry system of `
|
169
|
+
If the `retry_on` block is not defined, then the built-in retry system of `Skiplock` will kick in automatically. The retrying schedule is using an exponential formula (5 + 2**attempt). The `Skiplock` configuration `max_retries` determines the the limit of attempts before the failing job is marked as expired. The maximum retry limit can be set as high as 20; this allows up to 12 days of retrying before the job is marked as expired.
|
161
170
|
|
162
171
|
## Notification system
|
163
172
|
`Skiplock` can use existing exception notification library to notify errors and exceptions. It supports `airbrake`, `bugsnag`, and `exception_notification`. Custom notification can also be called whenever an exception occurs; it can be configured in an initializer like below:
|
@@ -165,15 +174,25 @@ If the `retry_on` block is not defined, then the built-in retry system of `skipl
|
|
165
174
|
# config/initializers/skiplock.rb
|
166
175
|
Skiplock.on_error do |ex, previous|
|
167
176
|
if ex.backtrace != previous.try(:backtrace)
|
168
|
-
# sends
|
177
|
+
# sends text message using Amazon SNS on new exceptions only
|
169
178
|
# the same repeated exceptions will only be sent once to avoid SPAM
|
170
179
|
# NOTE: exceptions generated from Job executions will not provide 'previous' exceptions
|
180
|
+
sms = Aws::SNS::Client.new(region: 'us-west-2', access_key_id: Rails.application.credentials[:aws][:access_key_id], secret_access_key: Rails.application.credentials[:aws][:secret_access_key])
|
181
|
+
sms.publish({ phone_number: '+122233334444', message: "Exception: #{ex.message}"[0..130] })
|
171
182
|
end
|
172
183
|
end
|
173
184
|
# supports multiple 'on_error' event callbacks
|
174
185
|
```
|
175
186
|
## ClassMethod extension
|
176
|
-
`Skiplock` can add extension to allow
|
187
|
+
`Skiplock` can add extension to allow class methods to be performed as a background job; it is disabled in the default configuration. To enable globally for all classes and modules, edit the `config/skiplock.yml` configuration file and change `extensions` to `true`; this can expose remote code execution if the `skiplock.jobs` database table is not secured properly.
|
188
|
+
|
189
|
+
To enable extension for specific classes and modules only then set the configuration to an array of names of the classes and modules eg. `['MyClass', 'MyModule']`
|
190
|
+
- An example of remote code execution if the extension is enabled globally (ie: configuration is set to `true`) and attacker can insert `skiplock.jobs`
|
191
|
+
```sql
|
192
|
+
INSERT INTO skiplock.jobs(job_class, data)
|
193
|
+
VALUES ('Skiplock::Extension::ProxyJob',
|
194
|
+
'{"arguments":["---\n- !ruby/module ''Kernel''\n- :system\n- - rm -rf /tmp/*\n"]}');
|
195
|
+
```
|
177
196
|
- Queue class method `generate_thumbnails` of class `Image` as background job to run as soon as possible
|
178
197
|
```ruby
|
179
198
|
Image.skiplock.generate_thumbnails(height: 100, ratio: true)
|
@@ -187,6 +206,42 @@ If the `retry_on` block is not defined, then the built-in retry system of `skipl
|
|
187
206
|
Subscription.skiplock(wait_until: Date.tomorrow.noon).charge(amount: 100)
|
188
207
|
```
|
189
208
|
|
209
|
+
## Fault tolerant
|
210
|
+
`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` global configuration or `purge` job option); failed or interrupted jobs are marked for retry.
|
211
|
+
|
212
|
+
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.
|
213
|
+
|
214
|
+
This also protects long running in-progress jobs that are terminated abruptly during a graceful shutdown with timeout; these will be queued for retry.
|
215
|
+
|
216
|
+
## Scalability
|
217
|
+
`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.
|
218
|
+
|
219
|
+
## Statistics, analytics and counters
|
220
|
+
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.
|
221
|
+
|
222
|
+
The `skiplock.jobs` database table contains all the `Skiplob` jobs. Each job's successful execution stores the result to its `data['result']` field column. If job completions are not purged then their execution results can be used for analytic purposes.
|
223
|
+
|
224
|
+
The `skiplock.counters` database table contains all the counters for 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.
|
225
|
+
- **completions**: numbers of jobs completed successfully
|
226
|
+
- **dispatches**: number of jobs dispatched for the first time (**retries** are not counted here)
|
227
|
+
- **expiries**: number of jobs exceeded `max_retries` and still failed to complete
|
228
|
+
- **failures**: number of jobs interrupted by graceful shutdown or unable to complete due to errors (exceptions)
|
229
|
+
- **retries**: number of jobs dispatched for retrying
|
230
|
+
|
231
|
+
Code examples of gathering counters information:
|
232
|
+
- get counter information for today
|
233
|
+
```ruby
|
234
|
+
Skiplock::Counter.where(day: Date.today).first
|
235
|
+
```
|
236
|
+
- get total number of successfully completed jobs within the past 30 days
|
237
|
+
```ruby
|
238
|
+
Skiplock::Counter.where("day >= ?", 30.days.ago).sum(:completions)
|
239
|
+
```
|
240
|
+
- get total number of expired jobs
|
241
|
+
```ruby
|
242
|
+
Skiplock::Counter.sum(:expiries)
|
243
|
+
```
|
244
|
+
|
190
245
|
## Contributing
|
191
246
|
|
192
247
|
Bug reports and pull requests are welcome on GitHub at https://github.com/vtt/skiplock.
|
data/bin/skiplock
CHANGED
@@ -1,15 +1,20 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'optparse'
|
3
|
+
require_relative '../lib/skiplock/version'
|
3
4
|
options = {}
|
4
5
|
begin
|
5
6
|
op = OptionParser.new do |opts|
|
6
7
|
opts.banner = "Usage: #{File.basename($0)} [options]"
|
7
8
|
opts.on('-e', '--environment STRING', String, 'Rails environment')
|
8
|
-
opts.on('-l', '--logfile STRING', String, '
|
9
|
+
opts.on('-l', '--logfile STRING', String, 'Log filename')
|
9
10
|
opts.on('-s', '--graceful-shutdown NUM', Integer, 'Number of seconds to wait for graceful shutdown')
|
10
11
|
opts.on('-r', '--max-retries NUM', Integer, 'Number of maxixum retries')
|
11
12
|
opts.on('-t', '--max-threads NUM', Integer, 'Number of maximum threads')
|
12
13
|
opts.on('-T', '--min-threads NUM', Integer, 'Number of minimum threads')
|
14
|
+
opts.on('-v', '--version', 'Show version information') do
|
15
|
+
puts "Skiplock Version #{Skiplock::VERSION}"
|
16
|
+
exit
|
17
|
+
end
|
13
18
|
opts.on('-w', '--workers NUM', Integer, 'Number of workers')
|
14
19
|
opts.on_tail('-h', '--help', 'Show this message') do
|
15
20
|
exit
|
@@ -25,4 +30,4 @@ options.transform_keys! { |k| k.to_s.gsub('-', '_').to_sym }
|
|
25
30
|
env = options.delete(:environment)
|
26
31
|
ENV['RAILS_ENV'] = env if env
|
27
32
|
require File.expand_path("config/environment.rb")
|
28
|
-
Rails.application.config.skiplock.standalone(**options
|
33
|
+
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 (
|
59
|
+
ELSIF (TG_OP = 'UPDATE' AND OLD.running = TRUE AND NEW.running = FALSE) 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
@@ -2,17 +2,16 @@ 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')
|
9
8
|
cron = j.const_get('CRON')
|
10
|
-
job = Job.find_by('job_class = ? AND cron IS NOT NULL', j.name) || Job.new(job_class: j.name, cron: cron, locale: I18n.locale, timezone: Time.zone.name)
|
9
|
+
job = Job.find_by('job_class = ? AND cron IS NOT NULL', j.name) || Job.new(job_class: j.name, queue_name: j.queue_as, cron: cron, locale: I18n.locale, timezone: Time.zone.name)
|
11
10
|
time = self.next_schedule_at(cron)
|
12
11
|
if time
|
13
12
|
job.cron = cron
|
14
13
|
job.running = false
|
15
|
-
job.scheduled_at = Time.at(time)
|
14
|
+
job.scheduled_at = Time.at(time) unless job.try(:executions).to_i > 0 # do not update schedule of retrying cron jobs
|
16
15
|
job.save
|
17
16
|
cronjobs << j.name
|
18
17
|
end
|
data/lib/skiplock/extension.rb
CHANGED
@@ -11,9 +11,12 @@ module Skiplock
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
class ProxyError < StandardError; end
|
15
|
+
|
14
16
|
class ProxyJob < ActiveJob::Base
|
15
17
|
def perform(yml)
|
16
|
-
target, method_name, args = ::YAML.load(yml)
|
18
|
+
target, method_name, args = ::YAML.load(yml) rescue nil
|
19
|
+
raise ProxyError, "Skiplock extension is not allowed for:\n#{yml}" unless target.respond_to?(:skiplock)
|
17
20
|
target.__send__(method_name, *args)
|
18
21
|
end
|
19
22
|
end
|
data/lib/skiplock/job.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Job < ActiveRecord::Base
|
3
|
-
self.implicit_order_column = '
|
3
|
+
self.implicit_order_column = 'updated_at'
|
4
|
+
attr_accessor :activejob_retry
|
5
|
+
belongs_to :worker, inverse_of: :jobs, required: false
|
4
6
|
|
5
|
-
def self.dispatch(
|
7
|
+
def self.dispatch(purge_completion: true, max_retries: 20)
|
6
8
|
job = nil
|
7
9
|
self.connection.transaction do
|
8
10
|
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
11
|
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,
|
12
|
+
job = self.find_by_sql("UPDATE skiplock.jobs SET running = TRUE, updated_at = NOW() WHERE id = '#{job.id}' RETURNING *").first
|
11
13
|
end
|
12
|
-
self.dispatch(
|
14
|
+
self.dispatch(purge_completion: purge_completion, max_retries: max_retries) if job.execute(purge_completion: purge_completion, max_retries: max_retries)
|
13
15
|
end
|
14
16
|
|
15
17
|
def self.enqueue(activejob)
|
@@ -18,87 +20,113 @@ module Skiplock
|
|
18
20
|
|
19
21
|
def self.enqueue_at(activejob, timestamp)
|
20
22
|
timestamp = Time.at(timestamp) if timestamp
|
21
|
-
if Thread.current[:
|
22
|
-
Thread.current[:
|
23
|
-
Thread.current[:
|
24
|
-
Thread.current[:
|
25
|
-
Thread.current[:
|
23
|
+
if Thread.current[:skiplock_job].try(:id) == activejob.job_id
|
24
|
+
Thread.current[:skiplock_job].activejob_retry = true
|
25
|
+
Thread.current[:skiplock_job].data['activejob_retry'] = true
|
26
|
+
Thread.current[:skiplock_job].executions = activejob.executions
|
27
|
+
Thread.current[:skiplock_job].exception_executions = activejob.exception_executions
|
28
|
+
Thread.current[:skiplock_job].scheduled_at = timestamp
|
29
|
+
Thread.current[:skiplock_job]
|
26
30
|
else
|
31
|
+
options = activejob.instance_variable_get('@skiplock_options') || {}
|
27
32
|
serialize = activejob.serialize
|
28
|
-
self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'] }, 'scheduled_at' => timestamp))
|
33
|
+
self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'], 'options' => options }, 'scheduled_at' => timestamp))
|
29
34
|
end
|
30
35
|
end
|
31
36
|
|
37
|
+
# resynchronize jobs that could not commit to database and reset any abandoned jobs for retry
|
38
|
+
def self.flush
|
39
|
+
Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
|
40
|
+
Dir.glob('tmp/skiplock/*').each do |f|
|
41
|
+
disposed = true
|
42
|
+
if self.exists?(id: File.basename(f), running: true)
|
43
|
+
job = Marshal.load(File.binread(f)) rescue nil
|
44
|
+
disposed = job.dispose if job.is_a?(Skiplock::Job)
|
45
|
+
end
|
46
|
+
(File.delete(f) rescue nil) if disposed
|
47
|
+
end
|
48
|
+
self.where(running: true).where.not(worker_id: Worker.ids).update_all(running: false, worker_id: nil)
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
32
52
|
def self.reset_retry_schedules
|
33
|
-
self.where('scheduled_at > NOW() AND executions
|
53
|
+
self.where('scheduled_at > NOW() AND executions > 0 AND expired_at IS NULL AND finished_at IS NULL').update_all(scheduled_at: nil, updated_at: Time.now)
|
34
54
|
end
|
35
55
|
|
36
|
-
def dispose
|
37
|
-
|
56
|
+
def dispose
|
57
|
+
return unless @max_retries
|
58
|
+
dump = Marshal.dump(self)
|
59
|
+
purging = false
|
38
60
|
self.running = false
|
39
61
|
self.worker_id = nil
|
40
|
-
self.updated_at =
|
41
|
-
if
|
42
|
-
self.exception_executions["[#{
|
43
|
-
if self.executions >= max_retries || self.
|
62
|
+
self.updated_at = Time.now > self.updated_at ? Time.now : self.updated_at + 1 # in case of clock drifting
|
63
|
+
if @exception
|
64
|
+
self.exception_executions["[#{@exception.class.name}]"] = self.exception_executions["[#{@exception.class.name}]"].to_i + 1 unless self.activejob_retry
|
65
|
+
if (self.executions.to_i >= @max_retries + 1) || self.data.key?('activejob_retry') || @exception.is_a?(Skiplock::Extension::ProxyError)
|
44
66
|
self.expired_at = Time.now
|
45
67
|
else
|
46
|
-
self.scheduled_at = Time.now + (5 * 2**self.executions)
|
68
|
+
self.scheduled_at = Time.now + (5 * 2**self.executions.to_i)
|
47
69
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
70
|
+
elsif self.finished_at
|
71
|
+
if self.cron
|
72
|
+
self.data['cron'] ||= {}
|
73
|
+
self.data['cron']['executions'] = self.data['cron']['executions'].to_i + 1
|
74
|
+
self.data['cron']['last_finished_at'] = self.finished_at.utc.to_s
|
75
|
+
self.data['cron']['last_result'] = self.data['result']
|
76
|
+
next_cron_at = Cron.next_schedule_at(self.cron)
|
77
|
+
if next_cron_at
|
78
|
+
# update job to record completions counter before resetting finished_at to nil
|
79
|
+
self.update_columns(self.attributes.slice(*self.changes.keys))
|
80
|
+
self.finished_at = nil
|
81
|
+
self.executions = nil
|
82
|
+
self.exception_executions = nil
|
83
|
+
self.data.delete('result')
|
84
|
+
self.scheduled_at = Time.at(next_cron_at)
|
85
|
+
else
|
86
|
+
Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
|
87
|
+
purging = true
|
88
|
+
end
|
89
|
+
elsif @purge == true
|
90
|
+
purging = true
|
65
91
|
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
92
|
end
|
73
|
-
self
|
93
|
+
purging ? self.delete : self.update_columns(self.attributes.slice(*self.changes.keys))
|
74
94
|
rescue Exception => e
|
95
|
+
File.binwrite("tmp/skiplock/#{self.id}", dump) rescue nil
|
75
96
|
if Skiplock.logger
|
76
97
|
Skiplock.logger.error(e.to_s)
|
77
98
|
Skiplock.logger.error(e.backtrace.join("\n"))
|
78
|
-
File.write("tmp/skiplock/#{self.id}", yaml)
|
79
99
|
end
|
100
|
+
Skiplock.on_errors.each { |p| p.call(e) }
|
80
101
|
nil
|
81
102
|
end
|
82
103
|
|
83
104
|
def execute(purge_completion: true, max_retries: 20)
|
105
|
+
raise 'Job has already been completed' if self.finished_at
|
84
106
|
Skiplock.logger.info("[Skiplock] Performing #{self.job_class} (#{self.id}) from queue '#{self.queue_name || 'default'}'...") if Skiplock.logger
|
85
107
|
self.data ||= {}
|
108
|
+
self.data.delete('result')
|
86
109
|
self.exception_executions ||= {}
|
110
|
+
self.activejob_retry = false
|
111
|
+
@max_retries = (self.data['options'].key?('max_retries') ? self.data['options']['max_retries'].to_i : max_retries) rescue max_retries
|
112
|
+
@max_retries = 20 if @max_retries < 0 || @max_retries > 20
|
113
|
+
@purge = (self.data['options'].key?('purge') ? self.data['options']['purge'] : purge_completion) rescue purge_completion
|
87
114
|
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 =
|
89
|
-
Thread.current[:
|
90
|
-
activejob = ActiveJob::Base.deserialize(job_data)
|
115
|
+
self.executions = self.executions.to_i + 1
|
116
|
+
Thread.current[:skiplock_job] = self
|
91
117
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
92
118
|
begin
|
93
|
-
|
119
|
+
self.data['result'] = ActiveJob::Base.execute(job_data)
|
94
120
|
rescue Exception => ex
|
121
|
+
@exception = ex
|
122
|
+
Skiplock.on_errors.each { |p| p.call(@exception) }
|
95
123
|
end
|
96
124
|
if Skiplock.logger
|
97
|
-
if
|
98
|
-
Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.
|
99
|
-
if
|
100
|
-
Skiplock.logger.error(
|
101
|
-
Skiplock.logger.error(
|
125
|
+
if @exception || self.activejob_retry
|
126
|
+
Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.activejob_retry }")
|
127
|
+
if @exception
|
128
|
+
Skiplock.logger.error(@exception.to_s)
|
129
|
+
Skiplock.logger.error(@exception.backtrace.join("\n"))
|
102
130
|
end
|
103
131
|
else
|
104
132
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -110,7 +138,9 @@ module Skiplock
|
|
110
138
|
Skiplock.logger.info "[Skiplock] Performed #{job_name} (#{self.id}) from queue '#{self.queue_name || 'default'}' in #{end_time - start_time} seconds"
|
111
139
|
end
|
112
140
|
end
|
113
|
-
|
141
|
+
ensure
|
142
|
+
self.finished_at ||= Time.now if self.data.key?('result') && !self.activejob_retry
|
143
|
+
self.dispose
|
114
144
|
end
|
115
145
|
end
|
116
146
|
end
|
data/lib/skiplock/manager.rb
CHANGED
@@ -1,17 +1,16 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Manager
|
3
|
-
def initialize
|
3
|
+
def initialize
|
4
4
|
@config = Skiplock::DEFAULT_CONFIG.dup
|
5
5
|
@config.merge!(YAML.load_file('config/skiplock.yml')) rescue nil
|
6
6
|
@config.symbolize_keys!
|
7
7
|
@config.transform_values! {|v| v.is_a?(String) ? v.downcase : v}
|
8
|
-
@
|
9
|
-
@config[:hostname] = `hostname -f`.strip
|
8
|
+
@hostname = Socket.gethostname
|
10
9
|
configure
|
11
|
-
|
10
|
+
setup_logger
|
12
11
|
if (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0)
|
13
|
-
|
14
|
-
@worker =
|
12
|
+
Worker.cleanup(@hostname)
|
13
|
+
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
15
14
|
@worker.start(**@config)
|
16
15
|
at_exit { @worker.shutdown }
|
17
16
|
end
|
@@ -22,20 +21,22 @@ module Skiplock
|
|
22
21
|
|
23
22
|
def standalone(**options)
|
24
23
|
@config.merge!(options)
|
25
|
-
Rails.logger.reopen('/dev/null')
|
24
|
+
Rails.logger.reopen('/dev/null') rescue Rails.logger.reopen('NUL') # supports Windows NUL device
|
26
25
|
Rails.logger.extend(ActiveSupport::Logger.broadcast(@logger))
|
27
26
|
@config[:workers] = 1 if @config[:workers] <= 0
|
27
|
+
@config[:standalone] = true
|
28
28
|
banner
|
29
|
-
|
30
|
-
@worker =
|
29
|
+
Worker.cleanup(@hostname)
|
30
|
+
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
31
31
|
@parent_id = Process.pid
|
32
32
|
@shutdown = false
|
33
|
-
Signal.trap(
|
34
|
-
Signal.trap(
|
33
|
+
Signal.trap('INT') { @shutdown = true }
|
34
|
+
Signal.trap('TERM') { @shutdown = true }
|
35
|
+
Signal.trap('HUP') { setup_logger }
|
35
36
|
(@config[:workers] - 1).times do |n|
|
36
37
|
fork do
|
37
38
|
sleep 1
|
38
|
-
worker =
|
39
|
+
worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false)
|
39
40
|
worker.start(worker_num: n + 1, **@config)
|
40
41
|
loop do
|
41
42
|
sleep 0.5
|
@@ -78,24 +79,7 @@ module Skiplock
|
|
78
79
|
@logger.warn "[Skiplock] Custom notification has no registered 'on_error' callback" if Skiplock.on_errors.count == 0
|
79
80
|
end
|
80
81
|
|
81
|
-
def cleanup_workers
|
82
|
-
delete_ids = []
|
83
|
-
Worker.where(hostname: @config[:hostname]).each do |worker|
|
84
|
-
sid = Process.getsid(worker.pid) rescue nil
|
85
|
-
delete_ids << worker.id if worker.sid != sid || worker.updated_at < 30.minutes.ago
|
86
|
-
end
|
87
|
-
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
|
-
end
|
90
|
-
|
91
|
-
def create_worker(master: true)
|
92
|
-
Worker.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: @config[:hostname], capacity: @config[:max_threads])
|
93
|
-
rescue
|
94
|
-
Worker.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: @config[:hostname], capacity: @config[:max_threads])
|
95
|
-
end
|
96
|
-
|
97
82
|
def configure
|
98
|
-
@config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s)
|
99
83
|
@config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
|
100
84
|
@config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
|
101
85
|
@config[:max_retries] = 20 if @config[:max_retries] > 20
|
@@ -104,15 +88,6 @@ module Skiplock
|
|
104
88
|
@config[:max_threads] = 20 if @config[:max_threads] > 20
|
105
89
|
@config[:min_threads] = 0 if @config[:min_threads] < 0
|
106
90
|
@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
91
|
@config[:queues].values.each { |v| raise 'Queue value must be an integer' unless v.is_a?(Integer) } if @config[:queues].is_a?(Hash)
|
117
92
|
if @config[:notification] == 'auto'
|
118
93
|
if defined?(Airbrake)
|
@@ -144,7 +119,28 @@ module Skiplock
|
|
144
119
|
else
|
145
120
|
@config[:notification] = 'custom'
|
146
121
|
end
|
122
|
+
Rails.application.eager_load! if Rails.env.development?
|
123
|
+
if @config[:extensions] == true
|
124
|
+
Module.__send__(:include, Skiplock::Extension)
|
125
|
+
elsif @config[:extensions].is_a?(Array)
|
126
|
+
@config[:extensions].each { |n| n.constantize.__send__(:extend, Skiplock::Extension) if n.safe_constantize }
|
127
|
+
end
|
147
128
|
Skiplock.on_errors.freeze unless Skiplock.on_errors.frozen?
|
148
129
|
end
|
130
|
+
|
131
|
+
def setup_logger
|
132
|
+
@config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s.downcase)
|
133
|
+
@logger = ActiveSupport::Logger.new(STDOUT)
|
134
|
+
@logger.level = @config[:loglevel].downcase.to_sym
|
135
|
+
Skiplock.logger = @logger
|
136
|
+
if @config[:logfile].to_s.length > 0
|
137
|
+
@logger.extend(ActiveSupport::Logger.broadcast(::Logger.new(File.join(Rails.root, 'log', @config[:logfile].to_s), 'daily')))
|
138
|
+
ActiveJob::Base.logger = nil
|
139
|
+
end
|
140
|
+
rescue Exception => ex
|
141
|
+
@logger.error "Exception with logger: #{ex.to_s}"
|
142
|
+
@logger.error ex.backtrace.join("\n")
|
143
|
+
Skiplock.on_errors.each { |p| p.call(ex) }
|
144
|
+
end
|
149
145
|
end
|
150
146
|
end
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
@@ -1,17 +1,34 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Worker < ActiveRecord::Base
|
3
|
-
self.implicit_order_column = '
|
3
|
+
self.implicit_order_column = 'updated_at'
|
4
|
+
has_many :jobs, inverse_of: :worker
|
5
|
+
|
6
|
+
def self.cleanup(hostname = nil)
|
7
|
+
delete_ids = []
|
8
|
+
self.where(hostname: hostname || Socket.gethostname).each do |worker|
|
9
|
+
sid = Process.getsid(worker.pid) rescue nil
|
10
|
+
delete_ids << worker.id if worker.sid != sid || worker.updated_at < 10.minutes.ago
|
11
|
+
end
|
12
|
+
self.where(id: delete_ids).delete_all if delete_ids.count > 0
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.generate(capacity:, hostname:, master: true)
|
16
|
+
self.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: hostname, capacity: capacity)
|
17
|
+
rescue
|
18
|
+
self.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: hostname, capacity: capacity)
|
19
|
+
end
|
4
20
|
|
5
21
|
def start(worker_num: 0, **config)
|
22
|
+
if self.master
|
23
|
+
Job.flush
|
24
|
+
Cron.setup
|
25
|
+
end
|
6
26
|
@config = config
|
7
27
|
@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
|
-
@next_schedule_at = Time.now.to_f
|
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)
|
10
28
|
@running = true
|
29
|
+
@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)
|
30
|
+
@executor.post { run }
|
11
31
|
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
|
15
32
|
end
|
16
33
|
|
17
34
|
def shutdown
|
@@ -23,22 +40,9 @@ module Skiplock
|
|
23
40
|
|
24
41
|
private
|
25
42
|
|
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
43
|
def get_next_available_job
|
40
44
|
@connection.transaction do
|
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
|
45
|
+
job = Job.find_by_sql("SELECT id, running, 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
|
42
46
|
if job && job.scheduled_at.to_f <= Time.now.to_f
|
43
47
|
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
48
|
end
|
@@ -47,38 +51,34 @@ module Skiplock
|
|
47
51
|
end
|
48
52
|
|
49
53
|
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
54
|
error = false
|
55
|
+
listen = false
|
56
|
+
next_schedule_at = Time.now.to_f
|
58
57
|
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
59
58
|
while @running
|
60
59
|
Rails.application.reloader.wrap do
|
61
60
|
begin
|
61
|
+
unless listen
|
62
|
+
@connection = self.class.connection
|
63
|
+
@connection.exec_query('LISTEN "skiplock::jobs"')
|
64
|
+
listen = true
|
65
|
+
end
|
62
66
|
if error
|
63
67
|
unless @connection.active?
|
64
68
|
@connection.reconnect!
|
65
69
|
sleep(0.5)
|
66
70
|
@connection.exec_query('LISTEN "skiplock::jobs"')
|
67
|
-
|
71
|
+
next_schedule_at = Time.now.to_f
|
68
72
|
end
|
69
|
-
|
73
|
+
Job.flush if self.master
|
70
74
|
error = false
|
71
75
|
end
|
72
|
-
if Time.now.to_f >=
|
76
|
+
if Time.now.to_f >= next_schedule_at && @executor.remaining_capacity > 0
|
73
77
|
job = get_next_available_job
|
74
78
|
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
|
79
|
+
@executor.post { Rails.application.reloader.wrap { job.execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
|
80
80
|
else
|
81
|
-
|
81
|
+
next_schedule_at = (job ? job.scheduled_at.to_f : Float::INFINITY)
|
82
82
|
end
|
83
83
|
end
|
84
84
|
job_notifications = []
|
@@ -92,7 +92,7 @@ module Skiplock
|
|
92
92
|
job_notifications.each do |n|
|
93
93
|
op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
|
94
94
|
next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
|
95
|
-
|
95
|
+
next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
|
96
96
|
end
|
97
97
|
end
|
98
98
|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
|
@@ -105,11 +105,7 @@ module Skiplock
|
|
105
105
|
Skiplock.logger.error(ex.backtrace.join("\n"))
|
106
106
|
Skiplock.on_errors.each { |p| p.call(ex, @last_exception) }
|
107
107
|
error = true
|
108
|
-
|
109
|
-
while @running
|
110
|
-
sleep(0.5)
|
111
|
-
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - t > 5
|
112
|
-
end
|
108
|
+
wait(5)
|
113
109
|
@last_exception = ex
|
114
110
|
end
|
115
111
|
sleep(0.3)
|
@@ -117,5 +113,13 @@ module Skiplock
|
|
117
113
|
end
|
118
114
|
@connection.exec_query('UNLISTEN *')
|
119
115
|
end
|
116
|
+
|
117
|
+
def wait(timeout)
|
118
|
+
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
119
|
+
while @running
|
120
|
+
sleep(0.5)
|
121
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) - t > timeout
|
122
|
+
end
|
123
|
+
end
|
120
124
|
end
|
121
125
|
end
|
data/lib/skiplock.rb
CHANGED
@@ -6,11 +6,12 @@ require 'skiplock/cron'
|
|
6
6
|
require 'skiplock/extension'
|
7
7
|
require 'skiplock/job'
|
8
8
|
require 'skiplock/manager'
|
9
|
+
require 'skiplock/patch'
|
9
10
|
require 'skiplock/worker'
|
10
11
|
require 'skiplock/version'
|
11
12
|
|
12
13
|
module Skiplock
|
13
|
-
DEFAULT_CONFIG = { 'extensions' => false, 'logfile' => '
|
14
|
+
DEFAULT_CONFIG = { 'extensions' => false, 'logfile' => 'skiplock.log', 'loglevel' => 'info', 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' => 10, 'max_retries' => 20, 'notification' => 'custom', 'purge_completion' => true, 'queues' => { 'default' => 100, 'mailers' => 999 }, 'workers' => 0 }.freeze
|
14
15
|
|
15
16
|
def self.logger=(l)
|
16
17
|
@logger = l
|
@@ -33,4 +34,5 @@ module Skiplock
|
|
33
34
|
def self.table_name_prefix
|
34
35
|
'skiplock.'
|
35
36
|
end
|
36
|
-
end
|
37
|
+
end
|
38
|
+
ActiveJob::Base.__send__(:include, Skiplock::Patch)
|
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.18
|
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-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activejob
|
@@ -87,6 +87,7 @@ files:
|
|
87
87
|
- lib/skiplock/extension.rb
|
88
88
|
- lib/skiplock/job.rb
|
89
89
|
- lib/skiplock/manager.rb
|
90
|
+
- lib/skiplock/patch.rb
|
90
91
|
- lib/skiplock/version.rb
|
91
92
|
- lib/skiplock/worker.rb
|
92
93
|
homepage: https://github.com/vtt/skiplock
|