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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f760861754e12e2de29870716638d38d93ceaa09
4
- data.tar.gz: 553f61b9c2b9653afc9217dc45f8439273311a06
3
+ metadata.gz: b9c434bd9ef7b517ac674178db86450d4220458e
4
+ data.tar.gz: 7ad486c09cd6dcd27b068172fcf4888975d7ecdf
5
5
  SHA512:
6
- metadata.gz: e558f3fef406f0bf9b39740b697c05e889dda701a6aecc24684c6dd5106ce9b28ffd163da394ede37bcd972d9f9590033f1ed51ff786dc7f0ae4fe7c965fe7ab
7
- data.tar.gz: f671a673aa14aacece35e289639331fd3dee4b5a723c7fa4efdce1ec10c806d71e194f2b0820aa6f8dc11e457e4c668bfb8cf998fad43c3c0c7829c05c15b1f2
6
+ metadata.gz: ab4a3fc4b9a73e1ff7687cd00e648a71a7f7e1206294d453c44dbdde61bf6a29d03e3de2b7544175c1380fca79d5a3755fdc9449eb700146dcd228adaf97fe0a
7
+ data.tar.gz: a93fcdacfbfa92cb621ad935062171b5b3437dc452756773e487f87e005fc21ca1d60672b315aabefc237c9a4294d44834551181cf3c0bb3cc53a67a234b9c7b
data/.travis.yml CHANGED
@@ -1,5 +1,6 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
+ - 2.3.4
4
5
  - 2.4.1
5
6
  before_install: gem install bundler -v 1.15.4
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 enqueing of jobs.
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
- ## Duplicate enqueues are still possible
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
- While this plugin will greatly reduce duplicate instances of a job from being
79
- enqueued, there are two scenarios where duplicates may still be enqueued,
80
- so be sure to check out other gems for locking if your job is not idempotent.
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
- 1. When multiple processes simultaneously attempt to enqueue the same job, two or
83
- more instances may be enqueued.
84
- 2. If your queue has many jobs, and workers remove a job while Solo scans
85
- the queue, it's possible for the original enqueued job to be missed. Solo will allow
86
- the new instance of the job to be enqueued.
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
- Solo errors towards allowing a duplicate job instance to be enqueued rather than
89
- prevent a job from being enqueued at all.
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/[USERNAME]/active_job_resque_solo/blob/master/CODE_OF_CONDUCT.md).
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
- if !job_enqueued?(job) && !job_executing?(job)
22
- block.call
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
- page_size = 250
35
- pages = (size/page_size).to_i + 1
36
- jobs = []
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
- (jobs.size-1).downto(0) do |i|
50
- scheduled_job = jobs[i]
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
@@ -1,3 +1,3 @@
1
1
  module ActiveJobResqueSolo
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0.pre"
3
3
  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.2.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-03 00:00:00.000000000 Z
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: '0'
131
+ version: 1.3.1
130
132
  requirements: []
131
133
  rubyforge_project:
132
134
  rubygems_version: 2.6.6