barricade 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.md +98 -0
- data/init.rb +1 -0
- data/lib/barricade.rb +99 -0
- data/spec/barricade_spec.rb +68 -0
- data/spec/schema.rb +7 -0
- data/spec/spec_helper.rb +8 -0
- metadata +71 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Envato & Pete Yandell
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
Barricade
|
2
|
+
=========
|
3
|
+
|
4
|
+
Better locking for ActiveRecord.
|
5
|
+
|
6
|
+
|
7
|
+
Installation
|
8
|
+
------------
|
9
|
+
|
10
|
+
gem install barricade
|
11
|
+
|
12
|
+
Don’t forget to add it to your environment.rb or Gemfile.
|
13
|
+
|
14
|
+
|
15
|
+
Usage
|
16
|
+
-----
|
17
|
+
|
18
|
+
ActiveRecord provides the `lock!` method, but it's not very robust
|
19
|
+
for anything beyond really simple locks.
|
20
|
+
|
21
|
+
This plugin provides a couple of useful methods:
|
22
|
+
|
23
|
+
Post.transaction_with_locks(post, user) do
|
24
|
+
...
|
25
|
+
end
|
26
|
+
|
27
|
+
This starts a new transaction, and immediately locks all the passed
|
28
|
+
in objects.
|
29
|
+
|
30
|
+
The transaction *MUST* be the outermost transaction, not a nested
|
31
|
+
transaction, otherwise `transaction_with_locks` will raise a
|
32
|
+
`Barricade::LockMustBeOutermostTransaction` exception.
|
33
|
+
|
34
|
+
It sorts the locked objects before locking, to help avoid deadlocks. If a
|
35
|
+
deadlock does occur, it retries the locks and continues with the
|
36
|
+
transaction.
|
37
|
+
|
38
|
+
Within the transaction block, you can raise a
|
39
|
+
`Barricade::RetryTransaction` exception to retry the transaction
|
40
|
+
from the beginning and make sure all the locks are in place.
|
41
|
+
|
42
|
+
You can double-check that you have a lock on an object by calling
|
43
|
+
its `confirm_locked!` method. This raises a `Barricade::LockNotHeld`
|
44
|
+
exception if you don't have the lock.
|
45
|
+
|
46
|
+
It's safe to re-acquire a lock inside an existing transaction, so
|
47
|
+
the following will work:
|
48
|
+
|
49
|
+
Post.transaction_with_locks(post) do
|
50
|
+
Post.transaction_with_locks(post) do
|
51
|
+
...
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
but this will raise an exception:
|
56
|
+
|
57
|
+
Post.transaction_with_locks(post) do
|
58
|
+
Post.transaction_with_locks(user) do
|
59
|
+
...
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
Background
|
65
|
+
----------
|
66
|
+
|
67
|
+
There are a few things you need to know to understand the difficulty of
|
68
|
+
using ActiveRecord for locking.
|
69
|
+
|
70
|
+
InnoDB is pretty good at detected and flagging deadlocks.
|
71
|
+
(See [Deadlock Detection and Rollback](http://dev.mysql.com/doc/refman/5.1/en/innodb-deadlock-detection.html))
|
72
|
+
|
73
|
+
Any attempt to grab an exclusive lock on a record can result in a
|
74
|
+
deadlock, and a deadlock causes a couple of things to happen:
|
75
|
+
|
76
|
+
1. MySQL will roll back the outermost current transaction.
|
77
|
+
|
78
|
+
2. ActiveRecord will throw a `ActiveRecord::StatementInvalid` exception
|
79
|
+
|
80
|
+
The end result is that your ActiveRecord objects may end up out of sync
|
81
|
+
with their corresponding database records.
|
82
|
+
|
83
|
+
Barricade avoids this by doing all the locking at the very start of
|
84
|
+
the transaction. A deadlock when grabbing the locks will cause an
|
85
|
+
immediate retry, before any code that has side effects can be run.
|
86
|
+
|
87
|
+
The downside is that you have to do your locking in the outermost
|
88
|
+
transaction, which can make it difficult to encapsulate logic in
|
89
|
+
your model without placing restrictions on where your model can be
|
90
|
+
called from.
|
91
|
+
|
92
|
+
|
93
|
+
Credits
|
94
|
+
-------
|
95
|
+
|
96
|
+
Copyright © 2010 [Envato](http://envato.com).
|
97
|
+
Initially developed by [Pete Yandell](http://notahat.com).
|
98
|
+
Released under an MIT license.
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'barricade'
|
data/lib/barricade.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
# Methods defined here are included as instance methods on ActiveRecord::Base.
|
2
|
+
module Barricade
|
3
|
+
|
4
|
+
def self.configuration #:nodoc:
|
5
|
+
@configuration ||= Configuration.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.configure
|
9
|
+
yield(configuration)
|
10
|
+
end
|
11
|
+
|
12
|
+
class Configuration
|
13
|
+
# Set this in your tests if you're using transactional_fixtures, so
|
14
|
+
# Barricade will know not to complain about a containing
|
15
|
+
# transaction when you call transaction_with_locks.
|
16
|
+
attr_accessor :running_inside_transactional_fixtures
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@running_inside_transactional_fixtures = false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.included(base) #:nodoc:
|
24
|
+
base.extend(ClassMethods)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Confirms that this object has been locked in an enclosing call to
|
28
|
+
# transaction_with_locks. Raises LockNotHeld if the lock isn't held.
|
29
|
+
def confirm_locked!
|
30
|
+
raise LockNotHeld unless ActiveRecord::Base.locked_objects.include?(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Methods defined here are included as class methods on ActiveRecord::Base.
|
34
|
+
module ClassMethods
|
35
|
+
# Perform a transaction with the given ActiveRecord objects locked.
|
36
|
+
#
|
37
|
+
# e.g.
|
38
|
+
# Post.transaction_with_locks(post) do
|
39
|
+
# post.comments.create!(...)
|
40
|
+
# end
|
41
|
+
def transaction_with_locks(*objects)
|
42
|
+
objects = objects.flatten.compact
|
43
|
+
return if objects.all? {|object| ActiveRecord::Base.locked_objects.include?(object) }
|
44
|
+
|
45
|
+
minimum_transaction_level = Barricade.configuration.running_inside_transactional_fixtures ? 1 : 0
|
46
|
+
raise LockMustBeOutermostTransaction unless connection.open_transactions == minimum_transaction_level
|
47
|
+
|
48
|
+
objects.sort_by {|object| [object.class.name, object.send(object.class.primary_key)] }
|
49
|
+
begin
|
50
|
+
ActiveRecord::Base.locked_objects = nil
|
51
|
+
transaction do
|
52
|
+
begin
|
53
|
+
objects.each(&:lock!)
|
54
|
+
ActiveRecord::Base.locked_objects = objects
|
55
|
+
rescue ActiveRecord::StatementInvalid => exception
|
56
|
+
if exception.message =~ /Deadlock/
|
57
|
+
raise RetryTransaction
|
58
|
+
else
|
59
|
+
raise
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
yield
|
64
|
+
end
|
65
|
+
rescue RetryTransaction
|
66
|
+
retry
|
67
|
+
ensure
|
68
|
+
ActiveRecord::Base.locked_objects = nil
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def locked_objects=(objects) #:nodoc:
|
73
|
+
@locked_objects = objects
|
74
|
+
end
|
75
|
+
|
76
|
+
def locked_objects #:nodoc:
|
77
|
+
@locked_objects || []
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Raised when transaction_with_locks is called inside an existing transaction.
|
82
|
+
class LockMustBeOutermostTransaction < RuntimeError
|
83
|
+
end
|
84
|
+
|
85
|
+
# Raised when confirm_locked! is called on an object that's not locked.
|
86
|
+
class LockNotHeld < RuntimeError
|
87
|
+
end
|
88
|
+
|
89
|
+
# Raise this to retry the current transaction from the beginning.
|
90
|
+
class RetryTransaction < RuntimeError
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
module ActiveRecord #:nodoc:
|
96
|
+
class Base #:nodoc:#
|
97
|
+
include Barricade
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'active_record'
|
3
|
+
require 'barricade'
|
4
|
+
|
5
|
+
describe Barricade do
|
6
|
+
|
7
|
+
before(:suite) do
|
8
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/spec.log")
|
9
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :database => ":memory:")
|
10
|
+
load(File.dirname(__FILE__) + "/schema.rb")
|
11
|
+
end
|
12
|
+
|
13
|
+
class Post < ActiveRecord::Base
|
14
|
+
end
|
15
|
+
|
16
|
+
before(:all) do
|
17
|
+
@post = Post.create!
|
18
|
+
@another_post = Post.create!
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should not allow locking inside a transaction" do
|
22
|
+
Post.transaction do
|
23
|
+
lambda do
|
24
|
+
Post.transaction_with_locks(@post) { }
|
25
|
+
end.should raise_error(Barricade::LockMustBeOutermostTransaction)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should not allow locking additional objects inside an existing locked transaction" do
|
30
|
+
Post.transaction_with_locks(@post) do
|
31
|
+
lambda do
|
32
|
+
Post.transaction_with_locks(@another_post) { }
|
33
|
+
end.should raise_error(Barricade::LockMustBeOutermostTransaction)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should allow locking an object if a lock is already set on that object" do
|
38
|
+
Post.transaction_with_locks(@post) do
|
39
|
+
lambda do
|
40
|
+
Post.transaction_with_locks(@post) { }
|
41
|
+
end.should_not raise_error
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should mark an object as locked inside a transaction with locks" do
|
46
|
+
lambda { @post.confirm_locked! }.should raise_error(Barricade::LockNotHeld)
|
47
|
+
Post.transaction_with_locks(@post) do
|
48
|
+
lambda { @post.confirm_locked! }.should_not raise_error
|
49
|
+
end
|
50
|
+
lambda { @post.confirm_locked! }.should raise_error(Barricade::LockNotHeld)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should flatten and compact the list of objects to be locked" do
|
54
|
+
Post.transaction_with_locks(@post, @another_post) do
|
55
|
+
ActiveRecord::Base.locked_objects.should == [@post, @another_post]
|
56
|
+
end
|
57
|
+
Post.transaction_with_locks([@post, @another_post]) do
|
58
|
+
ActiveRecord::Base.locked_objects.should == [@post, @another_post]
|
59
|
+
end
|
60
|
+
Post.transaction_with_locks([@post, [@another_post]]) do
|
61
|
+
ActiveRecord::Base.locked_objects.should == [@post, @another_post]
|
62
|
+
end
|
63
|
+
Post.transaction_with_locks([@post, nil, @another_post]) do
|
64
|
+
ActiveRecord::Base.locked_objects.should == [@post, @another_post]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
data/spec/schema.rb
ADDED
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: barricade
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Pete Yandell
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-04-22 00:00:00 +10:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Makes ActiveRecord locking more secure and robust
|
22
|
+
email: pete@envato.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- LICENSE
|
29
|
+
- README.md
|
30
|
+
files:
|
31
|
+
- README.md
|
32
|
+
- init.rb
|
33
|
+
- lib/barricade.rb
|
34
|
+
- spec/barricade_spec.rb
|
35
|
+
- spec/schema.rb
|
36
|
+
- spec/spec_helper.rb
|
37
|
+
- LICENSE
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://github.com/envato/barricade
|
40
|
+
licenses: []
|
41
|
+
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --charset=UTF-8
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.6
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Better ActiveRecord locking
|
68
|
+
test_files:
|
69
|
+
- spec/barricade_spec.rb
|
70
|
+
- spec/schema.rb
|
71
|
+
- spec/spec_helper.rb
|