my_stuff-multidb 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +13 -0
- data/README.rdoc +65 -0
- data/bin/my_stuff-multidb-unmangle +27 -0
- data/lib/my_stuff/multidb.rb +180 -0
- data/lib/my_stuff/multidb/base.rb +43 -0
- data/lib/my_stuff/multidb/sharded.rb +29 -0
- data/lib/my_stuff/multidb/unsharded.rb +29 -0
- metadata +61 -0
data/COPYING
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2011-present, Fred Emmott <copyright@fredemmott.co.uk>
|
2
|
+
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
4
|
+
purpose with or without fee is hereby granted, provided that the above
|
5
|
+
copyright notice and this permission notice appear in all copies.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
8
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
9
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
10
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
11
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
12
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
13
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
= Overview
|
2
|
+
|
3
|
+
This provides an API that's relatively convenient for:
|
4
|
+
* Reading from slaves
|
5
|
+
* Using multiple shards
|
6
|
+
|
7
|
+
= Usage
|
8
|
+
|
9
|
+
module MySpecProvider
|
10
|
+
def spec_for_new
|
11
|
+
{ :adapter => 'mysql', :host => ...}
|
12
|
+
end
|
13
|
+
|
14
|
+
def spec_for_master shard_id
|
15
|
+
...
|
16
|
+
end
|
17
|
+
|
18
|
+
def spec_for_slave shard_id
|
19
|
+
...
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module MyDB
|
24
|
+
class Widget < ActiveRecord::Base; end
|
25
|
+
include MyStuff::MultiDB::Sharded
|
26
|
+
extend MySpecProvider
|
27
|
+
end
|
28
|
+
|
29
|
+
MyDB.with_master_for_new do |db,spec|
|
30
|
+
db::Widget.create(...)
|
31
|
+
end
|
32
|
+
|
33
|
+
MyDB.with_slave_for(shard_id) do |db|
|
34
|
+
db::Widget.find(record_id);
|
35
|
+
...
|
36
|
+
end
|
37
|
+
|
38
|
+
Another option, if you only do primary key lookups, is to encode the shard
|
39
|
+
ID into the record id.
|
40
|
+
|
41
|
+
For more, see:
|
42
|
+
|
43
|
+
* ./examples/run
|
44
|
+
* comments at top of lib/mystuff/multidb.rb
|
45
|
+
|
46
|
+
= How It Works
|
47
|
+
|
48
|
+
For every spec, it defines a new sub-module of MyStuff::MultiDB, which
|
49
|
+
encodes the database details, and creates new subclasses of your
|
50
|
+
ActiveRecord::Base classes, within these modules. For example:
|
51
|
+
|
52
|
+
$ bin/my_stuff-multidb-unmangle MyStuff::MultiDB::MYSTUFF_MULTIDB_DB_216c6f63616c686f7374213333303621::MyDB::Widget
|
53
|
+
MyStuff::MultiDB::<localhost:3306/>::MyDB::Widget
|
54
|
+
$
|
55
|
+
|
56
|
+
There's also lots of deep voodoo making the ActiveRecord stack use the right
|
57
|
+
connection details :)
|
58
|
+
|
59
|
+
= Caveats
|
60
|
+
|
61
|
+
You can not do cross-shard operations simply, eg joins.
|
62
|
+
|
63
|
+
= Copying
|
64
|
+
|
65
|
+
See the COPYING file.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# Copyright 2011-present Fred Emmott. See COPYING file.
|
3
|
+
|
4
|
+
_FILE = File.readlink(__FILE__) rescue __FILE__
|
5
|
+
_DIR = File.expand_path(File.dirname(_FILE))
|
6
|
+
$LOAD_PATH.push(File.expand_path(_DIR + '/../lib'))
|
7
|
+
|
8
|
+
require 'my_stuff/multidb'
|
9
|
+
|
10
|
+
def filter line
|
11
|
+
line.gsub(/MYSTUFF_MULTIDB_DB_[a-z0-9]+/){ |mangled|
|
12
|
+
data = MyStuff::MultiDB.unmangle(mangled)
|
13
|
+
"<%s:%d/%s>" % [
|
14
|
+
data[:host],
|
15
|
+
data[:port],
|
16
|
+
data[:database],
|
17
|
+
]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
if ARGV.empty?
|
22
|
+
STDIN.each_line do |line|
|
23
|
+
puts filter(line.strip)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
puts filter(ARGV.first)
|
27
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
# Copyright 2011-present Fred Emmott. See COPYING file.
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module MyStuff
|
6
|
+
# =Example
|
7
|
+
#
|
8
|
+
# module Foo
|
9
|
+
# class SomeTable < ActiveRecord::Base
|
10
|
+
# end
|
11
|
+
# include MyStuff::MultiDB::Unsharded
|
12
|
+
# end
|
13
|
+
# module Bar
|
14
|
+
# class SomeTable < ActiveRecord::Base
|
15
|
+
# end
|
16
|
+
# include MyStuff::MultiDB::Sharded
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# Foo.with_master do |db|
|
20
|
+
# p db::SomeTable.where(:some_column = 'bar')
|
21
|
+
# end
|
22
|
+
# Foo.with_slave do |db|
|
23
|
+
# p db::SomeTable.where(:some_column = 'bar')
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Bar.with_master_for(id) do |db|
|
27
|
+
# p db::SomeTable.where(:some_column = 'bar')
|
28
|
+
# end
|
29
|
+
# Bar.with_master_for_new do |db|
|
30
|
+
# db::SomeTable.new do
|
31
|
+
# ...
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# Bar.with_slave_for(id) do |db|
|
35
|
+
# p db::SomeTable.where(:some_column = 'bar')
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# See MyStuff::MultiDB::Unsharded and MyStuff::MultiDB::Sharded
|
39
|
+
#
|
40
|
+
# = Details
|
41
|
+
#
|
42
|
+
# When you call <tt>with_*</tt>, it:
|
43
|
+
# * Looks for a class called MyStuff::MultiDB::MANGLED_DATABASE_NAME
|
44
|
+
# * If it doesn't exist, it creates a new ActiveRecord::Base subclass
|
45
|
+
# * It then looks for a module within that class with the same name as your
|
46
|
+
# module
|
47
|
+
# * If it's not there:
|
48
|
+
# * It creates the module
|
49
|
+
# * It creates a subclass of each of your ActiveRecord definitions within this
|
50
|
+
# module
|
51
|
+
# * It delegates connection handling to the database class
|
52
|
+
# * It then returns the database class
|
53
|
+
#
|
54
|
+
# So, you end up with an ActiveRecord class like:
|
55
|
+
# MyStuff::MultiDB::MANGLED_DATABASE_NAME::YourModule::YourClass
|
56
|
+
module MultiDB
|
57
|
+
def self.open_connections
|
58
|
+
Hash.new.tap do |result|
|
59
|
+
constants.each do |name|
|
60
|
+
if name.to_s.starts_with? 'MYSTUFF_MULTIDB_DB'
|
61
|
+
klass = const_get(name, false)
|
62
|
+
checked_out = klass.connection_pool.instance_eval { @checked_out.size }
|
63
|
+
result[name] = checked_out if checked_out > 0
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Takes a module name and converts it to connection details.
|
70
|
+
#
|
71
|
+
# Example:
|
72
|
+
# mangled:: <tt>MyStuff::MultiDB::MYSTUFF_MULTIDB_DB_747970686f6e2e66726564656d6d6f74742e636f2e756b2c333330362c747474::ServiceLocator::Tier</tt>
|
73
|
+
# unmanagled:: <tt>MyStuff::MultiDB::<mysql://typhon.fredemmott.co.uk:3306/ttt>::ServiceLocator::Tier</tt>
|
74
|
+
#
|
75
|
+
# The initscript cat-log and tail-log commands will do this unmanggling for you.
|
76
|
+
def self.unmangle name
|
77
|
+
db = name.sub /^:?MYSTUFF_MULTIDB_DB_/, ''
|
78
|
+
# Format: "MYSTUFF_MULTIDB_DB_" + hex("host,port,database")
|
79
|
+
junk, host, port, database = db.each_char.each_slice(2).reduce(String.new){ |m,*nibbles| m += "%c" % nibbles.join.hex }.split('!')
|
80
|
+
{
|
81
|
+
:host => host,
|
82
|
+
:port => port.to_i,
|
83
|
+
:database => database,
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.included othermod # :nodoc:
|
88
|
+
class <<othermod
|
89
|
+
def for_spec spec
|
90
|
+
MyStuff::MultiDB.for_spec(spec, self)
|
91
|
+
end
|
92
|
+
def with_slave *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Unsharded"; end
|
93
|
+
def with_master *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Unsharded"; end
|
94
|
+
def with_master_for *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Sharded"; end
|
95
|
+
def with_master_for_new; raise NotImplementedError.new "Available in MyStuff::MultiDB::Sharded"; end
|
96
|
+
def with_slave_for *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Sharded"; end
|
97
|
+
def sharded?; raise NotImplementedError.new; end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Fetch/create the magic classes.
|
102
|
+
def self.for_spec spec, mod # :nodoc:
|
103
|
+
db_key = ("MYSTUFF_MULTIDB_DB_" + ("!%s!%s!%s" % [
|
104
|
+
spec[:host],
|
105
|
+
spec[:port],
|
106
|
+
spec[:database],
|
107
|
+
]
|
108
|
+
).each_byte.map{ |x| "%x" % x }.join).to_sym
|
109
|
+
|
110
|
+
# db: class representing the logical database
|
111
|
+
if self.const_defined? db_key
|
112
|
+
db = self.const_get(db_key)
|
113
|
+
else
|
114
|
+
db = Class.new ActiveRecord::Base
|
115
|
+
def db.abstract_class?; true; end
|
116
|
+
self.const_set(db_key, db)
|
117
|
+
db.establish_connection(spec)
|
118
|
+
end
|
119
|
+
|
120
|
+
mod_key = mod.name.split(':').last.to_sym
|
121
|
+
# db_mod: class representing the module from the AR definition
|
122
|
+
if db.const_defined? mod_key, false
|
123
|
+
db_mod = db.const_get(mod_key, false)
|
124
|
+
else
|
125
|
+
db_mod = Module.new
|
126
|
+
db.const_set(mod_key, db_mod)
|
127
|
+
db_mod.send(:define_singleton_method, :magic_database) { db }
|
128
|
+
db_mod.send(:define_singleton_method, :muggle) { mod }
|
129
|
+
|
130
|
+
# klass: a specific table's AR class
|
131
|
+
def db_mod.const_missing name
|
132
|
+
klass = muggle.const_get(name)
|
133
|
+
klass_sym = klass.name.split(':').last.to_sym
|
134
|
+
|
135
|
+
# subklass: klass tied to a specific DB
|
136
|
+
subklass = Class.new(klass)
|
137
|
+
const_set klass_sym, subklass
|
138
|
+
define_singleton_method(klass_sym) { subklass }
|
139
|
+
|
140
|
+
subklass.send :include, MyStuff::MultiDB::Base
|
141
|
+
|
142
|
+
# Make associations work.
|
143
|
+
klass.reflect_on_all_associations.each do |reflection|
|
144
|
+
subklass.send(
|
145
|
+
reflection.macro, # eg :has_one
|
146
|
+
reflection.name, # eg :some_table
|
147
|
+
reflection.options
|
148
|
+
)
|
149
|
+
end
|
150
|
+
|
151
|
+
return subklass
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
return db_mod
|
156
|
+
end
|
157
|
+
|
158
|
+
protected
|
159
|
+
def self.with_db db, id, writable # :nodoc:
|
160
|
+
if writable == :writable
|
161
|
+
if id == :new
|
162
|
+
spec = db.spec_for_new
|
163
|
+
else
|
164
|
+
spec = db.spec_for_master(id)
|
165
|
+
end
|
166
|
+
elsif writable == :read_only
|
167
|
+
spec = db.spec_for_slave(id)
|
168
|
+
end
|
169
|
+
klass = db.for_spec(spec)
|
170
|
+
|
171
|
+
klass.magic_database.connection_pool.with_connection do
|
172
|
+
yield klass, spec
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
require 'my_stuff/multidb/base'
|
179
|
+
require 'my_stuff/multidb/sharded'
|
180
|
+
require 'my_stuff/multidb/unsharded'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# Copyright 2011-present Fred Emmott. See COPYING file.
|
2
|
+
|
3
|
+
module MyStuff
|
4
|
+
module MultiDB
|
5
|
+
module Base # :nodoc:
|
6
|
+
def connection
|
7
|
+
self.class.connection
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.included(klass)
|
11
|
+
klass.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def magic_database
|
16
|
+
@magic_database ||= self.name.split('::').tap(&:pop).join('::').constantize.magic_database
|
17
|
+
end
|
18
|
+
|
19
|
+
def arel_engine
|
20
|
+
magic_database.arel_engine
|
21
|
+
end
|
22
|
+
|
23
|
+
def connection
|
24
|
+
magic_database.connection
|
25
|
+
end
|
26
|
+
|
27
|
+
def abstract_class?; true; end
|
28
|
+
|
29
|
+
def model_name
|
30
|
+
# Rails form_for wants this
|
31
|
+
ActiveModel::Name.new(
|
32
|
+
self.name.split('::').last.tap{|s| def s.name; self; end}
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
def inherited(child)
|
37
|
+
def child.abstract_class?; false; end
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright 2011-present Fred Emmott. See COPYING file.
|
2
|
+
|
3
|
+
module MyStuff
|
4
|
+
module MultiDB
|
5
|
+
module Sharded
|
6
|
+
def self.included othermod # :nodoc:
|
7
|
+
othermod.send :include, MyStuff::MultiDB
|
8
|
+
class <<othermod
|
9
|
+
def sharded?; true; end;
|
10
|
+
def with_master_for id
|
11
|
+
MyStuff::MultiDB.with_db(
|
12
|
+
self, id, :writable
|
13
|
+
) { |*args| yield *args}
|
14
|
+
end
|
15
|
+
def with_master_for_new
|
16
|
+
MyStuff::MultiDB.with_db(
|
17
|
+
self, :new, :read_only
|
18
|
+
) { |*args| yield *args}
|
19
|
+
end
|
20
|
+
def with_slave_for id
|
21
|
+
MyStuff::MultiDB.with_db(
|
22
|
+
self, id, :read_only
|
23
|
+
) { |*args| yield *args}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright 2011-present Fred Emmott. See COPYING file.
|
2
|
+
|
3
|
+
module MyStuff
|
4
|
+
module MultiDB
|
5
|
+
# Mixin for databases with no sharding (eg ServiceLocator, which defines the sharding).
|
6
|
+
#
|
7
|
+
# See MyStuff::MultiDB for an example.
|
8
|
+
#
|
9
|
+
# Including this defines <tt>with_master</tt> and <tt>with_slave</tt>.
|
10
|
+
module Unsharded
|
11
|
+
def self.included othermod # :nodoc:
|
12
|
+
othermod.send :include, MyStuff::MultiDB
|
13
|
+
class <<othermod
|
14
|
+
def sharded?; false; end;
|
15
|
+
def with_master
|
16
|
+
MyStuff::MultiDB.with_db(
|
17
|
+
self, :unsharded, :writable
|
18
|
+
) { |*args| yield *args }
|
19
|
+
end
|
20
|
+
def with_slave
|
21
|
+
MyStuff::MultiDB.with_db(
|
22
|
+
self, :unsharded, :read_only
|
23
|
+
) { |*args| yield *args}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: my_stuff-multidb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Fred Emmott
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-08-17 00:00:00 Z
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: ""
|
17
|
+
email:
|
18
|
+
- mail@fredemmott.co.uk
|
19
|
+
executables:
|
20
|
+
- my_stuff-multidb-unmangle
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- COPYING
|
27
|
+
- README.rdoc
|
28
|
+
- bin/my_stuff-multidb-unmangle
|
29
|
+
- lib/my_stuff/multidb/sharded.rb
|
30
|
+
- lib/my_stuff/multidb/base.rb
|
31
|
+
- lib/my_stuff/multidb/unsharded.rb
|
32
|
+
- lib/my_stuff/multidb.rb
|
33
|
+
homepage: https://github.com/fredemmott/my_stuff-multidb
|
34
|
+
licenses:
|
35
|
+
- ISC
|
36
|
+
post_install_message:
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.8.6
|
57
|
+
signing_key:
|
58
|
+
specification_version: 3
|
59
|
+
summary: ActiveRecord sharding/slaves library
|
60
|
+
test_files: []
|
61
|
+
|