db-charmer 1.5.5 → 1.6.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/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