db-charmer 1.5.5 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,3 +1,10 @@
1
+ 1.6.0 (2010-03-31):
2
+
3
+ The major (and arguably the only noticeable) change in this version is our simple database
4
+ sharding support. The feature is still in alpha stage and should not be used in production
5
+ without complete understanding of the principles of its work.
6
+
7
+ ----------------------------------------------------------------------------------------
1
8
  1.5.5 (2010-03-15):
2
9
 
3
10
  Thanks to ngmoco.com (http://github.com/ngmoco) now DbCharmer supports one more use-case
@@ -17,7 +17,7 @@ There are two options when approaching db-charmer installation:
17
17
 
18
18
  To install as a gem, add this to your environment.rb:
19
19
 
20
- config.gem 'db-charmer', :lib => 'db_charmer', :source => 'http://gemcutter.org'
20
+ config.gem 'db-charmer', :lib => 'db_charmer'
21
21
 
22
22
  And then run the command:
23
23
 
@@ -249,6 +249,24 @@ and HABTM associations connection switching:
249
249
  @user.on_slave.posts
250
250
 
251
251
 
252
+ === Named Scopes Support
253
+
254
+ To make it easier for +DbCharmer+ users to use connections switching methods with named scopes,
255
+ we've added <tt>on_*</tt> methods support on the scopes as well. All the following scope chains
256
+ would do exactly the same way (the query would be executed on the :foo database connection):
257
+
258
+ Post.on_db(:foo).published.with_comments.spam_marked.count
259
+ Post.published.on_db(:foo).with_comments.spam_marked.count
260
+ Post.published.with_comments.on_db(:foo).spam_marked.count
261
+ Post.published.with_comments.spam_marked.on_db(:foo).count
262
+
263
+ And now, add this feature to our associations support and here is what we could do:
264
+
265
+ @user.on_db(:archive).posts.published.all
266
+ @user.posts.on_db(:olap).published.count
267
+ @user.posts.published.on_db(:foo).first
268
+
269
+
252
270
  === Bulk Connection Management
253
271
 
254
272
  Sometimes you want to run code where a large number of tables may be used, and you'd like
@@ -266,28 +284,178 @@ You can specify any number of remappings at once, and you can also use +:master+
266
284
  name that matches any model that has not had its connection set by +DbCharmer+ at all.
267
285
 
268
286
  *Note*: +DbCharmer+ works via +alias_method_chain+ in model classes. It is very careful
269
- to only patch the models it needs to. However, if you use +with_remapped_databases+ and
287
+ to only patch the models it needs to. However, if you use +with_remapped_databases+ and
270
288
  remap the default database (+:master+), then it has no choice but to patch all subclasses
271
289
  of +ActiveRecord::Base+. This should not cause any serious problems or any big performance
272
290
  impact, but it is worth noting.
273
291
 
274
292
 
275
- === Named Scopes Support
293
+ == Simple Sharding Support
276
294
 
277
- To make it easier for +DbCharmer+ users to use connections switching methods with named scopes,
278
- we've added <tt>on_*</tt> methods support on the scopes as well. All the following scope chains
279
- would do exactly the same way (the query would be executed on the :foo database connection):
295
+ Starting with the release 1.6.0 of +DbCharmer+ we have added support for simple database sharding
296
+ to our ActiveRecord extensions. The feature is still in alpha stage and should not be used in
297
+ production without complete understanding of the principles of its work.
280
298
 
281
- Post.on_db(:foo).published.with_comments.spam_marked.count
282
- Post.published.on_db(:foo).with_comments.spam_marked.count
283
- Post.published.with_comments.on_db(:foo).spam_marked.count
284
- Post.published.with_comments.spam_marked.on_db(:foo).count
299
+ At this point we support three sharding methods:
285
300
 
286
- And now, add this feature to our associations support and here is what we could do:
301
+ 1) range - really simple sharding method that allows you to take a table, slice is to a set of
302
+ smaller tables with pre-defined ranges of primary keys and then put those smaller tables to
303
+ different databases/servers. This could be useful for situations where you have a huge table that
304
+ is slowly growing and you just want to keep it simple and split the table load into a few servers
305
+ without building any complex sharding schemes.
287
306
 
