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 +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
|