{"id":20836526,"url":"https://github.com/corasaurus-hex/locker","last_synced_at":"2026-04-16T14:39:52.599Z","repository":{"id":66398900,"uuid":"493091150","full_name":"corasaurus-hex/locker","owner":"corasaurus-hex","description":"Distributed lock mechanism for ruby using postgres locks","archived":false,"fork":false,"pushed_at":"2022-05-17T04:14:57.000Z","size":56,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-12T09:31:20.854Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/corasaurus-hex.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-17T04:14:42.000Z","updated_at":"2022-05-17T04:15:06.000Z","dependencies_parsed_at":"2023-02-21T02:30:51.879Z","dependency_job_id":null,"html_url":"https://github.com/corasaurus-hex/locker","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/corasaurus-hex/locker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corasaurus-hex%2Flocker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corasaurus-hex%2Flocker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corasaurus-hex%2Flocker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corasaurus-hex%2Flocker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/corasaurus-hex","download_url":"https://codeload.github.com/corasaurus-hex/locker/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/corasaurus-hex%2Flocker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28003981,"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","status":"online","status_checked_at":"2025-12-24T02:00:07.193Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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-18T00:30:23.428Z","updated_at":"2025-12-24T15:12:38.015Z","avatar_url":"https://github.com/corasaurus-hex.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Locker [![Build Status](https://travis-ci.org/zencoder/locker.png)](https://travis-ci.org/zencoder/locker)\n\nLocker is a locking mechanism for limiting the concurrency of ruby code using the database.\n\nLocker is dependent on Postgres and the ActiveRecord (\u003e= 3.2.0) gem.\n\n## Supported Rubies\n\nSee [the travis configuration file](https://github.com/zencoder/locker/blob/master/.travis.yml) for which ruby versions we support.\n\n## The Basics\n\nIn its simplest form it can be used as follows:\n\n```ruby\nLocker.run(\"unique-key\") do\n  # Code that only one process should be running\nend\n```\n\n## What does it do?\n\nSuppose you have a process running on a server that continually performs a task. In our examples we'll use an RSS/Atom feed checker:\n\n### Server 1\n\n#### Code (lib/new_feed_checker.rb)\n\n```ruby\nloop do\n  FeedChecker.check_for_new_feeds\nend\n```\n\n`script/rails runner lib/new_feed_checker.rb`\n\nThis is great if you have only one server, or if you're okay with running the code on only one of your servers and don't care if the server goes down (and thus the code stops running until the server is back up). If you wanted to make this more fault tolerant you might add another server performing the same task:\n\n### Server 2\n\n*Same as Server 1*\n\nThis would work fantastic, so long as `FeedChecker.check_for_new_feeds` is safe to run concurrently on two or more servers. If it's not safe to run concurrently, you need to either make it concurrency-safe or make sure only one server runs the code at any given time. This is where Locker comes in. Lets change the code to take advantage of Locker.\n\n### Server 1 and 2\n\n#### Code (lib/new_feed_checker.rb)\n\n```ruby\nLocker.run(\"new-feed-checker\") do # One server will get the lock\n  # Only one server will get here\n  loop do\n    FeedChecker.check_for_new_feeds\n  end\nend # Lock is released at this point\n```\n\n`script/rails runner lib/new_feed_checker.rb`\n\nWhen we run this code on both servers, only one server will obtain the lock and run `FeedChecker.check_for_new_feeds`. The other server will simply skip the block entirely. Only the server that obtains the lock will run the code, and only one server can obtain the lock at any given time. The first server to get to the lock wins! After the server that obtained the lock finishes running the code block, the lock will be released.\n\nThis is great! We've made sure that only one server can run the code at any given time. But wait! Since the server that didn't obtain the lock just skips the code and finishes running we still can't handle one of the servers going down. If only we could wait for the lock to become available instead of skipping the block. Good news, we can!\n\n### Server 1 and 2\n\n#### Code (lib/new_feed_checker.rb)\n\n```ruby\nLocker.run(\"new-feed-checker\", :blocking =\u003e true) do\n  # Only one server will get here at a time. The other server will patiently wait.\n  loop do\n    FeedChecker.check_for_new_feeds\n  end\nend # Lock is released at this point\n```\n\n`script/rails runner lib/new_feed_checker.rb`\n\nThe addition of `:blocking =\u003e true` means that whichever server doesn't obtain the lock at first will simply wait and keep trying to get the lock. If the server that first obtains the lock goes down at any point, the second server will automatically take over. By using this technique we've made it so that we don't need to make the code handle concurrency while simultaneously making sure that the code stays running even if a server goes down.\n\n## Installation\n\nIf you're using bundler you can add it to your 'Gemfile':\n\n```ruby\ngem \"locker\"\n```\n\nThen, of course, `bundle install`.\n\nOtherwise you can just `gem install locker`.\n\n## Setup\n\nThis gem includes generators for Rails 3.0+:\n\n`script/rails generate locker [ModelName]`\n\nThe 'ModelName' defaults to 'Lock' if not specified. This will generate the Lock model and its migration.\n\n## Advanced Usage\n\nLocker uses some rather simple methods to accomplish its purpose. These simple methods include obtaining, renewing, and releasing the locks.\n\n```ruby\nlock = Locker.new(\"some-unique-key\")\nlock.get     # =\u003e true  (Lock obtained)\n# Do something that doesn't take too long here\nlock.renew   # =\u003e true  (Lock renewed)\n# Do another thing that doesn't take too long here\nlock.release # =\u003e false (Lock released)\n```\n\nThe locks consist of records in the `locks` table which have a the following columns: `key`, `locked_by`, `locked_at`, and `locked_until`. The `key` column has uniqueness enforced at the database level to prevent race conditions and duplicate locks. `locked_by` has an identifier unique to the process and object running the code block. This unique identifier makes sure that that we know if we should be able to renew our lock. `locked_at` is a utility column for checking how long a lock has been monopolized. `locked_until` tells us when the lock will expire if it is not renewed.\n\nWhen Locker is used via the `run` method, an auto-renewer thread is run until the `run` block finishes, at which time the lock is released. By default all locks are obtained for 30 seconds and auto-renewed every 10 seconds. Locks that expire can be taken over by other processes or threads. If your lock expires and another process or thread takes over, Locker will raise `Locker::LockStolen`. The lock duration and time between renewals can be customized.\n\n```ruby\n# :lock_for is the lock duration in seconds. Must be greater than 0 and greater than :renew_every\n# :renew_every is the time to sleep between renewals in seconds. Must be greater than 0 and less than :lock_for\nLocker.run(\"some-unique-key\", :lock_for =\u003e 60, :renew_every =\u003e 5) do\n  # Your code goes here\nend\n```\n\nIf you changed the name of the Lock model, or if you have multiple Lock models, you can customize the model to be used either when you run `Locker.run` or on the Locker class itself.\n\n```ruby\nLocker.model = SomeOtherOtherLockModel\n\nLocker.run(\"some-unique-key\") do\n  # Locked using SomeOtherOtherLockModel\nend\n\nLocker.run(\"some-unique-key\", :model =\u003e SomeOtherLockModel) do\n  # Locked using SomeOtherLockModel\nend\n```\n\nIf you need to know how many times the lock has been acquired, this is available as well.\n\n```ruby\nLocker.run(\"some-unique-key\") do |sequence|\n  # sequence is the number of times the lock has been acquired\nend\n```\n\n## A Common pattern\n\nIn our use we've settled on a common pattern, one that lets us distribute the load of our processes between our application and/or utility servers while making sure we have no single point of failure. This means that no single server going down (except the database) will stop the code from executing. Continuing from the code above, we'll use the example of the RSS/Atom feed checker, `FeedChecker.check_for_new_feeds`. To improve on the previous examples, we'll make the code rotate among our servers, so over a long enough time period each server will have spent an equal amount of time running the task.\n\n```ruby\nloop do\n  Locker.run(\"new-feed-checker\", :blocking =\u003e true) do\n    FeedChecker.check_for_new_feeds\n  end\n  sleep(Kernel.rand + 1) # Delay the next try so that the other servers will have a chance to obtain the lock\nend\n```\n\nInstead of the first server to obtain the lock having a monopoly, each server will obtain a lock only for the duration of the call to `FeedChecker.check_for_new_feeds`. We introduce a random delay so that other servers will have a chance to obtain the lock. If we didn't add that delay then after the first server finished running the FeedChecker it would immediately re-obtain the lock. This is due to how the 'blocking' mechanism works. The blocking mechanism will try to obtain the lock then sleep for half a second, repeating continually until the lock is obtained. The random delay, therefore, will make sure that another server will obtain the lock before the first server will attempt to obtain it again (since 1.Xs \u003e 0.5s), while also randomizing the chances of the first server obtaining locks in the future. In effect this will make sure that over a long enough time period each server will have obtained an equal number of locks. A side benefit of this pattern is that if you don't need the code to run constantly you could introduce a much larger sleep and random value.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcorasaurus-hex%2Flocker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcorasaurus-hex%2Flocker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcorasaurus-hex%2Flocker/lists"}