0.0
No release in over a year
Redis-based mutex library for using with sidekiq jobs and batches.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

~> 2.4, >= 2.4.20
~> 0.37.0
~> 3.12
~> 0.22.0
~> 1.50, >= 1.50
~> 1.50, >= 1.50.0.85
~> 2.20, >= 2.20
~> 0.9.8

Runtime

>= 5.0
 Project Readme

SimpleMutex ยท Gem Version Coverage Status

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 same lock_key. They don't interfere with each other if different lock_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 by SecureRandom.uuid if not provided
  • payload: - any object that can be serialized as JSON, nil if 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
  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

Manual 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 if true, 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)

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.signature

SimpleMutex::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
  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.

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_jobs

SimpleMutex::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_batches

SimpleMutex::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
end

DSL:

  • 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? - suppresses SimpleMutex::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

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/simple_mutex.

License

Released under MIT License.

Authors

Team Umbrellio


Supported by Umbrellio