master_slave_adapter 0.2.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/.gitignore +5 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +3 -0
- data/LICENSE +23 -0
- data/Rakefile +10 -0
- data/Readme.md +181 -0
- data/TODO.txt +8 -0
- data/VERSION +1 -0
- data/lib/active_record/connection_adapters/master_slave_adapter.rb +1 -0
- data/lib/active_record/connection_adapters/mysql_master_slave_adapter.rb +1 -0
- data/lib/master_slave_adapter.rb +624 -0
- data/master_slave_adapter.gemspec +25 -0
- data/spec/master_slave_adapter_spec.rb +563 -0
- metadata +111 -0
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Copyright (c) 2011 Maurício Linhares,
|
2
|
+
Torsten Curdt,
|
3
|
+
Kim Altintop,
|
4
|
+
Omid Aladini,
|
5
|
+
SoundCloud Ltd
|
6
|
+
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
9
|
+
in the Software without restriction, including without limitation the rights
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
12
|
+
furnished to do so, subject to the following conditions:
|
13
|
+
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
15
|
+
copies or substantial portions of the Software.
|
16
|
+
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
23
|
+
SOFTWARE.
|
data/Rakefile
ADDED
data/Readme.md
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
# Replication Aware Master Slave Adapter [][6]
|
2
|
+
|
3
|
+
Improved version of the [master_slave_adapter plugin][1], packaged as a gem.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
1. automatic selection of master or slave connection: `with_consistency`
|
8
|
+
2. manual selection of master or slave connection: `with_master`, `with_slave`
|
9
|
+
3. transaction callbacks: `on_commit`, `on_rollback`
|
10
|
+
4. also:
|
11
|
+
* support for multiple slaves
|
12
|
+
* (partial) support for [database_cleaner][2]
|
13
|
+
|
14
|
+
### Automatic Selection of Master or Slave
|
15
|
+
|
16
|
+
* _note that this feature currently only works with MySQL_
|
17
|
+
* _see also this [blog post][3] for a more detailed explanation_
|
18
|
+
|
19
|
+
The adapter will run all reads against a slave database, unless a) the read is inside an open transaction or b) the
|
20
|
+
adapter determines that the slave lags behind the master _relative to the last write_. For this to work, an initial
|
21
|
+
initial consistency requirement ("`Clock`") must be passed to the adapter. Based on this clock value, the adapter
|
22
|
+
determines if a (randomly chosen) slave meets this requirement. If not, all statements are executed against master,
|
23
|
+
otherwise, the slave connection is used until either a transaction is opened or a write occurs. After a successful write
|
24
|
+
or transaction, the adapter determines a new consistency requirement, which is returned and can be used for subsequent
|
25
|
+
operations. Note that after a write or transaction, the adapter keeps using the master connection.
|
26
|
+
|
27
|
+
As an example, a Rails application could run the following function as an `around_filter`:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
def with_consistency_filter
|
31
|
+
if logged_in?
|
32
|
+
# it's a good idea to use this feature on a per-user basis
|
33
|
+
cache_key = [ CACHE_NAMESPACE, current_user.id.to_s ].join(":")
|
34
|
+
|
35
|
+
clock = cached_clock(cache_key) ||
|
36
|
+
ActiveRecord::Base.connection.master_clock
|
37
|
+
|
38
|
+
new_clock = ActiveRecord::Base.with_consistency(clock) do
|
39
|
+
# inside the controller, ActiveRecord models can be used just as normal.
|
40
|
+
# The adapter will take care of choosing the right connection.
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
|
44
|
+
[ new_clock, clock ].compact.max.tap do |c|
|
45
|
+
cache_clock!(cache_key, c)
|
46
|
+
end if new_clock != clock
|
47
|
+
else
|
48
|
+
# anonymous users will have to wait until the slaves have caught up
|
49
|
+
with_slave { yield }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
```
|
53
|
+
|
54
|
+
Note that we use the current `master_clock` as a reference point. This will give the user a recent view of the data,
|
55
|
+
possibly reading from master, and if no write occurs inside the `with_consistency` block, we have a reasonable value to
|
56
|
+
cache and reuse on subsequent requests. Alternatively, we could have used
|
57
|
+
`ActiveRecord::ConnectionAdapters::MasterSlaveAdapter::Clock.zero` to indicate no particular consistency requirement.
|
58
|
+
Since `with_consistency` blocks can be nested, the controller code could later decide to require a more recent view on
|
59
|
+
the data.
|
60
|
+
|
61
|
+
### Manual Selection of Master or Slave
|
62
|
+
|
63
|
+
The original functionality of the adapter has been preserved:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
ActiveRecord::Base.with_master do
|
67
|
+
# everything inside here will go to master
|
68
|
+
end
|
69
|
+
|
70
|
+
ActiveRecord::Base.with_slave do
|
71
|
+
# everything inside here will go to one of the slaves
|
72
|
+
# opening a transaction or writing will switch to master
|
73
|
+
# for the rest of the block
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
`with_master`, `with_slave` as well as `with_consistency` can be nested deliberately.
|
78
|
+
|
79
|
+
### Transaction Callbacks
|
80
|
+
|
81
|
+
This feature was originally developed at [SoundCloud][4] for the standard `MysqlAdapter`. It allows arbitrary blocks of
|
82
|
+
code to be deferred for execution until the next transaction completes (or rolls back).
|
83
|
+
|
84
|
+
```irb
|
85
|
+
irb> ActiveRecord::Base.on_commit { puts "COMMITTED!" }
|
86
|
+
irb> ActiveRecord::Base.on_rollback { puts "ROLLED BACK!" }
|
87
|
+
irb> ActiveRecord::Base.connection.transaction do
|
88
|
+
irb* # ...
|
89
|
+
irb> end
|
90
|
+
COMMITTED!
|
91
|
+
=> nil
|
92
|
+
irb> ActiveRecord::Base.connection.transaction do
|
93
|
+
irb* # ...
|
94
|
+
irb* raise "failed operation"
|
95
|
+
irb> end
|
96
|
+
ROLLED BACK!
|
97
|
+
# stack trace omitted
|
98
|
+
=> nil
|
99
|
+
```
|
100
|
+
|
101
|
+
Note that a transaction callback will be fired only *once*, so you might want to do:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
class MyModel
|
105
|
+
after_save do
|
106
|
+
connection.on_commit do
|
107
|
+
# ...
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
```
|
112
|
+
|
113
|
+
### Support for Multiple Slaves
|
114
|
+
|
115
|
+
The adapter keeps a list of slave connections (see *Configuration*) and chooses randomly between them. The selection is
|
116
|
+
made at the beginning of a `with_slave` or `with_consistency` block and doesn't change until the block returns. Hence, a
|
117
|
+
nested `with_slave` or `with_consistency` might run against a different slave.
|
118
|
+
|
119
|
+
### Database Cleaner
|
120
|
+
|
121
|
+
At [SoundCloud][4], we're using [database_cleaner][2]'s 'truncation strategy' to wipe the database between [cucumber][5]
|
122
|
+
'feature's. As our cucumber suite proved valuable while testing the `with_consistency` feature, we had to support
|
123
|
+
`truncate_table` as an `ActiveRecord::Base.connection` instance method. We might add other strategies if there's enough
|
124
|
+
interest.
|
125
|
+
|
126
|
+
## Configuration
|
127
|
+
|
128
|
+
Example configuration for the development environment in `database.yml`:
|
129
|
+
|
130
|
+
```yaml
|
131
|
+
development:
|
132
|
+
adapter: master_slave # use master_slave adapter
|
133
|
+
connection_adapter: mysql # actual adapter to use (only mysql is supported atm)
|
134
|
+
disable_connection_test: false # when an instance is checked out from the connection pool,
|
135
|
+
# we check if the connections are still alive, reconnecting if necessary
|
136
|
+
# these values are picked up as defaults in the 'master' and 'slaves' sections:
|
137
|
+
database: aweapp_development
|
138
|
+
username: aweappuser
|
139
|
+
password: s3cr3t
|
140
|
+
|
141
|
+
master:
|
142
|
+
host: masterhost
|
143
|
+
username: readwrite_user # override default value
|
144
|
+
|
145
|
+
slaves:
|
146
|
+
- host: slave01
|
147
|
+
- host: slave02
|
148
|
+
```
|
149
|
+
|
150
|
+
## Installation
|
151
|
+
|
152
|
+
Using plain rubygems:
|
153
|
+
|
154
|
+
```sh
|
155
|
+
$ gem install master_slave_adapter_soundcloud
|
156
|
+
```
|
157
|
+
|
158
|
+
Using bundler:
|
159
|
+
|
160
|
+
```sh
|
161
|
+
$ cat >> Gemfile
|
162
|
+
gem 'master_slave_adapter_soundcloud', '~> 0.1', :require => 'master_slave_adaper'
|
163
|
+
^D
|
164
|
+
$ bundle install
|
165
|
+
```
|
166
|
+
|
167
|
+
## Credits
|
168
|
+
|
169
|
+
* Maurício Lenhares - _original master_slave_adapter plugin_
|
170
|
+
* Torsten Curdt - _with_consistency, maintainership & open source licenses_
|
171
|
+
* Sean Treadway - _chief everything & transaction callbacks_
|
172
|
+
* Kim Altintop - _strong lax monoidal endofunctors_
|
173
|
+
* Omid Aladini - _chief operator & everything else_
|
174
|
+
|
175
|
+
|
176
|
+
[1]: https://github.com/mauricio/master_slave_adapter
|
177
|
+
[2]: https://github.com/bmabey/database_cleaner
|
178
|
+
[3]: http://www.yourdailygeekery.com/2011/06/14/master-slave-consistency.html
|
179
|
+
[4]: http://backstage.soundcloud.com
|
180
|
+
[5]: http://cukes.info
|
181
|
+
[6]: http://travis-ci.org/soundcloud/master_slave_adapter
|
data/TODO.txt
ADDED
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'master_slave_adapter'
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'master_slave_adapter'
|