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.
Configuration
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
SimpleMutex::Mutex Usage
Initialization
Arguments
mandatory
- 
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 
optional
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 provided - 
signature:- string used to determine ownership of lock, checked when manually deleting lock, will be generated bySecureRandom.uuidif not provided - 
payload:- any object that can be serialized as JSON,nilif not provided 
Example
SimpleMutex::Mutex
  .new(
  "some_lock_key",
  expires_in: 3600,
  signature: "qwe123",
  payload: { "started_at" => Time.now }
)Wrapping block in mutex
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
  endMethod 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
  endManual lock control
Using mutex instance
  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 Using without instance
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,falseby 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)Getting signature from instance
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.signatureSimpleMutex::SidekiqSupport::JobWrapper Usage
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
  endparams 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.
SimpleMutex::SidekiqSupport::Batch Usage
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 key - 
expires_in:- optional TTL, 6 hours if not provided 
SimpleMutex::SidekiqSupport::JobCleaner Usage
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_jobsSimpleMutex::SidekiqSupport::BatchCleaner Usage
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_batchesSimpleMutex::Helper Usage
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 
SimpleMutex::SidekiqSupport::JobMixin Usage
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
endDSL:
- 
locking!- enables locking with simple_mutex for jobs of this class - 
lock_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
endYou 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
  endContributing
Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/simple_mutex.
License
Released under MIT License.
Authors
Team Umbrellio