active_job_resque_solo 0.2.0 → 0.3.0.pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/kinkade/active_job_resque_solo.svg?branch=master)](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
|