active_job_resque_solo 0.2.0 → 0.3.0.pre
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/.travis.yml +1 -0
- data/CHANGELOG.md +13 -0
- data/README.md +35 -13
- data/active_job_resque_solo.gemspec +1 -0
- data/lib/active_job/plugins/resque/solo/inspector.rb +34 -17
- data/lib/active_job/plugins/resque/solo/lock.rb +86 -0
- data/lib/active_job/plugins/resque/solo.rb +8 -1
- data/lib/active_job_resque_solo/version.rb +1 -1
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b9c434bd9ef7b517ac674178db86450d4220458e
|
|
4
|
+
data.tar.gz: 7ad486c09cd6dcd27b068172fcf4888975d7ecdf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab4a3fc4b9a73e1ff7687cd00e648a71a7f7e1206294d453c44dbdde61bf6a29d03e3de2b7544175c1380fca79d5a3755fdc9449eb700146dcd228adaf97fe0a
|
|
7
|
+
data.tar.gz: a93fcdacfbfa92cb621ad935062171b5b3437dc452756773e487f87e005fc21ca1d60672b315aabefc237c9a4294d44834551181cf3c0bb3cc53a67a234b9c7b
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 0.3.0.pre
|
|
2
|
+
|
|
3
|
+
* Adds locking to prevent two or more processes in a race condition from enquing multiple copies of a job.
|
|
4
|
+
|
|
5
|
+
* Adds the `solo_lock_key_prefix` directive to set the lock key prefix for your job.
|
|
6
|
+
|
|
7
|
+
# 0.2.0
|
|
8
|
+
|
|
9
|
+
* Removes internal methods used by Solo from your Job class' namespace.
|
|
10
|
+
|
|
11
|
+
# 0.1.0
|
|
12
|
+
|
|
13
|
+
* Initial version of ActiveJob::Plugins::Resque::Solo.
|
data/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# ActiveJobResqueSolo
|
|
2
2
|
|
|
3
|
-
A plugin for ActiveJob with Resque to prevent duplicate
|
|
3
|
+
A plugin for ActiveJob with Resque to prevent duplicate enqueuing of jobs.
|
|
4
|
+
|
|
5
|
+
[](https://travis-ci.org/kinkade/active_job_resque_solo)
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -72,21 +74,41 @@ class MyJob < ActiveJob::Base
|
|
|
72
74
|
end
|
|
73
75
|
end
|
|
74
76
|
```
|
|
77
|
+
## Locking
|
|
75
78
|
|
|
76
|
-
|
|
79
|
+
Solo uses an internal locking mechanism to prevent multiple processes from
|
|
80
|
+
enqueuing the same job during race conditions. This gem does not perform any
|
|
81
|
+
locking around job execution.
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
enqueued,
|
|
80
|
-
|
|
83
|
+
The lock prevents competing jobs of the same class and arguments from being
|
|
84
|
+
enqueued, complying with the argument filtering programmed with `solo_only_args`
|
|
85
|
+
and `solo_except_args`.
|
|
86
|
+
|
|
87
|
+
The default Redis key prefix is "ajr_solo". It can be set to a different,
|
|
88
|
+
arbitrary string of your choice using `solo_lock_key_prefix`:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
class MyJob < ActiveJob::Base
|
|
92
|
+
|
|
93
|
+
include ActiveJob::Plugins::Resque::Solo
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
95
|
+
solo_lock_key_prefix "my_lock_prefix"
|
|
96
|
+
|
|
97
|
+
def perform(*args)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Duplicate enqueues are possible
|
|
103
|
+
|
|
104
|
+
While this plugin will greatly reduce duplicate instances of a job from being
|
|
105
|
+
enqueued, a job may be enqueued multiple times if the Redis response times are
|
|
106
|
+
very slow. Slowness could be caused by extremely high load on Redis or networking
|
|
107
|
+
issues.
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
|
|
109
|
+
The locks are acquired for dynamic amounts of time, but expire quickly, typically
|
|
110
|
+
in one second. Killed workers will not leave long-lived, orphaned locks to
|
|
111
|
+
adversely block jobs from being enqueued.
|
|
90
112
|
|
|
91
113
|
## Contributing
|
|
92
114
|
|
|
@@ -98,4 +120,4 @@ The gem is available as open source under the terms of the [MIT License](http://
|
|
|
98
120
|
|
|
99
121
|
## Code of Conduct
|
|
100
122
|
|
|
101
|
-
Everyone interacting in the ActiveJobResqueSolo project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/
|
|
123
|
+
Everyone interacting in the ActiveJobResqueSolo project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/kinkade/active_job_resque_solo/blob/master/CODE_OF_CONDUCT.md).
|
|
@@ -12,6 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.summary = %q{Prevents duplicate ActiveJob+Resque jobs from being enqueued.}
|
|
13
13
|
spec.description = %q{If you are using ActiveJob with the Resque Adapter, this gem will help prevent duplicate jobs, based on arguments, from being enqueued to Resque.}
|
|
14
14
|
spec.license = "MIT"
|
|
15
|
+
spec.homepage = "https://github.com/kinkade/active_job_resque_solo"
|
|
15
16
|
|
|
16
17
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
|
17
18
|
f.match(%r{^(test|spec|features)/})
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
require_relative "lock"
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'digest/sha1'
|
|
4
|
+
|
|
1
5
|
module ActiveJob
|
|
2
6
|
module Plugins
|
|
3
7
|
module Resque
|
|
4
8
|
module Solo
|
|
5
9
|
class Inspector
|
|
6
10
|
|
|
7
|
-
def initialize(only_args, except_args)
|
|
11
|
+
def initialize(only_args, except_args, lock_key_prefix)
|
|
8
12
|
@only_args = only_args
|
|
9
13
|
@except_args = except_args || []
|
|
10
14
|
# always ignore the ActiveJob symbol hash key.
|
|
11
15
|
@except_args << "_aj_symbol_keys" unless @except_args.include?("_aj_symbol_keys")
|
|
16
|
+
@lock_key_prefix = lock_key_prefix.present? ? lock_key_prefix : "ajr_solo"
|
|
12
17
|
end
|
|
13
18
|
|
|
14
19
|
def self.resque_present?
|
|
@@ -18,8 +23,13 @@ module ActiveJob
|
|
|
18
23
|
def around_enqueue(job, block)
|
|
19
24
|
if Inspector::resque_present?
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
Lock.try_acquire_release(lock_key(job)) do |lock, extend_at|
|
|
27
|
+
@lock = lock
|
|
28
|
+
@extend_lock_at = extend_at
|
|
29
|
+
|
|
30
|
+
if !job_enqueued?(job) && !job_executing?(job)
|
|
31
|
+
block.call
|
|
32
|
+
end
|
|
23
33
|
end
|
|
24
34
|
else
|
|
25
35
|
# if resque is not present, always enqueue
|
|
@@ -31,25 +41,18 @@ module ActiveJob
|
|
|
31
41
|
size = ::Resque.size(job.queue_name)
|
|
32
42
|
return false if size.zero?
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
# It's possible for this loop to skip jobs if they
|
|
39
|
-
# are dequeued while the loop is in progress.
|
|
40
|
-
(0..pages).each do |i|
|
|
41
|
-
page_start = i * page_size
|
|
42
|
-
page = ::Resque.peek(job.queue_name, page_start, page_size)
|
|
43
|
-
break if page.empty?
|
|
44
|
-
jobs += page
|
|
45
|
-
end
|
|
44
|
+
scheduled_jobs = ::Resque.peek(job.queue_name, 0, 0)
|
|
45
|
+
|
|
46
|
+
extend_lock
|
|
46
47
|
|
|
47
48
|
job_class, job_arguments = job(job)
|
|
48
49
|
|
|
49
|
-
(
|
|
50
|
-
scheduled_job =
|
|
50
|
+
(scheduled_jobs.size-1).downto(0) do |i|
|
|
51
|
+
scheduled_job = scheduled_jobs[i]
|
|
51
52
|
return true if job_enqueued_with_args?(job_class, job_arguments, scheduled_job)
|
|
53
|
+
extend_lock
|
|
52
54
|
end
|
|
55
|
+
|
|
53
56
|
false
|
|
54
57
|
end
|
|
55
58
|
|
|
@@ -62,6 +65,8 @@ module ActiveJob
|
|
|
62
65
|
args = processing["payload"]["args"][0]
|
|
63
66
|
job_with_args_eq?(job_class, job_arguments, args)
|
|
64
67
|
end
|
|
68
|
+
|
|
69
|
+
extend_lock
|
|
65
70
|
end
|
|
66
71
|
|
|
67
72
|
def job_enqueued_with_args?(job_class, job_arguments, scheduled_job)
|
|
@@ -97,6 +102,18 @@ module ActiveJob
|
|
|
97
102
|
|
|
98
103
|
args
|
|
99
104
|
end
|
|
105
|
+
|
|
106
|
+
def lock_key(job)
|
|
107
|
+
job_class, job_arguments = job(job)
|
|
108
|
+
sha1 = Digest::SHA1.hexdigest(job_arguments.to_json)
|
|
109
|
+
"#{@lock_key_prefix}:#{job_class}:#{sha1}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extend_lock
|
|
113
|
+
if Time.now.utc >= @extend_lock_at
|
|
114
|
+
@extend_lock_at = @lock.extend
|
|
115
|
+
end
|
|
116
|
+
end
|
|
100
117
|
end
|
|
101
118
|
end
|
|
102
119
|
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module ActiveJob
|
|
2
|
+
module Plugins
|
|
3
|
+
module Resque
|
|
4
|
+
module Solo
|
|
5
|
+
class Lock
|
|
6
|
+
# TTLs in seconds
|
|
7
|
+
ACQUIRE_TTL = 1.0
|
|
8
|
+
EXECUTE_TTL = 5.0
|
|
9
|
+
|
|
10
|
+
def initialize(key)
|
|
11
|
+
@redis = ::Resque.redis
|
|
12
|
+
@uuid = ::SecureRandom.uuid
|
|
13
|
+
@acquired = nil
|
|
14
|
+
@key = key
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Attempts to acquire a lock named by the key in Redis. If the lock
|
|
18
|
+
# is acquired, the block is executed. If the lock is not acquired,
|
|
19
|
+
# the block is not executed.
|
|
20
|
+
#
|
|
21
|
+
# @param [String] key the key used to articulate the lock
|
|
22
|
+
# @yield the block to execute if the lock can be acquired
|
|
23
|
+
# @return [Boolean] x if the block was executed, false if the block was not executed
|
|
24
|
+
def self.try_acquire_release(key)
|
|
25
|
+
lock = Lock.new(key)
|
|
26
|
+
|
|
27
|
+
extend_at = lock.try_acquire
|
|
28
|
+
return false if extend_at.nil?
|
|
29
|
+
|
|
30
|
+
begin
|
|
31
|
+
yield(lock, extend_at)
|
|
32
|
+
ensure
|
|
33
|
+
lock.release
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def try_acquire
|
|
40
|
+
extend_at = Time.now.utc + (ACQUIRE_TTL.to_f / 2)
|
|
41
|
+
px = (ACQUIRE_TTL.to_f * 1000).to_i
|
|
42
|
+
|
|
43
|
+
if @redis.set(@key, @uuid, px: px, nx: true)
|
|
44
|
+
@acquired = @uuid
|
|
45
|
+
else
|
|
46
|
+
extend_at = Time.now.utc
|
|
47
|
+
@acquired = @redis.get(@key)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Consider the lock not acquired if it is proven
|
|
51
|
+
# that another process has acquired the lock.
|
|
52
|
+
#
|
|
53
|
+
# It is unlikely that acquired will be nil, but
|
|
54
|
+
# it is possible if Redis is slow due to extreme load.
|
|
55
|
+
(@acquired.nil? || @acquired == @uuid) ? extend_at : nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def extend
|
|
59
|
+
extend_at = 1.year.from_now
|
|
60
|
+
|
|
61
|
+
@redis.watch(@key) do
|
|
62
|
+
if @redis.get(@key) == @uuid
|
|
63
|
+
extend_at = Time.now.utc + (EXECUTE_TTL.to_f / 2)
|
|
64
|
+
@redis.multi do |multi|
|
|
65
|
+
multi.expire(@key, EXECUTE_TTL.to_i)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
extend_at
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def release
|
|
74
|
+
@redis.watch(@key) do
|
|
75
|
+
if @redis.get(@key) == @uuid
|
|
76
|
+
@redis.multi do |multi|
|
|
77
|
+
multi.del(@key)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require_relative 'solo/inspector'
|
|
2
|
+
require_relative 'solo/lock'
|
|
2
3
|
|
|
3
4
|
module ActiveJob
|
|
4
5
|
module Plugins
|
|
@@ -24,7 +25,13 @@ module ActiveJob
|
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
def solo_inspector
|
|
27
|
-
@solo_inspector ||= Inspector.new(@solo_only_args, @solo_except_args)
|
|
28
|
+
@solo_inspector ||= Inspector.new(@solo_only_args, @solo_except_args, @solo_lock_key_prefix)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def solo_lock_key_prefix(key_prefix)
|
|
32
|
+
@solo_lock_key_prefix = key_prefix.strip
|
|
33
|
+
raise ArgumentError, "solo_lock_key_prefix cannot be blank or only spaces." if @solo_lock_key_prefix.blank?
|
|
34
|
+
|
|
28
35
|
end
|
|
29
36
|
end
|
|
30
37
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_job_resque_solo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0.pre
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Phillip Kinkade
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2017-09-
|
|
11
|
+
date: 2017-09-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -97,6 +97,7 @@ files:
|
|
|
97
97
|
- ".gitignore"
|
|
98
98
|
- ".rspec"
|
|
99
99
|
- ".travis.yml"
|
|
100
|
+
- CHANGELOG.md
|
|
100
101
|
- CODE_OF_CONDUCT.md
|
|
101
102
|
- Gemfile
|
|
102
103
|
- LICENSE.txt
|
|
@@ -107,9 +108,10 @@ files:
|
|
|
107
108
|
- bin/setup
|
|
108
109
|
- lib/active_job/plugins/resque/solo.rb
|
|
109
110
|
- lib/active_job/plugins/resque/solo/inspector.rb
|
|
111
|
+
- lib/active_job/plugins/resque/solo/lock.rb
|
|
110
112
|
- lib/active_job_resque_solo.rb
|
|
111
113
|
- lib/active_job_resque_solo/version.rb
|
|
112
|
-
homepage:
|
|
114
|
+
homepage: https://github.com/kinkade/active_job_resque_solo
|
|
113
115
|
licenses:
|
|
114
116
|
- MIT
|
|
115
117
|
metadata: {}
|
|
@@ -124,9 +126,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
124
126
|
version: '0'
|
|
125
127
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
128
|
requirements:
|
|
127
|
-
- - "
|
|
129
|
+
- - ">"
|
|
128
130
|
- !ruby/object:Gem::Version
|
|
129
|
-
version:
|
|
131
|
+
version: 1.3.1
|
|
130
132
|
requirements: []
|
|
131
133
|
rubyforge_project:
|
|
132
134
|
rubygems_version: 2.6.6
|