288
- @user.on_db(:archive).posts.published.all
289
- @user.posts.on_db(:olap).published.count
290
- @user.posts.published.on_db(:foo).first
307
+ 2) hash_map - pretty simple sharding method that allows you to take a table and slice it to a set
308
+ of smaller tables by some key that has a pre-defined key of values. For example, list of US mailing
309
+ addresses could be sharded by states, where you'd be able to define which states are stored in which
310
+ databases/servers.
311
+
312
+ 3) db_block_map - this is a really complex sharding method that allows you to shard your table into a
313
+ set of small fixed-size blocks that then would be assigned to a set of shards (databases/servers).
314
+ Whenever you would need an additional blocks they would be allocated automatically and then balanced
315
+ across the shards you have defined in your database. This method could be used to scale out huge
316
+ tables with hundreds of millions to billions of rows and allows relatively easy re-sharding techniques
317
+ to be implemented on top.
318
+
319
+
320
+ === How to enable sharding?
321
+
322
+ To enable sharding extensions you need to take a few things:
323
+
324
+ 1) Create a Rails initializer (on run this code when you initialize your script/application) with a
325
+ set of sharded connections defined. Each connection would have a name, sharding method and an optional
326
+ set of parameters to initialize the sharding method of your choice.
327
+
328
+ 2) Specify sharding connection you want to use in your models.
329
+
330
+ 3) Specify the shard you want to use before doing any operations on your models.
331
+
332
+ For more details please check out the following documentation sections.
333
+
334
+
335
+ === Sharded Connections
336
+
337
+ Sharded connection is a simple abstractions that allows you to specify all sharding parameters for a
338
+ cluster in one place and then use this centralized configuration in your models. Here are a few examples
339
+ of sharded connections initizlization calls:
340
+
341
+ 1) Sample range-based sharded connection:
342
+
343
+ TEXTS_SHARDING_RANGES = {
344
+ 0...100 => :shard1,
345
+ 100..200 => :shard2,
346
+ :default => :shard3
347
+ }
348
+
349
+ DbCharmer::Sharding.register_connection(
350
+ :name => :texts,
351
+ :method => :range,
352
+ :ranges => TEXTS_SHARDING_RANGES
353
+ )
354
+
355
+ 2) Sample hash map sharded connection:
356
+
357
+ SHARDING_MAP = {
358
+ 'US' => :us_users,
359
+ 'CA' => :ca_users,
360
+ :default => :other_users
361
+ }
362
+
363
+ DbCharmer::Sharding.register_connection(
364
+ :name => :texts,
365
+ :method => :range,
366
+ :map => SHARDING_MAP
367
+ )
368
+
369
+ 3) Sample database block map sharded connection:
370
+
371
+ DbCharmer::Sharding.register_connection(
372
+ :name => :social,
373
+ :method => :db_block_map,
374
+ :block_size => 10000, # Number of keys per block
375
+ :map_table => :event_shards_map, # Table with blocks to shards mapping
376
+ :shards_table => :event_shards_info, # Shards connection information table
377
+ :connection => :social_shard_info # What connection to use to read the map
378
+ )
379
+
380
+ After your sharded connection is defined, you could use it in your models:
381
+
382
+ class Text < ActiveRecord::Base
383
+ db_magic :sharded => {
384
+ :key => :id,
385
+ :sharded_connection => :texts
386
+ }
387
+ end
388
+
389
+ class Event < ActiveRecord::Base
390
+ set_table_name :timeline_events
391
+
392
+ db_magic :sharded => {
393
+ :key => :to_uid,
394
+ :sharded_connection => :social
395
+ }
396
+ end
397
+
398
+
399
+ === Switching connections in sharded models
400
+
401
+ Every time you need to perform an operation on a sharded model, you need to specify on which shard
402
+ you want to do it. We have a method for this which would look familiar for the people that use
403
+ +DbCharmer+ for non-sharded environments since it looks and works just like those per-query
404
+ connection management methods:
405
+
406
+ Event.shard_for(10).find(:conditions => { :to_uid => 123 }, :limit => 5)
407
+ Text.shard_for(123).find_by_id(123)
408
+
409
+ There is another method that could be used with range and hash_map sharding methods, this method
410
+ allows you to switch to the default shard:
411
+
412
+ Text.on_default_shard.create(:body => 'hello', :user_id => 123)
413
+
414
+ And finally, there is a method that allows you to run your code on each shard in the system (at this
415
+ point the method is supported in db_block_map method only):
416
+
417
+ Event.on_each_shard { |event| event.delete_all }
418
+
419
+
420
+ === Defining your own sharding methods
421
+
422
+ It is possing with +DbCharmer+ for the users to define their own sharding methods. You need to do a
423
+ few things to implement your very own sharding scheme:
424
+
425
+ 1) Create a class with a name DbCharmer::Sharding::Method::YourOwnName
426
+
427
+ 2) Implement at least a constructor <tt>initialize(config)</tt> and a lookup instance
428
+ method <tt>shard_for_key(key)</tt> that would return either a connection name from <tt>database.yml</tt>
429
+ file or just a hash of connection parameters for rails connection adapters.
430
+
431
+ 3) Register your sharded connection using the following call:
432
+
433
+ DbCharmer::Sharding.register_connection(
434
+ :name => :some_name,
435
+ :method => :your_own_name, # your sharder class name in lower case
436
+ ... some additional parameters if needed ...
437
+ )
438
+
439
+ 4) Use your sharded connection as any standard one.
440
+
441
+
442
+ === Adding support for default shards in your custom sharding methods
443
+
444
+ If you want to be able to use +on_default_shard+ method on your custom-sharded models, you
445
+ need to do two things:
446
+
447
+ 1) implement <tt>support_default_shard?</tt> instance method on your sharded class that
448
+ would return +true+ if you do support default shard specification and +false+ otherwise.
449
+
450
+ 2) implement <tt>:default</tt> symbol support as a key in your +shard_for_key+ method.
451
+
452
+
453
+ === Adding support for shards enumeration in your custom sharding methods
454
+
455
+ To add shards enumeration support to your custom-sharded models you need to implement
456
+ just one instance method +shard_connections+ on your sharded class. This method should
457
+ return an array of sharding connection names or connection configurations to be used to
458
+ establish shard connections in a loop.
291
459
 
