fatalistic 0.0.1

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.
@@ -0,0 +1,12 @@
1
+ Gemfile
2
+ Gemfile.lock
3
+ doc
4
+ docs
5
+ pkg
6
+ .DS_Store
7
+ coverage
8
+ .yardoc
9
+ *.gem
10
+ *.sqlite3
11
+ *.rbc
12
+ *.lock
@@ -0,0 +1,96 @@
1
+ # Fatalistic
2
+
3
+ Fatalistic is a Ruby gem that adds table-level locking to Active Record.
4
+
5
+ ## Table locks
6
+
7
+ Table-level locks can be used to restrict read and write access to a table.
8
+ Neither Postgres nor MySQL currently support truly serializabile transactions,
9
+ so table locks are sometimes necessary to reliably avoid the "phantom record"
10
+ problem. See this [Wikipedia
11
+ article](http://en.wikipedia.org/wiki/Isolation_\(database_systems\)#Isolation_Levels.2C_Read_Phenomena_and_Locks)
12
+ for more details.
13
+
14
+ The [MySQL
15
+ docs](http://dev.mysql.com/doc/refman/5.1/en/lock-tables-restrictions.html) show
16
+ a classic usage scenario for table locking:
17
+
18
+ LOCK TABLES trans READ, customer WRITE;
19
+ SELECT SUM(value) FROM trans WHERE customer_id=some_id;
20
+ UPDATE customer
21
+ SET total_value=sum_from_previous_statement
22
+ WHERE customer_id=some_id;
23
+ UNLOCK TABLES;
24
+
25
+
26
+ Table-level locks are generally best avoided when possible because of their
27
+ potential impact on performance. MySQL/Innodb's locking implementation is also
28
+ clunky and fraught with bizarre behaviors, [particularly when used with
29
+ transactions](http://dev.mysql.com/doc/refman/5.1/en/lock-tables-and-transactions.html).
30
+ Before relying on table locks, see if there's some other way to accomplish your
31
+ goal. However, they can be useful when used sparingly.
32
+
33
+ ## Doesn't Active Record already support locking?
34
+
35
+ Active Record supports row-level locking, but not table locking.
36
+
37
+ If you do something like `Person.lock` with Active Record will emit the query
38
+ `SELECT * FROM people FOR UPDATE`. This is bad for performance because if you
39
+ have a lot of rows, it's going to be very slow. It's also nearly useless,
40
+ because it still doesn't prevent new records from being inserted. Finally, it's
41
+ foolish because if you want to lock every row in a table, it makes much more
42
+ sense to lock the table itself.
43
+
44
+ Active Record comes with 2 locking modules: optimistic and pessimistic. Since
45
+ this locking mode is the most "extreme" of the three, I've named it
46
+ "fatalistic."
47
+
48
+ ## What Fatalistic does
49
+
50
+ MySQL and Postgres use the same row locking syntax, but quite different table
51
+ locking syntax. This library provides the abstraction needed to use them both
52
+ with Active Record. SQLite does not support table level locking, so the methods
53
+ provided here are just no-ops with SQLite.
54
+
55
+ Fatalistic changes the behavior of the top-level `lock` method so that
56
+ `Person.lock` will lock the entire table, but `Person.where(...).lock` will
57
+ continue to lock just the selected records.
58
+
59
+ ## Example
60
+
61
+ class Person < ActiveRecord::Base
62
+ end
63
+
64
+ Person.lock do
65
+ # your code here
66
+ end
67
+
68
+
69
+ ## Getting it
70
+
71
+ Just install with RubyGems:
72
+
73
+ gem install fatalistic
74
+
75
+ The source code is [on Github](https://github.com/bvision/fatalistic).
76
+
77
+ ## License
78
+
79
+ Copyright (c) 2011-2012 Norman Clarke and Business Vision SA
80
+
81
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
82
+ this software and associated documentation files (the "Software"), to deal in
83
+ the Software without restriction, including without limitation the rights to
84
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
85
+ the Software, and to permit persons to whom the Software is furnished to do so,
86
+ subject to the following conditions:
87
+
88
+ The above copyright notice and this permission notice shall be included in all
89
+ copies or substantial portions of the Software.
90
+
91
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
92
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
93
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
94
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
95
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
96
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ require "rake/testtask"
2
+
3
+ task :default => :test
4
+
5
+ Rake::TestTask.new {|t| t.test_files = ['test/test.rb']}
6
+
7
+ task :clean do
8
+ sh "rm -rf *.gem doc pkg coverage `find . -name '*.rbc'`"
9
+ end
10
+
11
+ task :gem do
12
+ sh "gem build fatalistic.gemspec"
13
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path("../lib/fatalistic", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "fatalistic"
5
+ s.version = Fatalistic::VERSION
6
+ s.authors = ["Norman Clarke"]
7
+ s.email = ["norman@njclarke.com"]
8
+ s.homepage = "http://github.com/bvision/fatalistic"
9
+ s.summary = "Table-level locking for Active Record"
10
+ s.files = `git ls-files`.split("\n")
11
+ s.test_files = `git ls-files -- {test}/*`.split("\n")
12
+ s.require_paths = ["lib"]
13
+
14
+ s.add_development_dependency "minitest"
15
+
16
+ s.description = <<-EOM
17
+ Active Record provides "optimistic" and "pessimistic" modules for row-level
18
+ locking, but provide nothing to do full-table locking. Fatalistic provides this.
19
+ EOM
20
+ end
@@ -0,0 +1,37 @@
1
+ require "active_record"
2
+ require "fatalistic"
3
+
4
+ module ActiveRecord
5
+ module Locking
6
+ module Fatalistic
7
+ # Performs a table-level lock. If this method is invoked with a block,
8
+ # then a new transaction is begun, and the table is locked inside the
9
+ # transaction. If invoked without a block, then it simple emits the
10
+ # appropriate +LOCK TABLE+ statement.
11
+ def lock(lock_statement = nil, &block)
12
+ locker = ::Fatalistic::Locker.for(self)
13
+ if block_given?
14
+ transaction do
15
+ begin
16
+ locker.lock(lock_statement)
17
+ yield
18
+ ensure
19
+ locker.unlock
20
+ end
21
+ end
22
+ else
23
+ locker.lock(lock_statement)
24
+ end
25
+ end
26
+
27
+ # Unlock the table. This is only needed by MySQL. If you called +lock+
28
+ # with a block, then this is invoked for you automatically.
29
+ def unlock
30
+ ::Fatalistic::Locker.for(self).unlock
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ ActiveRecord::Base.method(:lock).owner.send :remove_method, :lock
37
+ ActiveRecord::Base.extend ActiveRecord::Locking::Fatalistic
@@ -0,0 +1,66 @@
1
+ require "forwardable"
2
+
3
+ # Table locking for Active Record.
4
+ module Fatalistic
5
+
6
+ VERSION = "0.0.1"
7
+
8
+ # This class provides syntax abstraction for table locking. If this
9
+ # functionality is ever added to Active Record, the final code will end up
10
+ # looking much different: it's currently set up with as much functionality
11
+ # outside AR as possible, in order to simplify testing and reduce
12
+ # dependencies.
13
+ class Locker
14
+ extend Forwardable
15
+ attr :model_class
16
+ def_delegators :model_class, :connection, :quoted_table_name
17
+
18
+ # Factory method to get an instance of the appropriate Locker subclass.
19
+ def self.for(model_class)
20
+ adapter_name = model_class.connection.adapter_name.downcase
21
+ klass = if adapter_name.index("post")
22
+ PostgresLocker
23
+ elsif adapter_name.index("my")
24
+ MySQLLocker
25
+ else
26
+ self
27
+ end
28
+ klass.new(model_class)
29
+ end
30
+
31
+ def initialize(model_class)
32
+ @model_class = model_class
33
+ end
34
+
35
+ # Lock the table.
36
+ def lock(lock_statement = nil)
37
+ end
38
+
39
+ # Unlock the table. This is a no-op on all databases other than MySQL.
40
+ def unlock
41
+ end
42
+ end
43
+
44
+ # Table locking for Postgres.
45
+ class PostgresLocker < Locker
46
+ def lock(lock_statement = nil)
47
+ stmt = "LOCK TABLE #{quoted_table_name}"
48
+ stmt.insert(-1, " #{lock_statement}") if lock_statement
49
+ connection.execute(stmt)
50
+ end
51
+ end
52
+
53
+ # Table locking for MySQL.
54
+ class MySQLLocker < Locker
55
+ def lock(lock_statement = nil)
56
+ stmt = "LOCK TABLES #{quoted_table_name}"
57
+ lock_statement ||= "WRITE"
58
+ stmt.insert(-1, " #{lock_statement}")
59
+ connection.execute(stmt)
60
+ end
61
+
62
+ def unlock
63
+ connection.execute("UNLOCK TABLES")
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,72 @@
1
+ require "rubygems"
2
+
3
+ $LOAD_PATH << File.expand_path("../../lib", __FILE__)
4
+ $LOAD_PATH.uniq!
5
+
6
+ require "minitest/spec"
7
+ require "minitest/autorun"
8
+ require "fatalistic"
9
+
10
+ class MockModel
11
+ attr_accessor :connection
12
+
13
+ def quoted_table_name
14
+ "dummy_table"
15
+ end
16
+ end
17
+
18
+ describe Fatalistic::PostgresLocker do
19
+
20
+ describe "#lock" do
21
+
22
+ before do
23
+ @model = MockModel.new
24
+ @model.connection = MiniTest::Mock.new
25
+ @locker = Fatalistic::PostgresLocker.new(@model)
26
+ end
27
+
28
+ it "should execute a default lock statement" do
29
+ @model.connection.expect :execute, 'LOCK TABLE "dummy_table"', [String]
30
+ @locker.lock
31
+ assert @model.connection.verify
32
+ end
33
+
34
+ it "should execute a modified lock statement" do
35
+ @model.connection.expect :execute, 'LOCK TABLE "dummy_table" foo', [String]
36
+ @locker.lock "foo"
37
+ assert @model.connection.verify
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ describe Fatalistic::MySQLLocker do
44
+
45
+ before do
46
+ @model = MockModel.new
47
+ @model.connection = MiniTest::Mock.new
48
+ @locker = Fatalistic::MySQLLocker.new(@model)
49
+ end
50
+
51
+ describe "#lock" do
52
+ it "should execute a default lock statement" do
53
+ @model.connection.expect :execute, 'LOCK TABLES "dummy_table" WRITE', [String]
54
+ @locker.lock
55
+ assert @model.connection.verify
56
+ end
57
+
58
+ it "should execute a modified lock statement" do
59
+ @model.connection.expect :execute, 'LOCK TABLES "dummy_table" foo', [String]
60
+ @locker.lock "foo"
61
+ assert @model.connection.verify
62
+ end
63
+ end
64
+
65
+ describe "#unlock" do
66
+ it "should execute an unlock statement" do
67
+ @model.connection.expect :execute, 'UNLOCK TABLES', [String]
68
+ @locker.unlock
69
+ assert @model.connection.verify
70
+ end
71
+ end
72
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fatalistic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Norman Clarke
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: minitest
16
+ requirement: &70320304314940 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70320304314940
25
+ description: ! 'Active Record provides "optimistic" and "pessimistic" modules for
26
+ row-level
27
+
28
+ locking, but provide nothing to do full-table locking. Fatalistic provides this.
29
+
30
+ '
31
+ email:
32
+ - norman@njclarke.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - README.md
39
+ - Rakefile
40
+ - fatalistic.gemspec
41
+ - lib/active_record/locking/fatalistic.rb
42
+ - lib/fatalistic.rb
43
+ - test/test.rb
44
+ homepage: http://github.com/bvision/fatalistic
45
+ licenses: []
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubyforge_project:
64
+ rubygems_version: 1.8.10
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: Table-level locking for Active Record
68
+ test_files: []
69
+ has_rdoc: