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 +7 -0
- data/README.rdoc +182 -14
- data/VERSION +1 -1
- data/db-charmer.gemspec +8 -2
- data/lib/db_charmer.rb +11 -12
- data/lib/db_charmer/active_record_extensions.rb +7 -5
- data/lib/db_charmer/connection_factory.rb +46 -16
- data/lib/db_charmer/connection_proxy.rb +1 -0
- data/lib/db_charmer/connection_switch.rb +6 -0
- data/lib/db_charmer/db_magic.rb +18 -4
- data/lib/db_charmer/multi_db_migrations.rb +26 -5
- data/lib/db_charmer/multi_db_proxy.rb +4 -2
- data/lib/db_charmer/sharding.rb +51 -0
- data/lib/db_charmer/sharding/connection.rb +27 -0
- data/lib/db_charmer/sharding/method/db_block_map.rb +184 -0
- data/lib/db_charmer/sharding/method/hash_map.rb +23 -0
- data/lib/db_charmer/sharding/method/range.rb +29 -0
- data/lib/db_charmer/stub_connection.rb +9 -0
- metadata +10 -4
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
|
data/README.rdoc
CHANGED
@@ -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'
|
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
|
-
|
293
|
+
== Simple Sharding Support
|
276
294
|
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
289
|
-
|
290
|
-
|
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.
|
1
|
+
1.6.0
|
data/db-charmer.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{db-charmer}
|
8
|
-
s.version = "1.
|
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-
|
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}
|
data/lib/db_charmer.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
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
|
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(
|
17
|
-
|
18
|
-
@@connection_classes[
|
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(
|
23
|
-
abstract_class = generate_abstract_class(
|
24
|
-
DbCharmer::ConnectionProxy.new(abstract_class,
|
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(
|
29
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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(
|
43
|
-
"::AutoGeneratedAbstractConnectionClass#{
|
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
|
@@ -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
|
data/lib/db_charmer/db_magic.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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 |
|
14
|
-
res =
|
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
|
-
-
|
8
|
-
-
|
9
|
-
version: 1.
|
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-
|
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
|