Sidekiq::Lock
Note: This is a complete piece of software, it should work across all future sidekiq & ruby versions.
Redis-based simple locking mechanism for sidekiq. Uses SET command introduced in Redis 2.6.16.
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
time - it provides a lock method that you can use in whatever way you want.
Installation
This gem requires at least:
- redis 2.6.12
- sidekiq 6
Add this line to your application's Gemfile:
gem 'sidekiq-lock'And then execute:
$ bundleUsage
Sidekiq-lock is a middleware/module combination, let me go through my thought process here :).
In your worker class include Sidekiq::Lock::Worker module and provide lock attribute inside sidekiq_options,
for example:
class Worker
include Sidekiq::Worker
include Sidekiq::Lock::Worker
# static lock that expires after one second
sidekiq_options lock: { timeout: 1000, name: 'lock-worker' }
def perform
# ...
end
endWhat will happen is:
-
middleware will setup a
Sidekiq::Lock::RedisLockobject underThread.current[Sidekiq::Lock::THREAD_KEY](it should work in most use cases without any problems - but it's configurable, more below) - assuming you providedlockoptions, otherwise it will do nothing, just execute your worker's code -
Sidekiq::Lock::Workermodule provides alockmethod that just simply points to that thread variable, just as a convenience
So now in your worker class you can call (whenever you need):
-
lock.acquire!- will try to acquire the lock, if returns false on failure (that means some other process / thread took the lock first) -
lock.acquired?- set totruewhen lock is successfully acquired -
lock.release!- deletes the lock (only if it's: acquired by current thread and not already expired)
Lock options
sidekiq_options lock will accept static values or Proc that will be called on argument(s) passed to perform method.
- timeout - specified expire time, in milliseconds
- name - name of the redis key that will be used as lock name
- value - (optional) value of the lock, if not provided it's set to random hex
Dynamic lock example:
class Worker
include Sidekiq::Worker
include Sidekiq::Lock::Worker
sidekiq_options lock: {
timeout: proc { |user_id, timeout| timeout * 2 },
name: proc { |user_id, timeout| "lock:peruser:#{user_id}" },
value: proc { |user_id, timeout| "#{user_id}" }
}
def perform(user_id, timeout)
# ...
# do some work
# only at this point I want to acquire the lock
if lock.acquire!
begin
# I can do the work
# ...
ensure
# You probably want to manually release the lock after work is done.
lock.release!
end
else
# reschedule, raise an error or do whatever you want
end
end
endJust be sure to provide valid redis key as a lock name.
Customizing lock method name
You can change lock to something else globally before starting Sidekiq:
Sidekiq.lock_method = :redis_lockCustomizing lock container
If you would like to change default behavior of storing lock instance in Thread.current for whatever reason you
can do that as well before starting Sidekiq:
# Any thread-safe class that implements .fetch and .store methods will do
class CustomStorage
def fetch
# returns stored lock instance
end
def store(lock_instance)
# store lock
end
end
Sidekiq.lock_container = CustomStorage.newInline testing
perform_inline invokes Sidekiq's normal server middleware chain. Sidekiq::Testing.inline!, however, uses a
separate testing middleware chain. Add the lock middleware to that chain if you want inline jobs to behave like
production jobs:
Sidekiq::Testing.server_middleware do |chain|
chain.add Sidekiq::Lock::Middleware
endFor tests that call perform directly, require sidekiq/lock/testing/inline in your test/spec helper to make these
manual setup and cleanup methods available:
-
set_sidekiq_lock(worker_class, payload)- note: payload should be an array of worker arguments clear_sidekiq_lock
Contributing
- Fork it
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create new Pull Request
