{"id":19134003,"url":"https://github.com/umbrellio/simple_mutex","last_synced_at":"2025-05-06T19:23:04.291Z","repository":{"id":41628551,"uuid":"420022659","full_name":"umbrellio/simple_mutex","owner":"umbrellio","description":"Redis-based mutex library for using with Sidekiq jobs and batches","archived":false,"fork":false,"pushed_at":"2023-10-11T08:17:50.000Z","size":37,"stargazers_count":3,"open_issues_count":0,"forks_count":2,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-10-14T02:18:29.219Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/umbrellio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2021-10-22T08:29:31.000Z","updated_at":"2024-05-30T22:51:03.000Z","dependencies_parsed_at":"2022-08-10T06:47:29.263Z","dependency_job_id":null,"html_url":"https://github.com/umbrellio/simple_mutex","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umbrellio%2Fsimple_mutex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umbrellio%2Fsimple_mutex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umbrellio%2Fsimple_mutex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/umbrellio%2Fsimple_mutex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/umbrellio","download_url":"https://codeload.github.com/umbrellio/simple_mutex/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223793145,"owners_count":17203760,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T06:24:42.018Z","updated_at":"2024-11-09T06:24:43.477Z","avatar_url":"https://github.com/umbrellio.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"\n# SimpleMutex \u0026middot; [![Gem Version](https://badge.fury.io/rb/simple_mutex.svg)](https://badge.fury.io/rb/simple_mutex) [![Coverage Status](https://coveralls.io/repos/github/umbrellio/simple_mutex/badge.svg?branch=main)](https://coveralls.io/github/umbrellio/simple_mutex?branch=main)\n\n\n`SimpleMutex::Mutex` - Redis-based locks with ability to store custom data inside them.\n\n`SimpleMutex::SidekiqSupport::JobWrapper` - wrapper for Sidekiq jobs that generates locks using\njob's class name and arguments (optional)\n\n`SimpleMutex:SidekiqSupport::JobMixin` - mixin for Sidekiq jobs with DSL simplifying usage\nof `SimpleMutex::SidekiqSupport::JobWrapper`\n\n`SimpleMutex::SidekiqSupport::JobCleaner` - cleaner for leftover locks created by SimpleMutex::Job\nif Sidekiq dies unexpectedly.\n\n`SimpleMutex::SidekiqSupport::Batch` - wrapper for Sidekiq Pro batches that use SimpleMutex::Mutex\nto prevent running multiple batch instances.\n\n`SimpleMutex:SidekiqSupport::BatchCleaner` - cleaner for leftover lock created by SimpleMutex::Batch\nif Sidekiq dies unexpectedly.\n\n`SimpleMutex::Helper` - auxiliary class for debugging purposes. Allows to inspect existing locks.\n\n\n## Configuration\n\nProviding Redis instance before using gem is mandatory.\n\n```ruby\nSimpleMutex.redis = Redis.new(\n  # ...\n)\n```\n\nProviding logger is optional (used by `SimpleMutex::SidekiqSupport::JobCleaner` and\n`SimpleMutex::SidekiqSupport::BatchCleaner`).\n\n```ruby\nSimpleMutex.logger = Logger.new(\n  # ...\n)\n```\n\nWhen using gem with Ruby on Rails you can set those in initializers\n\n## SimpleMutex::Mutex Usage\n\n### Initialization\n\n#### Arguments\n\n##### mandatory\n\n* `lock_key` - string that identifies lock, mandatory. Two pieces of locked code can't be run\n  simultaneously if they use same `lock_key`. They don't interfere with each other if different\n  `lock_key`'s are used\n\n##### optional\n\nKeyword arguments are used for optional args.\n\n* `expires_in:` - mutex TTL in second (or ActiveSupport::Numeric time interval), lock will be\n  removed by redis automatically when expired, lock will expire in 1 hour (`3600`) if not provided\n* `signature:` - string used to determine ownership of lock, checked when manually deleting lock,\n  will be generated by `SecureRandom.uuid` if not provided\n* `payload:` - any object that can be serialized as JSON, `nil` if not provided\n\n#### Example\n\n```ruby\nSimpleMutex::Mutex\n  .new(\n  \"some_lock_key\",\n  expires_in: 3600,\n  signature: \"qwe123\",\n  payload: { \"started_at\" =\u003e Time.now }\n)\n```\n\n### Wrapping block in mutex\n\nYou can use method `#with_lock` to wrap code block in mutex\n\n```ruby\n  SimpleMutex::Mutex\n    .new(\n      \"some_lock_key\",\n      expires_in: 3600,\n      signature: \"qwe123\",\n      payload: { \"started_at\" =\u003e Time.now }\n    ).with_lock do\n    # your code\n  end\n```\n\nMethod has delegator defined on class, so it can be used without manual instantiation\n\n```ruby\n  SimpleMutex::Mutex\n    .with_lock(\n      \"some_lock_key\",\n      expires_in: 3600,\n      signature: \"qwe123\",\n      payload: { \"started_at\" =\u003e Time.now }\n    ) do\n    # your code\n  end\n```\n\n### Manual lock control\n\n##### Using mutex instance\n\n```ruby\n  mutex = SimpleMutex::Mutex.new(\n            \"some_lock_key\",\n             expires_in: 3600,\n             signature: \"qwe123\",\n             payload: { \"started_at\" =\u003e Time.now }\n          )\n\n  mutex.lock!\n  # your code\n  mutex.unlock!\n```\n\nIf you for some reason don't want exceptions to be raised when obtaining/deleting lock is failed,\nyou can use non-! methods.\n\n```ruby\n  mutex = SimpleMutex::Mutex.new(\"some_lock_key\")\n  # obtaining of lock is not guaranteed\n  mutex.lock\n  # but you can check if it is obtained (true if lock with correct signature exists)\n  mutex.lock_obtained?\n  # releasing of lock is not guaranteed\n  mutex.unlock \n```\n\n##### Using without instance\n\nThere are `::lock`/`::lock!`/`::unlock`/`::unlock!` methods defined on class if you don't want to \nexplicitly use initializer (though it still will be used behind the scenes as `::lock` and `::lock!`\nclass methods are just delegators).\n\nMutexes have random `signature` stored inside to determine ownership. By default it prevents\ndeleting locks with signature different from provided. You can use `force: true` to ignore\nsignature check.\n\n`::lock` and `::lock!` class methods accept same arguments as in `::new`\n\n`::unlock` and `::unlock!` accept next arguments:\n\n* `lock_key` - same as in `::new`\n* `signature:` - same as in `::new`\n* `force:` - boolean, signature will be ignored if `true`, optional, `false` by default\n\n```ruby\n  SimpleMutex::Mutex.lock!(\"some_lock_key\", signature: \"abra_kadabra\")\n\n  # This will work because signature is same as in lock\n  SimpleMutex::Mutex.unlock!(\"some_lock_key\", signature: \"abra_kadabra\")\n\n  # This won't work, because signature is missing\n  SimpleMutex::Mutex.unlock!(\"some_lock_key\")\n\n  # This won't work, because signature is different\n  SimpleMutex::Mutex.unlock!(\"some_lock_key\", signature: \"alakazam\")\n\n  # This will work because of force: true\n  SimpleMutex::Mutex.unlock!(\"some_lock_key\", force: true)\n\n  # This will work because of force: true\n  SimpleMutex::Mutex.unlock!(\"some_lock_key\", signature: \"alakazam\", force: true)\n```\n\n### Getting signature from instance\n\nYou can get signature from instance if you want. By default it is UUID generated by SecureRandom.\n\n```ruby\n  mutex = SimpleMutex::Mutex.new(\"some_lock_key\")\n  mutex.signature\n```\n\n## SimpleMutex::SidekiqSupport::JobWrapper Usage\n\nThis class made to simplify usage for locking of sidekiq jobs. It will create lock with\n`lock_key` based on job's `class.name` and it's arguments if `lock_with_params: true`.\n\nJob's ID (`jid`) and time when job's execution is started will be stored inside mutex value.\n\n```ruby\n  class SomeJob\n    include Sidekiq::Worker\n\n    def perform(*args)\n      SimpleMutex::SidekiqSupport::JobWrapper.new(\n        self,\n        params:           args,\n        lock_with_params: true,\n        expires_in:       1.hour,\n        payload:          { this_is_optional: true }\n      ).with_redlock do\n        # your code\n      end\n    end\n  end\n```\n\n`params` will be used to generate `lock_key` if `lock_with_params: true`.\n\n`expires_in:` is in seconds, optional, 5 hours by default.\n\n`payload:` optional serializable object.\n\n## SimpleMutex::SidekiqSupport::Batch Usage\n\nThis is wrapper for `Sidekiq::Batch` (from Sidekiq Pro) that helps to prevent running two\nsimilar batches.\n\n```ruby\n  batch = SimpleMutex::SidekiqSupport::Batch.new(\n      lock_key: \"my_batch\",\n      expires_in: 23.hours.to_i,\n    )\n\n    batch.description = \"batch of MyJobs\"\n    batch.on(:success, self.class, {}) # you can add custom callbacks like with Sidekiq::Batch\n    batch.on(:death ,  self.class, {})\n\n    batch.jobs do\n      set_of_job_attributes.each do |job_attributes|\n        MyJob.perform(job_attributes)\n      end\n    end\n```\n\n* `lock_key` - manatory lock key\n* `expires_in:` - optional TTL, 6 hours if not provided\n\n## SimpleMutex::SidekiqSupport::JobCleaner Usage\n\nIf you use SimpleMutex for locking jobs via `SimpleMutex::SidekiqSupport::Job`, when Sidekiq dies\nunexpectedely, there can be leftover mutexes for dead jobs. To delete them you can use:\n\n```ruby\n  SimpleMutex::SidekiqSupport::JobCleaner.unlock_dead_jobs\n```\n\n\n## SimpleMutex::SidekiqSupport::BatchCleaner Usage\n\nIf you use SimpleMutex for locking Batches via `SimpleMutex::SidekiqSupport::Batch`, when Sidekiq\ndies unexpectedely, there can be leftover mutexes for dead batches. To delete them you can use:\n\n```ruby\n  SimpleMutex::SidekiqSupport::BatchCleaner.unlock_dead_batches\n```\n\n## SimpleMutex::Helper Usage\n\nGetting lock by `lock_key` (returns nil if no such lock)\n\n```ruby\nSimpleMutex::Helper.get(\"some_lock_key\")\n```\n\nListing existing locks.\n\n```ruby\nSimpleMutex::Helper.list(mode: :default)\n```\n\n`mode:` paramater allows to filter locks by type:\n* `:all` - all locks including manual\n* `:job` - job locks\n* `:batch`   - batch locks\n* `:default` - job and batch locks\n\n## SimpleMutex::SidekiqSupport::JobMixin Usage\n\nBase Job class\n\n```ruby\nclass ApplicationJob\n  include Sidekiq::Worker\n  include SimpleMutex::SidekiqSupport::JobMixin\n\n  class \u003c\u003c self\n    def inherited(job_class)\n      # Setting default timeout for mutex.\n      job_class.set_job_timeout(5 * 60 * 60) # 5 hours\n\n      job_class.prepend(\n        Module.new do\n          def perform(*args)\n              with_redlock(args) { super }\n          end\n        end,\n      )\n    end\n  end\nend\n```\n\nDSL:\n - `locking!` - enables locking with simple_mutex for jobs of this class\n - `lock_with_params!` - locks are specific for set of arguments. Same job with other arguments\ncan still be called.\n - `skip_locking_error?` - suppresses `SimpleMutex::Mutex::LockError`\n - `set_job_timeout` - redis mutex TTL in seconds (will be removed by redis itself on timeout)\n\nExample:\n\n```ruby\nclass SpecificJob \u003c ApplicaionJob\n  locking!\n  lock_with_params!\n  set_job_timeout 6 * 60 * 60\n\n  def perform\n    # ...\n  end\nend\n```\n\nYou can also override error processing for SimpleMutex::Mutex::LockError\n\n```ruby\n  # DEFAULT ERROR PROCESSING\n  # def process_locking_error(error)\n  #   raise error unless self.class.skip_locking_error?\n  # end\n\n  class SpecificJob \u003c ApplicaionJob\n    locking!\n\n    def perform\n      # ...\n    end\n\n    def process_locking_error(error)\n      SomeLogger.error(error.msg)\n      raise error unless self.class.skip_locking_error?\n    end\n  end\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/umbrellio/simple_mutex.\n\n## License\n\nReleased under MIT License.\n\n## Authors\n\nTeam Umbrellio\n\n---\n\n\u003ca href=\"https://github.com/umbrellio/\"\u003e\n\u003cimg style=\"float: left;\" src=\"https://umbrellio.github.io/Umbrellio/supported_by_umbrellio.svg\" alt=\"Supported by Umbrellio\" width=\"439\" height=\"72\"\u003e\n\u003c/a\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fumbrellio%2Fsimple_mutex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fumbrellio%2Fsimple_mutex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fumbrellio%2Fsimple_mutex/lists"}