SimpleMutex::Mutex
- Redis-based locks with ability to store custom data inside them.
SimpleMutex::SidekiqSupport::JobWrapper
- wrapper for Sidekiq jobs that generates locks using
job's class name and arguments (optional)
SimpleMutex:SidekiqSupport::JobMixin
- mixin for Sidekiq jobs with DSL simplifying usage
of SimpleMutex::SidekiqSupport::JobWrapper
SimpleMutex::SidekiqSupport::JobCleaner
- cleaner for leftover locks created by SimpleMutex::Job
if Sidekiq dies unexpectedly.
SimpleMutex::SidekiqSupport::Batch
- wrapper for Sidekiq Pro batches that use SimpleMutex::Mutex
to prevent running multiple batch instances.
SimpleMutex:SidekiqSupport::BatchCleaner
- cleaner for leftover lock created by SimpleMutex::Batch
if Sidekiq dies unexpectedly.
SimpleMutex::Helper
- auxiliary class for debugging purposes. Allows to inspect existing locks.
Providing Redis instance before using gem is mandatory.
SimpleMutex.redis = Redis.new(
# ...
)
Providing logger is optional (used by SimpleMutex::SidekiqSupport::JobCleaner
and
SimpleMutex::SidekiqSupport::BatchCleaner
).
SimpleMutex.logger = Logger.new(
# ...
)
When using gem with Ruby on Rails you can set those in initializers
lock_key
- string that identifies lock, mandatory. Two pieces of locked code can't be run simultaneously if they use samelock_key
. They don't interfere with each other if differentlock_key
's are used
Keyword arguments are used for optional args.
expires_in:
- mutex TTL in second (or ActiveSupport::Numeric time interval), lock will be removed by redis automatically when expired, lock will expire in 1 hour (3600
) if not providedsignature:
- string used to determine ownership of lock, checked when manually deleting lock, will be generated bySecureRandom.uuid
if not providedpayload:
- any object that can be serialized as JSON,nil
if not provided
SimpleMutex::Mutex
.new(
"some_lock_key",
expires_in: 3600,
signature: "qwe123",
payload: { "started_at" => Time.now }
)
You can use method #with_lock
to wrap code block in mutex
SimpleMutex::Mutex
.new(
"some_lock_key",
expires_in: 3600,
signature: "qwe123",
payload: { "started_at" => Time.now }
).with_lock do
# your code
end
Method has delegator defined on class, so it can be used without manual instantiation
SimpleMutex::Mutex
.with_lock(
"some_lock_key",
expires_in: 3600,
signature: "qwe123",
payload: { "started_at" => Time.now }
) do
# your code
end
mutex = SimpleMutex::Mutex.new(
"some_lock_key",
expires_in: 3600,
signature: "qwe123",
payload: { "started_at" => Time.now }
)
mutex.lock!
# your code
mutex.unlock!
If you for some reason don't want exceptions to be raised when obtaining/deleting lock is failed, you can use non-! methods.
mutex = SimpleMutex::Mutex.new("some_lock_key")
# obtaining of lock is not guaranteed
mutex.lock
# but you can check if it is obtained (true if lock with correct signature exists)
mutex.lock_obtained?
# releasing of lock is not guaranteed
mutex.unlock
There are ::lock
/::lock!
/::unlock
/::unlock!
methods defined on class if you don't want to
explicitly use initializer (though it still will be used behind the scenes as ::lock
and ::lock!
class methods are just delegators).
Mutexes have random signature
stored inside to determine ownership. By default it prevents
deleting locks with signature different from provided. You can use force: true
to ignore
signature check.
::lock
and ::lock!
class methods accept same arguments as in ::new
::unlock
and ::unlock!
accept next arguments:
lock_key
- same as in::new
signature:
- same as in::new
force:
- boolean, signature will be ignored iftrue
, optional,false
by default
SimpleMutex::Mutex.lock!("some_lock_key", signature: "abra_kadabra")
# This will work because signature is same as in lock
SimpleMutex::Mutex.unlock!("some_lock_key", signature: "abra_kadabra")
# This won't work, because signature is missing
SimpleMutex::Mutex.unlock!("some_lock_key")
# This won't work, because signature is different
SimpleMutex::Mutex.unlock!("some_lock_key", signature: "alakazam")
# This will work because of force: true
SimpleMutex::Mutex.unlock!("some_lock_key", force: true)
# This will work because of force: true
SimpleMutex::Mutex.unlock!("some_lock_key", signature: "alakazam", force: true)
You can get signature from instance if you want. By default it is UUID generated by SecureRandom.
mutex = SimpleMutex::Mutex.new("some_lock_key")
mutex.signature
This class made to simplify usage for locking of sidekiq jobs. It will create lock with
lock_key
based on job's class.name
and it's arguments if lock_with_params: true
.
Job's ID (jid
) and time when job's execution is started will be stored inside mutex value.
class SomeJob
include Sidekiq::Worker
def perform(*args)
SimpleMutex::SidekiqSupport::JobWrapper.new(
self,
params: args,
lock_with_params: true,
expires_in: 1.hour,
payload: { this_is_optional: true }
).with_redlock do
# your code
end
end
end
params
will be used to generate lock_key
if lock_with_params: true
.
expires_in:
is in seconds, optional, 5 hours by default.
payload:
optional serializable object.
This is wrapper for Sidekiq::Batch
(from Sidekiq Pro) that helps to prevent running two
similar batches.
batch = SimpleMutex::SidekiqSupport::Batch.new(
lock_key: "my_batch",
expires_in: 23.hours.to_i,
)
batch.description = "batch of MyJobs"
batch.on(:success, self.class, {}) # you can add custom callbacks like with Sidekiq::Batch
batch.on(:death , self.class, {})
batch.jobs do
set_of_job_attributes.each do |job_attributes|
MyJob.perform(job_attributes)
end
end
lock_key
- manatory lock keyexpires_in:
- optional TTL, 6 hours if not provided
If you use SimpleMutex for locking jobs via SimpleMutex::SidekiqSupport::Job
, when Sidekiq dies
unexpectedely, there can be leftover mutexes for dead jobs. To delete them you can use:
SimpleMutex::SidekiqSupport::JobCleaner.unlock_dead_jobs
If you use SimpleMutex for locking Batches via SimpleMutex::SidekiqSupport::Batch
, when Sidekiq
dies unexpectedely, there can be leftover mutexes for dead batches. To delete them you can use:
SimpleMutex::SidekiqSupport::BatchCleaner.unlock_dead_batches
Getting lock by lock_key
(returns nil if no such lock)
SimpleMutex::Helper.get("some_lock_key")
Listing existing locks.
SimpleMutex::Helper.list(mode: :default)
mode:
paramater allows to filter locks by type:
:all
- all locks including manual:job
- job locks:batch
- batch locks:default
- job and batch locks
Base Job class
class ApplicationJob
include Sidekiq::Worker
include SimpleMutex::SidekiqSupport::JobMixin
class << self
def inherited(job_class)
# Setting default timeout for mutex.
job_class.set_job_timeout(5 * 60 * 60) # 5 hours
job_class.prepend(
Module.new do
def perform(*args)
with_redlock(args) { super }
end
end,
)
end
end
end
DSL:
locking!
- enables locking with simple_mutex for jobs of this classlock_with_params!
- locks are specific for set of arguments. Same job with other arguments can still be called.skip_locking_error?
- suppressesSimpleMutex::Mutex::LockError
set_job_timeout
- redis mutex TTL in seconds (will be removed by redis itself on timeout)
Example:
class SpecificJob < ApplicaionJob
locking!
lock_with_params!
set_job_timeout 6 * 60 * 60
def perform
# ...
end
end
You can also override error processing for SimpleMutex::Mutex::LockError
# DEFAULT ERROR PROCESSING
# def process_locking_error(error)
# raise error unless self.class.skip_locking_error?
# end
class SpecificJob < ApplicaionJob
locking!
def perform
# ...
end
def process_locking_error(error)
SomeLogger.error(error.msg)
raise error unless self.class.skip_locking_error?
end
end
Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/simple_mutex.
Released under MIT License.
Team Umbrellio