dynashard 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +16 -0
- data/Gemfile.lock +100 -0
- data/LICENSE.txt +20 -0
- data/README.md +72 -0
- data/Rakefile +45 -0
- data/VERSION +1 -0
- data/lib/dynashard/arel_sql_engine.rb +22 -0
- data/lib/dynashard/associations.rb +19 -0
- data/lib/dynashard/connection_handler.rb +45 -0
- data/lib/dynashard/model.rb +92 -0
- data/lib/dynashard/validations.rb +23 -0
- data/lib/dynashard.rb +167 -0
- data/spec/arel_sql_engine_spec.rb +51 -0
- data/spec/associations_spec.rb +182 -0
- data/spec/connection_handler_spec.rb +61 -0
- data/spec/db/schema.rb +67 -0
- data/spec/dynashard_spec.rb +126 -0
- data/spec/model_spec.rb +104 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/factories.rb +32 -0
- data/spec/support/models.rb +99 -0
- data/spec/validation_spec.rb +101 -0
- metadata +188 -0
data/.document
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem 'activerecord', '>= 3.0'
|
4
|
+
|
5
|
+
group :development do
|
6
|
+
gem "shoulda", ">= 0"
|
7
|
+
gem "bundler", "~> 1.0.0"
|
8
|
+
gem "jeweler", "~> 1.5.2"
|
9
|
+
gem "rcov", ">= 0"
|
10
|
+
end
|
11
|
+
|
12
|
+
group :test do
|
13
|
+
gem 'rspec', '>= 2.0'
|
14
|
+
gem 'sqlite3-ruby', :require => 'sqlite3'
|
15
|
+
gem 'factory_girl_rails'
|
16
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
abstract (1.0.0)
|
5
|
+
actionmailer (3.0.1)
|
6
|
+
actionpack (= 3.0.1)
|
7
|
+
mail (~> 2.2.5)
|
8
|
+
actionpack (3.0.1)
|
9
|
+
activemodel (= 3.0.1)
|
10
|
+
activesupport (= 3.0.1)
|
11
|
+
builder (~> 2.1.2)
|
12
|
+
erubis (~> 2.6.6)
|
13
|
+
i18n (~> 0.4.1)
|
14
|
+
rack (~> 1.2.1)
|
15
|
+
rack-mount (~> 0.6.12)
|
16
|
+
rack-test (~> 0.5.4)
|
17
|
+
tzinfo (~> 0.3.23)
|
18
|
+
activemodel (3.0.1)
|
19
|
+
activesupport (= 3.0.1)
|
20
|
+
builder (~> 2.1.2)
|
21
|
+
i18n (~> 0.4.1)
|
22
|
+
activerecord (3.0.1)
|
23
|
+
activemodel (= 3.0.1)
|
24
|
+
activesupport (= 3.0.1)
|
25
|
+
arel (~> 1.0.0)
|
26
|
+
tzinfo (~> 0.3.23)
|
27
|
+
activeresource (3.0.1)
|
28
|
+
activemodel (= 3.0.1)
|
29
|
+
activesupport (= 3.0.1)
|
30
|
+
activesupport (3.0.1)
|
31
|
+
arel (1.0.1)
|
32
|
+
activesupport (~> 3.0.0)
|
33
|
+
builder (2.1.2)
|
34
|
+
diff-lcs (1.1.2)
|
35
|
+
erubis (2.6.6)
|
36
|
+
abstract (>= 1.0.0)
|
37
|
+
factory_girl (1.3.2)
|
38
|
+
factory_girl_rails (1.0)
|
39
|
+
factory_girl (~> 1.3)
|
40
|
+
rails (>= 3.0.0.beta4)
|
41
|
+
git (1.2.5)
|
42
|
+
i18n (0.4.2)
|
43
|
+
jeweler (1.5.2)
|
44
|
+
bundler (~> 1.0.0)
|
45
|
+
git (>= 1.2.5)
|
46
|
+
rake
|
47
|
+
mail (2.2.9)
|
48
|
+
activesupport (>= 2.3.6)
|
49
|
+
i18n (~> 0.4.1)
|
50
|
+
mime-types (~> 1.16)
|
51
|
+
treetop (~> 1.4.8)
|
52
|
+
mime-types (1.16)
|
53
|
+
polyglot (0.3.1)
|
54
|
+
rack (1.2.1)
|
55
|
+
rack-mount (0.6.13)
|
56
|
+
rack (>= 1.0.0)
|
57
|
+
rack-test (0.5.6)
|
58
|
+
rack (>= 1.0)
|
59
|
+
rails (3.0.1)
|
60
|
+
actionmailer (= 3.0.1)
|
61
|
+
actionpack (= 3.0.1)
|
62
|
+
activerecord (= 3.0.1)
|
63
|
+
activeresource (= 3.0.1)
|
64
|
+
activesupport (= 3.0.1)
|
65
|
+
bundler (~> 1.0.0)
|
66
|
+
railties (= 3.0.1)
|
67
|
+
railties (3.0.1)
|
68
|
+
actionpack (= 3.0.1)
|
69
|
+
activesupport (= 3.0.1)
|
70
|
+
rake (>= 0.8.4)
|
71
|
+
thor (~> 0.14.0)
|
72
|
+
rake (0.8.7)
|
73
|
+
rcov (0.9.9)
|
74
|
+
rspec (2.1.0)
|
75
|
+
rspec-core (~> 2.1.0)
|
76
|
+
rspec-expectations (~> 2.1.0)
|
77
|
+
rspec-mocks (~> 2.1.0)
|
78
|
+
rspec-core (2.1.0)
|
79
|
+
rspec-expectations (2.1.0)
|
80
|
+
diff-lcs (~> 1.1.2)
|
81
|
+
rspec-mocks (2.1.0)
|
82
|
+
shoulda (2.11.3)
|
83
|
+
sqlite3-ruby (1.3.2)
|
84
|
+
thor (0.14.4)
|
85
|
+
treetop (1.4.8)
|
86
|
+
polyglot (>= 0.3.1)
|
87
|
+
tzinfo (0.3.23)
|
88
|
+
|
89
|
+
PLATFORMS
|
90
|
+
ruby
|
91
|
+
|
92
|
+
DEPENDENCIES
|
93
|
+
activerecord (>= 3.0)
|
94
|
+
bundler (~> 1.0.0)
|
95
|
+
factory_girl_rails
|
96
|
+
jeweler (~> 1.5.2)
|
97
|
+
rcov
|
98
|
+
rspec (>= 2.0)
|
99
|
+
shoulda
|
100
|
+
sqlite3-ruby
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Nick Hengeveld
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
# Dynashard - Dynamic sharding for ActiveRecord
|
2
|
+
|
3
|
+
This package provides database sharding functionality for ActiveRecord models.
|
4
|
+
|
5
|
+
Sharding is disabled by default and is enabled with +Dynashard.enable+. This allows
|
6
|
+
sharding behavior to be enabled globally or only for specific environments so for
|
7
|
+
example production environments could be sharded while development environments could
|
8
|
+
use a single database.
|
9
|
+
|
10
|
+
Models may be configured to determine the appropriate shard (database connection) to
|
11
|
+
use based on context defined prior to performing queries.
|
12
|
+
|
13
|
+
class Widget < ActiveRecord::Base
|
14
|
+
shard :by => :user
|
15
|
+
end
|
16
|
+
|
17
|
+
class WidgetController < ApplicationController
|
18
|
+
around_filter :set_shard_context
|
19
|
+
|
20
|
+
def index
|
21
|
+
# Widgets will be loaded using the connection for the current user's shard
|
22
|
+
@widgets = Widget.find(:all)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def set_shard_context
|
28
|
+
Dynashard.with_context(:user => current_user.shard) do
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Associated models may be configured to use different shards determined by the
|
35
|
+
association's owner.
|
36
|
+
|
37
|
+
class Company < ActiveRecord::Base
|
38
|
+
shard :associated, :using => :shard
|
39
|
+
|
40
|
+
has_many :customers
|
41
|
+
|
42
|
+
def shard
|
43
|
+
# logic to find the company's shard
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Customer < ActiveRecord::Base
|
48
|
+
belongs_to :company
|
49
|
+
shard :by => :company
|
50
|
+
end
|
51
|
+
|
52
|
+
> c = Company.find(:first)
|
53
|
+
=> #<Company id:1>
|
54
|
+
|
55
|
+
Company is loaded using the default ActiveRecord connection.
|
56
|
+
|
57
|
+
> c.customers
|
58
|
+
=> [#<Dynashard::Shard0::Customer id: 1>, #<Dynashard::Shard0::Customer id: 2>]
|
59
|
+
|
60
|
+
Customers are loaded using the connection for the Company's shard. Associated models
|
61
|
+
are returned as shard-specific subclasses of the association class.
|
62
|
+
|
63
|
+
> c.customers.create(:name => 'Always right')
|
64
|
+
=> #<Dynashard::Shard0::Customer id: 3>
|
65
|
+
|
66
|
+
New associations are saved on the Company's shard.
|
67
|
+
|
68
|
+
## TODO: add gotcha section, eg:
|
69
|
+
|
70
|
+
- uniqueness validations should be scoped by whatever is sharding
|
71
|
+
- ways to shoot yourself in the foot with non-sharding association
|
72
|
+
owners of sharded models
|
data/Rakefile
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
begin
|
7
|
+
Bundler.setup(:default, :development)
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
require 'rake'
|
14
|
+
|
15
|
+
RSpec::Core::RakeTask.new(:spec)
|
16
|
+
|
17
|
+
task :default => :spec
|
18
|
+
|
19
|
+
require 'jeweler'
|
20
|
+
Jeweler::Tasks.new do |gem|
|
21
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
22
|
+
gem.name = "dynashard"
|
23
|
+
gem.homepage = "http://github.com/nickh/dynashard"
|
24
|
+
gem.license = "MIT"
|
25
|
+
gem.summary = 'Dynamic sharding for ActiveRecord'
|
26
|
+
gem.description = 'Dynashard allows you to shard your ActiveRecord models. Models can be configured to shard based on context that can be defined dynamically.'
|
27
|
+
gem.email = "nickh@verticalresponse.com"
|
28
|
+
gem.authors = ["Nick Hengeveld"]
|
29
|
+
# Include your dependencies below. Runtime dependencies are required when using your gem,
|
30
|
+
# and development dependencies are only needed for development (ie running rake tasks, tests, etc)
|
31
|
+
# gem.add_runtime_dependency 'jabber4r', '> 0.1'
|
32
|
+
# gem.add_development_dependency 'rspec', '> 1.2.3'
|
33
|
+
gem.add_runtime_dependency 'activerecord', '>= 3.0'
|
34
|
+
end
|
35
|
+
Jeweler::RubygemsDotOrgTasks.new
|
36
|
+
|
37
|
+
require 'rake/rdoctask'
|
38
|
+
Rake::RDocTask.new do |rdoc|
|
39
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
40
|
+
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = "dynashard #{version}"
|
43
|
+
rdoc.rdoc_files.include('README*')
|
44
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
45
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Dynashard
|
2
|
+
module ArelEngineExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :connection, :dynashard
|
5
|
+
end
|
6
|
+
|
7
|
+
def connection_with_dynashard
|
8
|
+
if @ar && @ar.respond_to?(:dynashard_klass)
|
9
|
+
@ar.dynashard_klass.connection
|
10
|
+
elsif @ar && @ar.sharding_enabled?
|
11
|
+
spec = Dynashard.shard_context[@ar.dynashard_context]
|
12
|
+
raise "Missing #{@ar.dynashard_context} shard context" if spec.nil?
|
13
|
+
spec = spec.call if spec.respond_to?(:call)
|
14
|
+
Dynashard.class_for(spec).connection
|
15
|
+
else
|
16
|
+
connection_without_dynashard
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Arel::Sql::Engine.send(:include, Dynashard::ArelEngineExtensions)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dynashard
|
2
|
+
module ProxyExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :initialize, :dynashard
|
5
|
+
end
|
6
|
+
|
7
|
+
# Initialize an association proxy. If the proxy owner class is configured to
|
8
|
+
# shard its associations and the reflection klass is sharded, use a custom
|
9
|
+
# reflection with a sharded class.
|
10
|
+
def initialize_with_dynashard(owner, reflection)
|
11
|
+
if owner.class.shards_associated? && reflection.klass.sharding_enabled?
|
12
|
+
reflection = Dynashard.reflection_for(owner, reflection)
|
13
|
+
end
|
14
|
+
initialize_without_dynashard(owner, reflection)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveRecord::Associations::AssociationProxy.send(:include, Dynashard::ProxyExtensions)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Dynashard
|
2
|
+
module ConnectionHandlerExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :retrieve_connection_pool, :dynashard
|
5
|
+
end
|
6
|
+
|
7
|
+
# Set an connection pool entry for the model class pointing at
|
8
|
+
# the shard class's pool.
|
9
|
+
# :nodoc:
|
10
|
+
# This is required for places that examine the connection pool to
|
11
|
+
# determine whether to use a given class's connection or a parent
|
12
|
+
# class's connection (eg. ActiveRecord::Base.arel_engine)
|
13
|
+
def dynashard_pool_alias(model_class, shard_class)
|
14
|
+
@connection_pools[model_class] = @connection_pools[shard_class]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return a connection pool for the specified class. If sharding
|
18
|
+
# is enabled, the correct pool for the configured sharding context
|
19
|
+
# will be used.
|
20
|
+
#
|
21
|
+
# :nodoc:
|
22
|
+
# Reflection classes generated for an association proxy will
|
23
|
+
# respond to :dynashard_klass and return a class with an
|
24
|
+
# established connection to the correct shard.
|
25
|
+
#
|
26
|
+
# Sharded models will have a dynashard context defined, which
|
27
|
+
# can be used as a key to the Dynashard.shard_context hash to
|
28
|
+
# access the shard's connection specification.
|
29
|
+
def retrieve_connection_pool_with_dynashard(klass)
|
30
|
+
if klass.respond_to?(:dynashard_klass)
|
31
|
+
retrieve_connection_pool_without_dynashard(klass.dynashard_klass)
|
32
|
+
elsif klass.sharding_enabled?
|
33
|
+
spec = Dynashard.shard_context[klass.dynashard_context]
|
34
|
+
raise "Missing #{klass.dynashard_context} shard context" if spec.nil?
|
35
|
+
spec = spec.call if spec.respond_to?(:call)
|
36
|
+
shard_klass = Dynashard.class_for(spec)
|
37
|
+
retrieve_connection_pool_without_dynashard(shard_klass)
|
38
|
+
else
|
39
|
+
retrieve_connection_pool_without_dynashard(klass)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.send(:include, Dynashard::ConnectionHandlerExtensions)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Dynashard
|
2
|
+
module ActiveRecordExtensions
|
3
|
+
def self.extended(base)
|
4
|
+
base.extend(ClassMethods)
|
5
|
+
|
6
|
+
# Change ActiveRecord::Base.arel_engine to create an engine for sharded models
|
7
|
+
# rather than using the ActiveRecord::Base class.
|
8
|
+
base.module_eval do
|
9
|
+
def self.arel_engine
|
10
|
+
if sharding_enabled?
|
11
|
+
Arel::Sql::Engine.new(self)
|
12
|
+
elsif self == ActiveRecord::Base
|
13
|
+
Arel::Table.engine
|
14
|
+
else
|
15
|
+
connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.arel_engine
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
# Configure sharding for the current model. The following options are supported:
|
23
|
+
# * +:by => :context+ - the database connection for this model will be determined using a connection
|
24
|
+
# proxy with the specified context.
|
25
|
+
# * +:associated, :using => :method+ - the database connection for associated models with sharding
|
26
|
+
# enabled will be determined using the association's defined context after first setting that
|
27
|
+
# context using the specified method on the current instance
|
28
|
+
#
|
29
|
+
# class MyModel < ActiveRecord::Base
|
30
|
+
# shard :by => :context
|
31
|
+
# shard :assocated, :using => :sharding_method
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# def sharding_method
|
35
|
+
# # return a valid shard descriptor:
|
36
|
+
# # - a string key into the configurations in databases.yml
|
37
|
+
# # - a hash with connection parameters
|
38
|
+
# # - an object that responds to :call with one of the above
|
39
|
+
# end
|
40
|
+
def shard(*args)
|
41
|
+
if args.first == :associated
|
42
|
+
using = args.last[:using] if args.last.respond_to?(:[])
|
43
|
+
raise ArgumentError.new(":associated specified without :using") if using.nil?
|
44
|
+
@dynashard_association_using = using
|
45
|
+
else
|
46
|
+
shard_by = args.first[:by] if args.first.respond_to?(:[])
|
47
|
+
raise ArgumentError.new("Invalid options") if shard_by.nil?
|
48
|
+
@dynashard_context = shard_by
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns true if sharding has been globally enabled and has been configured for this model
|
53
|
+
def sharding_enabled?
|
54
|
+
Dynashard.enabled? && !@dynashard_context.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns true if sharding has been globally enabled and is configured to be used for
|
58
|
+
# sharded models associated with this model
|
59
|
+
def shards_associated?
|
60
|
+
Dynashard.enabled? && !@dynashard_association_using.nil?
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns true if the class was generated by Dynashard
|
64
|
+
def dynashard_model?
|
65
|
+
false
|
66
|
+
end
|
67
|
+
|
68
|
+
# Return a subclass configured to connect to the appropriate shard
|
69
|
+
def dynashard_sharded_subclass
|
70
|
+
if sharding_enabled?
|
71
|
+
spec = Dynashard.shard_context[dynashard_context]
|
72
|
+
raise "Missing #{dynashard_context} shard context" if spec.nil?
|
73
|
+
spec = spec.call if spec.respond_to?(:call)
|
74
|
+
shard_klass = Dynashard.class_for(spec)
|
75
|
+
Dynashard.sharded_model_class(shard_klass, self)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the shard context for this model
|
80
|
+
def dynashard_context
|
81
|
+
@dynashard_context
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns the method used to set the context used for associated models
|
85
|
+
def dynashard_association_using
|
86
|
+
@dynashard_association_using
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
ActiveRecord::Base.extend(Dynashard::ActiveRecordExtensions)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Dynashard
|
2
|
+
module ValidationExtensions
|
3
|
+
def self.included(base)
|
4
|
+
base.alias_method_chain :find_finder_class_for, :dynashard
|
5
|
+
end
|
6
|
+
|
7
|
+
# Return the class that should be used to find other instances of
|
8
|
+
# the specified record's model class. If the record is an instance
|
9
|
+
# of a sharded model, it should be used; otherwise the default
|
10
|
+
# behavior should be used.
|
11
|
+
def find_finder_class_for_with_dynashard(record)
|
12
|
+
if record.class.dynashard_model?
|
13
|
+
record.class
|
14
|
+
elsif record.class.sharding_enabled?
|
15
|
+
record.class.dynashard_sharded_subclass
|
16
|
+
else
|
17
|
+
find_finder_class_for_without_dynashard(record)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
ActiveRecord::Validations::UniquenessValidator.send(:include, Dynashard::ValidationExtensions)
|
data/lib/dynashard.rb
ADDED
@@ -0,0 +1,167 @@
|
|
1
|
+
# = Dynashard - Dynamic sharding for ActiveRecord
|
2
|
+
#
|
3
|
+
# This package provides database sharding functionality for ActiveRecord models.
|
4
|
+
#
|
5
|
+
# Sharding is disabled by default and is enabled with +Dynashard.enable+. This allows
|
6
|
+
# sharding behavior to be enabled globally or only for specific environments so for
|
7
|
+
# example production environments could be sharded while development environments could
|
8
|
+
# use a single database.
|
9
|
+
#
|
10
|
+
# Models may be configured to determine the appropriate shard (database connection) to
|
11
|
+
# use based on context defined prior to performing queries.
|
12
|
+
#
|
13
|
+
# class Widget < ActiveRecord::Base
|
14
|
+
# shard :by => :user
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# class WidgetController < ApplicationController
|
18
|
+
# around_filter :set_shard_context
|
19
|
+
#
|
20
|
+
# def index
|
21
|
+
# # Widgets will be loaded using the connection for the current user's shard
|
22
|
+
# @widgets = Widget.find(:all)
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# private
|
26
|
+
#
|
27
|
+
# def set_shard_context
|
28
|
+
# Dynashard.with_context(:user => current_user.shard) do
|
29
|
+
# yield
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Associated models may be configured to use different shards determined by the
|
35
|
+
# association's owner.
|
36
|
+
#
|
37
|
+
# class Company < ActiveRecord::Base
|
38
|
+
# shard :associated, :using => :shard
|
39
|
+
#
|
40
|
+
# has_many :customers
|
41
|
+
#
|
42
|
+
# def shard
|
43
|
+
# # logic to find the company's shard
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# class Customer < ActiveRecord::Base
|
48
|
+
# belongs_to :company
|
49
|
+
# shard :by => :company
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# > c = Company.find(:first)
|
53
|
+
# => #<Company id:1>
|
54
|
+
#
|
55
|
+
# Company is loaded using the default ActiveRecord connection.
|
56
|
+
#
|
57
|
+
# > c.customers
|
58
|
+
# => [#<Dynashard::Shard0::Customer id: 1>, #<Dynashard::Shard0::Customer id: 2>]
|
59
|
+
#
|
60
|
+
# Customers are loaded using the connection for the Company's shard. Associated models
|
61
|
+
# are returned as shard-specific subclasses of the association class.
|
62
|
+
#
|
63
|
+
# > c.customers.create(:name => 'Always right')
|
64
|
+
# => #<Dynashard::Shard0::Customer id: 3>
|
65
|
+
#
|
66
|
+
# New associations are saved on the Company's shard.
|
67
|
+
#
|
68
|
+
# TODO: add gotcha section, eg:
|
69
|
+
# - uniqueness validations should be scoped by whatever is sharding
|
70
|
+
|
71
|
+
module Dynashard
|
72
|
+
# Enable sharding for all models configured to do so
|
73
|
+
def self.enable
|
74
|
+
@enabled = true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Disable sharding for all models configured to do so
|
78
|
+
def self.disable
|
79
|
+
@enabled = false
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return true if sharding is globally enabled
|
83
|
+
def self.enabled?
|
84
|
+
@enabled == true
|
85
|
+
end
|
86
|
+
|
87
|
+
# Execute a block within a given sharding context
|
88
|
+
def self.with_context(new_context, &block)
|
89
|
+
orig_context = shard_context.dup
|
90
|
+
shard_context.merge! new_context
|
91
|
+
result = nil
|
92
|
+
begin
|
93
|
+
result = yield
|
94
|
+
ensure
|
95
|
+
shard_context.replace orig_context
|
96
|
+
end
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
# Return a threadsafe(?) current mapping of shard context to connection spec
|
101
|
+
def self.shard_context
|
102
|
+
Thread.current[:shard_context] ||= {}
|
103
|
+
end
|
104
|
+
|
105
|
+
# Return a class with an established connection to a database shard
|
106
|
+
def self.class_for(spec)
|
107
|
+
@class_cache ||= {}
|
108
|
+
@class_cache[spec] ||= new_shard_class(spec)
|
109
|
+
@class_cache[spec]
|
110
|
+
end
|
111
|
+
|
112
|
+
# Return a reflection with a sharded class
|
113
|
+
def self.reflection_for(owner, reflection)
|
114
|
+
reflection_copy = reflection.dup
|
115
|
+
shard_klass = Dynashard.class_for(owner.send(owner.class.dynashard_association_using))
|
116
|
+
klass = sharded_model_class(shard_klass, reflection.klass)
|
117
|
+
reflection_copy.instance_variable_set('@klass', klass)
|
118
|
+
reflection_copy.instance_variable_set('@class_name', klass.name)
|
119
|
+
|
120
|
+
reflection_copy
|
121
|
+
end
|
122
|
+
|
123
|
+
# Return a model subclass configured to use a specific shard
|
124
|
+
def self.sharded_model_class(shard_klass, base_klass)
|
125
|
+
class_name = "#{shard_klass.name}::#{base_klass.name}"
|
126
|
+
unless shard_klass.constants.include?(base_klass.name.to_sym)
|
127
|
+
class_eval <<EOE
|
128
|
+
class #{class_name} < #{base_klass.name}
|
129
|
+
@@dynashard_klass = #{shard_klass.name}
|
130
|
+
|
131
|
+
def self.dynashard_model?()
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.dynashard_klass()
|
136
|
+
@@dynashard_klass
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.connection
|
140
|
+
dynashard_klass.connection
|
141
|
+
end
|
142
|
+
end
|
143
|
+
EOE
|
144
|
+
klass = class_name.constantize
|
145
|
+
klass.connection_handler.dynashard_pool_alias(klass.name, shard_klass.name)
|
146
|
+
end
|
147
|
+
class_name.constantize
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def self.new_shard_class(spec)
|
153
|
+
shard_number = @class_cache.size
|
154
|
+
klass_name = "Shard#{shard_number}"
|
155
|
+
unless const_defined?(klass_name)
|
156
|
+
module_eval("class #{klass_name} < ActiveRecord::Base ; end")
|
157
|
+
"Dynashard::#{klass_name}".constantize.establish_connection(spec)
|
158
|
+
end
|
159
|
+
"Dynashard::#{klass_name}".constantize
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
require 'dynashard/model'
|
164
|
+
require 'dynashard/associations'
|
165
|
+
require 'dynashard/connection_handler'
|
166
|
+
require 'dynashard/validations'
|
167
|
+
require 'dynashard/arel_sql_engine'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Dynashard::ArelEngineExtensions' do
|
4
|
+
before(:each) do
|
5
|
+
@mock_record = mock()
|
6
|
+
@engine = Arel::Sql::Engine.new(@mock_record)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#connection_with_dynashard' do
|
10
|
+
context 'with a sharding association owner' do
|
11
|
+
it "returns the generated model subclass connection" do
|
12
|
+
@mock_record.should_receive(:dynashard_klass).once.and_return(mock(:connection => :subclass_connection))
|
13
|
+
@engine.should_receive(:connection_without_dynashard).never
|
14
|
+
@engine.connection.should == :subclass_connection
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'with a sharding model' do
|
19
|
+
before(:each) do
|
20
|
+
@mock_record.should_receive(:sharding_enabled?).and_return(true)
|
21
|
+
@mock_record.should_receive(:dynashard_context).at_least(:once).and_return(:dynashard_context)
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'and no defined sharding context' do
|
25
|
+
it 'raises an exception' do
|
26
|
+
Dynashard.shard_context[:dynashard_context] = nil
|
27
|
+
lambda do
|
28
|
+
@engine.connection
|
29
|
+
end.should raise_error
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'and a defined sharding context' do
|
34
|
+
it 'returns the generated shard class connection' do
|
35
|
+
Dynashard.should_receive(:class_for).with(:shard_spec).and_return(mock(:connection => :shard_connection))
|
36
|
+
Dynashard.shard_context[:dynashard_context] = :shard_spec
|
37
|
+
@engine.should_receive(:connection_without_dynashard).never
|
38
|
+
@engine.connection.should == :shard_connection
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with a non-sharding model' do
|
44
|
+
it 'returns the default connection' do
|
45
|
+
@mock_record.should_receive(:sharding_enabled?).and_return(false)
|
46
|
+
@engine.should_receive(:connection_without_dynashard).and_return(:model_connection)
|
47
|
+
@engine.connection.should == :model_connection
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|