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 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
@@ -0,0 +1,7 @@
1
+ ActiveRecord::Schema.define(:version => 0) do
2
+ create_table :posts, :force => true do |t|
3
+ t.column :title, :string
4
+ t.column :body, :text
5
+ end
6
+ end
7
+
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
2
+ require 'rubygems'
3
+ require 'test/unit'
4
+ require 'spec'
5
+
6
+ Spec::Runner.configure do |config|
7
+ end
8
+
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