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 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