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 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
+