├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── Appraisals
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── docker-compose.yml
├── gemfiles
├── sidekiq_6_1.gemfile
├── sidekiq_6_2.gemfile
├── sidekiq_7_1.gemfile
└── sidekiq_7_2.gemfile
├── lib
├── sidekiq-lock.rb
└── sidekiq
│ ├── lock.rb
│ └── lock
│ ├── container.rb
│ ├── middleware.rb
│ ├── redis_lock.rb
│ ├── testing
│ └── inline.rb
│ ├── version.rb
│ └── worker.rb
├── logo.png
├── sidekiq-lock.gemspec
└── test
├── lib
├── container_test.rb
├── lock_test.rb
├── middleware_test.rb
├── redis_lock_test.rb
├── testing
│ └── inline_test.rb
└── worker_test.rb
├── test_helper.rb
└── test_workers.rb
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Run tests
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | test:
11 | strategy:
12 | fail-fast: false
13 | matrix:
14 | os: [ ubuntu-latest ]
15 | ruby: [ '2.7', '3.0', '3.1' ]
16 | gemfile: [ sidekiq_6_1, sidekiq_7_1, sidekiq_7_2 ]
17 | redis: [ '7.0-alpine3.18', '6.2.12-alpine3.18' ]
18 | env:
19 | BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
20 | runs-on: ${{ matrix.os }}
21 | services:
22 | redis:
23 | image: redis:${{ matrix.redis }}
24 | ports:
25 | - 6379:6379
26 | steps:
27 | - uses: actions/checkout@v4
28 | - uses: ruby/setup-ruby@v1
29 | with:
30 | ruby-version: ${{ matrix.ruby }}
31 | bundler-cache: true
32 | - name: Run tests
33 | run: bundle exec rake
34 | env:
35 | REDIS_URL: redis://localhost:6379/0
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 | .rvmrc
19 | .ruby-version
20 | .ruby-gemset
21 | *.gemfile.lock
22 | .idea/
23 | .DS_Store
24 | .tool-versions
25 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise 'sidekiq-6_1' do
2 | gem 'sidekiq', '~> 6.1', '>= 6.1.1', '< 6.2'
3 | end
4 |
5 | appraise 'sidekiq-6_2' do
6 | gem 'sidekiq', '~> 6.2', '>= 6.2'
7 | end
8 |
9 | appraise 'sidekiq-7_1' do
10 | gem 'sidekiq', '~> 7.1', '>= 7.1.0', '< 7.2'
11 | end
12 |
13 | appraise 'sidekiq-7_2' do
14 | gem 'sidekiq', '~> 7.2', '>= 7.2.0'
15 | end
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.7.0 (Feb 7, 2024)
2 |
3 | - support for Sidekiq 7.2 (thanks for the issue report [9mm](https://github.com/9mm))
4 | - dropped support for Sidekiq 5 and older [as it reached EOL](https://github.com/sidekiq/sidekiq/wiki/Commercial-Support#version-policy)
5 | - removed `redis` as gem dependency (relying on sidekiq version you're using to choose the right one)
6 |
7 | ## 0.6.0 (May 27, 2023)
8 |
9 | - support for Sidekiq 7 (thanks to [@stympy](https://github.com/stympy))
10 | - move CI pipelines to Github Actions - drop tests for everything below Sidekiq 5, run tests on redis 6.2 & 7.0 and ruby 2.6 - 3.1
11 |
12 | ## 0.5.0 (August 13, 2021)
13 |
14 | - maintenance - test on latest ruby versions (remove outdated rubies from build), add sidekiq 6 to build matrix, remove coveralls
15 | - fix for ruby 3 (thanks to [@ak15](https://github.com/ak15))
16 |
17 | ## 0.4.0 (July 18, 2018)
18 |
19 | - make lock container configurable (non breaking change) - in case you would like to something else than `Thread.current` - now you easily can
20 |
21 | ## 0.3.1 (March 3, 2018)
22 |
23 | - do not assume `ActiveSupport` is loaded / or old `Sidekiq` patches are present (add own symbolize keys logic)
24 | - make `options` and `payload` attr readers as `private` in `RedisLock` as it should be - **potentially breaking change** if you were accessing those (abusing) somehow for whatever reason (that shouldn't happen in the first place!)
25 | - run test on travis for sidekiq `2.17`, `3.5`, `4.2` and `>= 5.1` and all newest rubies (`2.2` - `2.5`)
26 |
27 | ## 0.3.0 (July 28, 2016)
28 |
29 | - ability to set custom lock value. Works just like setting timeout and name, handles procs as well (thanks to [@piokaczm](https://github.com/piokaczm))
30 |
31 | ``` ruby
32 | sidekiq_options lock: {
33 | timeout: timeout,
34 | name: name,
35 | value: custom_value
36 | }
37 | ```
38 |
39 | ## 0.2.0 (December 08, 2013)
40 |
41 | - ability to globally configure `lock` method name
42 |
43 | ``` ruby
44 | Sidekiq.configure_server do |config|
45 | config.lock_method = :redis_lock
46 | end
47 | ```
48 |
49 | - added inline test helper, by requiring `sidekiq/lock/testing/inline`
50 | you will have access to two methods:
51 |
52 | - `set_sidekiq_lock(worker_class, payload)`
53 |
54 | - `clear_sidekiq_lock`
55 |
56 | That will setup `RedisLock` under proper thread variable.
57 | This can be handy if you test your workers inline (without full stack middleware)
58 |
59 | ## 0.0.1 (October 14, 2013)
60 |
61 | - Initial release
62 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | group :test do
4 | gem "minitest"
5 | gem "mocha", require: false
6 | end
7 |
8 | gem "appraisal"
9 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2024 Rafal Wojsznis
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Sidekiq::Lock
6 |
7 | [](https://codeclimate.com/github/rwojsznis/sidekiq-lock)
8 | [](http://badge.fury.io/rb/sidekiq-lock)
9 |
10 | **Note:** This is a _complete_ piece of software, it should work across all future sidekiq & ruby versions.
11 |
12 | Redis-based simple locking mechanism for [sidekiq][2]. Uses [SET command][1] introduced in Redis 2.6.16.
13 |
14 | It can be handy if you push a lot of jobs into the queue(s), but you don't want to execute specific jobs at the same
15 | time - it provides a `lock` method that you can use in whatever way you want.
16 |
17 | ## Installation
18 |
19 | This gem requires at least:
20 | - redis 2.6.12
21 | - sidekiq 6
22 |
23 | Add this line to your application's Gemfile:
24 |
25 | ``` ruby
26 | gem 'sidekiq-lock'
27 | ```
28 |
29 | And then execute:
30 |
31 | ``` bash
32 | $ bundle
33 | ```
34 |
35 | ## Usage
36 |
37 | Sidekiq-lock is a middleware/module combination, let me go through my thought process here :).
38 |
39 | In your worker class include `Sidekiq::Lock::Worker` module and provide `lock` attribute inside `sidekiq_options`,
40 | for example:
41 |
42 | ``` ruby
43 | class Worker
44 | include Sidekiq::Worker
45 | include Sidekiq::Lock::Worker
46 |
47 | # static lock that expires after one second
48 | sidekiq_options lock: { timeout: 1000, name: 'lock-worker' }
49 |
50 | def perform
51 | # ...
52 | end
53 | end
54 | ```
55 |
56 | What will happen is:
57 |
58 | - middleware will setup a `Sidekiq::Lock::RedisLock` object under `Thread.current[Sidekiq::Lock::THREAD_KEY]`
59 | (it should work in most use cases without any problems - but it's configurable, more below) - assuming you provided
60 | `lock` options, otherwise it will do nothing, just execute your worker's code
61 |
62 | - `Sidekiq::Lock::Worker` module provides a `lock` method that just simply points to that thread variable, just as
63 | a convenience
64 |
65 | So now in your worker class you can call (whenever you need):
66 |
67 | - `lock.acquire!` - will try to acquire the lock, if returns false on failure (that means some other process / thread
68 | took the lock first)
69 | - `lock.acquired?` - set to `true` when lock is successfully acquired
70 | - `lock.release!` - deletes the lock (only if it's: acquired by current thread and not already expired)
71 |
72 | ### Lock options
73 |
74 | sidekiq_options lock will accept static values or `Proc` that will be called on argument(s) passed to `perform` method.
75 |
76 | - timeout - specified expire time, in milliseconds
77 | - name - name of the redis key that will be used as lock name
78 | - value - (optional) value of the lock, if not provided it's set to random hex
79 |
80 | Dynamic lock example:
81 |
82 | ``` ruby
83 | class Worker
84 | include Sidekiq::Worker
85 | include Sidekiq::Lock::Worker
86 | sidekiq_options lock: {
87 | timeout: proc { |user_id, timeout| timeout * 2 },
88 | name: proc { |user_id, timeout| "lock:peruser:#{user_id}" },
89 | value: proc { |user_id, timeout| "#{user_id}" }
90 | }
91 |
92 | def perform(user_id, timeout)
93 | # ...
94 | # do some work
95 | # only at this point I want to acquire the lock
96 | if lock.acquire!
97 | begin
98 | # I can do the work
99 | # ...
100 | ensure
101 | # You probably want to manually release lock after work is done
102 | # This method can be safely called even if lock wasn't acquired
103 | # by current worker (thread). For more references see RedisLock class
104 | lock.release!
105 | end
106 | else
107 | # reschedule, raise an error or do whatever you want
108 | end
109 | end
110 | end
111 | ```
112 |
113 | Just be sure to provide valid redis key as a lock name.
114 |
115 | ### Customizing lock method name
116 |
117 | You can change `lock` to something else (globally) in sidekiq server configuration:
118 |
119 | ``` ruby
120 | Sidekiq.configure_server do |config|
121 | config.lock_method = :redis_lock
122 | end
123 | ```
124 |
125 | ### Customizing lock _container_
126 |
127 | If you would like to change default behavior of storing lock instance in `Thread.current` for whatever reason you
128 | can do that as well via server configuration:
129 |
130 | ``` ruby
131 | # Any thread-safe class that implements .fetch and .store methods will do
132 | class CustomStorage
133 | def fetch
134 | # returns stored lock instance
135 | end
136 |
137 | def store(lock_instance)
138 | # store lock
139 | end
140 | end
141 |
142 | Sidekiq.configure_server do |config|
143 | config.lock_container = CustomStorage.new
144 | end
145 | ```
146 |
147 | ### Inline testing
148 |
149 | As you know middleware is not invoked when testing jobs inline, you can require in your test/spec helper file
150 | `sidekiq/lock/testing/inline` to include two methods that will help you setting / clearing up lock manually:
151 |
152 | - `set_sidekiq_lock(worker_class, payload)` - note: payload should be an array of worker arguments
153 | - `clear_sidekiq_lock`
154 |
155 | ## Contributing
156 |
157 | 1. Fork it
158 | 2. Create your feature branch (`git checkout -b my-new-feature`)
159 | 3. Commit your changes (`git commit -am 'Add some feature'`)
160 | 4. Push to the branch (`git push origin my-new-feature`)
161 | 5. Create new Pull Request
162 |
163 | [1]: http://redis.io/commands/set
164 | [2]: https://github.com/mperham/sidekiq
165 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require "bundler/gem_tasks"
3 | require "rake/testtask"
4 |
5 | task :default => :test
6 |
7 | Rake::TestTask.new do |t|
8 | t.libs << "lib"
9 | t.libs << "test"
10 | t.test_files = FileList["test/**/*_test.rb"]
11 | t.verbose = true
12 | end
13 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | redis:
4 | image: redis:6.0-alpine
5 | ports:
6 | - '6379:6379'
7 | healthcheck:
8 | test: redis-cli ping
9 | interval: 10s
10 | timeout: 3s
11 |
--------------------------------------------------------------------------------
/gemfiles/sidekiq_6_1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "sidekiq", "~> 6.1", ">= 6.1.1", "< 6.2"
7 |
8 | group :test do
9 | gem "minitest"
10 | gem "mocha", require: false
11 | end
12 |
--------------------------------------------------------------------------------
/gemfiles/sidekiq_6_2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "sidekiq", "~> 6.2", ">= 6.2"
7 |
8 | group :test do
9 | gem "minitest"
10 | gem "mocha", require: false
11 | end
12 |
--------------------------------------------------------------------------------
/gemfiles/sidekiq_7_1.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "sidekiq", "~> 7.1", ">= 7.1.0", "< 7.2"
7 |
8 | group :test do
9 | gem "minitest"
10 | gem "mocha", require: false
11 | end
12 |
--------------------------------------------------------------------------------
/gemfiles/sidekiq_7_2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "sidekiq", "~> 7.2", ">= 7.2.0"
7 |
8 | group :test do
9 | gem "minitest"
10 | gem "mocha", require: false
11 | end
12 |
--------------------------------------------------------------------------------
/lib/sidekiq-lock.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq/lock'
2 |
--------------------------------------------------------------------------------
/lib/sidekiq/lock.rb:
--------------------------------------------------------------------------------
1 | require 'sidekiq/lock/container'
2 | require 'sidekiq/lock/middleware'
3 | require 'sidekiq/lock/redis_lock'
4 | require 'sidekiq/lock/version'
5 | require 'sidekiq/lock/worker'
6 |
7 | module Sidekiq
8 | def self.lock_container
9 | @lock_container ||= Lock::Container.new
10 | end
11 |
12 | def self.lock_method
13 | @lock_method ||= Lock::METHOD_NAME
14 | end
15 |
16 | def self.lock_container=(container)
17 | @lock_container = container
18 | end
19 |
20 | def self.lock_method=(method)
21 | @lock_method = method
22 | end
23 |
24 | module Lock
25 | THREAD_KEY = :sidekiq_lock
26 | METHOD_NAME = :lock
27 | end
28 | end
29 |
30 | Sidekiq.configure_server do |config|
31 | config.server_middleware do |chain|
32 | chain.add Sidekiq::Lock::Middleware
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/sidekiq/lock/container.rb:
--------------------------------------------------------------------------------
1 | module Sidekiq
2 | module Lock
3 | class Container
4 | THREAD_KEY = :sidekiq_lock
5 |
6 | def fetch
7 | Thread.current[THREAD_KEY]
8 | end
9 |
10 | def store(lock)
11 | Thread.current[THREAD_KEY] = lock
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/sidekiq/lock/middleware.rb:
--------------------------------------------------------------------------------
1 | module Sidekiq
2 | module Lock
3 | class Middleware
4 | def call(worker, msg, _queue)
5 | options = lock_options(worker)
6 | setup_lock(options, msg['args']) unless options.nil?
7 |
8 | yield
9 | end
10 |
11 | private
12 |
13 | def setup_lock(options, payload)
14 | Sidekiq.lock_container.store(RedisLock.new(options, payload))
15 | end
16 |
17 | def lock_options(worker)
18 | worker.class.get_sidekiq_options['lock']
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/lib/sidekiq/lock/redis_lock.rb:
--------------------------------------------------------------------------------
1 | module Sidekiq
2 | module Lock
3 | class RedisLock
4 | # checks for configuration
5 | def initialize(options_hash, payload)
6 | @options = {}
7 |
8 | options_hash.each_key do |key|
9 | @options[key.to_sym] = options_hash[key]
10 | end
11 |
12 | @payload = payload
13 | @acquired = false
14 |
15 | timeout && name
16 | end
17 |
18 | def acquired?
19 | @acquired
20 | end
21 |
22 | # acquire lock using modified SET command introduced in Redis 2.6.12
23 | # this also requires redis-rb >= 3.0.5
24 | def acquire!
25 | @acquired ||= Sidekiq.redis do |r|
26 | if Sidekiq::VERSION >= '7.2'
27 | r.set(name, value, 'nx', 'px', timeout)
28 | else
29 | r.set(name, value, nx: true, px: timeout)
30 | end
31 | end
32 | end
33 |
34 | def release!
35 | # even if lock expired / was take over by another process
36 | # it still means from our perspective that we released it
37 | @acquired = false
38 |
39 | # https://redis.io/commands/del/#return
40 | release_lock == 1
41 | end
42 |
43 | def name
44 | raise ArgumentError, 'Provide a lock name inside sidekiq_options' if options[:name].nil?
45 |
46 | @name ||= (options[:name].respond_to?(:call) ? options[:name].call(*payload) : options[:name])
47 | end
48 |
49 | def timeout
50 | raise ArgumentError, 'Provide lock timeout inside sidekiq_options' if options[:timeout].nil?
51 |
52 | @timeout ||= (options[:timeout].respond_to?(:call) ? options[:timeout].call(*payload) : options[:timeout]).to_i
53 | end
54 |
55 | private
56 |
57 | attr_reader :options, :payload
58 |
59 | def release_lock
60 | # Sidekiq 7 uses redis-client gem, designed for redis 6+
61 | if Sidekiq::VERSION >= '7'
62 | Sidekiq.redis do |r|
63 | begin
64 | r.evalsha redis_lock_script_sha, [name], [value]
65 | rescue RedisClient::CommandError
66 | r.eval redis_lock_script, 1, [name], [value]
67 | end
68 | end
69 | else
70 | Sidekiq.redis do |r|
71 | begin
72 | r.evalsha redis_lock_script_sha, keys: [name], argv: [value]
73 | rescue Redis::CommandError
74 | r.eval redis_lock_script, keys: [name], argv: [value]
75 | end
76 | end
77 | end
78 | end
79 |
80 | def redis_lock_script_sha
81 | @lock_script_sha ||= Digest::SHA1.hexdigest redis_lock_script
82 | end
83 |
84 | def redis_lock_script
85 | <<-LUA
86 | if redis.call("get", KEYS[1]) == ARGV[1]
87 | then
88 | return redis.call("del",KEYS[1])
89 | else
90 | return 0
91 | end
92 | LUA
93 | end
94 |
95 | def value
96 | @value ||= set_lock_value(options[:value])
97 | end
98 |
99 | def set_lock_value(custom_value)
100 | return SecureRandom.hex(25) unless custom_value
101 | custom_value.respond_to?(:call) ? custom_value.call(*payload) : custom_value
102 | end
103 | end
104 | end
105 | end
106 |
--------------------------------------------------------------------------------
/lib/sidekiq/lock/testing/inline.rb:
--------------------------------------------------------------------------------
1 | def set_sidekiq_lock(worker_class, payload)
2 | options = worker_class.get_sidekiq_options['lock']
3 | Sidekiq.lock_container.store(Sidekiq::Lock::RedisLock.new(options, payload))
4 | end
5 |
6 | def clear_sidekiq_lock
7 | Sidekiq.lock_container.store(nil)
8 | end
9 |
--------------------------------------------------------------------------------
/lib/sidekiq/lock/version.rb:
--------------------------------------------------------------------------------
1 | module Sidekiq
2 | module Lock
3 | VERSION = '0.7.0'
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/sidekiq/lock/worker.rb:
--------------------------------------------------------------------------------
1 | module Sidekiq
2 | module Lock
3 | module Worker
4 | def self.included(base)
5 | base.send(:define_method, Sidekiq.lock_method) do
6 | Sidekiq.lock_container.fetch
7 | end
8 | end
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rwojsznis/sidekiq-lock/2ffec2a5ef53f0803438d4135523c22a51b0948d/logo.png
--------------------------------------------------------------------------------
/sidekiq-lock.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'sidekiq/lock/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "sidekiq-lock"
8 | spec.version = Sidekiq::Lock::VERSION
9 | spec.authors = ["Rafal Wojsznis"]
10 | spec.email = ["rafal.wojsznis@gmail.com"]
11 | spec.description = spec.summary = "Simple redis-based lock mechanism for your sidekiq workers"
12 | spec.homepage = "https://github.com/emq/sidekiq-lock"
13 | spec.license = "MIT"
14 |
15 | spec.files = Dir["lib/**/*"] + ["LICENSE.txt", "Rakefile", "README.md", "CHANGELOG.md"]
16 | spec.test_files = Dir["test/**/*"]
17 | spec.require_paths = ["lib"]
18 |
19 | spec.add_dependency "sidekiq", ">= 6"
20 | end
21 |
--------------------------------------------------------------------------------
/test/lib/container_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'open3'
3 |
4 | module Sidekiq
5 | module Lock
6 | describe Container do
7 | it 'stores and fetches given value under Thread.current' do
8 | begin
9 | container = Sidekiq::Lock::Container.new
10 | thread_key = Sidekiq::Lock::Container::THREAD_KEY
11 |
12 | Thread.current[thread_key] = 'value'
13 | assert_equal 'value', container.fetch
14 |
15 | container.store 'new-value'
16 | assert_equal Thread.current[thread_key], 'new-value'
17 | ensure
18 | Thread.current[thread_key] = nil
19 | end
20 | end
21 | end
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/test/lib/lock_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'open3'
3 |
4 | module Sidekiq
5 | describe Lock do
6 | it 'automatically loads lock middleware for sidekiq server' do
7 | skip 'Sidekiq 7+ does not print out middleware information' if Sidekiq::VERSION >= '7'
8 |
9 | cmd = 'sidekiq -r ./test/test_workers.rb -v'
10 | buffer_out = ''
11 |
12 | # very not fancy (https://78.media.tumblr.com/tumblr_lzkpw7DAl21qhy6c9o2_400.gif)
13 | # solution, but should do the job
14 | Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
15 | begin
16 | Timeout.timeout(3) do
17 | until stdout.eof? do
18 | buffer_out << stdout.read_nonblock(16)
19 | end
20 | end
21 |
22 | rescue Timeout::Error
23 | Process.kill('KILL', thread.pid)
24 | end
25 | end
26 |
27 | assert_match(/\s?Middleware:.*Sidekiq::Lock::Middleware/i, buffer_out)
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/lib/middleware_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | module Sidekiq
4 | module Lock
5 | describe Middleware do
6 | before do
7 | if Sidekiq::VERSION >= '7'
8 | Sidekiq.configure_server do |config|
9 | config.redis = { url: REDIS_URL }
10 | end
11 | else
12 | Sidekiq.redis = REDIS
13 | end
14 | Sidekiq.redis { |c| c.flushdb }
15 | reset_lock_variable!
16 | end
17 |
18 | it 'sets lock variable with provided static lock options' do
19 | handler = Sidekiq::Lock::Middleware.new
20 | handler.call(LockWorker.new, { 'class' => LockWorker, 'args' => [] }, 'default') do
21 | true
22 | end
23 |
24 | assert_kind_of RedisLock, lock_container_variable
25 | end
26 |
27 | it 'sets lock variable with provided dynamic options' do
28 | handler = Sidekiq::Lock::Middleware.new
29 | handler.call(DynamicLockWorker.new, { 'class' => DynamicLockWorker, 'args' => [1234, 1000] }, 'default') do
30 | true
31 | end
32 |
33 | assert_equal "lock:1234", lock_container_variable.name
34 | assert_equal 2000, lock_container_variable.timeout
35 | end
36 |
37 | it 'sets nothing for workers without lock options' do
38 | handler = Sidekiq::Lock::Middleware.new
39 | handler.call(RegularWorker.new, { 'class' => RegularWorker, 'args' => [] }, 'default') do
40 | true
41 | end
42 |
43 | assert_nil lock_container_variable
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/lib/redis_lock_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | module Sidekiq
4 | module Lock
5 | describe RedisLock do
6 | before do
7 | if Sidekiq::VERSION >= '7'
8 | Sidekiq.configure_client do |config|
9 | config.redis = { url: REDIS_URL }
10 | end
11 | else
12 | Sidekiq.redis = REDIS
13 | end
14 | Sidekiq.redis { |c| c.flushdb }
15 | end
16 |
17 | it "raises an error on missing timeout&name values" do
18 | assert_raises ArgumentError do
19 | RedisLock.new({},[])
20 | end
21 | end
22 |
23 | it "raises an error on missing timeout value" do
24 | assert_raises ArgumentError do
25 | RedisLock.new({ 'name' => 'this-is-lock' }, [])
26 | end
27 | end
28 |
29 | it "raises an error on missing name value" do
30 | assert_raises ArgumentError do
31 | RedisLock.new({ 'timeout' => 500 }, [])
32 | end
33 | end
34 |
35 | it "does not raise an error when timeout and name is provided" do
36 | assert RedisLock.new({ 'timeout' => 500, 'name' => 'lock-name' }, [])
37 | end
38 |
39 | it "is released by default" do
40 | lock = RedisLock.new({ 'timeout' => 500, 'name' => 'lock-name' }, [])
41 | refute lock.acquired?
42 | end
43 |
44 | it "can accept block as arguments" do
45 | lock = RedisLock.new({
46 | 'timeout' => proc { |options| options['timeout'] * 2 },
47 | 'name' => proc { |options| "#{options['test']}-sidekiq" },
48 | 'value' => proc { |options| "#{options['test']}-sidekiq" }
49 | }, ['timeout' => 500, 'test' => 'hello'])
50 |
51 | assert_equal 1000, lock.timeout
52 | assert_equal 'hello-sidekiq', lock.name
53 | lock.acquire!
54 | assert_equal 'hello-sidekiq', redis("get", lock.name)
55 | lock.release!
56 | end
57 |
58 | it "can acquire a lock" do
59 | lock = RedisLock.new({'timeout' => 100, 'name' => 'test-lock'}, [])
60 | assert lock.acquire!
61 | end
62 |
63 | it "sets proper lock value on first and second acquire" do
64 | lock = RedisLock.new({'timeout' => 1000, 'name' => 'test-lock', 'value' => 'lock value'}, [])
65 | assert lock.acquire!
66 | assert_equal 'lock value', redis("get", lock.name)
67 | assert lock.release!
68 | # at this point script should be used from evalsha
69 | assert lock.acquire!
70 | assert_equal 'lock value', redis("get", lock.name)
71 |
72 | redis("script", "flush")
73 | assert lock.acquire!
74 | assert_equal 'lock value', redis("get", lock.name)
75 | end
76 |
77 | it "cannot acquire lock if it's already taken by other process/thread" do
78 | faster_lock = RedisLock.new({'timeout' => 100, 'name' => 'test-lock'}, [])
79 | assert faster_lock.acquire!
80 |
81 | slower_lock = RedisLock.new({'timeout' => 100, 'name' => 'test-lock'}, [])
82 | refute slower_lock.acquire!
83 | end
84 |
85 | it "releases taken lock" do
86 | lock = RedisLock.new({'timeout' => 100, 'name' => 'test-lock'}, [])
87 | lock.acquire!
88 | assert redis("get", "test-lock")
89 |
90 | assert lock.release!
91 | assert_nil redis("get", "test-lock")
92 | end
93 |
94 | it "releases lock taken by another process without deleting lock key" do
95 | lock = RedisLock.new({'timeout' => 100, 'name' => 'test-lock'}, [])
96 | lock.acquire!
97 | lock_value = redis("get", "test-lock")
98 | assert lock_value
99 | sleep 0.11 # timeout lock
100 |
101 | new_lock = RedisLock.new({'timeout' => 100, 'name' => 'test-lock'}, [])
102 | new_lock.acquire!
103 | new_lock_value = redis("get", "test-lock")
104 |
105 | refute lock.release!
106 |
107 | assert_equal new_lock_value, redis("get", "test-lock")
108 | end
109 |
110 | it "releases taken lock" do
111 | lock = RedisLock.new({'timeout' => 100, 'name' => 'test-lock', 'value' => 'custom_value'}, [])
112 | lock.acquire!
113 | assert redis("get", "test-lock")
114 |
115 | lock.release!
116 | assert_nil redis("get", "test-lock")
117 | end
118 | end
119 | end
120 | end
121 |
--------------------------------------------------------------------------------
/test/lib/testing/inline_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "sidekiq/lock/testing/inline"
3 |
4 | describe "inline test helper" do
5 | after { reset_lock_variable! }
6 |
7 | it "has helper fuction for setting lock" do
8 | Sidekiq::Lock::RedisLock
9 | .expects(:new)
10 | .with({ timeout: 1, name: 'lock-worker' }, 'worker argument')
11 | .returns('lock set')
12 |
13 | set_sidekiq_lock(LockWorker, 'worker argument')
14 | assert_equal 'lock set', lock_container_variable
15 | end
16 |
17 | it "has helper fuction for clearing lock" do
18 | set_lock_variable! "test"
19 | assert_equal "test", lock_container_variable
20 |
21 | clear_sidekiq_lock
22 | assert_nil lock_container_variable
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/test/lib/worker_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | module Sidekiq
4 | module Lock
5 | describe Worker do
6 | class CustomContainer
7 | def initialize
8 | @lock = nil
9 | end
10 |
11 | def fetch
12 | @lock
13 | end
14 |
15 | def store(lock)
16 | @lock = lock
17 | end
18 | end
19 |
20 | it 'allows method name configuration' do
21 | Sidekiq.lock_method = :custom_lock_name
22 |
23 | class WorkerWithCustomLockName
24 | include Sidekiq::Worker
25 | include Sidekiq::Lock::Worker
26 | end
27 |
28 | set_lock_variable! "custom_name"
29 |
30 | assert_equal "custom_name", WorkerWithCustomLockName.new.custom_lock_name
31 |
32 | reset_lock_variable!
33 | ensure
34 |
35 | Sidekiq.lock_method = Sidekiq::Lock::METHOD_NAME
36 | end
37 |
38 | it 'allows container configuration' do
39 | container = CustomContainer.new
40 | Sidekiq.lock_container = container
41 |
42 | class WorkerWithCustomContainer
43 | include Sidekiq::Worker
44 | include Sidekiq::Lock::Worker
45 | end
46 |
47 | container.store "lock-variable"
48 |
49 | assert_equal "lock-variable", WorkerWithCustomContainer.new.lock
50 | ensure
51 | Sidekiq.lock_container = Sidekiq::Lock::Container.new
52 | end
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'minitest/autorun'
2 | require 'mocha/minitest'
3 | require 'sidekiq'
4 | require 'test_workers'
5 |
6 | Sidekiq.logger.level = Logger::ERROR
7 |
8 | REDIS_URL = ENV['REDIS_URL'] || 'redis://localhost/15'
9 | REDIS = Sidekiq::RedisConnection.create(url: REDIS_URL)
10 |
11 | def redis(command, *args)
12 | Sidekiq.redis do |c|
13 | c.send(command, *args)
14 | end
15 | end
16 |
17 | def set_lock_variable!(value)
18 | Sidekiq.lock_container.store(value)
19 | end
20 |
21 | def reset_lock_variable!
22 | set_lock_variable!(nil)
23 | end
24 |
25 | def lock_container_variable
26 | Sidekiq.lock_container.fetch
27 | end
28 |
--------------------------------------------------------------------------------
/test/test_workers.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
2 | require 'sidekiq-lock'
3 |
4 | class LockWorker
5 | include Sidekiq::Worker
6 | include Sidekiq::Lock::Worker
7 | sidekiq_options lock: { timeout: 1, name: 'lock-worker' }
8 | end
9 |
10 | class DynamicLockWorker
11 | include Sidekiq::Worker
12 | include Sidekiq::Lock::Worker
13 | sidekiq_options lock: {
14 | timeout: proc { |user_id, timeout| timeout*2 },
15 | name: proc { |user_id, timeout| "lock:#{user_id}" }
16 | }
17 | end
18 |
19 | class RegularWorker
20 | include Sidekiq::Worker
21 | include Sidekiq::Lock::Worker
22 | end
23 |
--------------------------------------------------------------------------------