dm-master-slave-adapter 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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +199 -0
- data/Rakefile +1 -0
- data/dm-master-slave-adapter.gemspec +30 -0
- data/lib/data_mapper/adapters/master_slave_adapter.rb +76 -0
- data/lib/data_mapper/adapters/reader_pool_adapter.rb +48 -0
- data/lib/data_mapper/master_slave_adapter/middleware/write_unbinding.rb +30 -0
- data/lib/data_mapper/master_slave_adapter/version.rb +5 -0
- data/lib/dm-master-slave-adapter.rb +4 -0
- data/spec/public/adapters/master_slave_adapter_spec.rb +135 -0
- data/spec/public/adapters/reader_pool_adapter_spec.rb +69 -0
- data/spec/public/middleware/write_unbinding_spec.rb +28 -0
- data/spec/spec_helper.rb +3 -0
- metadata +85 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Chris Corbyn
|
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,199 @@
|
|
1
|
+
# DataMapper Master/Slave Adapter (for MySQL replication etc)
|
2
|
+
|
3
|
+
This DataMapper adapter provides a thin layer infront of at least two
|
4
|
+
real DataMaper adapters, splitting reads and writes between a 'master'
|
5
|
+
and a 'slave'.
|
6
|
+
|
7
|
+
The adapter comes in two parts:
|
8
|
+
|
9
|
+
1. The MasterSlaveAdapter, which knows of only two 'real' adapters
|
10
|
+
2. A ReaderPoolAdapter, which knows of any number of 'real' adapters
|
11
|
+
to use as readers. You can set the ReaderPoolAdapter as the reader
|
12
|
+
for the MasterSlaveAdapter.
|
13
|
+
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Via rubygems:
|
18
|
+
|
19
|
+
gem install dm-master-slave-adapter
|
20
|
+
|
21
|
+
|
22
|
+
## Usage
|
23
|
+
|
24
|
+
The adapter is configured, at a basic level in the following way:
|
25
|
+
|
26
|
+
DataMapper.setup(:default, {
|
27
|
+
:adapter => :master_slave,
|
28
|
+
:master => {
|
29
|
+
:adapter => :mysql,
|
30
|
+
:host => "master.db.site.com",
|
31
|
+
:username => "root",
|
32
|
+
:password => ""
|
33
|
+
},
|
34
|
+
:slave => {
|
35
|
+
:adapter => :mysql,
|
36
|
+
:host => "slave.db.site.com",
|
37
|
+
:username => "root",
|
38
|
+
:password => ""
|
39
|
+
}
|
40
|
+
})
|
41
|
+
|
42
|
+
Here we create a repository named :default, which uses MySQL adapters for the
|
43
|
+
master and the slave.
|
44
|
+
|
45
|
+
In YAML, this looks like this:
|
46
|
+
|
47
|
+
default:
|
48
|
+
adapter: master_slave
|
49
|
+
master:
|
50
|
+
adapter: mysql
|
51
|
+
host: "master.db.site.com"
|
52
|
+
username: root
|
53
|
+
password:
|
54
|
+
slave:
|
55
|
+
adapter: mysql
|
56
|
+
host: "slave.db.site.com"
|
57
|
+
username: root
|
58
|
+
password:
|
59
|
+
|
60
|
+
Both the master and the slave are named :default, but you cannot access them directly
|
61
|
+
with DataMapper.repository( ... ); you can only access the MasterSlaveAdapter.
|
62
|
+
|
63
|
+
It is possible to access both the master and the slave using accessors on the
|
64
|
+
MasterSlaveAdapter, however:
|
65
|
+
|
66
|
+
DataMapper.repository(:default).adapter.master
|
67
|
+
DataMapper.repository(:default).adapter.slave
|
68
|
+
|
69
|
+
### Bind to master on first write
|
70
|
+
|
71
|
+
It is important to note one particular behaviour with this adapter. By design, after
|
72
|
+
the first write operation has occurred, all subsquent queries, including reads, will
|
73
|
+
be sent directly to the master. This is almost always the desirable behaviour, since
|
74
|
+
you will undoubtedly experience race conditions due to reader-lag if not.
|
75
|
+
|
76
|
+
You can force the binding to the master at any time, using:
|
77
|
+
|
78
|
+
DataMapper.repository(:default).adapter.bind_to_master
|
79
|
+
|
80
|
+
This is a state changing method and will remain in effect until you reset the binding
|
81
|
+
with:
|
82
|
+
|
83
|
+
DataMapper.repository(:default).adapter.reset_binding
|
84
|
+
|
85
|
+
In a web application, you'll typically want to reset the binding to master at the end
|
86
|
+
of each request, to ensure subsquent requests are not permanently bound to the master.
|
87
|
+
|
88
|
+
A Rack middleware is provided to do this automatically. The easiest way to use this in
|
89
|
+
a Rails application, is to mount it inside your ApplicationController:
|
90
|
+
|
91
|
+
class ApplicationController < ActionController::Base
|
92
|
+
use DataMapper::MasterSlaveAdapter::Binding, :default
|
93
|
+
end
|
94
|
+
|
95
|
+
You can use the middleware anywhere a Rack middleware can be used, however, but it must
|
96
|
+
be executed after DataMapper has been initialized.
|
97
|
+
|
98
|
+
Note that accessing the master directly, (again, by design) will not cause all subsquent
|
99
|
+
queries to be sent to the master in the same way implicit querying does. This is useful
|
100
|
+
when logic is isolated to a specific part of your application and you know other parts of
|
101
|
+
the application need not query the same storage backend. I personally do this for
|
102
|
+
session storage.
|
103
|
+
|
104
|
+
Lastly, you can force all queries to be implicitlty sent to the master in the context of
|
105
|
+
a block, simply by passing a block to #bind_to_master, like so:
|
106
|
+
|
107
|
+
DataMapper.repository(:default).adapter.bind_to_master do
|
108
|
+
...
|
109
|
+
end
|
110
|
+
|
111
|
+
Once the block has completed, the adapter will be restored to its original state,
|
112
|
+
regardless of what writes may have occurred. Note that if the adapter was already
|
113
|
+
implictly bound to master before the block was invoked, this will have no effect.
|
114
|
+
|
115
|
+
|
116
|
+
### Using the ReaderPoolAdapter
|
117
|
+
|
118
|
+
The ReaderPoolAdapter simply allows you to use more than one adapter as the 'slave' when
|
119
|
+
configuring the MasterSlaveAdapter. For every read query it receives, it picks a random
|
120
|
+
adapter from its pool.
|
121
|
+
|
122
|
+
It is configured like so:
|
123
|
+
|
124
|
+
DataMapper.setup(:default, {
|
125
|
+
:adapter => :master_slave,
|
126
|
+
:master => {
|
127
|
+
...
|
128
|
+
},
|
129
|
+
:slave => {
|
130
|
+
:adapter => :reader_pool,
|
131
|
+
:pool => [
|
132
|
+
{
|
133
|
+
:adapter => :mysql
|
134
|
+
:host => "slave1.db.site.com",
|
135
|
+
:username => "root",
|
136
|
+
:password => ""
|
137
|
+
},
|
138
|
+
{
|
139
|
+
:adapter => :mysql
|
140
|
+
:host => "slave2.db.site.com",
|
141
|
+
:username => "root",
|
142
|
+
:password => ""
|
143
|
+
}
|
144
|
+
]
|
145
|
+
}
|
146
|
+
})
|
147
|
+
|
148
|
+
In the above setup, we simply have two MySQL hosts specified as available
|
149
|
+
slaves to the MasterSlaveAdapter. In YAML, that looks like this:
|
150
|
+
|
151
|
+
default:
|
152
|
+
adapter: master_slave
|
153
|
+
master:
|
154
|
+
...
|
155
|
+
slave:
|
156
|
+
adapter: reader_pool
|
157
|
+
pool:
|
158
|
+
- adapter: mysql
|
159
|
+
host: "slave1.db.site.com"
|
160
|
+
username: root
|
161
|
+
password:
|
162
|
+
- adapter: mysql
|
163
|
+
host: "slave2.db.site.com"
|
164
|
+
username: root
|
165
|
+
password:
|
166
|
+
|
167
|
+
## Reporting Issues
|
168
|
+
|
169
|
+
Please file any issues in the issue tracker at GitHub:
|
170
|
+
|
171
|
+
- https://github.com/d11wtq/dm-master-slave-adapter/issues
|
172
|
+
|
173
|
+
## Potential TODOs
|
174
|
+
|
175
|
+
- Raise an exception for #create, #update and #delete on the reader
|
176
|
+
- Enhanced logging to include the details of the adapter being used
|
177
|
+
|
178
|
+
## Copyright and Licensing
|
179
|
+
|
180
|
+
Copyright (c) 2011 Chris Corbyn
|
181
|
+
|
182
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
183
|
+
a copy of this software and associated documentation files (the
|
184
|
+
"Software"), to deal in the Software without restriction, including
|
185
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
186
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
187
|
+
permit persons to whom the Software is furnished to do so, subject to
|
188
|
+
the following conditions:
|
189
|
+
|
190
|
+
The above copyright notice and this permission notice shall be
|
191
|
+
included in all copies or substantial portions of the Software.
|
192
|
+
|
193
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
194
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
195
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
196
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
197
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
198
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
199
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "data_mapper/master_slave_adapter/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "dm-master-slave-adapter"
|
7
|
+
s.version = DataMapper::MasterSlaveAdapter::VERSION
|
8
|
+
s.authors = ["Chris Corbyn"]
|
9
|
+
s.email = ["chris@w3style.co.uk"]
|
10
|
+
s.homepage = "https://github.com/d11wtq/dm-master-slave-adapter"
|
11
|
+
s.summary = %q{Master/Slave Adapter for DataMapper}
|
12
|
+
s.description = (<<-TEXT)
|
13
|
+
Provides the ability to use DataMapper in an environment where
|
14
|
+
database replication draws the need for using separate connections
|
15
|
+
for reading and writing data.
|
16
|
+
|
17
|
+
This adapter simply wraps two other "real" DataMapper adapters,
|
18
|
+
rather than providing any direct I/O logic
|
19
|
+
TEXT
|
20
|
+
|
21
|
+
s.rubyforge_project = "dm-master-slave-adapter"
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split("\n")
|
24
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
25
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
26
|
+
s.require_paths = ["lib"]
|
27
|
+
|
28
|
+
s.add_development_dependency "rspec"
|
29
|
+
s.add_runtime_dependency "dm-core"
|
30
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Adapters
|
5
|
+
|
6
|
+
class MasterSlaveAdapter < AbstractAdapter
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :slave
|
10
|
+
attr_reader :master
|
11
|
+
|
12
|
+
def_delegators :reader, :read, :aggregate
|
13
|
+
def_delegators :writer, :create, :update, :delete
|
14
|
+
|
15
|
+
def initialize(name, options)
|
16
|
+
super
|
17
|
+
|
18
|
+
@slave = if @options[:slave].kind_of?(AbstractAdapter)
|
19
|
+
@options[:slave]
|
20
|
+
else
|
21
|
+
assert_kind_of 'options', @options[:slave], Hash
|
22
|
+
Adapters.new(name, @options[:slave])
|
23
|
+
end
|
24
|
+
|
25
|
+
@master = if @options[:master].kind_of?(AbstractAdapter)
|
26
|
+
@options[:master]
|
27
|
+
else
|
28
|
+
assert_kind_of 'options', @options[:master], Hash
|
29
|
+
Adapters.new(name, @options[:master])
|
30
|
+
end
|
31
|
+
|
32
|
+
@reader = @slave
|
33
|
+
end
|
34
|
+
|
35
|
+
def bind_to_master
|
36
|
+
original_reader, @reader = @reader, @master
|
37
|
+
|
38
|
+
if block_given?
|
39
|
+
begin
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
@reader = original_reader
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def bound_to_master?
|
50
|
+
@reader.equal?(@master)
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset_binding
|
54
|
+
@reader = @slave
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def reader
|
61
|
+
@reader
|
62
|
+
end
|
63
|
+
|
64
|
+
def writer
|
65
|
+
bind_to_master
|
66
|
+
@master
|
67
|
+
end
|
68
|
+
|
69
|
+
def method_missing(meth, *args, &block)
|
70
|
+
writer.send(meth, *args, &block)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
const_added(:MasterSlaveAdapter)
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
module Adapters
|
5
|
+
|
6
|
+
class ReaderPoolAdapter < AbstractAdapter
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
attr_reader :pool
|
10
|
+
|
11
|
+
def_delegators :random_adapter, :create, :read, :update, :delete
|
12
|
+
|
13
|
+
def initialize(name, options)
|
14
|
+
super
|
15
|
+
|
16
|
+
assert_kind_of 'options', @options[:pool], Array
|
17
|
+
|
18
|
+
raise ArgumentError, "The are no adapters in the adapter pool" if @options[:pool].empty?
|
19
|
+
|
20
|
+
@pool = []
|
21
|
+
@options[:pool].each do |adapter_options|
|
22
|
+
adapter = if adapter_options.kind_of?(AbstractAdapter)
|
23
|
+
adapter_options
|
24
|
+
else
|
25
|
+
assert_kind_of 'pool_adapter_options', adapter_options, Hash
|
26
|
+
Adapters.new(name, adapter_options)
|
27
|
+
end
|
28
|
+
|
29
|
+
@pool.push(adapter)
|
30
|
+
end
|
31
|
+
|
32
|
+
@number_generator = Random.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(meth, *args, &block)
|
36
|
+
random_adapter.send(meth, *args, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def random_adapter
|
42
|
+
@pool[@number_generator.rand(0...@pool.length)]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
const_added(:ReaderPoolAdapter)
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
=begin
|
2
|
+
Use this Rack middleware after your DataMapper repositories have been set up.
|
3
|
+
|
4
|
+
It will ensure the binding to master is reset at the end of every request.
|
5
|
+
|
6
|
+
use DataMapper::MasterSlaveAdapter::Middleware::WriteUnbinding, :your_repository
|
7
|
+
|
8
|
+
If you are using Rails, it is possible to do this from inside of your ApplicationController.
|
9
|
+
=end
|
10
|
+
module DataMapper
|
11
|
+
module MasterSlaveAdapter
|
12
|
+
module Middleware
|
13
|
+
|
14
|
+
class WriteUnbinding
|
15
|
+
def initialize(app, name = :default)
|
16
|
+
@app = app
|
17
|
+
@name = name.to_sym
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
@app.call(env)
|
22
|
+
ensure
|
23
|
+
adapter = DataMapper.repository(@name).adapter
|
24
|
+
adapter.reset_binding if adapter.respond_to?(:reset_binding)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DataMapper::Adapters::MasterSlaveAdapter do
|
4
|
+
before(:each) do
|
5
|
+
@master = double(:kind_of? => true)
|
6
|
+
@slave = double(:kind_of? => true)
|
7
|
+
@args = stub()
|
8
|
+
@result = stub()
|
9
|
+
|
10
|
+
@adapter = DataMapper::Adapters::MasterSlaveAdapter.new(:test, {
|
11
|
+
:master => @master,
|
12
|
+
:slave => @slave
|
13
|
+
})
|
14
|
+
end
|
15
|
+
|
16
|
+
context "delegation" do
|
17
|
+
it "sends all reads to the slave" do
|
18
|
+
@slave.should_receive(:read).with(@args).and_return(@result)
|
19
|
+
@adapter.read(@args).should be(@result)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "sends aggregate queries to the slave" do
|
23
|
+
@slave.should_receive(:aggregate).with(@args).and_return(@result)
|
24
|
+
@adapter.aggregate(@args).should be(@result)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "sends create to the master" do
|
28
|
+
@master.should_receive(:create).with(@args).and_return(@result)
|
29
|
+
@adapter.create(@args).should be(@result)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "sends update to the master" do
|
33
|
+
@master.should_receive(:update).with(@args).and_return(@result)
|
34
|
+
@adapter.update(@args).should be(@result)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "sends destroy to the master" do
|
38
|
+
@master.should_receive(:destroy).with(@args).and_return(@result)
|
39
|
+
@adapter.destroy(@args).should be(@result)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "sends any unknown method to the master" do
|
43
|
+
@master.should_receive(:prepare_statement).with(@args).and_return(@result)
|
44
|
+
@adapter.prepare_statement(@args).should be(@result)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "provides direct access to the master" do
|
48
|
+
@adapter.master.should == @master
|
49
|
+
end
|
50
|
+
|
51
|
+
it "provides direct access to the slave" do
|
52
|
+
@adapter.slave.should == @slave
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "state" do
|
57
|
+
it "allows binding reads to the master" do
|
58
|
+
@adapter.bind_to_master
|
59
|
+
@master.should_receive(:read).with(@args).and_return(@result)
|
60
|
+
@adapter.read(@args).should be(@result)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "reports if it is bound to master" do
|
64
|
+
@adapter.bind_to_master
|
65
|
+
@adapter.should be_bound_to_master
|
66
|
+
end
|
67
|
+
|
68
|
+
it "binds all reads to the master after the first write" do
|
69
|
+
@master.should_receive(:update)
|
70
|
+
@master.should_receive(:read).with(@args).and_return(@result)
|
71
|
+
@adapter.update(stub())
|
72
|
+
@adapter.read(@args).should be(@result)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "can be unbound from master" do
|
76
|
+
@adapter.bind_to_master
|
77
|
+
@adapter.reset_binding
|
78
|
+
@slave.should_receive(:read).with(@args).and_return(@result)
|
79
|
+
@adapter.read(@args).should be(@result)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "does not remain bound to master when using the adapter directly" do
|
83
|
+
@master.stub(:execute => nil)
|
84
|
+
@adapter.master.execute(stub())
|
85
|
+
@adapter.should_not be_bound_to_master
|
86
|
+
end
|
87
|
+
|
88
|
+
it "can be bound to master in the context of a block" do
|
89
|
+
@master.should_receive(:read).with(@args).and_return(@result)
|
90
|
+
@adapter.bind_to_master do
|
91
|
+
@adapter.read(@args).should be(@result)
|
92
|
+
end
|
93
|
+
@adapter.should_not be_bound_to_master
|
94
|
+
end
|
95
|
+
|
96
|
+
it "does not unbind from master after binding for a block if it was already bound before the block" do
|
97
|
+
@adapter.bind_to_master
|
98
|
+
@master.should_receive(:read).with(@args).and_return(@result)
|
99
|
+
@adapter.bind_to_master do
|
100
|
+
@adapter.read(@args).should be(@result)
|
101
|
+
end
|
102
|
+
@adapter.should be_bound_to_master
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "configured with an options hash" do
|
107
|
+
it "delegates to DataMapper::Adapters to create a master and a slave of the same name" do
|
108
|
+
DataMapper::Adapters.should_receive(:new).with(:test, { :adapter => :test_slave }).and_return(@slave)
|
109
|
+
DataMapper::Adapters.should_receive(:new).with(:test, { :adapter => :test_master }).and_return(@master)
|
110
|
+
|
111
|
+
adapter = DataMapper::Adapters::MasterSlaveAdapter.new(:test, {
|
112
|
+
:master => { :adapter => :test_master },
|
113
|
+
:slave => { :adapter => :test_slave }
|
114
|
+
})
|
115
|
+
|
116
|
+
adapter.master.should be(@master)
|
117
|
+
adapter.slave.should be(@slave)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "configured with already instantiated adapters" do
|
122
|
+
it "uses the provided adapters directly" do
|
123
|
+
@master.should_receive(:kind_of?).with(DataMapper::Adapters::AbstractAdapter).and_return(true)
|
124
|
+
@slave.should_receive(:kind_of?).with(DataMapper::Adapters::AbstractAdapter).and_return(true)
|
125
|
+
|
126
|
+
adapter = DataMapper::Adapters::MasterSlaveAdapter.new(:test, {
|
127
|
+
:master => @master,
|
128
|
+
:slave => @slave
|
129
|
+
})
|
130
|
+
|
131
|
+
adapter.master.should be(@master)
|
132
|
+
adapter.slave.should be(@slave)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DataMapper::Adapters::ReaderPoolAdapter do
|
4
|
+
before(:each) do
|
5
|
+
@random_number = double()
|
6
|
+
Random.stub(:new).and_return(@random_number)
|
7
|
+
|
8
|
+
@delegate_a = double(:delegate_a, :kind_of? => true)
|
9
|
+
@delegate_b = double(:delegate_b, :kind_of? => true)
|
10
|
+
@args = stub()
|
11
|
+
@result = stub()
|
12
|
+
|
13
|
+
@adapter = DataMapper::Adapters::ReaderPoolAdapter.new(:test, {
|
14
|
+
:pool => [@delegate_a, @delegate_b]
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
context "delegation" do
|
19
|
+
it "sends CRUD operations to a random adapter from the pool" do
|
20
|
+
@random_number.should_receive(:rand).exactly(4).times.and_return(1)
|
21
|
+
@delegate_b.should_receive(:create).with(@args).and_return(@result)
|
22
|
+
@delegate_b.should_receive(:read).with(@args).and_return(@result)
|
23
|
+
@delegate_b.should_receive(:update).with(@args).and_return(@result)
|
24
|
+
@delegate_b.should_receive(:delete).with(@args).and_return(@result)
|
25
|
+
@adapter.create(@args).should be(@result)
|
26
|
+
@adapter.read(@args).should be(@result)
|
27
|
+
@adapter.update(@args).should be(@result)
|
28
|
+
@adapter.delete(@args).should be(@result)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "sends aggregate queries to a random adapter from the pool" do
|
32
|
+
@random_number.should_receive(:rand).once.and_return(0)
|
33
|
+
@delegate_a.should_receive(:aggregate).with(@args).and_return(@result)
|
34
|
+
@adapter.aggregate(@args).should be(@result)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "sends unknown methods to a random adapter from the pool" do
|
38
|
+
@random_number.should_receive(:rand).once.and_return(0)
|
39
|
+
@delegate_a.should_receive(:select).with(@args).and_return(@result)
|
40
|
+
@adapter.select(@args).should be(@result)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "configured with an options hash" do
|
45
|
+
it "delegates to DataMapper::Adapters to create a pool of readers of the same name" do
|
46
|
+
DataMapper::Adapters.should_receive(:new).with(:test, { :adapter => :test_adapter }).and_return(@delegate_a)
|
47
|
+
|
48
|
+
adapter = DataMapper::Adapters::ReaderPoolAdapter.new(:test, {
|
49
|
+
:pool => [{ :adapter => :test_adapter }]
|
50
|
+
})
|
51
|
+
|
52
|
+
adapter.pool.should include(@delegate_a)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "configured with already instantiated adapters" do
|
57
|
+
it "uses the adapters directly" do
|
58
|
+
@delegate_a.should_receive(:kind_of?).with(DataMapper::Adapters::AbstractAdapter).and_return(true)
|
59
|
+
@delegate_b.should_receive(:kind_of?).with(DataMapper::Adapters::AbstractAdapter).and_return(true)
|
60
|
+
|
61
|
+
adapter = DataMapper::Adapters::ReaderPoolAdapter.new(:test, {
|
62
|
+
:pool => [@delegate_a, @delegate_b]
|
63
|
+
})
|
64
|
+
|
65
|
+
adapter.pool.should include(@delegate_a)
|
66
|
+
adapter.pool.should include(@delegate_b)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DataMapper::MasterSlaveAdapter::Middleware::WriteUnbinding do
|
4
|
+
let(:app) { double(:call => nil) }
|
5
|
+
let(:env) { stub() }
|
6
|
+
let(:middleware) { DataMapper::MasterSlaveAdapter::Middleware::WriteUnbinding.new(app, :test) }
|
7
|
+
let(:adapter) { double(:reset_binding => nil) }
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
DataMapper.should_receive(:repository).with(:test).and_return(stub(:adapter => adapter))
|
11
|
+
end
|
12
|
+
|
13
|
+
it "invokes the application" do
|
14
|
+
app.should_receive(:call).with(env)
|
15
|
+
middleware.call(env)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "resets the adapter binding at the end of the request" do
|
19
|
+
adapter.should_receive(:reset_binding)
|
20
|
+
middleware.call(env)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "ensures the binding is reset when an error occurs" do
|
24
|
+
app.stub(:call) { raise "An error" }
|
25
|
+
adapter.should_receive(:reset_binding)
|
26
|
+
middleware.call(env) rescue Exception
|
27
|
+
end
|
28
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dm-master-slave-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Corbyn
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-15 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &16062280 !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: *16062280
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: dm-core
|
27
|
+
requirement: &16061600 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *16061600
|
36
|
+
description: ! " Provides the ability to use DataMapper in an environment where\n
|
37
|
+
\ database replication draws the need for using separate connections\n for
|
38
|
+
reading and writing data.\n\n This adapter simply wraps two other \"real\" DataMapper
|
39
|
+
adapters,\n rather than providing any direct I/O logic\n"
|
40
|
+
email:
|
41
|
+
- chris@w3style.co.uk
|
42
|
+
executables: []
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- .gitignore
|
47
|
+
- Gemfile
|
48
|
+
- LICENSE
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- dm-master-slave-adapter.gemspec
|
52
|
+
- lib/data_mapper/adapters/master_slave_adapter.rb
|
53
|
+
- lib/data_mapper/adapters/reader_pool_adapter.rb
|
54
|
+
- lib/data_mapper/master_slave_adapter/middleware/write_unbinding.rb
|
55
|
+
- lib/data_mapper/master_slave_adapter/version.rb
|
56
|
+
- lib/dm-master-slave-adapter.rb
|
57
|
+
- spec/public/adapters/master_slave_adapter_spec.rb
|
58
|
+
- spec/public/adapters/reader_pool_adapter_spec.rb
|
59
|
+
- spec/public/middleware/write_unbinding_spec.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
homepage: https://github.com/d11wtq/dm-master-slave-adapter
|
62
|
+
licenses: []
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubyforge_project: dm-master-slave-adapter
|
81
|
+
rubygems_version: 1.8.10
|
82
|
+
signing_key:
|
83
|
+
specification_version: 3
|
84
|
+
summary: Master/Slave Adapter for DataMapper
|
85
|
+
test_files: []
|