my_stuff-multidb 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-present, Fred Emmott <copyright@fredemmott.co.uk>
1
+ Copyright (c) 2011-2012, Fred Emmott <copyright@fredemmott.co.uk>
2
2
 
3
3
  Permission to use, copy, modify, and/or distribute this software for any
4
4
  purpose with or without fee is hereby granted, provided that the above
@@ -5,11 +5,11 @@ _FILE = File.readlink(__FILE__) rescue __FILE__
5
5
  _DIR = File.expand_path(File.dirname(_FILE))
6
6
  $LOAD_PATH.push(File.expand_path(_DIR + '/../lib'))
7
7
 
8
- require 'my_stuff/multidb'
8
+ require 'my_stuff/multidb/mangling'
9
9
 
10
10
  def filter line
11
11
  line.gsub(/MYSTUFF_MULTIDB_DB_[a-z0-9]+/){ |mangled|
12
- data = MyStuff::MultiDB.unmangle(mangled)
12
+ data = MyStuff::MultiDB::Mangling.unmangle(mangled)
13
13
  "<%s:%d/%s>" % [
14
14
  data[:host],
15
15
  data[:port],
@@ -1,7 +1,12 @@
1
1
  # Copyright 2011-present Fred Emmott. See COPYING file.
2
2
 
3
+ require 'my_stuff/multidb/connection'
4
+ require 'my_stuff/multidb/core_ext/base'
5
+
3
6
  require 'base64'
4
- require 'my_stuff/logger'
7
+
8
+ require 'rubygems'
9
+ require 'active_record'
5
10
 
6
11
  module MyStuff
7
12
  # =Example
@@ -55,135 +60,28 @@ module MyStuff
55
60
  # So, you end up with an ActiveRecord class like:
56
61
  # MyStuff::MultiDB::MANGLED_DATABASE_NAME::YourModule::YourClass
57
62
  module MultiDB
58
- def self.open_connections
59
- Hash.new.tap do |result|
60
- constants.each do |name|
61
- if name.to_s.starts_with? 'MYSTUFF_MULTIDB_DB'
62
- klass = const_get(name, false)
63
- checked_out = klass.connection_pool.instance_eval { @checked_out.size }
64
- result[name] = checked_out if checked_out > 0
65
- end
66
- end
67
- end
68
- end
69
-
70
- # Takes a module name and converts it to connection details.
71
- #
72
- # Example:
73
- # mangled:: <tt>MyStuff::MultiDB::MYSTUFF_MULTIDB_DB_747970686f6e2e66726564656d6d6f74742e636f2e756b2c333330362c747474::ServiceLocator::Tier</tt>
74
- # unmanagled:: <tt>MyStuff::MultiDB::<mysql://typhon.fredemmott.co.uk:3306/ttt>::ServiceLocator::Tier</tt>
75
- #
76
- # The initscript cat-log and tail-log commands will do this unmanggling for you.
77
- def self.unmangle name
78
- db = name.sub /^:?MYSTUFF_MULTIDB_DB_/, ''
79
- # Format: "MYSTUFF_MULTIDB_DB_" + hex("host,port,database")
80
- junk, host, port, database = db.each_char.each_slice(2).reduce(String.new){ |m,*nibbles| m += "%c" % nibbles.join.hex }.split('!')
81
- {
82
- :host => host,
83
- :port => port.to_i,
84
- :database => database,
85
- }
86
- end
87
-
88
63
  def self.included othermod # :nodoc:
89
64
  class <<othermod
90
- def for_spec spec
91
- MyStuff::MultiDB.for_spec(spec, self)
92
- end
93
- def with_slave *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Unsharded"; end
94
- def with_master *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Unsharded"; end
95
- def with_master_for *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Sharded"; end
96
- def with_master_for_new; raise NotImplementedError.new "Available in MyStuff::MultiDB::Sharded"; end
97
- def with_slave_for *args; raise NotImplementedError.new "Available in MyStuff::MultiDB::Sharded"; end
98
- def sharded?; raise NotImplementedError.new; end
99
- end
100
- end
101
-
102
- # Fetch/create the magic classes.
103
- def self.for_spec spec, mod # :nodoc:
104
- db_key = ("MYSTUFF_MULTIDB_DB_" + ("!%s!%s!%s" % [
105
- spec[:host],
106
- spec[:port],
107
- spec[:database],
108
- ]
109
- ).each_byte.map{ |x| "%x" % x }.join).to_sym
110
-
111
- # db: class representing the logical database
112
- if self.const_defined? db_key
113
- db = self.const_get(db_key)
114
- else
115
- l 'connecting to database: ', spec
116
- db = Class.new ActiveRecord::Base
117
- def db.abstract_class?; true; end
118
- self.const_set(db_key, db)
119
- db.establish_connection(spec)
120
- end
121
-
122
- mod_key = mod.name.split(':').last.to_sym
123
- # db_mod: class representing the module from the AR definition
124
- if db.const_defined? mod_key, false
125
- db_mod = db.const_get(mod_key, false)
126
- else
127
- db_mod = Module.new
128
- db.const_set(mod_key, db_mod)
129
- db_mod.send(:define_singleton_method, :magic_database) { db }
130
- db_mod.send(:define_singleton_method, :muggle) { mod }
131
-
132
- # klass: a specific table's AR class
133
- def db_mod.const_missing name
134
- klass = muggle.const_get(name)
135
- klass_sym = klass.name.split(':').last.to_sym
136
-
137
- # subklass: klass tied to a specific DB
138
- subklass = Class.new(klass)
139
- const_set klass_sym, subklass
140
- define_singleton_method(klass_sym) { subklass }
141
-
142
- subklass.send :include, MyStuff::MultiDB::Base
143
-
144
- # Make associations work.
145
- klass.reflect_on_all_associations.each do |reflection|
146
- subklass.send(
147
- reflection.macro, # eg :has_one
148
- reflection.name, # eg :some_table
149
- reflection.options
150
- )
151
- end
152
-
153
- return subklass
65
+ def with_spec spec, &block
66
+ MyStuff::MultiDB.with_spec(self, spec, &block)
154
67
  end
155
68
  end
156
-
157
- return db_mod
158
69
  end
159
70
 
160
- def self.with_spec db, spec # :nodoc:
161
- klass = db.for_spec(spec)
162
-
163
- klass.magic_database.connection_pool.with_connection do
164
- yield klass, spec
165
- end
166
- end
71
+ def self.with_spec original_module, spec, &block # :nodoc:
72
+ ar_base = Connection.base_class_for_spec(spec)
73
+ rebased_module = ar_base.rebased_module(original_module)
167
74
 
168
- protected
169
- def self.with_db db, id, writable # :nodoc:
170
- if writable == :writable
171
- if id == :new
172
- spec = db.spec_for_new
75
+ ar_base.connection_pool.with_connection do
76
+ if block.arity == 1
77
+ block.call rebased_module
173
78
  else
174
- spec = db.spec_for_master(id)
79
+ block.call rebased_module, spec
175
80
  end
176
- elsif writable == :read_only
177
- spec = db.spec_for_slave(id)
178
- end
179
-
180
- with_spec(db, spec) do |*args|
181
- yield *args
182
81
  end
183
82
  end
184
83
  end
185
84
  end
186
85
 
187
- require 'my_stuff/multidb/base'
188
86
  require 'my_stuff/multidb/sharded'
189
87
  require 'my_stuff/multidb/unsharded'
@@ -0,0 +1,108 @@
1
+ # Copyright 2011-present Fred Emmott. See COPYING file.
2
+
3
+ require 'my_stuff/multidb/mangling'
4
+ require 'my_stuff/multidb/core_ext/base'
5
+
6
+ require 'rubygems'
7
+ require 'active_record'
8
+
9
+ module MyStuff
10
+ module MultiDB
11
+ module Connections; end
12
+
13
+ class Connection < ActiveRecord::Base
14
+ class << self
15
+ def base_class_for_spec spec
16
+ name = MyStuff::MultiDB::Mangling.mangle(spec).to_sym
17
+ if Connections.const_defined?(name)
18
+ return Connections.const_get(name)
19
+ end
20
+
21
+ connection = Class.new(self)
22
+ Connections.const_set(name, connection)
23
+ connection.establish_connection(spec)
24
+ connection
25
+ end
26
+
27
+ def abstract_class?
28
+ true
29
+ end
30
+
31
+ def rebased_module original_module
32
+ name = original_module.name.gsub(':', '__').to_sym
33
+
34
+ if have_rebased_module?(name)
35
+ self.const_get(name)
36
+ else
37
+ self.rebase_module! name, original_module
38
+ end
39
+ end
40
+
41
+ def rebased_model name, original_module, rebased_module
42
+ if rebased_module.const_defined? name
43
+ rebased_module.const_get(name)
44
+ else
45
+ self.rebase_model! name, original_module, rebased_module
46
+ end
47
+ end
48
+
49
+ protected
50
+
51
+ def rebase_model! name, original_module, rebased_module
52
+ name = name.to_sym
53
+ original = original_module.const_get(name)
54
+
55
+ rebased = Class.new(original)
56
+ rebased_module.const_set(name, rebased)
57
+ rebased.send :include, MyStuff::MultiDB::CoreExt::Base
58
+
59
+ # Make associations work.
60
+ rebased.reflect_on_all_associations.each do |reflection|
61
+ rebased.send(
62
+ reflection.macro, # eg :has_one
63
+ reflection.name, # eg :some_table
64
+ reflection.options
65
+ )
66
+ end
67
+
68
+ return rebased
69
+ end
70
+
71
+ def rebase_module! name, original_module
72
+ ar_base = self
73
+ rebased = Module.new
74
+ ar_base.const_set(name, rebased)
75
+
76
+ # Generate wrapper classes on demand
77
+ def rebased.const_missing (name)
78
+ MyStuff::MultiDB::Connection.rebased_model(
79
+ name,
80
+ muggle,
81
+ magic_database
82
+ )
83
+ end
84
+
85
+ # Not using define_singleton_method, as that's not in 1.8.7
86
+ singleton = class << rebased; self; end
87
+ singleton.send(:define_method, :magic_database) { ar_base }
88
+ singleton.send(:define_method, :muggle) { original_module }
89
+
90
+ return rebased
91
+ end
92
+
93
+ def have_rebased_module? name
94
+ # 1.8.7: const_defined? does not include constants defined
95
+ # in other modules, and it only takes 1 arg
96
+ # 1.9: it does include, and needs a second argument to change
97
+ # this.
98
+ old_const_defined = self.method(:const_defined?).arity == 1
99
+ new_const_defined = !old_const_defined
100
+ return (
101
+ (old_const_defined && self.const_defined?(name)) ||
102
+ (new_const_defined && self.const_defined?(name, false))
103
+ )
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,59 @@
1
+ # Copyright 2011-present Fred Emmott. See COPYING file.
2
+
3
+ module MyStuff
4
+ module MultiDB
5
+ module CoreExt
6
+ # Mixin for subclasses of ActiveRecord::Base.
7
+ #
8
+ # This deals with routing connections via the 'magic'
9
+ # ActiveRecord::Base subclass for the appropriate database.
10
+ module Base # :nodoc:
11
+ def connection
12
+ self.class.connection
13
+ end
14
+
15
+ def self.included(klass)
16
+ klass.extend(ClassMethods)
17
+ end
18
+
19
+ module ClassMethods
20
+ def base_class
21
+ self
22
+ end
23
+
24
+ def magic_database
25
+ @magic_database ||=
26
+ self.name.split('::')[0..-2].join('::').constantize
27
+ end
28
+
29
+ def arel_engine
30
+ magic_database.arel_engine
31
+ end
32
+
33
+ def connection
34
+ magic_database.connection
35
+ end
36
+
37
+ def connection_pool
38
+ magic_database.connection_pool
39
+ end
40
+
41
+ def abstract_class?; true; end
42
+
43
+ def model_name
44
+ # Rails form_for wants this
45
+ ActiveModel::Name.new(
46
+ self.name.split('::').last.tap{|s| def s.name; self; end}
47
+ )
48
+ end
49
+
50
+ def inherited(child)
51
+ def child.abstract_class?; false; end
52
+ def child.base_class; self; end
53
+ super
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,38 @@
1
+ # Copyright 2011-2012 Fred Emmott. See COPYING file.
2
+
3
+ module MyStuff
4
+ module MultiDB
5
+ module Mangling
6
+ # Takes a module name and converts it to connection details.
7
+ #
8
+ # Example:
9
+ # mangled:: <tt>MyStuff::MultiDB::MYSTUFF_MULTIDB_DB_747970686f6e2e66726564656d6d6f74742e636f2e756b2c333330362c747474::ServiceLocator::Tier</tt>
10
+ # unmanagled:: <tt>MyStuff::MultiDB::<mysql://typhon.fredemmott.co.uk:3306/ttt>::ServiceLocator::Tier</tt>
11
+ #
12
+ # The initscript cat-log and tail-log commands will do this unmanggling for you.
13
+ def self.unmangle name
14
+ db = name.sub /^:?MYSTUFF_MULTIDB_DB_/, ''
15
+ # Format: "MYSTUFF_MULTIDB_DB_" + hex("host,port,database")
16
+ host, port, database = db.each_char.each_slice(2).reduce(String.new){ |m,*nibbles| m += "%c" % nibbles.join.hex }.split('!')
17
+ {
18
+ :host => host,
19
+ :port => port.to_i,
20
+ :database => database,
21
+ }
22
+ end
23
+
24
+ # Create a valid constant name from an ActiveRecord spec.
25
+ #
26
+ # This can be revered by {#unmangle}.
27
+ def self.mangle spec
28
+ 'MYSTUFF_MULTIDB_DB_' + (
29
+ "%s!%s!%s" % [
30
+ spec[:host] || spec['host'],
31
+ spec[:port] || spec['port'],
32
+ spec[:database] || spec['database'],
33
+ ]
34
+ ).each_byte.map{ |x| "%x" % x }.join
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,27 +1,41 @@
1
1
  # Copyright 2011-present Fred Emmott. See COPYING file.
2
2
 
3
+ require 'my_stuff/multidb'
4
+
3
5
  module MyStuff
4
6
  module MultiDB
5
7
  module Sharded
6
8
  def self.included othermod # :nodoc:
7
9
  othermod.send :include, MyStuff::MultiDB
10
+
8
11
  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}
12
+ def with_master_for id, &block
13
+ MyStuff::MultiDB.with_spec(
14
+ self,
15
+ self.spec_for_master(id),
16
+ &block
17
+ )
14
18
  end
15
- def with_master_for_new
16
- MyStuff::MultiDB.with_db(
17
- self, :new, :read_only
18
- ) { |*args| yield *args}
19
+
20
+ def with_master_for_new &block
21
+ MyStuff::MultiDB.with_spec(
22
+ self,
23
+ self.spec_for_new,
24
+ &block
25
+ )
19
26
  end
20
- def with_slave_for id
21
- MyStuff::MultiDB.with_db(
22
- self, id, :read_only
23
- ) { |*args| yield *args}
27
+
28
+ def with_slave_for id, &block
29
+ MyStuff::MultiDB.with_spec(
30
+ self,
31
+ self.spec_for_slave(id),
32
+ &block
33
+ )
24
34
  end
35
+
36
+ def spec_for_new; raise NotImplementedError.new; end
37
+ def spec_for_master(shard_id); raise NotImplementedError.new; end
38
+ def spec_for_slave(shard_id); raise NotImplementedError.new; end
25
39
  end
26
40
  end
27
41
  end
@@ -1,4 +1,6 @@
1
1
  # Copyright 2011-present Fred Emmott. See COPYING file.
2
+ #
3
+ require 'my_stuff/multidb'
2
4
 
3
5
  module MyStuff
4
6
  module MultiDB
@@ -10,18 +12,26 @@ module MyStuff
10
12
  module Unsharded
11
13
  def self.included othermod # :nodoc:
12
14
  othermod.send :include, MyStuff::MultiDB
15
+
13
16
  class <<othermod
14
- def sharded?; false; end;
15
- def with_master
16
- MyStuff::MultiDB.with_db(
17
- self, :unsharded, :writable
18
- ) { |*args| yield *args }
17
+ def with_master &block
18
+ MyStuff::MultiDB.with_spec(
19
+ self,
20
+ self.spec_for_master,
21
+ &block
22
+ )
19
23
  end
20
- def with_slave
21
- MyStuff::MultiDB.with_db(
22
- self, :unsharded, :read_only
23
- ) { |*args| yield *args}
24
+
25
+ def with_slave &block
26
+ MyStuff::MultiDB.with_spec(
27
+ self,
28
+ self.spec_for_slave,
29
+ &block
30
+ )
24
31
  end
32
+
33
+ def spec_for_master; raise NotImplementedError.new; end
34
+ def spec_for_slave; raise NotImplementedError.new; end
25
35
  end
26
36
  end
27
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: my_stuff-multidb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,30 +9,22 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-04 00:00:00.000000000Z
12
+ date: 2012-02-13 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
- name: my_stuff-logger
16
- requirement: &70289582101220 !ruby/object:Gem::Requirement
15
+ name: activerecord
16
+ requirement: &70321517374640 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 0.0.3
22
- type: :runtime
23
- prerelease: false
24
- version_requirements: *70289582101220
25
- - !ruby/object:Gem::Dependency
26
- name: activerecord
27
- requirement: &70289582100640 !ruby/object:Gem::Requirement
28
- none: false
29
- requirements:
21
+ version: '3.2'
30
22
  - - ! '>='
31
23
  - !ruby/object:Gem::Version
32
- version: '0'
24
+ version: 3.2.1
33
25
  type: :runtime
34
26
  prerelease: false
35
- version_requirements: *70289582100640
27
+ version_requirements: *70321517374640
36
28
  description: ''
37
29
  email:
38
30
  - mail@fredemmott.co.uk
@@ -42,9 +34,10 @@ extensions: []
42
34
  extra_rdoc_files: []
43
35
  files:
44
36
  - COPYING
45
- - README.rdoc
46
37
  - bin/my_stuff-multidb-unmangle
47
- - lib/my_stuff/multidb/base.rb
38
+ - lib/my_stuff/multidb/connection.rb
39
+ - lib/my_stuff/multidb/core_ext/base.rb
40
+ - lib/my_stuff/multidb/mangling.rb
48
41
  - lib/my_stuff/multidb/sharded.rb
49
42
  - lib/my_stuff/multidb/unsharded.rb
50
43
  - lib/my_stuff/multidb.rb
data/README.rdoc DELETED
@@ -1,65 +0,0 @@
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.
@@ -1,58 +0,0 @@
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 base_class
16
- self
17
- end
18
-
19
- def with_spec spec
20
- MyStuff::MultiDB.with_spec(
21
- db, spec
22
- ) { |*args| yield *args }
23
- end
24
-
25
- def magic_database
26
- @magic_database ||= self.name.split('::').tap(&:pop).join('::').constantize.magic_database
27
- end
28
-
29
- def arel_engine
30
- magic_database.arel_engine
31
- end
32
-
33
- def connection
34
- magic_database.connection
35
- end
36
-
37
- def connection_pool
38
- magic_database.connection_pool
39
- end
40
-
41
- def abstract_class?; true; end
42
-
43
- def model_name
44
- # Rails form_for wants this
45
- ActiveModel::Name.new(
46
- self.name.split('::').last.tap{|s| def s.name; self; end}
47
- )
48
- end
49
-
50
- def inherited(child)
51
- def child.abstract_class?; false; end
52
- def child.base_class; self; end
53
- super
54
- end
55
- end
56
- end
57
- end
58
- end