skiplock 1.0.16 → 1.0.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +29 -14
- data/lib/generators/skiplock/templates/migration.rb.erb +1 -1
- data/lib/skiplock/cron.rb +1 -1
- data/lib/skiplock/job.rb +52 -43
- data/lib/skiplock/manager.rb +16 -27
- data/lib/skiplock/patch.rb +8 -0
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +26 -11
- 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: d9533e3df15a24e784000c160588b96207271986bf927bff095752e556c3376a
|
4
|
+
data.tar.gz: dcc40ab796f55fc41363d671d5e4f73600c58c468d7c4543e496fad38c3e9a8a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0a0c21835b52f749022d7f3a135f041d122425defffaf6aebb6d8510d4dd0836aed2fb9977daff9e7d3bf2547e139d2dd3ea49f5f55a869ce851c93aaeb56a12
|
7
|
+
data.tar.gz: d94c6801c82e1bddb12cff0152b71e8554d03de4d28f51ec0d742807311adecca1b3b142aeadf3cd5e81336416b03206d81848f6da6abf8e0f8c726631a5d898
|
data/README.md
CHANGED
@@ -51,7 +51,7 @@ 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
56
|
logfile: skiplock.log
|
57
57
|
loglevel: info
|
@@ -70,8 +70,8 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
70
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
|
|
@@ -89,6 +89,7 @@ The library is quite small compared to other PostgreSQL job queues (eg. *delay_j
|
|
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,26 @@ 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 ran 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', '{"arguments":[1,2,3]}');
|
122
|
+
VALUES ('MyJob', 'my_queue', 10, NOW() + INTERVAL '5 min', '{"arguments":[1,2,3],"options":{"purge":false,"max_retries":5}}');
|
115
123
|
```
|
116
124
|
## Queue priority vs Job priority
|
117
125
|
*Why do queues use priorities when jobs already have priorities?*
|
@@ -165,15 +173,21 @@ If the `retry_on` block is not defined, then the built-in retry system of `Skipl
|
|
165
173
|
# config/initializers/skiplock.rb
|
166
174
|
Skiplock.on_error do |ex, previous|
|
167
175
|
if ex.backtrace != previous.try(:backtrace)
|
168
|
-
# sends
|
176
|
+
# sends text message using Amazon SNS on new exceptions only
|
169
177
|
# the same repeated exceptions will only be sent once to avoid SPAM
|
170
178
|
# NOTE: exceptions generated from Job executions will not provide 'previous' exceptions
|
179
|
+
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])
|
180
|
+
sms.publish({ phone_number: '+122233334444', message: "Exception: #{ex.message}"[0..130] })
|
171
181
|
end
|
172
182
|
end
|
173
183
|
# supports multiple 'on_error' event callbacks
|
174
184
|
```
|
175
185
|
## ClassMethod extension
|
176
|
-
`Skiplock` can add extension to allow
|
186
|
+
`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 execution if the `skiplock.jobs` database table is not secured properly. 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']`
|
187
|
+
- An example of remote execution if the extension is enabled globally (ie: configuration is set to `true`) and attacker can insert `skiplock.jobs`
|
188
|
+
```sql
|
189
|
+
INSERT INTO skiplock.jobs(job_class, data) VALUES ('Skiplock::Extension::ProxyJob', '{"arguments":["---\n- !ruby/module ''Kernel''\n- :system\n- - rm -rf /tmp/*\n"]}');
|
190
|
+
```
|
177
191
|
- Queue class method `generate_thumbnails` of class `Image` as background job to run as soon as possible
|
178
192
|
```ruby
|
179
193
|
Image.skiplock.generate_thumbnails(height: 100, ratio: true)
|
@@ -188,24 +202,25 @@ If the `retry_on` block is not defined, then the built-in retry system of `Skipl
|
|
188
202
|
```
|
189
203
|
|
190
204
|
## 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`
|
205
|
+
`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.
|
192
206
|
|
193
207
|
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.
|
194
208
|
|
195
|
-
This also protects in-progress jobs that
|
209
|
+
This also protects long running in-progress jobs that are terminated abruptly during a graceful shutdown with timeout; these will be queued for retry.
|
196
210
|
|
197
211
|
## Scalability
|
198
212
|
`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.
|
199
213
|
|
200
|
-
## Statistics and counters
|
214
|
+
## Statistics, analytics and counters
|
201
215
|
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.
|
202
216
|
|
203
|
-
The `skiplock.
|
217
|
+
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.
|
204
218
|
|
219
|
+
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.
|
205
220
|
- **completions**: numbers of jobs completed successfully
|
206
221
|
- **dispatches**: number of jobs dispatched for the first time (**retries** are not counted here)
|
207
|
-
- **expiries**: number of jobs exceeded `
|
208
|
-
- **failures**: number of jobs interrupted by graceful shutdown or errors (exceptions)
|
222
|
+
- **expiries**: number of jobs exceeded `max_retries` and still failed to complete
|
223
|
+
- **failures**: number of jobs interrupted by graceful shutdown or unable to complete due to errors (exceptions)
|
209
224
|
- **retries**: number of jobs dispatched for retrying
|
210
225
|
|
211
226
|
Code examples of gathering counters information:
|
@@ -56,7 +56,7 @@ class CreateSkiplockSchema < ActiveRecord::Migration<%= "[#{ActiveRecord::VERSIO
|
|
56
56
|
INSERT INTO skiplock.counters (day,completions) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET completions = skiplock.counters.completions + 1;
|
57
57
|
ELSIF (record.expired_at IS NOT NULL) THEN
|
58
58
|
INSERT INTO skiplock.counters (day,expiries) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET expiries = skiplock.counters.expiries + 1;
|
59
|
-
ELSIF (
|
59
|
+
ELSIF (TG_OP = 'UPDATE' AND OLD.running = TRUE AND NEW.running = FALSE) THEN
|
60
60
|
INSERT INTO skiplock.counters (day,failures) VALUES (NOW(),1) ON CONFLICT (day) DO UPDATE SET failures = skiplock.counters.failures + 1;
|
61
61
|
END IF;
|
62
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/cron.rb
CHANGED
@@ -6,7 +6,7 @@ module Skiplock
|
|
6
6
|
ActiveJob::Base.descendants.each do |j|
|
7
7
|
next unless j.const_defined?('CRON')
|
8
8
|
cron = j.const_get('CRON')
|
9
|
-
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)
|
10
10
|
time = self.next_schedule_at(cron)
|
11
11
|
if time
|
12
12
|
job.cron = cron
|
data/lib/skiplock/job.rb
CHANGED
@@ -4,21 +4,6 @@ module Skiplock
|
|
4
4
|
attr_accessor :activejob_retry
|
5
5
|
belongs_to :worker, inverse_of: :jobs, required: false
|
6
6
|
|
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
7
|
def self.dispatch(purge_completion: true, max_retries: 20)
|
23
8
|
job = nil
|
24
9
|
self.connection.transaction do
|
@@ -35,44 +20,61 @@ module Skiplock
|
|
35
20
|
|
36
21
|
def self.enqueue_at(activejob, timestamp)
|
37
22
|
timestamp = Time.at(timestamp) if timestamp
|
38
|
-
if Thread.current[:
|
39
|
-
Thread.current[:
|
40
|
-
Thread.current[:
|
41
|
-
Thread.current[:
|
42
|
-
Thread.current[:
|
43
|
-
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].executions = activejob.executions
|
26
|
+
Thread.current[:skiplock_job].exception_executions = activejob.exception_executions
|
27
|
+
Thread.current[:skiplock_job].scheduled_at = timestamp
|
28
|
+
Thread.current[:skiplock_job]
|
44
29
|
else
|
30
|
+
options = activejob.instance_variable_get('@skiplock_options') || {}
|
45
31
|
serialize = activejob.serialize
|
46
|
-
self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'] }, 'scheduled_at' => timestamp))
|
32
|
+
self.create!(serialize.slice(*self.column_names).merge('id' => serialize['job_id'], 'data' => { 'arguments' => serialize['arguments'], 'options' => options }, 'scheduled_at' => timestamp))
|
47
33
|
end
|
48
34
|
end
|
49
35
|
|
36
|
+
# resynchronize jobs that could not commit to database and retry any abandoned jobs
|
37
|
+
def self.flush
|
38
|
+
Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
|
39
|
+
Dir.glob('tmp/skiplock/*').each do |f|
|
40
|
+
disposed = true
|
41
|
+
if self.exists?(id: File.basename(f), running: true)
|
42
|
+
job = Marshal.load(File.binread(f)) rescue nil
|
43
|
+
disposed = job.dispose if job.is_a?(Skiplock::Job)
|
44
|
+
end
|
45
|
+
(File.delete(f) rescue nil) if disposed
|
46
|
+
end
|
47
|
+
self.where(running: true).where.not(worker_id: Worker.ids).update_all(running: false, worker_id: nil)
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
50
51
|
def self.reset_retry_schedules
|
51
|
-
self.where('scheduled_at > NOW() AND executions
|
52
|
+
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)
|
52
53
|
end
|
53
54
|
|
54
|
-
def dispose
|
55
|
-
|
55
|
+
def dispose
|
56
|
+
return unless @max_retries
|
57
|
+
dump = Marshal.dump(self)
|
56
58
|
purging = false
|
57
59
|
self.running = false
|
58
60
|
self.worker_id = nil
|
59
61
|
self.updated_at = Time.now > self.updated_at ? Time.now : self.updated_at + 1 # in case of clock drifting
|
60
|
-
if
|
61
|
-
self.exception_executions
|
62
|
-
|
63
|
-
if self.executions.to_i >= max_retries || self.activejob_retry
|
62
|
+
if @exception
|
63
|
+
self.exception_executions["[#{@exception.class.name}]"] = self.exception_executions["[#{@exception.class.name}]"].to_i + 1 unless self.activejob_retry
|
64
|
+
if (self.executions.to_i >= @max_retries + 1) || self.activejob_retry
|
64
65
|
self.expired_at = Time.now
|
65
66
|
else
|
66
67
|
self.scheduled_at = Time.now + (5 * 2**self.executions.to_i)
|
67
68
|
end
|
68
|
-
Skiplock.on_errors.each { |p| p.call(ex) }
|
69
69
|
elsif self.finished_at
|
70
70
|
if self.cron
|
71
|
-
self.data ||= {}
|
72
|
-
self.data['
|
73
|
-
self.data['
|
71
|
+
self.data['cron'] ||= {}
|
72
|
+
self.data['cron']['executions'] = self.data['cron']['executions'].to_i + 1
|
73
|
+
self.data['cron']['last_finished_at'] = self.finished_at.utc.to_s
|
74
74
|
next_cron_at = Cron.next_schedule_at(self.cron)
|
75
75
|
if next_cron_at
|
76
|
+
# update job to record completions counter before resetting finished_at to nil
|
77
|
+
self.update_columns(self.attributes.slice(*self.changes.keys))
|
76
78
|
self.finished_at = nil
|
77
79
|
self.executions = nil
|
78
80
|
self.exception_executions = nil
|
@@ -81,13 +83,13 @@ module Skiplock
|
|
81
83
|
Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
|
82
84
|
purging = true
|
83
85
|
end
|
84
|
-
elsif
|
86
|
+
elsif @purge == true
|
85
87
|
purging = true
|
86
88
|
end
|
87
89
|
end
|
88
90
|
purging ? self.delete : self.update_columns(self.attributes.slice(*self.changes.keys))
|
89
91
|
rescue Exception => e
|
90
|
-
File.
|
92
|
+
File.binwrite("tmp/skiplock/#{self.id}", dump) rescue nil
|
91
93
|
if Skiplock.logger
|
92
94
|
Skiplock.logger.error(e.to_s)
|
93
95
|
Skiplock.logger.error(e.backtrace.join("\n"))
|
@@ -97,25 +99,31 @@ module Skiplock
|
|
97
99
|
end
|
98
100
|
|
99
101
|
def execute(purge_completion: true, max_retries: 20)
|
102
|
+
raise 'Job has already been completed' if self.finished_at
|
100
103
|
Skiplock.logger.info("[Skiplock] Performing #{self.job_class} (#{self.id}) from queue '#{self.queue_name || 'default'}'...") if Skiplock.logger
|
101
104
|
self.data ||= {}
|
105
|
+
self.data.delete('result')
|
102
106
|
self.exception_executions ||= {}
|
103
107
|
self.activejob_retry = false
|
108
|
+
@max_retries = (self.data['options'].key?('max_retries') ? self.data['options']['max_retries'].to_i : max_retries) rescue max_retries
|
109
|
+
@max_retries = 20 if @max_retries < 0 || @max_retries > 20
|
110
|
+
@purge = (self.data['options'].key?('purge') ? self.data['options']['purge'] : purge_completion) rescue purge_completion
|
104
111
|
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'] || []))
|
105
112
|
self.executions = self.executions.to_i + 1
|
106
|
-
Thread.current[:
|
113
|
+
Thread.current[:skiplock_job] = self
|
107
114
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
108
115
|
begin
|
109
|
-
ActiveJob::Base.execute(job_data)
|
110
|
-
self.finished_at = Time.now unless self.activejob_retry
|
116
|
+
self.data['result'] = ActiveJob::Base.execute(job_data)
|
111
117
|
rescue Exception => ex
|
118
|
+
@exception = ex
|
119
|
+
Skiplock.on_errors.each { |p| p.call(@exception) }
|
112
120
|
end
|
113
121
|
if Skiplock.logger
|
114
|
-
if
|
122
|
+
if @exception || self.activejob_retry
|
115
123
|
Skiplock.logger.error("[Skiplock] Job #{self.job_class} (#{self.id}) was interrupted by an exception#{ ' (rescued and retried by ActiveJob)' if self.activejob_retry }")
|
116
|
-
if
|
117
|
-
Skiplock.logger.error(
|
118
|
-
Skiplock.logger.error(
|
124
|
+
if @exception
|
125
|
+
Skiplock.logger.error(@exception.to_s)
|
126
|
+
Skiplock.logger.error(@exception.backtrace.join("\n"))
|
119
127
|
end
|
120
128
|
else
|
121
129
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
@@ -128,7 +136,8 @@ module Skiplock
|
|
128
136
|
end
|
129
137
|
end
|
130
138
|
ensure
|
131
|
-
self.
|
139
|
+
self.finished_at ||= Time.now if self.data.key?('result') && !self.activejob_retry
|
140
|
+
self.dispose
|
132
141
|
end
|
133
142
|
end
|
134
143
|
end
|
data/lib/skiplock/manager.rb
CHANGED
@@ -6,13 +6,12 @@ module Skiplock
|
|
6
6
|
@config.symbolize_keys!
|
7
7
|
@config.transform_values! {|v| v.is_a?(String) ? v.downcase : v}
|
8
8
|
@config.merge!(config)
|
9
|
-
@
|
9
|
+
@hostname = Socket.gethostname
|
10
10
|
configure
|
11
11
|
setup_logger
|
12
|
-
Module.__send__(:include, Skiplock::Extension) if @config[:extensions] == true
|
13
12
|
if (caller.any?{ |l| l =~ %r{/rack/} } && @config[:workers] == 0)
|
14
|
-
|
15
|
-
@worker =
|
13
|
+
Worker.cleanup(@hostname)
|
14
|
+
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
16
15
|
@worker.start(**@config)
|
17
16
|
at_exit { @worker.shutdown }
|
18
17
|
end
|
@@ -28,8 +27,8 @@ module Skiplock
|
|
28
27
|
@config[:workers] = 1 if @config[:workers] <= 0
|
29
28
|
@config[:standalone] = true
|
30
29
|
banner
|
31
|
-
|
32
|
-
@worker =
|
30
|
+
Worker.cleanup(@hostname)
|
31
|
+
@worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname)
|
33
32
|
@parent_id = Process.pid
|
34
33
|
@shutdown = false
|
35
34
|
Signal.trap('INT') { @shutdown = true }
|
@@ -38,7 +37,7 @@ module Skiplock
|
|
38
37
|
(@config[:workers] - 1).times do |n|
|
39
38
|
fork do
|
40
39
|
sleep 1
|
41
|
-
worker =
|
40
|
+
worker = Worker.generate(capacity: @config[:max_threads], hostname: @hostname, master: false)
|
42
41
|
worker.start(worker_num: n + 1, **@config)
|
43
42
|
loop do
|
44
43
|
sleep 0.5
|
@@ -81,22 +80,6 @@ module Skiplock
|
|
81
80
|
@logger.warn "[Skiplock] Custom notification has no registered 'on_error' callback" if Skiplock.on_errors.count == 0
|
82
81
|
end
|
83
82
|
|
84
|
-
def cleanup_workers
|
85
|
-
Rails.application.eager_load! if Rails.env.development?
|
86
|
-
delete_ids = []
|
87
|
-
Worker.where(hostname: @config[:hostname]).each do |worker|
|
88
|
-
sid = Process.getsid(worker.pid) rescue nil
|
89
|
-
delete_ids << worker.id if worker.sid != sid || worker.updated_at < 10.minutes.ago
|
90
|
-
end
|
91
|
-
Worker.where(id: delete_ids).delete_all if delete_ids.count > 0
|
92
|
-
end
|
93
|
-
|
94
|
-
def create_worker(master: true)
|
95
|
-
Worker.create!(pid: Process.pid, sid: Process.getsid(), master: master, hostname: @config[:hostname], capacity: @config[:max_threads])
|
96
|
-
rescue
|
97
|
-
Worker.create!(pid: Process.pid, sid: Process.getsid(), master: false, hostname: @config[:hostname], capacity: @config[:max_threads])
|
98
|
-
end
|
99
|
-
|
100
83
|
def configure
|
101
84
|
@config[:graceful_shutdown] = 300 if @config[:graceful_shutdown] > 300
|
102
85
|
@config[:graceful_shutdown] = nil if @config[:graceful_shutdown] < 0
|
@@ -137,21 +120,27 @@ module Skiplock
|
|
137
120
|
else
|
138
121
|
@config[:notification] = 'custom'
|
139
122
|
end
|
123
|
+
Rails.application.eager_load! if Rails.env.development?
|
124
|
+
if @config[:extensions] == true
|
125
|
+
Module.__send__(:include, Skiplock::Extension)
|
126
|
+
elsif @config[:extensions].is_a?(Array)
|
127
|
+
@config[:extensions].each { |n| n.constantize.__send__(:extend, Skiplock::Extension) if n.safe_constantize }
|
128
|
+
end
|
140
129
|
Skiplock.on_errors.freeze unless Skiplock.on_errors.frozen?
|
141
130
|
end
|
142
131
|
|
143
132
|
def setup_logger
|
144
|
-
@config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s)
|
133
|
+
@config[:loglevel] = 'info' unless ['debug','info','warn','error','fatal','unknown'].include?(@config[:loglevel].to_s.downcase)
|
145
134
|
@logger = ActiveSupport::Logger.new(STDOUT)
|
146
|
-
@logger.level = @config[:loglevel].to_sym
|
135
|
+
@logger.level = @config[:loglevel].downcase.to_sym
|
147
136
|
Skiplock.logger = @logger
|
148
137
|
if @config[:logfile].to_s.length > 0
|
149
138
|
@logger.extend(ActiveSupport::Logger.broadcast(::Logger.new(File.join(Rails.root, 'log', @config[:logfile].to_s), 'daily')))
|
150
139
|
ActiveJob::Base.logger = nil
|
151
140
|
end
|
152
141
|
rescue Exception => ex
|
153
|
-
|
154
|
-
|
142
|
+
@logger.error "Exception with logger: #{ex.to_s}"
|
143
|
+
@logger.error ex.backtrace.join("\n")
|
155
144
|
Skiplock.on_errors.each { |p| p.call(ex) }
|
156
145
|
end
|
157
146
|
end
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
@@ -3,18 +3,32 @@ module Skiplock
|
|
3
3
|
self.implicit_order_column = 'created_at'
|
4
4
|
has_many :jobs, inverse_of: :worker
|
5
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
|
20
|
+
|
6
21
|
def start(worker_num: 0, **config)
|
7
|
-
@config = config
|
8
|
-
@queues_order_query = @config[:queues].map { |q,v| "WHEN queue_name = '#{q}' THEN #{v}" }.join(' ') if @config[:queues].is_a?(Hash) && @config[:queues].count > 0
|
9
|
-
@next_schedule_at = Time.now.to_f
|
10
|
-
@executor = Concurrent::ThreadPoolExecutor.new(min_threads: @config[:min_threads] + 1, max_threads: @config[:max_threads] + 1, max_queue: @config[:max_threads], idletime: 60, auto_terminate: true, fallback_policy: :discard)
|
11
22
|
if self.master
|
12
|
-
Job.
|
23
|
+
Job.flush
|
13
24
|
Cron.setup
|
14
25
|
end
|
26
|
+
@config = config
|
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
|
15
28
|
@running = true
|
16
|
-
|
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)
|
17
30
|
@executor.post { run }
|
31
|
+
Process.setproctitle("skiplock-#{self.master ? 'master[0]' : 'worker[' + worker_num.to_s + ']'}") if @config[:standalone]
|
18
32
|
end
|
19
33
|
|
20
34
|
def shutdown
|
@@ -39,6 +53,7 @@ module Skiplock
|
|
39
53
|
def run
|
40
54
|
error = false
|
41
55
|
listen = false
|
56
|
+
next_schedule_at = Time.now.to_f
|
42
57
|
timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
43
58
|
while @running
|
44
59
|
Rails.application.reloader.wrap do
|
@@ -53,17 +68,17 @@ module Skiplock
|
|
53
68
|
@connection.reconnect!
|
54
69
|
sleep(0.5)
|
55
70
|
@connection.exec_query('LISTEN "skiplock::jobs"')
|
56
|
-
|
71
|
+
next_schedule_at = Time.now.to_f
|
57
72
|
end
|
58
|
-
Job.
|
73
|
+
Job.flush if self.master
|
59
74
|
error = false
|
60
75
|
end
|
61
|
-
if Time.now.to_f >=
|
76
|
+
if Time.now.to_f >= next_schedule_at && @executor.remaining_capacity > 0
|
62
77
|
job = get_next_available_job
|
63
78
|
if job.try(:running)
|
64
79
|
@executor.post { Rails.application.reloader.wrap { job.execute(purge_completion: @config[:purge_completion], max_retries: @config[:max_retries]) } }
|
65
80
|
else
|
66
|
-
|
81
|
+
next_schedule_at = (job ? job.scheduled_at.to_f : Float::INFINITY)
|
67
82
|
end
|
68
83
|
end
|
69
84
|
job_notifications = []
|
@@ -77,7 +92,7 @@ module Skiplock
|
|
77
92
|
job_notifications.each do |n|
|
78
93
|
op, id, worker_id, job_class, queue_name, running, expired_at, finished_at, scheduled_at = n.split(',')
|
79
94
|
next if op == 'DELETE' || running == 'true' || expired_at.to_f > 0 || finished_at.to_f > 0
|
80
|
-
|
95
|
+
next_schedule_at = scheduled_at.to_f if scheduled_at.to_f < next_schedule_at
|
81
96
|
end
|
82
97
|
end
|
83
98
|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) - timestamp > 60
|
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' => 'skiplock.log', 'loglevel' => 'info', 'graceful_shutdown' => 15, 'min_threads' => 1, 'max_threads' =>
|
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.17
|
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
|