my_stuff-multidb 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/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
+