forked from salsify/with_advisory_lock
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit a270603
Showing
18 changed files
with
448 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
*.gem | ||
*.rbc | ||
*.idea | ||
.bundle | ||
.config | ||
.yardoc | ||
Gemfile.lock | ||
InstalledFiles | ||
_yardoc | ||
coverage | ||
doc/ | ||
lib/bundler/man | ||
pkg | ||
rdoc | ||
spec/reports | ||
test/tmp | ||
test/version_tmp | ||
tmp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
language: ruby | ||
|
||
rvm: | ||
- 1.8.7 | ||
- 1.9.3 | ||
|
||
env: | ||
- DB=sqlite | ||
- DB=mysql | ||
- DB=pg | ||
|
||
script: bundle exec rake | ||
|
||
before_script: | ||
- mysql -e 'create database with_advisory_lock_test' | ||
- psql -c 'create database with_advisory_lock_test' -U postgres | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
source 'https://rubygems.org' | ||
|
||
gemspec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
Copyright (c) 2013 Matthew McEachen | ||
|
||
MIT License | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
"Software"), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# with_advisory_lock [![Build Status](https://api.travis-ci.org/mceachen/with_advisory_lock.png?branch=master)](https://travis-ci.org/mceachen/with_advisory_lock) | ||
|
||
Adds advisory locking to ActiveRecord 3.x. MySQL and PostgreSQL are supported natively. | ||
SQLite resorts to file locking (which won't span hosts, of course). | ||
|
||
## Usage | ||
|
||
```ruby | ||
Tag.with_advisory_lock(lock_name) do | ||
do_something_that_needs_locking | ||
end | ||
``` | ||
|
||
### What happens | ||
|
||
1. The thread will wait indefinitely until the lock is acquired. | ||
2. While inside the block, you will exclusively own the advisory lock. | ||
3. The lock will be released after your block ends, even if an exception is raised in the block. | ||
|
||
### Lock wait timeouts | ||
|
||
The second parameter for ```with_advisory_lock``` is ```timeout_seconds```, and defaults to ```nil```, | ||
which means wait indefinitely for the lock. | ||
|
||
If a non-nil value is provided, the block may not be invoked. | ||
|
||
The return value of ```with_advisory_lock``` will be the result of the yielded block, | ||
if the lock was able to be acquired and the block yielded, or ```false```, if you provided | ||
a timeout_seconds value and the lock was not able to be acquired in time. | ||
|
||
### When to use | ||
|
||
If you want to prevent duplicate inserts, and there isn't a row to lock yet, you need | ||
a [shared mutex](http://en.wikipedia.org/wiki/Mutual_exclusion), either though | ||
a [table-level lock](https://github.com/mceachen/monogamy), or through an advisory lock. | ||
|
||
When possible, use [optimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html) | ||
or [pessimistic](http://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html) row locking. | ||
|
||
## Installation | ||
|
||
Add this line to your application's Gemfile: | ||
|
||
``` ruby | ||
gem 'with_advisory_lock' | ||
``` | ||
|
||
And then execute: | ||
|
||
$ bundle | ||
|
||
## Changelog | ||
|
||
### 0.0.1 | ||
|
||
* First whack |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
require "bundler/gem_tasks" | ||
|
||
require 'yard' | ||
YARD::Rake::YardocTask.new do |t| | ||
t.files = ['lib/**/*.rb', 'README.md'] | ||
end | ||
|
||
require 'rake/testtask' | ||
|
||
Rake::TestTask.new do |t| | ||
t.libs.push "lib" | ||
t.libs.push "test" | ||
t.pattern = 'test/**/*_test.rb' | ||
t.verbose = true | ||
end | ||
|
||
task :default => :test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
require 'with_advisory_lock/concern' | ||
|
||
ActiveSupport.on_load :active_record do | ||
ActiveRecord::Base.send :include, WithAdvisoryLock::Concern | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
module WithAdvisoryLock | ||
class Base | ||
attr_reader :connection, :lock_name, :timeout_seconds | ||
|
||
def initialize(connection, lock_name, timeout_seconds) | ||
@connection = connection | ||
@lock_name = lock_name | ||
@timeout_seconds = timeout_seconds | ||
end | ||
|
||
def quoted_lock_name | ||
connection.quote(lock_name) | ||
end | ||
|
||
def with_advisory_lock(&block) | ||
give_up_at = Time.now + @timeout_seconds if @timeout_seconds | ||
while @timeout_seconds.nil? || Time.now < give_up_at do | ||
if try_lock | ||
begin | ||
return yield | ||
ensure | ||
release_lock | ||
end | ||
else | ||
sleep(0.1) | ||
end | ||
end | ||
false # failed to get lock in time. | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
# Tried desperately to monkeypatch the polymorphic connection object, | ||
# but rails autoloading is too clever by half. Pull requests are welcome. | ||
|
||
# Think of this module as a hipster, using "case" ironically. | ||
|
||
require 'with_advisory_lock/base' | ||
require 'with_advisory_lock/mysql' | ||
require 'with_advisory_lock/postgresql' | ||
require 'with_advisory_lock/flock' | ||
require 'active_support/concern' | ||
|
||
module WithAdvisoryLock | ||
module Concern | ||
extend ActiveSupport::Concern | ||
|
||
def with_advisory_lock(lock_name, timeout_seconds=nil, &block) | ||
self.class.with_advisory_lock(lock_name, timeout_seconds, &block) | ||
end | ||
|
||
module ClassMethods | ||
def with_advisory_lock(lock_name, timeout_seconds=nil, &block) | ||
case (connection.adapter_name.downcase) | ||
when "postgresql" | ||
WithAdvisoryLock::PostgreSQL | ||
when "mysql", "mysql2" | ||
WithAdvisoryLock::MySQL | ||
else | ||
WithAdvisoryLock::Flock | ||
end.new(connection, lock_name, timeout_seconds).with_advisory_lock(&block) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
require 'fileutils' | ||
|
||
module WithAdvisoryLock | ||
class Flock < Base | ||
|
||
def filename | ||
@filename ||= begin | ||
safe = @lock_name.gsub(/[^a-z0-9]/i, '') | ||
fn = ".lock-#{safe}-#{@lock_name.to_s.hash}" | ||
# Let the user specify a directory besides CWD. | ||
ENV['FLOCK_DIR'] ? File.expand_path(fn, ENV['FLOCK_DIR']) : fn | ||
end | ||
end | ||
|
||
def file_io | ||
@file_io ||= begin | ||
FileUtils.touch(filename) | ||
File.open(filename, 'r+') | ||
end | ||
end | ||
|
||
def try_lock | ||
0 == file_io.flock(File::LOCK_EX|File::LOCK_NB) | ||
end | ||
|
||
def release_lock | ||
0 == file_io.flock(File::LOCK_UN) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
module WithAdvisoryLock | ||
class MySQL < Base | ||
|
||
# See http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock | ||
|
||
def try_lock | ||
# Returns 1 if the lock was obtained successfully, | ||
# 0 if the attempt timed out (for example, because another client has | ||
# previously locked the name), or NULL if an error occurred | ||
# (such as running out of memory or the thread was killed with mysqladmin kill). | ||
1 == connection.select_value("SELECT GET_LOCK(#{quoted_lock_name}, 0)") | ||
end | ||
|
||
def release_lock | ||
# Returns 1 if the lock was released, | ||
# 0 if the lock was not established by this thread ( | ||
# in which case the lock is not released), and | ||
# NULL if the named lock did not exist. | ||
1 == connection.select_value("SELECT RELEASE_LOCK(#{quoted_lock_name})") | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
module WithAdvisoryLock | ||
class PostgreSQL < Base | ||
|
||
# See http://www.postgresql.org/docs/9.1/static/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS | ||
|
||
def try_lock | ||
# pg_try_advisory_lock will either obtain the lock immediately | ||
# and return true, or return false if the lock cannot be acquired immediately | ||
"t" == connection.select_value("SELECT pg_try_advisory_lock(#{numeric_lock})") | ||
end | ||
|
||
def release_lock | ||
"t" == connection.select_value("SELECT pg_advisory_unlock(#{numeric_lock})") | ||
end | ||
|
||
def numeric_lock | ||
@numeric_lock ||= begin | ||
if lock_name.is_a? Numeric | ||
lock_name.to_i | ||
else | ||
lock_name.to_s.hash | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module WithAdvisoryLock | ||
VERSION = "0.0.1" | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
sqlite: | ||
adapter: <%= "jdbc" if defined? JRUBY_VERSION %>sqlite3 | ||
database: test.sqlite3.db | ||
pool: 50 | ||
pg: | ||
adapter: postgresql | ||
username: postgres | ||
database: with_advisory_lock_test | ||
min_messages: ERROR | ||
pool: 50 | ||
mysql: | ||
adapter: mysql2 | ||
host: localhost | ||
username: root | ||
database: with_advisory_lock_test | ||
pool: 50 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
require 'erb' | ||
require 'active_record' | ||
require 'with_advisory_lock' | ||
require 'database_cleaner' | ||
require 'tmpdir' | ||
|
||
db_config = File.expand_path("database.yml", File.dirname(__FILE__)) | ||
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(db_config)).result) | ||
ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite") | ||
ActiveRecord::Migration.verbose = false | ||
|
||
require 'test_models' | ||
require 'minitest/autorun' | ||
|
||
Thread.abort_on_exception = true | ||
|
||
DatabaseCleaner.strategy = :deletion | ||
class MiniTest::Spec | ||
before :each do | ||
DatabaseCleaner.start | ||
end | ||
after :each do | ||
DatabaseCleaner.clean | ||
end | ||
before :all do | ||
ENV['FLOCK_DIR'] = Dir.mktmpdir | ||
end | ||
after :all do | ||
FileUtils.remove_entry_secure ENV['FLOCK_DIR'] | ||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
ActiveRecord::Schema.define(:version => 0) do | ||
create_table "tags", :force => true do |t| | ||
t.string "name" | ||
end | ||
create_table "tag_audits", :id => false, :force => true do |t| | ||
t.string "tag_name" | ||
end | ||
create_table "labels", :id => false, :force => true do |t| | ||
t.string "name" | ||
end | ||
end | ||
|
||
class Tag < ActiveRecord::Base | ||
after_save do | ||
TagAudit.create { |ea| ea.tag_name = name } | ||
Label.create { |ea| ea.name = name } | ||
end | ||
end | ||
|
||
class TagAudit < ActiveRecord::Base | ||
end | ||
|
||
class Label < ActiveRecord::Base | ||
end |
Oops, something went wrong.