292
460
 
293
461
  == Documentation
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.5.5
1
+ 1.6.0
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{db-charmer}
8
- s.version = "1.5.5"
8
+ s.version = "1.6.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Alexey Kovyrin"]
12
- s.date = %q{2010-03-15}
12
+ s.date = %q{2010-03-31}
13
13
  s.description = %q{ActiveRecord Connections Magic (slaves, multiple connections, etc)}
14
14
  s.email = %q{alexey@kovyrin.net}
15
15
  s.extra_rdoc_files = [
@@ -38,6 +38,12 @@ Gem::Specification.new do |s|
38
38
  "lib/db_charmer/multi_db_migrations.rb",
39
39
  "lib/db_charmer/multi_db_proxy.rb",
40
40
  "lib/db_charmer/scope_proxy.rb",
41
+ "lib/db_charmer/sharding.rb",
42
+ "lib/db_charmer/sharding/connection.rb",
43
+ "lib/db_charmer/sharding/method/db_block_map.rb",
44
+ "lib/db_charmer/sharding/method/hash_map.rb",
45
+ "lib/db_charmer/sharding/method/range.rb",
46
+ "lib/db_charmer/stub_connection.rb",
41
47
  "lib/tasks/databases.rake"
42
48
  ]
43
49
  s.homepage = %q{http://github.com/kovyrin/db-charmer}
@@ -1,5 +1,3 @@
1
- #puts "Loading DbCharmer..."
2
-
3
1
  module DbCharmer
4
2
  @@migration_connections_should_exist = Rails.env.production?
5
3
  mattr_accessor :migration_connections_should_exist
@@ -19,7 +17,7 @@ module DbCharmer
19
17
  return Rails.logger if defined?(Rails)
20
18
  @logger ||= Logger.new(STDERR)
21
19
  end
22
-
20
+
23
21
  def self.with_remapped_databases(mappings, &proc)
24
22
  old_mappings = ActiveRecord::Base.db_charmer_database_remappings
25
23
  begin
@@ -33,18 +31,21 @@ module DbCharmer
33
31
  ActiveRecord::Base.db_charmer_database_remappings = old_mappings
34
32
  end
35
33
  end
36
-
34
+
37
35
  def self.hijack_new_classes?
38
36
  @@hijack_new_classes
39
37
  end
40
-
41
- private
38
+
39
+ private
40
+
42
41
  @@hijack_new_classes = false
43
42
  def self.with_all_hijacked
44
43
  old_hijack_new_classes = @@hijack_new_classes
45
44
  begin
46
45
  @@hijack_new_classes = true
47
- ActiveRecord::Base.send(:subclasses).each { |s| s.hijack_connection! }
46
+ ActiveRecord::Base.send(:subclasses).each do |subclass|
47
+ subclass.hijack_connection!
48
+ end
48
49
  yield
49
50
  ensure
50
51
  @@hijack_new_classes = old_hijack_new_classes
@@ -52,6 +53,8 @@ module DbCharmer
52
53
  end
53
54
  end
54
55
 
56
+ # These methods are added to all objects so we could call proxy? on anything
57
+ # and figure if an object is a proxy w/o hitting method_missing or respond_to?
55
58
  class Object
56
59
  def self.proxy?
57
60
  false
@@ -65,8 +68,6 @@ end
65
68
  # We need blankslate for all the proxies we have
66
69
  require 'blankslate'
67
70
 
68
- #puts "Extending AR..."
69
-
70
71
  require 'db_charmer/active_record_extensions'
71
72
  require 'db_charmer/abstract_adapter_extensions'
72
73
 
@@ -116,8 +117,6 @@ module ActiveRecord
116
117
  end
117
118
  end
118
119
 
119
- #puts "Doing the magic..."
120
-
121
120
  require 'db_charmer/db_magic'
122
121
  require 'db_charmer/finder_overrides'
123
122
  require 'db_charmer/association_preload'
@@ -140,7 +139,7 @@ class ActiveRecord::Base
140
139
  hijack_connection! if DbCharmer.hijack_new_classes?
141
140
  out
142
141
  end
143
-
142
+
144
143
  alias_method_chain :inherited, :hijacking
145
144
  end
146
145
  end
@@ -3,14 +3,16 @@ module DbCharmer
3
3
  module ClassMethods
4
4
 
5
5
  def establish_real_connection_if_exists(name, should_exist = false)
6
- unless config = configurations[RAILS_ENV][name.to_s]
6
+ name = name.to_s
7
+ config = configurations[RAILS_ENV][name]
8
+ unless config
7
9
  if should_exist
8
10
  raise ArgumentError, "Invalid connection name (does not exist in database.yml): #{RAILS_ENV}/#{name}"
9
11
  end
10
12
  return # No need to establish connection - they do not want us to
11
13
  end
12
14
  # Pass connection name with config
13
- config[:connection_name] = name.to_s
15
+ config[:connection_name] = name
14
16
  establish_connection(config)
15
17
  end
16
18
 
@@ -69,15 +71,15 @@ module DbCharmer
69
71
  return nil if (db_charmer_connection_level || 0) > 0
70
72
  name = db_charmer_connection_proxy.db_charmer_connection_name if db_charmer_connection_proxy
71
73
  name = (name || :master).to_sym
72
-
74
+
73
75
  remapped = @@db_charmer_database_remappings[name]
74
76
  return DbCharmer::ConnectionFactory.connect(remapped, true) if remapped
75
77
  end
76
-
78
+
77
79
  def db_charmer_database_remappings
78
80
  @@db_charmer_database_remappings
79
81
  end
80
-
82
+
81
83
  def db_charmer_database_remappings=(mappings)
82
84
  raise "Mappings must be nil or respond to []" if mappings && (! mappings.respond_to?(:[]))
83
85
  @@db_charmer_database_remappings = mappings || { }
@@ -13,34 +13,64 @@ module DbCharmer
13
13
  end
14
14
 
15
15
  # Establishes connection or return an existing one from cache
16
- def self.connect(db_name, should_exist = false)
17
- db_name = db_name.to_s
18
- @@connection_classes[db_name] ||= establish_connection(db_name, should_exist)
16
+ def self.connect(connection_name, should_exist = false)
17
+ connection_name = connection_name.to_s
18
+ @@connection_classes[connection_name] ||= establish_connection(connection_name, should_exist)
19
+ end
20
+
21
+ # Establishes connection or return an existing one from cache (not using AR database configs)
22
+ def self.connect_to_db(connection_name, config)
23
+ connection_name = connection_name.to_s
24
+ @@connection_classes[connection_name] ||= establish_connection_to_db(connection_name, config)
19
25
  end
20
26
 
21
27
  # Establish connection with a specified name
22
- def self.establish_connection(db_name, should_exist = false)
23
- abstract_class = generate_abstract_class(db_name, should_exist)
24
- DbCharmer::ConnectionProxy.new(abstract_class, db_name)
28
+ def self.establish_connection(connection_name, should_exist = false)
29
+ abstract_class = generate_abstract_class(connection_name, should_exist)
30
+ DbCharmer::ConnectionProxy.new(abstract_class, connection_name)
31
+ end
32
+
33
+ # Establish connection with a specified name (not using AR database configs)
34
+ def self.establish_connection_to_db(connection_name, config)
35
+ abstract_class = generate_abstract_class_for_db(connection_name, config)
36
+ DbCharmer::ConnectionProxy.new(abstract_class, connection_name)
25
37
  end
26
38
 
27
39
  # Generate an abstract AR class with specified connection established
28
- def self.generate_abstract_class(db_name, should_exist = false)
29
- klass = abstract_connection_class_name(db_name)
40
+ def self.generate_abstract_class(connection_name, should_exist = false)
41
+ # Generate class
42
+ klass = generate_empty_abstract_ar_class(abstract_connection_class_name(connection_name))
43
+
44
+ # Establish connection
45
+ klass.establish_real_connection_if_exists(connection_name.to_sym, !!should_exist)
46
+
47
+ # Return the class
48
+ return klass
49
+ end
50
+
51
+ # Generate an abstract AR class with specified connection established (not using AR database configs)
52
+ def self.generate_abstract_class_for_db(connection_name, config)
30
53
  # Generate class
31
- module_eval <<-EOF, __FILE__, __LINE__ + 1
32
- class #{klass} < ActiveRecord::Base
33
- self.abstract_class = true
34
- establish_real_connection_if_exists(:#{db_name}, #{!!should_exist})
35
- end
36
- EOF
54
+ klass = generate_empty_abstract_ar_class(abstract_connection_class_name(connection_name))
55
+
56
+ # Establish connection
57
+ klass.establish_connection(config)
58
+
59
+ # Return the class
60
+ return klass
61
+ end
62
+
63
+ def self.generate_empty_abstract_ar_class(klass)
64
+ # Define class
65
+ module_eval "class #{klass} < ActiveRecord::Base; self.abstract_class = true; end"
66
+
37
67
  # Return class
38
68
  klass.constantize
39
69
  end
40
70
 
41
71
  # Generates unique names for our abstract AR classes
42
- def self.abstract_connection_class_name(db_name)
43
- "::AutoGeneratedAbstractConnectionClass#{db_name.to_s.camelize}"
72
+ def self.abstract_connection_class_name(connection_name)
73
+ "::AutoGeneratedAbstractConnectionClass#{connection_name.to_s.camelize}"
44
74
  end
45
75
  end
46
76
  end
@@ -1,3 +1,4 @@
1
+ # Simple proxy that sends all method calls to a real database connection
1
2
  module DbCharmer
2
3
  class ConnectionProxy < BlankSlate
3
4
  def initialize(abstract_class, db_name)
@@ -8,6 +8,12 @@ module DbCharmer
8
8
  return DbCharmer::ConnectionFactory.connect(conn, should_exist)
9
9
  end
10
10
 
11
+ if conn.kind_of?(Hash)
12
+ conn = conn.symbolize_keys
13
+ raise ArgumentError, "Missing required :name parameter" unless conn[:name]
14
+ return DbCharmer::ConnectionFactory.connect_to_db(conn[:name], conn)
15
+ end
16
+
11
17
  if conn.respond_to?(:db_charmer_connection_proxy)
12
18
  return conn.db_charmer_connection_proxy
13
19
  end
@@ -9,15 +9,22 @@ module DbCharmer
9
9
  should_exist = opt[:should_exist] || DbCharmer.connections_should_exist?
10
10
 
11
11
  # Main connection management
12
- db_magic_connection(opt[:connection], should_exist) if opt[:connection]
12
+ setup_connection_magic(opt[:connection], should_exist) if opt[:connection]
13
13
 
14
14
  # Set up slaves pool
15
15
  opt[:slaves] ||= []
16
16
  opt[:slaves] << opt[:slave] if opt[:slave]
17
- db_magic_slaves(opt[:slaves], should_exist) if opt[:slaves].any?
17
+ setup_slaves_magic(opt[:slaves], should_exist) if opt[:slaves].any?
18
18
 
19
19
  # Setup inheritance magic
20
20
  setup_children_magic(opt)
21
+
22
+ # Setup sharding if needed
23
+ if opt[:sharded]
24
+ raise ArgumentError, "Can't use sharding on a model with slaves!" if opt[:slaves].any?
25
+ setup_sharding_magic(opt[:sharded])
26
+ setup_connection_magic(DbCharmer::StubConnection.new)
27
+ end
21
28
  end
22
29
 
23
30
  private
@@ -31,11 +38,17 @@ module DbCharmer
31
38
  end
32
39
  end
33
40
 
34
- def db_magic_connection(conn, should_exist = false)
41
+ def setup_sharding_magic(config)
42
+ self.extend(DbCharmer::Sharding::ClassMethods)
43
+ name = config[:sharded_connection] or raise ArgumentError, "No :sharded_connection!"
44
+ self.sharded_connection = DbCharmer::Sharding.sharded_connection(name)
45
+ end
46
+
47
+ def setup_connection_magic(conn, should_exist = false)
35
48
  switch_connection_to(conn, should_exist)
36
49
  end
37
50
 
38
- def db_magic_slaves(slaves, should_exist = false)
51
+ def setup_slaves_magic(slaves, should_exist = false)
39
52
  self.db_charmer_slaves = slaves.collect do |slave|
40
53
  coerce_to_connection_proxy(slave, should_exist)
41
54
  end
@@ -47,3 +60,4 @@ module DbCharmer
47
60
  end
48
61
  end
49
62
  end
63
+
@@ -3,15 +3,23 @@ module DbCharmer
3
3
  @multi_db_names = nil
4
4
 
5
5
  def migrate_with_db_wrapper(direction)
6
- @multi_db_names.each { |multi_db_name| on_db(multi_db_name) { migrate_without_db_wrapper(direction) } }
6
+ @multi_db_names.each do |multi_db_name|
7
+ on_db(multi_db_name) do
8
+ migrate_without_db_wrapper(direction)
9
+ end
10
+ end
7
11
  end
8
12
 
9
13
  def on_db(db_name)
10
- announce "Switching connection to #{db_name}"
14
+ name = db_name.is_a?(Hash) ? db_name[:name] : db_name.inspect
15
+ announce "Switching connection to #{name}"
16
+ # Switch connection
11
17
  old_proxy = ActiveRecord::Base.db_charmer_connection_proxy
12
18
  ActiveRecord::Base.switch_connection_to(db_name, DbCharmer.migration_connections_should_exist?)
19
+ # Yield the block
13
20
  yield
14
21
  ensure
22
+ # Switch it back
15
23
  announce "Checking all database connections"
16
24
  ActiveRecord::Base.verify_active_connections!
17
25
  announce "Switching connection back to default"
@@ -19,12 +27,25 @@ module DbCharmer
19
27
  end
20
28
 
21
29
  def db_magic(opts = {})
22
- opts[:connection] = opts[:connections] if opts[:connections]
23
- raise ArgumentError, "No connection name - no magic!" unless opts[:connection]
24
- @multi_db_names = [opts[:connection]].flatten
30
+ # Collect connections from all possible options
31
+ conns = [ opts[:connection], opts[:connections] ]
32
+ conns << shard_connections(opts[:sharded_connection]) if opts[:sharded_connection]
33
+
34
+ # Get a unique set of connections
35
+ conns = conns.flatten.compact.uniq
36
+ raise ArgumentError, "No connection name - no magic!" unless conns.any?
37
+
38
+ # Save connections
39
+ @multi_db_names = conns
25
40
  class << self
26
41
  alias_method_chain :migrate, :db_wrapper
27
42
  end
28
43
  end
44
+
45
+ # Return a list of connections to shards in a sharded connection
46
+ def shard_connections(conn_name)
47
+ conn = DbCharmer::Sharding.sharded_connection(conn_name)
48
+ conn.shard_connections
49
+ end
29
50
  end
30
51
  end
@@ -1,5 +1,7 @@
1
1
  module DbCharmer
2
2
  module MultiDbProxy
3
+ # Simple proxy class that switches connections and then proxies all the calls
4
+ # This class is used to implement chained on_db calls
3
5
  class OnDbProxy < BlankSlate
4
6
  def initialize(proxy_target, slave)
5
7
  @proxy_target = proxy_target
@@ -10,8 +12,8 @@ module DbCharmer
10
12
 
11
13
  def method_missing(meth, *args, &block)
12
14
  # Switch connection and proxy the method call
13
- @proxy_target.on_db(@slave) do |m|
14
- res = m.__send__(meth, *args, &block)
15
+ @proxy_target.on_db(@slave) do |proxy_target|
16
+ res = proxy_target.__send__(meth, *args, &block)
15
17
 
16
18
  # If result is a scope/association, return a new proxy for it, otherwise return the result itself
17
19
  (res.proxy?) ? OnDbProxy.new(res, @slave) : res
@@ -0,0 +1,51 @@
1
+ module DbCharmer
2
+ module Sharding
3
+ module ClassMethods
4
+ def self.extended(model)
5
+ model.cattr_accessor(:sharded_connection)
6
+ end
7
+
8
+ def shard_for(key, proxy_target = nil, &block)
9
+ raise ArgumentError, "No sharded connection configured!" unless sharded_connection
10
+ conn = sharded_connection.sharder.shard_for_key(key)
11
+ on_db(conn, proxy_target, &block)
12
+ end
13
+
14
+ # Run on default shard (if supported by the sharding method)
15
+ def on_default_shard(proxy_target = nil, &block)
16
+ raise ArgumentError, "No sharded connection configured!" unless sharded_connection
17
+
18
+ if sharded_connection.support_default_shard?
19
+ shard_for(:default, proxy_target, &block)
20
+ else
21
+ raise ArgumentError, "This model's sharding method does not support default shard"
22
+ end
23
+ end
24
+
25
+ # Enumerate shards
26
+ def on_each_shard(proxy_target = nil, &block)
27
+ raise ArgumentError, "No sharded connection configured!" unless sharded_connection
28
+
29
+ conns = sharded_connection.shard_connections
30
+ raise ArgumentError, "This model's sharding method does not support shards enumeration" unless conns
31
+
32
+ conns.each do |conn|
33
+ on_db(conn, proxy_target, &block)
34
+ end
35
+ end
36
+ end
37
+
38
+ #-------------------------------------------------------------
39
+ @@sharded_connections = {}
40
+
41
+ def self.register_connection(config)
42
+ name = config[:name] or raise ArgumentError, "No :name in connection!"
43
+ @@sharded_connections[name] = DbCharmer::Sharding::Connection.new(config)
44
+ end
45
+
46
+ def self.sharded_connection(name)
47
+ @@sharded_connections[name] or raise ArgumentError, "Invalid sharded connection name!"
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,27 @@
1
+ module DbCharmer
2
+ module Sharding
3
+ class Connection
4
+ attr_accessor :config, :sharder
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ @sharder = self.instantiate_sharder
9
+ end
10
+
11
+ def instantiate_sharder
12
+ raise ArgumentError, "No :method passed!" unless config[:method]
13
+ sharder_class_name = "DbCharmer::Sharding::Method::#{config[:method].to_s.classify}"
14
+ sharder_class = sharder_class_name.constantize
15
+ sharder_class.new(config)
16
+ end
17
+
18
+ def shard_connections
19
+ sharder.respond_to?(:shard_connections) ? sharder.shard_connections : nil
20
+ end
21
+
22
+ def support_default_shard?
23
+ sharder.respond_to?(:support_default_shard?) && sharder.support_default_shard?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,184 @@
1
+ # This is a more sophisticated sharding method based on a database-backed
2
+ # blocks map that holds block-shard associations. It automatically
3
+ # creates new blocks for new keys and assigns them to shards.
4
+ #
5
+ module DbCharmer
6
+ module Sharding
7
+ module Method
8
+ class DbBlockMap
9
+ # Sharder name
10
+ attr_accessor :name
11
+
12
+ # Mapping db connection
13
+ attr_accessor :connection, :connection_name
14
+
15
+ # Mapping table name
16
+ attr_accessor :map_table
17
+
18
+ # Shards table name
19
+ attr_accessor :shards_table
20
+
21
+ # Sharding keys block size
22
+ attr_accessor :block_size
23
+
24
+ def initialize(config)
25
+ @name = config[:name] or raise(ArgumentError, "Missing required :name parameter!")
26
+ @connection = DbCharmer::ConnectionFactory.connect(config[:connection])
27
+ @block_size = (config[:block_size] || 10000).to_i
28
+
29
+ @map_table = config[:map_table] or raise(ArgumentError, "Missing required :map_table parameter!")
30
+ @shards_table = config[:shards_table] or raise(ArgumentError, "Missing required :shards_table parameter!")
31
+
32
+ # Local caches
33
+ @shard_info_cache = {}
34
+ @blocks_cache = {}
35
+ end
36
+
37
+ def shard_for_key(key)
38
+ block = block_for_key(key)
39
+
40
+ # Auto-allocate new blocks
41
+ block ||= allocate_new_block_for_key(key)
42
+ raise ArgumentError, "Invalid key value, no shards found for this key and could not create a new block!" unless block
43
+
44
+ # Bail if no shard found
45
+ shard_id = block['shard_id'].to_i
46
+ shard_info = shard_info_by_id(shard_id)
47
+ raise ArgumentError, "Invalid shard_id: #{shard_id}" unless shard_info
48
+
49
+ # Get config
50
+ shard_connection_config(shard_info)
51
+ end
52
+
53
+ class ShardInfo < ActiveRecord::Base
54
+ validates_presence_of :db_host
55
+ validates_presence_of :db_port
56
+ validates_presence_of :db_user
57
+ validates_presence_of :db_pass
58
+ validates_presence_of :db_name
59
+ end
60
+
61
+ # Returns a block for a key
62
+ def block_for_key(key, cache = true)
63
+ # Cleanup the cache if asked to
64
+ key_range = [ block_start_for_key(key), block_end_for_key(key) ]
65
+ block_cache_key = "%d-%d" % key_range
66
+ @blocks_cache[block_cache_key] = nil unless cache
67
+
68
+ # Fetch cached value or load from db
69
+ @blocks_cache[block_cache_key] ||= begin
70
+ sql = "SELECT * FROM #{map_table} WHERE start_id = #{key_range.first} AND end_id = #{key_range.last} LIMIT 1"
71
+ connection.select_one(sql, 'Find a shard block')
72
+ end
73
+ end
74
+
75
+ # Load shard info
76
+ def shard_info_by_id(shard_id, cache = true)
77
+ # Cleanup the cache if asked to
78
+ @shard_info_cache[shard_id] = nil unless cache
79
+
80
+ # Either load from cache or from db
81
+ @shard_info_cache[shard_id] ||= begin
82
+ prepare_shard_model
83
+ ShardInfo.find_by_id(shard_id)
84
+ end
85
+ end
86
+
87
+ def allocate_new_block_for_key(key)
88
+ # Can't find any shards to use for blocks allocation!
89
+ return nil unless shard = least_loaded_shard
90
+
91
+ # Figure out block limits
92
+ start_id = block_start_for_key(key)
93
+ end_id = block_end_for_key(key)
94
+
95
+ # Try to insert a new mapping (ignore duplicate key errors)
96
+ sql = <<-SQL
97
+ INSERT IGNORE INTO #{map_table}
98
+ SET start_id = #{start_id},
99
+ end_id = #{end_id},
100
+ shard_id = #{shard.id},
101
+ block_size = #{block_size},
102
+ created_at = NOW(),
103
+ updated_at = NOW()
104
+ SQL
105
+ connection.execute(sql, "Allocate new block")
106
+
107
+ # Retry block search after creation
108
+ block_for_key(key)
109
+ end
110
+
111
+ def least_loaded_shard
112
+ prepare_shard_model
113
+
114
+ # Select shard
115
+ shard = ShardInfo.all(:conditions => { :enabled => true, :open => true }, :order => 'blocks_count ASC', :limit => 1).first
116
+ raise "Can't find any shards to use for blocks allocation!" unless shard
117
+ return shard
118
+ end
119
+
120
+ def block_start_for_key(key)
121
+ block_size.to_i * (key.to_i / block_size.to_i)
122
+ end
123
+
124
+ def block_end_for_key(key)
125
+ block_size.to_i + block_start_for_key(key)
126
+ end
127
+
128
+ # Create configuration (use mapping connection as a template)
129
+ def shard_connection_config(shard)
130
+ # Format connection name
131
+ shard_name = "db_charmer_db_block_map_#{name}_shard_%05d" % shard.id
132
+
133
+ # Here we get the mapping connection's configuration
134
+ # They do not expose configs so we hack in and get the instance var
135
+ # FIXME: Find a better way, maybe move config method to our ar extenstions
136
+ connection.instance_variable_get(:@config).clone.merge(
137
+ # Name for the connection factory
138
+ :name => shard_name,
139
+ # Connection params
140
+ :host => shard.db_host,
141
+ :port => shard.db_port,
142
+ :username => shard.db_user,
143
+ :password => shard.db_pass,
144
+ :database => shard.db_name
145
+ )
146
+ end
147
+
148
+ def create_shard(params)
149
+ params = params.symbolize_keys
150
+ [ :db_host, :db_port, :db_user, :db_pass, :db_name ].each do |arg|
151
+ raise ArgumentError, "Missing required parameter: #{arg}" unless params[arg]
152
+ end
153
+
154
+ # Prepare model
155
+ prepare_shard_model
156
+
157
+ # Create the record
158
+ ShardInfo.create! do |shard|
159
+ shard.db_host = params[:db_host]
160
+ shard.db_port = params[:db_port]
161
+ shard.db_user = params[:db_user]
162
+ shard.db_pass = params[:db_pass]
163
+ shard.db_name = params[:db_name]
164
+ end
165
+ end
166
+
167
+ def shard_connections
168
+ # Find all shards
169
+ prepare_shard_model
170
+ shards = ShardInfo.all(:conditions => { :enabled => true })
171
+ # Map them to connections
172
+ shards.map { |shard| shard_connection_config(shard) }
173
+ end
174
+
175
+ # Prepare model for working with our shards table
176
+ def prepare_shard_model
177
+ ShardInfo.set_table_name(shards_table)
178
+ ShardInfo.switch_connection_to(connection)
179
+ end
180
+
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,23 @@
1
+ module DbCharmer
2
+ module Sharding
3
+ module Method
4
+ class HashMap
5
+ attr_accessor :map
6
+
7
+ def initialize(config)
8
+ @map = config[:map].clone or raise ArgumentError, "No :map defined!"
9
+ end
10
+
11
+ def shard_for_key(key)
12
+ res = map[key] || map[:default]
13
+ raise ArgumentError, "Invalid key value, no shards found for this key!" unless res
14
+ return res
15
+ end
16
+
17
+ def support_default_shard?
18
+ map.has_key?(:default)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ module DbCharmer
2
+ module Sharding
3
+ module Method
4
+ class Range
5
+ attr_accessor :ranges
6
+
7
+ def initialize(config)
8
+ @ranges = config[:ranges] ? config[:ranges].clone : raise(ArgumentError, "No :ranges defined!")
9
+ end
10
+
11
+ def shard_for_key(key)
12
+ return ranges[:default] if key == :default
13
+
14
+ ranges.each do |range, shard|
15
+ next if range == :default
16
+ return shard if range.member?(key.to_i)
17
+ end
18
+
19
+ return ranges[:default] if ranges[:default]
20
+ raise ArgumentError, "Invalid key value, no shards found for this key!"
21
+ end
22
+
23
+ def support_default_shard?
24
+ ranges.has_key?(:default)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,9 @@
1
+ module DbCharmer
2
+ class StubConnection < ActiveRecord::ConnectionAdapters::AbstractAdapter
3
+ def initialize; end
4
+
5
+ def method_missing(*arg)
6
+ raise ActiveRecord::ConnectionNotEstablished, "You have to switch connection on your model before using it"
7
+ end
8
+ end
9
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 1
7
- - 5
8
- - 5
9
- version: 1.5.5
7
+ - 6
8
+ - 0
9
+ version: 1.6.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Alexey Kovyrin
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-03-15 00:00:00 -04:00
17
+ date: 2010-03-31 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -74,6 +74,12 @@ files:
74
74
  - lib/db_charmer/multi_db_migrations.rb
75
75
  - lib/db_charmer/multi_db_proxy.rb
76
76
  - lib/db_charmer/scope_proxy.rb
77
+ - lib/db_charmer/sharding.rb
78
+ - lib/db_charmer/sharding/connection.rb
79
+ - lib/db_charmer/sharding/method/db_block_map.rb
80
+ - lib/db_charmer/sharding/method/hash_map.rb
81
+ - lib/db_charmer/sharding/method/range.rb
82
+ - lib/db_charmer/stub_connection.rb
77
83
  - lib/tasks/databases.rake
78
84
  has_rdoc: true
79
85
  homepage: http://github.com/kovyrin/db-charmer