resque-serializer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ea193acc18e7717c6a3f990c82ef5d31d9682219
4
+ data.tar.gz: 40d82f79180b84382efdddf99a7f4fa998127487
5
+ SHA512:
6
+ metadata.gz: 21271809497bc8c8ece3344fc7f9e351b9b9ea18653f2565e0518cee8ee349df96d9dd69fb728c34f3dc3be682da9527bb0685b1d56f493477884cac3f1516bc
7
+ data.tar.gz: cd46348ba7b8481816cae2fabd904c8fe06925fc8ccbf2462650aedf49e9819841d6244960e3c8d2433f4c246bff14e42dc67e10995ceb851b760176e07a8b8c
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
13
+
14
+ # Misc
15
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.3
5
+ before_install: gem install bundler -v 1.14.6
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in resque-serializer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Ryan Ringler
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,140 @@
1
+ # Resque::Serializer
2
+
3
+ A Resque plugin which ensures for a given queue, that only one worker is executing a job at any given time.
4
+
5
+ It is slightly more flexible than [Resque::LonelyJob](https://github.com/wallace/resque-lonely_job).
6
+
7
+ This gem may be helpful to avoid database lock contention.
8
+
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'resque-serializer'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install resque-serializer
25
+
26
+
27
+ ## Usage
28
+
29
+ ### TLDR
30
+ ```
31
+ | Lock Lifetime | :queue | :job | :both | :combined |
32
+ | -------------: | :----: | :---: | :----: | :-------: |
33
+ | before_enqueue | ✓ | | ✓ | ✓ |
34
+ | enqueue | | | | | | | |
35
+ | after_enqueue | | | | | | | |
36
+ | before_dequeue | | | ✓ | | ✓ | | |
37
+ | dequeue | | | | | | | | | |
38
+ | after_dequeue | ✗ | | | ✗ | | | |
39
+ | before_perform | | | | | | | |
40
+ | perform | | | | | | | |
41
+ | after_perform | | ✗ | ✗ | ✗ |
42
+ ```
43
+
44
+ ### Example #1 -- Serializing the queue
45
+
46
+ ```ruby
47
+ require 'resque-serializer'
48
+
49
+ class SerializedJob
50
+ extend Resque::Plugins::Serializer
51
+
52
+ @queue = :default
53
+
54
+ serialize :queue
55
+
56
+ def self.perform
57
+ # work
58
+ end
59
+ end
60
+ ```
61
+
62
+ Only one of job with identical arguments will be allowed to be queued at a time. As soon as the job is dequeued to begin executing, an identical job may be queued (and may begin executing.)
63
+
64
+
65
+ ### Example #2 -- Serializing the job
66
+
67
+ ```ruby
68
+ require 'resque-serializer'
69
+
70
+ class SerializedJob
71
+ extend Resque::Plugins::Serializer
72
+
73
+ @queue = :default
74
+
75
+ serialize :job
76
+
77
+ def self.perform
78
+ # work
79
+ end
80
+ end
81
+ ```
82
+
83
+ Any number of these jobs may be queued, but only one job with identical arguments will be executed at a time. As soon as the executing job is completed, another queued job may be dequeued to execute.
84
+
85
+
86
+ ### Example #3 -- Serializing both the queue & job (independently)
87
+
88
+ ```ruby
89
+ require 'resque-serializer'
90
+
91
+ class SerializedJob
92
+ extend Resque::Plugins::Serializer
93
+
94
+ @queue = :default
95
+
96
+ serialize :both
97
+
98
+ def self.perform
99
+ # work
100
+ end
101
+ end
102
+ ```
103
+
104
+ A combination of the first two examples; both the queue and the execution of identical jobs is serialized (independently.) An additional job may be queued if no identical job exists in the queue. Similarly, an additional job may begin executing if no identical job is currently executing.
105
+
106
+
107
+ ### Example #4 -- Serializing both the queue & job (combined)
108
+
109
+ ```ruby
110
+ require 'resque-serializer'
111
+
112
+ class SerializedJob
113
+ extend Resque::Plugins::Serializer
114
+
115
+ @queue = :default
116
+
117
+ serialize :combined
118
+
119
+ def self.perform
120
+ # work
121
+ end
122
+ end
123
+ ```
124
+
125
+ Also a combination of the first two examples; both the queue and execution of identical jobs is serialized (together.) An additional job may not be queued if an identical job exists in the queue or execution.
126
+
127
+
128
+ ## Contributing
129
+
130
+ 1. Fork it
131
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
132
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
133
+ 4. Push to the branch (`git push origin my-new-feature`)
134
+ 5. Create new Pull Request
135
+
136
+
137
+ ## License
138
+
139
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
140
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'resque/serializer'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,46 @@
1
+ require 'active_support/core_ext/numeric/time'
2
+
3
+ module Resque
4
+ module Plugins
5
+ module Serializer
6
+ class Mutex
7
+ class LockFailed < StandardError; end
8
+
9
+ attr_reader :key, :ttl
10
+
11
+ delegate :redis,
12
+ to: Resque
13
+
14
+ def initialize(key, ttl: 5.minutes)
15
+ @key = key
16
+ @ttl = ttl.to_i
17
+ end
18
+
19
+ def lock
20
+ !!redis.set(key, true, set_options)
21
+ end
22
+
23
+ def lock!
24
+ !!redis.set(key, true, set_options) || fail(LockFailed)
25
+ end
26
+
27
+ def locked?
28
+ !!redis.get(key)
29
+ end
30
+
31
+ def unlock
32
+ !!redis.del(key)
33
+ end
34
+
35
+ private
36
+
37
+ def set_options
38
+ {
39
+ nx: true,
40
+ px: ttl * 1000 # msecs
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,62 @@
1
+ module Resque
2
+ module Plugins
3
+ module Serializer
4
+ module Serializers
5
+ module Both
6
+
7
+ # before_enqueue: ✓
8
+ # enqueue: |
9
+ # after_enqueue: |
10
+ # before_dequeue: | ✓
11
+ # dequeue: | |
12
+ # after_dequeue: ✗ |
13
+ # before_perform: |
14
+ # perform: |
15
+ # after_perform: ✗
16
+
17
+ def before_enqueue_set_queue_lock(*args)
18
+ queue_mutex(args).lock
19
+ end
20
+
21
+ def before_dequeue_set_job_lock(*args)
22
+ job_mutex(args).lock
23
+ end
24
+
25
+ def after_dequeue_clear_queue_lock(*args)
26
+ queue_mutex(args).unlock
27
+ end
28
+
29
+ def around_perform_clear_job_lock(*args)
30
+ yield
31
+ ensure
32
+ job_mutex(args).unlock
33
+ end
34
+
35
+ def queue_mutex(args)
36
+ Serializer::Mutex.new(queue_key(args))
37
+ end
38
+
39
+ def job_mutex(args)
40
+ Serializer::Mutex.new(job_key(args))
41
+ end
42
+
43
+ private
44
+
45
+ def queue_key(args)
46
+ klass = self.name.tableize.singularize
47
+ args = args.map(&:to_s).join(',')
48
+
49
+ "resque-serializer:queue:#{klass}:#{args}"
50
+ end
51
+
52
+ def job_key(args)
53
+ klass = self.name.tableize.singularize
54
+ args = args.map(&:to_s).join(',')
55
+
56
+ "resque-serializer:job:#{klass}:#{args}"
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ module Resque
2
+ module Plugins
3
+ module Serializer
4
+ module Serializers
5
+ module Combined
6
+
7
+ # before_enqueue: ✓
8
+ # enqueue: |
9
+ # after_enqueue: |
10
+ # before_dequeue: |
11
+ # dequeue: |
12
+ # after_dequeue: |
13
+ # before_perform: |
14
+ # perform: |
15
+ # after_perform: ✗
16
+
17
+ def before_enqueue_set_lock(*args)
18
+ mutex(args).lock
19
+ end
20
+
21
+ def around_perform_clear_lock(*args)
22
+ yield
23
+ ensure
24
+ mutex(args).unlock
25
+ end
26
+
27
+ def mutex(args)
28
+ Serializer::Mutex.new(key(args))
29
+ end
30
+
31
+ private
32
+
33
+ def key(args)
34
+ klass = self.name.tableize.singularize
35
+ args = args.map(&:to_s).join(',')
36
+
37
+ "resque-serializer:#{klass}:#{args}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ module Resque
2
+ module Plugins
3
+ module Serializer
4
+ module Serializers
5
+ module Job
6
+
7
+ # before_enqueue:
8
+ # enqueue:
9
+ # after_enqueue:
10
+ # before_dequeue: ✓
11
+ # dequeue: |
12
+ # after_dequeue: |
13
+ # before_perform: |
14
+ # perform: |
15
+ # after_perform: ✗
16
+
17
+ def before_dequeue_set_lock(*args)
18
+ mutex(args).lock
19
+ end
20
+
21
+ def around_perform_clear_lock(*args)
22
+ yield
23
+ ensure
24
+ mutex(args).unlock
25
+ end
26
+
27
+ def mutex(args)
28
+ Serializer::Mutex.new(key(args))
29
+ end
30
+
31
+ private
32
+
33
+ def key(args)
34
+ klass = self.name.tableize.singularize
35
+ args = args.map(&:to_s).join(',')
36
+
37
+ "resque-serializer:#{klass}:#{args}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,41 @@
1
+ module Resque
2
+ module Plugins
3
+ module Serializer
4
+ module Serializers
5
+ module Queue
6
+
7
+ # before_enqueue: ✓
8
+ # enqueue: |
9
+ # after_enqueue: |
10
+ # before_dequeue: |
11
+ # dequeue: |
12
+ # after_dequeue: ✗
13
+ # before_perform:
14
+ # perform:
15
+ # after_perform:
16
+
17
+ def before_enqueue_set_lock(*args)
18
+ mutex(args).lock
19
+ end
20
+
21
+ def after_dequeue_clear_lock(*args)
22
+ mutex(args).unlock
23
+ end
24
+
25
+ def mutex(args)
26
+ Serializer::Mutex.new(key(args))
27
+ end
28
+
29
+ private
30
+
31
+ def key(args)
32
+ klass = self.name.tableize.singularize
33
+ args = args.map(&:to_s).join(',')
34
+
35
+ "resque-serializer:#{klass}:#{args}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,7 @@
1
+ module Resque
2
+ module Plugins
3
+ module Serializer
4
+ VERSION = "0.1.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,32 @@
1
+ require 'resque-serializer/version'
2
+ require 'resque-serializer/mutex'
3
+ require 'resque-serializer/serializers/both'
4
+ require 'resque-serializer/serializers/combined'
5
+ require 'resque-serializer/serializers/job'
6
+ require 'resque-serializer/serializers/queue'
7
+
8
+ module Resque
9
+ module Plugins
10
+ module Serializer
11
+ def serialize(resource)
12
+ case resource
13
+ when :job then extend(Serializers::Job)
14
+ when :queue then extend(Serializers::Queue)
15
+ when :both then extend(Serializers::Both)
16
+ when :combined then extend(Serializers::Combined)
17
+ else raise_invalid_resource
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def raise_invalid_resource
24
+ error_msg = begin
25
+ 'The passed argument must be one of: [:job, :queue, :both, :combined]'
26
+ end
27
+
28
+ raise ArgumentError, error_msg
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ require File.expand_path('../lib/resque-serializer/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'resque-serializer'
6
+ gem.version = Resque::Plugins::Serializer::VERSION
7
+ gem.authors = ['Ryan Ringler']
8
+ gem.email = ['rringler@gmail.com']
9
+
10
+ gem.summary = 'Serializes Resque jobs'
11
+ gem.description = 'Ensures that only one Resque job with unique arguments is running at a time.'
12
+ gem.homepage = 'https://github.com/rringler/resque-serializer'
13
+ gem.license = 'MIT'
14
+
15
+ gem.files = `git ls-files`.split($\)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency 'activesupport'
21
+ gem.add_dependency 'resque'
22
+
23
+ gem.add_development_dependency 'bundler', '~> 1.14'
24
+ gem.add_development_dependency 'mock_redis'
25
+ gem.add_development_dependency 'pry-byebug'
26
+ gem.add_development_dependency 'pry-stack_explorer'
27
+ gem.add_development_dependency 'rake'
28
+ gem.add_development_dependency 'resque_spec'
29
+ gem.add_development_dependency 'rspec', '~> 3.0'
30
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ describe Resque::Plugins::Serializer::Mutex do
4
+ let(:redis) { Resque.redis }
5
+
6
+ describe 'instance methods' do
7
+ describe '#lock' do
8
+ let(:key) { 'key' }
9
+ let(:options) { {} }
10
+ let(:mutex) { described_class.new(key, options) }
11
+
12
+ subject(:lock) { mutex.lock }
13
+
14
+ it 'calls redis#set with the correct arguments' do
15
+ default_ttl = 5.minutes
16
+
17
+ expect(redis).to receive(:set).with(
18
+ key,
19
+ true,
20
+ nx: true,
21
+ px: default_ttl.to_i * 1000
22
+ ).and_return(true)
23
+
24
+ lock
25
+ end
26
+
27
+ context 'when the lock gets set' do
28
+ before { allow(redis).to receive(:set).and_return(true) }
29
+
30
+ it 'returns true' do
31
+ expect(lock).to eq(true)
32
+ end
33
+ end
34
+
35
+ context 'when the lock does not get set' do
36
+ before { allow(redis).to receive(:set).and_return(false) }
37
+
38
+ it 'returns false' do
39
+ expect(lock).to eq(false)
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '#lock!' do
45
+ let(:key) { 'key' }
46
+ let(:options) { {} }
47
+ let(:mutex) { described_class.new(key, options) }
48
+
49
+ subject(:lock!) { mutex.lock! }
50
+
51
+ it 'calls redis#set with the correct arguments' do
52
+ default_ttl = 5.minutes
53
+
54
+ expect(redis).to receive(:set).with(
55
+ key,
56
+ true,
57
+ nx: true,
58
+ px: default_ttl.to_i * 1000
59
+ ).and_return(true)
60
+
61
+ lock!
62
+ end
63
+
64
+ context 'when the lock gets set' do
65
+ before { allow(redis).to receive(:set).and_return(true) }
66
+
67
+ it 'returns true' do
68
+ expect(lock!).to eq(true)
69
+ end
70
+ end
71
+
72
+ context 'when the lock does not get set' do
73
+ let(:error) { described_class::LockFailed }
74
+
75
+ before { allow(redis).to receive(:set).and_raise(error) }
76
+
77
+ it 'raises a Mutex::LockFailed error' do
78
+ expect { lock! }.to raise_error(error)
79
+ end
80
+ end
81
+ end
82
+
83
+ describe '#unlock' do
84
+ let(:key) { 'key' }
85
+ let(:mutex) { described_class.new(key) }
86
+
87
+ subject(:unlock) { mutex.unlock }
88
+
89
+ it 'calls redis#del with its key' do
90
+ expect(redis).to receive(:del).with(key)
91
+ unlock
92
+ end
93
+
94
+ it 'returns true' do
95
+ expect(unlock).to eq(true)
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ class JobSerializedByBoth
4
+ extend Resque::Plugins::Serializer
5
+
6
+ @queue = :default
7
+
8
+ serialize :both
9
+
10
+ # before_enqueue: ✓
11
+ # enqueue: |
12
+ # after_enqueue: |
13
+ # before_dequeue: | ✓
14
+ # dequeue: | |
15
+ # after_dequeue: ✗ |
16
+ # before_perform: |
17
+ # perform: |
18
+ # after_perform: ✗
19
+
20
+ def self.perform(*args); end
21
+ end
22
+
23
+ RSpec.describe JobSerializedByBoth do
24
+ let(:args) { %w(arg1 arg2) }
25
+
26
+ before do
27
+ ResqueSpec.reset!
28
+ Resque.redis.redis.flushall
29
+ end
30
+
31
+ describe 'before enqueuing the job' do
32
+ let(:mutex) { described_class.queue_mutex(args) }
33
+
34
+ subject(:enqueue_job) { Resque.enqueue(described_class, *args) }
35
+
36
+ context 'when a lock for the job exists' do
37
+ before { mutex.lock! }
38
+
39
+ it 'does not enqueue the job' do
40
+ expect { enqueue_job }.to_not change {
41
+ ResqueSpec.queue_for(described_class).size
42
+ }.from(0)
43
+ end
44
+
45
+ it 'does not unlock the mutex' do
46
+ expect { enqueue_job }.to_not change {
47
+ mutex.locked?
48
+ }.from(true)
49
+ end
50
+ end
51
+
52
+ context 'when a lock for the job does not exist' do
53
+ it 'enqueues the job' do
54
+ expect { enqueue_job }.to change {
55
+ ResqueSpec.queue_for(described_class).size
56
+ }.from(0).to(1)
57
+ end
58
+
59
+ it 'locks the mutex' do
60
+ expect { enqueue_job }.to change {
61
+ mutex.locked?
62
+ }.from(false).to(true)
63
+ end
64
+ end
65
+ end
66
+
67
+ describe 'after dequeuing the job' do
68
+ let(:mutex) { described_class.queue_mutex(args) }
69
+
70
+ subject(:dequeue_job) { Resque.dequeue(described_class, *args) }
71
+
72
+ before { mutex.lock! }
73
+
74
+ it 'unlocks the mutex' do
75
+ expect { dequeue_job }.to change {
76
+ mutex.locked?
77
+ }.from(true).to(false)
78
+ end
79
+ end
80
+
81
+ describe 'before dequeuing the job' do
82
+ let(:mutex) { described_class.job_mutex(args) }
83
+
84
+ subject(:dequeue_job) { Resque.dequeue(described_class, *args) }
85
+
86
+ it 'locks the mutex' do
87
+ expect { dequeue_job }.to change {
88
+ mutex.locked?
89
+ }.from(false).to(true)
90
+ end
91
+ end
92
+
93
+ describe 'after performing the job' do
94
+ let(:mutex) { described_class.job_mutex(args) }
95
+
96
+ before do
97
+ Resque.enqueue(described_class, *args)
98
+ mutex.lock!
99
+ end
100
+
101
+ subject(:perform_job) { ResqueSpec.perform_next(:default) }
102
+
103
+ context 'if the job completes successfully' do
104
+ it 'releases the lock after execution' do
105
+ expect { perform_job }.to change {
106
+ mutex.locked?
107
+ }.from(true).to(false)
108
+ end
109
+ end
110
+
111
+ context 'if the job raises an exception' do
112
+ let(:error) { StandardError }
113
+
114
+ before do
115
+ allow(described_class).to receive(:perform).and_raise(error)
116
+ end
117
+
118
+ it 'still releases the lock after execution' do
119
+ expect(mutex.locked?).to eq(true)
120
+
121
+ expect { perform_job }.to raise_error(error)
122
+
123
+ expect(mutex.locked?).to eq(false)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ class JobSerializedByCombined
4
+ extend Resque::Plugins::Serializer
5
+
6
+ @queue = :default
7
+
8
+ serialize :combined
9
+
10
+ # before_enqueue: ✓
11
+ # enqueue: |
12
+ # after_enqueue: |
13
+ # before_dequeue: |
14
+ # dequeue: |
15
+ # after_dequeue: |
16
+ # before_perform: |
17
+ # perform: |
18
+ # after_perform: ✗
19
+
20
+ def self.perform(*args); end
21
+ end
22
+
23
+ RSpec.describe JobSerializedByCombined do
24
+ let(:args) { %w(arg1 arg2) }
25
+
26
+ before do
27
+ ResqueSpec.reset!
28
+ Resque.redis.redis.flushall
29
+ end
30
+
31
+ describe 'before enqueuing the job' do
32
+ let(:mutex) { described_class.mutex(args) }
33
+
34
+ subject(:enqueue_job) { Resque.enqueue(described_class, *args) }
35
+
36
+ context 'when a lock for the job exists' do
37
+ before { mutex.lock! }
38
+
39
+ it 'does not enqueue the job' do
40
+ expect { enqueue_job }.to_not change {
41
+ ResqueSpec.queue_for(described_class).size
42
+ }.from(0)
43
+ end
44
+
45
+ it 'does not change the mutex' do
46
+ expect { enqueue_job }.to_not change {
47
+ mutex.locked?
48
+ }.from(true)
49
+ end
50
+ end
51
+
52
+ context 'when a lock for the job does not exist' do
53
+ before { mutex.unlock }
54
+
55
+ it 'enqueues the job' do
56
+ expect { enqueue_job }.to change {
57
+ ResqueSpec.queue_for(described_class).size
58
+ }.from(0).to(1)
59
+ end
60
+
61
+ it 'locks the mutex' do
62
+ expect { enqueue_job }.to change {
63
+ mutex.locked?
64
+ }.from(false).to(true)
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'after performing the job' do
70
+ let(:mutex) { described_class.mutex(args) }
71
+
72
+ before do
73
+ # Note: this locks the mutex on the :before_enqueue_* hook
74
+ Resque.enqueue(described_class, *args)
75
+ end
76
+
77
+ subject(:perform_job) { ResqueSpec.perform_next(:default) }
78
+
79
+ context 'when the job completes successfully' do
80
+ it 'releases the lock after execution' do
81
+ expect { perform_job }.to change {
82
+ mutex.locked?
83
+ }.from(true).to(false)
84
+ end
85
+ end
86
+
87
+ context 'when the job raises an exception' do
88
+ let(:error) { StandardError }
89
+
90
+ before { allow(described_class).to receive(:perform).and_raise(error) }
91
+
92
+ it 'still releases the lock after execution' do
93
+ expect(mutex.locked?).to eq(true)
94
+
95
+ expect { perform_job }.to raise_error(error)
96
+
97
+ expect(mutex.locked?).to eq(false)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ class JobSerializedByJob
4
+ extend Resque::Plugins::Serializer
5
+
6
+ @queue = :default
7
+
8
+ serialize :job
9
+
10
+ # before_enqueue:
11
+ # enqueue:
12
+ # after_enqueue:
13
+ # before_dequeue: ✓
14
+ # dequeue: |
15
+ # after_dequeue: |
16
+ # before_perform: |
17
+ # perform: |
18
+ # after_perform: ✗
19
+
20
+ def self.perform(*args); end
21
+ end
22
+
23
+ RSpec.describe JobSerializedByJob do
24
+ let(:args) { %w(arg1 arg2) }
25
+
26
+ before do
27
+ ResqueSpec.reset!
28
+ Resque.redis.redis.flushall
29
+ end
30
+
31
+ describe 'before dequeuing the job' do
32
+ let(:mutex) { described_class.mutex(args) }
33
+
34
+ subject(:dequeue_job) { Resque.dequeue(described_class, *args) }
35
+
36
+ it 'locks the mutex' do
37
+ expect { dequeue_job }.to change {
38
+ mutex.locked?
39
+ }.from(false).to(true)
40
+ end
41
+ end
42
+
43
+ describe 'after performing the job' do
44
+ let(:mutex) { described_class.mutex(args) }
45
+
46
+ before do
47
+ Resque.enqueue(described_class, *args)
48
+ mutex.lock!
49
+ end
50
+
51
+ subject(:perform_job) { ResqueSpec.perform_next(:default) }
52
+
53
+ context 'when the job completes successfully' do
54
+ it 'releases the lock after execution' do
55
+ expect { perform_job }.to change {
56
+ mutex.locked?
57
+ }.from(true).to(false)
58
+ end
59
+ end
60
+
61
+ context 'when the job raises an exception' do
62
+ let(:error) { StandardError }
63
+
64
+ before { allow(described_class).to receive(:perform).and_raise(error) }
65
+
66
+ it 'still releases the lock after execution' do
67
+ expect(mutex.locked?).to eq(true)
68
+
69
+ expect { perform_job }.to raise_error(error)
70
+
71
+ expect(mutex.locked?).to eq(false)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ class JobSerializedByQueue
4
+ extend Resque::Plugins::Serializer
5
+
6
+ @queue = :default
7
+
8
+ serialize :queue
9
+
10
+ # before_enqueue: ✓
11
+ # enqueue: |
12
+ # after_enqueue: |
13
+ # before_dequeue: |
14
+ # dequeue: |
15
+ # after_dequeue: ✗
16
+ # before_perform:
17
+ # perform:
18
+ # after_perform:
19
+
20
+ def self.perform(*args); end
21
+ end
22
+
23
+ RSpec.describe JobSerializedByQueue do
24
+ let(:args) { %w(arg1 arg2) }
25
+
26
+ before do
27
+ ResqueSpec.reset!
28
+ Resque.redis.redis.flushall
29
+ end
30
+
31
+ describe 'before enqueuing the job' do
32
+ let(:mutex) { described_class.mutex(args) }
33
+
34
+ subject(:enqueue_job) { Resque.enqueue(described_class, *args) }
35
+
36
+ context 'when a lock for the job exists' do
37
+ before { mutex.lock! }
38
+
39
+ it 'does not enqueue the job' do
40
+ expect { enqueue_job }.to_not change {
41
+ ResqueSpec.queue_for(described_class).size
42
+ }.from(0)
43
+ end
44
+
45
+ it 'does not change the mutex' do
46
+ expect { enqueue_job }.to_not change {
47
+ mutex.locked?
48
+ }.from(true)
49
+ end
50
+ end
51
+
52
+ context 'when a lock for the job does not exist' do
53
+ before { mutex.unlock }
54
+
55
+ it 'enqueues the job' do
56
+ expect { enqueue_job }.to change {
57
+ ResqueSpec.queue_for(described_class).size
58
+ }.from(0).to(1)
59
+ end
60
+
61
+ it 'locks the mutex' do
62
+ expect { enqueue_job }.to change {
63
+ mutex.locked?
64
+ }.from(false).to(true)
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'after dequeuing the job' do
70
+ let(:mutex) { described_class.mutex(args) }
71
+
72
+ subject(:dequeue_job) { Resque.dequeue(described_class, *args) }
73
+
74
+ before { mutex.lock! }
75
+
76
+ it 'unlocks the mutex' do
77
+ expect { dequeue_job }.to change {
78
+ mutex.locked?
79
+ }.from(true).to(false)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,17 @@
1
+ require 'resque-serializer'
2
+
3
+ require 'bundler/setup'
4
+ require 'mock_redis'
5
+ require 'resque'
6
+ require 'resque_spec'
7
+ require 'pry-byebug'
8
+
9
+ RSpec.configure do |config|
10
+ config.expect_with(:rspec) do |c|
11
+ c.syntax = :expect
12
+ end
13
+
14
+ config.before(:suite) do
15
+ Resque.redis = MockRedis.new
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,202 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque-serializer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ryan Ringler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-04-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: resque
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.14'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.14'
55
+ - !ruby/object:Gem::Dependency
56
+ name: mock_redis
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry-byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry-stack_explorer
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: resque_spec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ description: Ensures that only one Resque job with unique arguments is running at
140
+ a time.
141
+ email:
142
+ - rringler@gmail.com
143
+ executables:
144
+ - console
145
+ - setup
146
+ extensions: []
147
+ extra_rdoc_files: []
148
+ files:
149
+ - ".gitignore"
150
+ - ".rspec"
151
+ - ".travis.yml"
152
+ - Gemfile
153
+ - LICENSE.txt
154
+ - README.md
155
+ - Rakefile
156
+ - bin/console
157
+ - bin/setup
158
+ - lib/resque-serializer.rb
159
+ - lib/resque-serializer/mutex.rb
160
+ - lib/resque-serializer/serializers/both.rb
161
+ - lib/resque-serializer/serializers/combined.rb
162
+ - lib/resque-serializer/serializers/job.rb
163
+ - lib/resque-serializer/serializers/queue.rb
164
+ - lib/resque-serializer/version.rb
165
+ - resque-serializer.gemspec
166
+ - spec/lib/resque-serializer/mutex_spec.rb
167
+ - spec/lib/resque-serializer/serializers/both_spec.rb
168
+ - spec/lib/resque-serializer/serializers/combined_spec.rb
169
+ - spec/lib/resque-serializer/serializers/job_spec.rb
170
+ - spec/lib/resque-serializer/serializers/queue_spec.rb
171
+ - spec/spec_helper.rb
172
+ homepage: https://github.com/rringler/resque-serializer
173
+ licenses:
174
+ - MIT
175
+ metadata: {}
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ requirements: []
191
+ rubyforge_project:
192
+ rubygems_version: 2.4.5.1
193
+ signing_key:
194
+ specification_version: 4
195
+ summary: Serializes Resque jobs
196
+ test_files:
197
+ - spec/lib/resque-serializer/mutex_spec.rb
198
+ - spec/lib/resque-serializer/serializers/both_spec.rb
199
+ - spec/lib/resque-serializer/serializers/combined_spec.rb
200
+ - spec/lib/resque-serializer/serializers/job_spec.rb
201
+ - spec/lib/resque-serializer/serializers/queue_spec.rb
202
+ - spec/spec_helper.rb