skiplock 1.0.17 → 1.0.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -5
- data/lib/skiplock/cron.rb +1 -1
- data/lib/skiplock/extension.rb +4 -1
- data/lib/skiplock/job.rb +6 -3
- data/lib/skiplock/manager.rb +2 -3
- data/lib/skiplock/version.rb +1 -1
- data/lib/skiplock/worker.rb +2 -2
- metadata +1 -1
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
@@ -106,7 +106,7 @@ Inside the Rails application:
|
|
106
106
|
MyJob.set(wait_until: Day.tomorrow.noon).perform_later(1,2,3)
|
107
107
|
```
|
108
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
|
109
|
+
- **purge** (*boolean*): whether to remove this job after it has completed successfully
|
110
110
|
- **max_retries** (*integer*): set maximum retry attempt for this job
|
111
111
|
```ruby
|
112
112
|
MyJob.set(purge: false, max_retries: 5).perform_later(1,2,3)
|
@@ -119,7 +119,8 @@ Outside the Rails application:
|
|
119
119
|
- with scheduling, priority, queue, arguments and custom options
|
120
120
|
```sql
|
121
121
|
INSERT INTO skiplock.jobs(job_class, queue_name, priority, scheduled_at, data)
|
122
|
-
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}}');
|
123
124
|
```
|
124
125
|
## Queue priority vs Job priority
|
125
126
|
*Why do queues use priorities when jobs already have priorities?*
|
@@ -183,10 +184,14 @@ If the `retry_on` block is not defined, then the built-in retry system of `Skipl
|
|
183
184
|
# supports multiple 'on_error' event callbacks
|
184
185
|
```
|
185
186
|
## ClassMethod extension
|
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.
|
187
|
-
|
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`
|
188
191
|
```sql
|
189
|
-
INSERT INTO skiplock.jobs(job_class, data)
|
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"]}');
|
190
195
|
```
|
191
196
|
- Queue class method `generate_thumbnails` of class `Image` as background job to run as soon as possible
|
192
197
|
```ruby
|
data/lib/skiplock/cron.rb
CHANGED
@@ -11,7 +11,7 @@ module Skiplock
|
|
11
11
|
if time
|
12
12
|
job.cron = cron
|
13
13
|
job.running = false
|
14
|
-
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
|
15
15
|
job.save
|
16
16
|
cronjobs << j.name
|
17
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,6 +1,6 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Job < ActiveRecord::Base
|
3
|
-
self.implicit_order_column = '
|
3
|
+
self.implicit_order_column = 'updated_at'
|
4
4
|
attr_accessor :activejob_retry
|
5
5
|
belongs_to :worker, inverse_of: :jobs, required: false
|
6
6
|
|
@@ -22,6 +22,7 @@ module Skiplock
|
|
22
22
|
timestamp = Time.at(timestamp) if timestamp
|
23
23
|
if Thread.current[:skiplock_job].try(:id) == activejob.job_id
|
24
24
|
Thread.current[:skiplock_job].activejob_retry = true
|
25
|
+
Thread.current[:skiplock_job].data['activejob_retry'] = true
|
25
26
|
Thread.current[:skiplock_job].executions = activejob.executions
|
26
27
|
Thread.current[:skiplock_job].exception_executions = activejob.exception_executions
|
27
28
|
Thread.current[:skiplock_job].scheduled_at = timestamp
|
@@ -33,7 +34,7 @@ module Skiplock
|
|
33
34
|
end
|
34
35
|
end
|
35
36
|
|
36
|
-
# resynchronize jobs that could not commit to database and
|
37
|
+
# resynchronize jobs that could not commit to database and reset any abandoned jobs for retry
|
37
38
|
def self.flush
|
38
39
|
Dir.mkdir('tmp/skiplock') unless Dir.exist?('tmp/skiplock')
|
39
40
|
Dir.glob('tmp/skiplock/*').each do |f|
|
@@ -61,7 +62,7 @@ module Skiplock
|
|
61
62
|
self.updated_at = Time.now > self.updated_at ? Time.now : self.updated_at + 1 # in case of clock drifting
|
62
63
|
if @exception
|
63
64
|
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
|
65
|
+
if (self.executions.to_i >= @max_retries + 1) || self.data.key?('activejob_retry') || @exception.is_a?(Skiplock::Extension::ProxyError)
|
65
66
|
self.expired_at = Time.now
|
66
67
|
else
|
67
68
|
self.scheduled_at = Time.now + (5 * 2**self.executions.to_i)
|
@@ -71,6 +72,7 @@ module Skiplock
|
|
71
72
|
self.data['cron'] ||= {}
|
72
73
|
self.data['cron']['executions'] = self.data['cron']['executions'].to_i + 1
|
73
74
|
self.data['cron']['last_finished_at'] = self.finished_at.utc.to_s
|
75
|
+
self.data['cron']['last_result'] = self.data['result']
|
74
76
|
next_cron_at = Cron.next_schedule_at(self.cron)
|
75
77
|
if next_cron_at
|
76
78
|
# update job to record completions counter before resetting finished_at to nil
|
@@ -78,6 +80,7 @@ module Skiplock
|
|
78
80
|
self.finished_at = nil
|
79
81
|
self.executions = nil
|
80
82
|
self.exception_executions = nil
|
83
|
+
self.data.delete('result')
|
81
84
|
self.scheduled_at = Time.at(next_cron_at)
|
82
85
|
else
|
83
86
|
Skiplock.logger.error("[Skiplock] ERROR: Invalid CRON '#{self.cron}' for Job #{self.job_class}") if Skiplock.logger
|
data/lib/skiplock/manager.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
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
|
-
@config.merge!(config)
|
9
8
|
@hostname = Socket.gethostname
|
10
9
|
configure
|
11
10
|
setup_logger
|
@@ -22,7 +21,7 @@ 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
|
28
27
|
@config[:standalone] = true
|
data/lib/skiplock/version.rb
CHANGED
data/lib/skiplock/worker.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Skiplock
|
2
2
|
class Worker < ActiveRecord::Base
|
3
|
-
self.implicit_order_column = '
|
3
|
+
self.implicit_order_column = 'updated_at'
|
4
4
|
has_many :jobs, inverse_of: :worker
|
5
5
|
|
6
6
|
def self.cleanup(hostname = nil)
|
@@ -42,7 +42,7 @@ module Skiplock
|
|
42
42
|
|
43
43
|
def get_next_available_job
|
44
44
|
@connection.transaction do
|
45
|
-
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
|
46
46
|
if job && job.scheduled_at.to_f <= Time.now.to_f
|
47
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
|
48
48
|
end
|