dynashard 0.1.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/.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
|