my_stuff-multidb 0.0.3 → 0.1.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/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