active_shard 0.0.1 → 0.1.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,8 @@
1
+ ## 0.1.0.alpha ##
2
+
3
+ - initial release of code and gem
4
+ - includes working code base, missing completed railtie, better specs.
5
+
6
+ ## 0.0.1 ##
7
+
8
+ - stub gem while setting up git repo, etc.
data/README.md ADDED
@@ -0,0 +1,245 @@
1
+ ActiveShard - Multi-schema sharding for ActiveRecord
2
+ ====================================================
3
+
4
+ ActiveShard is a library built primarily for sharding in ActiveRecord. It also supports multiple databases with differing schemas. As with the other sharding libraries for ActiveRecord (there are a few), this library represents the best solution to the authors' sharding needs. If you've been unhappy with other options, ActiveShard might be for you.
5
+
6
+
7
+ ## CAVEATS ##
8
+
9
+ ### Railtie isn't finished ... ###
10
+
11
+ ... so there are some additional steps you'll need to take to get this working. Specifically:
12
+
13
+ Add the following to the end of your config/application.rb:
14
+
15
+ ActiveShard.config do |c|
16
+ definitions = ActiveShard::ShardDefinition.from_yaml_file( File.expand_path( '../shards.yml', __FILE__ ) )
17
+
18
+ definitions[ Rails.env.to_sym ].each do |shard|
19
+ c.add_shard( shard )
20
+ end
21
+ end
22
+
23
+ require 'active_shard/active_record'
24
+
25
+ ActiveRecord::Base.send( :include, ActiveShard::ActiveRecord::ShardSupport )
26
+
27
+ ActiveRecord::Base.connection_handler =
28
+ ActiveShard::ActiveRecord::ConnectionHandler.new(
29
+ ActiveShard.config.shard_definitions,
30
+ :shard_lookup => ActiveShard::ShardLookupHandler.new( :scope => ActiveShard.scope, :config => ActiveShard.config )
31
+ )
32
+
33
+ Once we complete the ActiveShard Railtie, these lines will not be necessary.
34
+
35
+
36
+ ### Where are the specs? ###
37
+
38
+ Good eye. This library is being extracted from an existing project where application-specific tests were written to test the sharding functionality.
39
+
40
+ Generic and more detailed specs are being written and will be added shortly.
41
+
42
+
43
+ ## Design goals ##
44
+
45
+ The fundamental purpose of ActiveShard is to provide a framework that allows ActiveRecord to connect to multiple databases with multiple different schemas. All other features are a subset of this framework (sharding, replication, etc).
46
+
47
+ ### More framework, less magic ###
48
+
49
+ ActiveShard doesn't do much guessing or sleight of hand. Queries are executed against whatever shard is marked as active for the schema used. Instances of models do not remember what shards they belong to.
50
+
51
+ ActiveShard.with( :main => :db1 ) do
52
+ user = User.find( 100 )
53
+ end
54
+
55
+ # this will fail, as no shard is selected at this point -->
56
+ user.save!
57
+
58
+ This is an intentional design decision. Other libraries go to great lengths to provide smarter implementations, but do so at the expense of flexibility.
59
+
60
+ In contrast to other implementations, ActiveShard does not reopen or monkey-patch any Rails or ActiveRecord classes. It has been the authors' experience that any Rails application large enough to need sharding probably contains a fair amount of customization already. Sharding libraries which hack up core Rails classes often do not play nice with existing code. ActiveShard (hopefully!) does.
61
+
62
+
63
+ ## Install ##
64
+
65
+ ### Rails 3.x ###
66
+
67
+ Add this line to your Gemfile:
68
+
69
+ gem 'active_shard'
70
+
71
+ Install bundle:
72
+
73
+ $ bundle install
74
+
75
+ Add to config/application.rb, right under "require 'rails/all'":
76
+
77
+ require 'active_shard/active_record/railtie'
78
+
79
+
80
+ ## Most common usage ##
81
+
82
+ ### config/shards.yml (if used in Rails) ###
83
+
84
+ Create a config/shards.yml file with your desired database configuration. Shards.yml contains the following structure (pseudo-code):
85
+
86
+ <environment>
87
+ <schema_name>
88
+ <shard_name>
89
+ <shard_specification>
90
+ <shard_name>
91
+ <shard_specification>
92
+ <schema_name>
93
+ <shard_name>
94
+ <shard_specification>
95
+
96
+ Example shards.yml:
97
+
98
+ production:
99
+ directories:
100
+ directory:
101
+ adapter: mysql2
102
+ database: dir_db
103
+ host: localhost
104
+ username: root
105
+
106
+ main:
107
+ db1:
108
+ adapter: mysql2
109
+ host: localhost
110
+ database: db1_db
111
+ username: root
112
+
113
+ db2:
114
+ adapter: mysql2
115
+ host: localhost
116
+ database: db2_db
117
+ username: root
118
+
119
+ db3:
120
+ adapter: mysql2
121
+ host: localhost
122
+ database: db3_db
123
+ username: root
124
+
125
+ development:
126
+ directories:
127
+ directory:
128
+ adapter: mysql2
129
+ host: localhost
130
+ database: dir_db_development
131
+ username: root
132
+
133
+ main:
134
+ db1:
135
+ adapter: mysql2
136
+ host: localhost
137
+ database: db1_db_development
138
+ username: root
139
+
140
+
141
+ ### Schema name for models ###
142
+
143
+ ActiveRecord models must have a schema name associated with them. Given the shards.yml file specified above, you might see the following models:
144
+
145
+ # contains user lookup fields and the shard name on which user's primary data resides
146
+ class UserShard < ActiveRecord::Base
147
+ schema_name :directories
148
+
149
+ # ...
150
+ end
151
+
152
+ # primary user data
153
+ class User < ActiveRecord::Base
154
+ schema_name :main
155
+
156
+ # ...
157
+ end
158
+
159
+
160
+ ### Selecting the active shard(s) ###
161
+
162
+ In order to use a model -- such as the ones specified above -- a shard must be selected as the current active shard *for each schema used.* The easiest way to do this is to pass a block containing the queries to the ActiveShard.with( ... ) method, specifying the active shards in the parameters.
163
+
164
+ Nested blocks maintain active shard settings for any schemas they do not explicitly set. Example:
165
+
166
+ ActiveShard.with( :directories => :directory ) do
167
+
168
+ # We can now use the UserShard model as a shard has been
169
+ # selected for the 'directories' schema.
170
+ #
171
+ user_shard = UserShard.find_by_login( 'xavier' )
172
+
173
+ # Using the User model here would raise an exception since there
174
+ # is no active shard for the schema that User belongs to ('main').
175
+ #
176
+ # However, if we select a shard for that schema ...
177
+ #
178
+
179
+ ActiveShard.with( :main => user_shard.shard_name ) do
180
+
181
+ user = User.find( user_shard.user_id ) # <-- this works.
182
+
183
+ # ActiveShard effectively 'merges' nested shard selections rather
184
+ # than replace them. The active shards at this point are:
185
+ #
186
+ # :directories => :directory
187
+ # :main => <user_shard.shard_name>
188
+ #
189
+
190
+ end
191
+
192
+ # ... the active shard for the :main schema has been de-selected,
193
+ # leaving only the :directories => :directory shard as active.
194
+
195
+ end
196
+
197
+ To better understand what's happening here, see ActiveShard::Scope.
198
+
199
+
200
+ ### Migrations ###
201
+
202
+ Migrations for each schema must reside in a directory under db/migrate that corresponds to the schema name.
203
+
204
+ Example:
205
+ db/migrate/main
206
+ 20110810103523_create_users_table.rb
207
+ db/migrate/directories
208
+ 20110810105318_create_user_shards_table.rb
209
+
210
+
211
+ You can run migrations against a shard by passing the *shard name* into the shards:migrate rake task, like so:
212
+
213
+ $ rake shards:migrate[db1]
214
+
215
+ The schema is discovered from the ActiveShard configuration and the proper migrations are executed.
216
+
217
+ Each shard maintains its own schema_migrations table and can be (must be) migrated independently. This allows you to spin up additional shards in the future by simply adding an entry to your ActiveShard configuration and running migrations against that shard.
218
+
219
+
220
+ ### Rails Controllers ###
221
+
222
+ If you want to send a specified action, or all actions from a controller, to a specific shard, use this syntax:
223
+
224
+ class ApplicationController < ActionController::Base
225
+ around_filter :activate_shards
226
+
227
+ def activate_shards(&block)
228
+ ActiveShard.with( :directories => :directory, :main => :db1 )
229
+ end
230
+ end
231
+
232
+
233
+ ## Other similar libraries ##
234
+
235
+ There are several, but Octopus (ar-octopus) is the most popular.
236
+
237
+
238
+ ## Authors ##
239
+
240
+ Brasten Sager ( brasten@dashwire.com )
241
+ Matt Baker ( matt@dashwire.com )
242
+
243
+ ## Copyright
244
+
245
+ Copyright (c) 2011 Dashwire Inc., released under the MIT license.
@@ -0,0 +1,72 @@
1
+ require 'active_record/connection_adapters/abstract/connection_pool'
2
+
3
+ module ActiveShard
4
+ module ActiveRecord
5
+
6
+ autoload :SchemaConnectionPool, 'active_shard/active_record/schema_connection_pool'
7
+
8
+ class ConnectionHandler < ::ActiveRecord::ConnectionAdapters::ConnectionHandler
9
+
10
+ # Used to look up shard names
11
+ #
12
+ attr_accessor :shard_lookup
13
+
14
+ # Initializes a new ConnectionHandler
15
+ #
16
+ # @param [Array<ShardDefinition>] shard_definitions
17
+ # @param [Hash] options
18
+ # @option options [ShardLookupHandler] :shard_lookup
19
+ #
20
+ def initialize( shard_definitions, options={} )
21
+ @shard_lookup = options[ :shard_lookup ]
22
+
23
+ @connection_pools = {}
24
+ @schema_pools = {}
25
+
26
+ initialize_shard_definitions( shard_definitions )
27
+ end
28
+
29
+ def initialize_shard_definitions( definitions )
30
+ definitions.each do |definition|
31
+ schema_pools[ definition.schema.to_sym ] ||= SchemaConnectionPool.new(
32
+ ::ActiveRecord::Base::ConnectionSpecification.new( definition.connection_spec, definition.adapter_method )
33
+ )
34
+
35
+
36
+ connection_pools[ connection_pool_id( definition.schema, definition.name ) ] =
37
+ ::ActiveRecord::ConnectionAdapters::ConnectionPool.new(
38
+ ::ActiveRecord::Base::ConnectionSpecification.new( definition.connection_spec, definition.adapter_method )
39
+ )
40
+ end
41
+ end
42
+
43
+ #def establish_connection( *args )
44
+ # raise NoMethodError, "Sharded models do not support establish_connection"
45
+ #end
46
+
47
+ # Retrieve connection pool for class
48
+ #
49
+ def retrieve_connection_pool( klass )
50
+ schema_name = klass.schema_name
51
+
52
+ active_shard_name = shard_lookup.lookup_active_shard( schema_name )
53
+
54
+ Rails.logger.debug( "RetrieveConnectionPool for #{klass.name}, schema_name #{schema_name}, active_shard #{active_shard_name}")
55
+
56
+ ( active_shard_name.nil? ?
57
+ schema_pools[ schema_name.to_sym ] :
58
+ connection_pools[ connection_pool_id( schema_name, active_shard_name ) ] )
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :schema_pools
64
+
65
+ def connection_pool_id( schema_name, shard_name )
66
+ "#{schema_name.to_s}+#{shard_name.to_s}".to_sym
67
+ end
68
+
69
+
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,56 @@
1
+ require "active_shard"
2
+ require "rails"
3
+
4
+ module ActiveShard
5
+ module ActiveRecord
6
+ class Railtie < Rails::Railtie
7
+
8
+ config.active_shard = ActiveSupport::OrderedOptions.new
9
+
10
+ rake_tasks do
11
+ load "active_shard/active_record/rails/database.rake"
12
+ end
13
+
14
+ initializer "active_shard.logger" do
15
+ ActiveSupport.on_load(:active_shard) { self.logger ||= ::Rails.logger }
16
+ end
17
+
18
+ initializer "active_shard.set_configs" do |app|
19
+ ActiveSupport.on_load(:active_record) do
20
+ app.config.active_record.each do |k,v|
21
+ send "#{k}=", v
22
+ end
23
+ end
24
+ end
25
+
26
+ initializer "active_shard.initialize_database" do |app|
27
+ ActiveSupport.on_load(:active_record) do
28
+ self.configurations = app.config.database_configuration
29
+ establish_connection
30
+ end
31
+ end
32
+
33
+ ActiveShard.config do |c|
34
+ definitions = ActiveShard::ShardDefinition.from_yaml_file( File.expand_path( '../shards.yml', __FILE__ ) )
35
+
36
+ definitions[ Rails.env.to_sym ].each do |shard|
37
+ c.add_shard( shard )
38
+ end
39
+ end
40
+
41
+ require 'active_shard/active_record'
42
+
43
+ ActiveRecord::Base.send( :include, ActiveShard::ActiveRecord::ShardSupport )
44
+
45
+ ActiveRecord::Base.connection_handler =
46
+ ActiveShard::ActiveRecord::ConnectionHandler.new(
47
+ ActiveShard.config.shard_definitions,
48
+ :shard_lookup => ActiveShard::ShardLookupHandler.new( :scope => ActiveShard.scope, :config => ActiveShard.config )
49
+ )
50
+
51
+ ActiveRecord::Base.schema_name( :main )
52
+
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ require 'active_shard/exceptions'
2
+
3
+ module ActiveShard
4
+ module ActiveRecord
5
+ class SchemaConnectionAdapter
6
+
7
+ delegate :columns, :verify, :verify!, :run_callbacks, :quote_table_name, :quote_value, :quote, :to => :adapter
8
+
9
+ def initialize( adapter )
10
+ @adapter = adapter
11
+ end
12
+
13
+ def method_missing( sym, *args, &block )
14
+ raise ::ActiveShard::NoActiveShardError
15
+ end
16
+
17
+ private
18
+ def adapter
19
+ @adapter
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_record/connection_adapters/abstract/connection_pool'
2
+
3
+ module ActiveShard
4
+ module ActiveRecord
5
+
6
+ autoload :SchemaConnectionAdapter, 'active_shard/active_record/schema_connection_adapter'
7
+
8
+ class SchemaConnectionPool < ::ActiveRecord::ConnectionAdapters::ConnectionPool
9
+
10
+ private
11
+ def new_connection
12
+ SchemaConnectionAdapter.new( super )
13
+ end
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,30 @@
1
+ module ActiveShard
2
+ module ActiveRecord
3
+
4
+ # To enable ActiveShard support in ActiveRecord, mix this module in to your
5
+ # model superclass.
6
+ #
7
+ # eg: ActiveRecord::Base.send( :include, ActiveShard::ActiveRecord::ShardSupport )
8
+ #
9
+ # For an explanation of why you'd use ShardedBase or ShardSupport, @see ActiveShard::ActiveRecord
10
+ #
11
+ module ShardSupport
12
+
13
+ def self.included( base )
14
+ base.extend( ClassMethods )
15
+ end
16
+
17
+ module ClassMethods
18
+
19
+ # Specifies the schema name for the current model class (and its subclasses)
20
+ #
21
+ def schema_name( schema_name=:not_specified )
22
+ write_inheritable_attribute( :schema_name, schema_name ) unless ( schema_name == :not_specified )
23
+ read_inheritable_attribute( :schema_name )
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_record/base'
2
+ require 'active_shard/active_record/shard_support'
3
+
4
+ module ActiveShard
5
+ module ActiveRecord
6
+
7
+ # ShardedBase is a subclass of ActiveRecord::Base which mixes in the ShardSupport
8
+ # module.
9
+ #
10
+ # For an explanation of why you'd use ShardedBase or ShardSupport, @see ActiveShard::ActiveRecord
11
+ #
12
+ class ShardedBase < ::ActiveRecord::Base
13
+ include ::ActiveShard::ActiveRecord::ShardSupport
14
+
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveShard
2
+
3
+ # The ActiveShard::ActiveRecord module contains the code necessary for ActiveRecord
4
+ # integration.
5
+ #
6
+ # -- Need to add explanation of ShardedBase VS ShardSupport --
7
+ #
8
+ module ActiveRecord
9
+
10
+ autoload :ConnectionHandler, 'active_shard/active_record/connection_handler'
11
+ autoload :ShardSupport, 'active_shard/active_record/shard_support'
12
+ autoload :ShardedBase, 'active_shard/active_record/sharded_base'
13
+
14
+ end
15
+ end
@@ -0,0 +1,52 @@
1
+ module ActiveShard
2
+ autoload :ShardDefinition, 'active_shard/shard_definition'
3
+ autoload :ShardCollection, 'active_shard/shard_collection'
4
+
5
+ class Config
6
+
7
+ # Returns a filtered list of shards that belong to
8
+ # the specified schema
9
+ #
10
+ # @return [Array] shards belonging to schema
11
+ #
12
+ def shards_by_schema( schema_name )
13
+ shard_collection.by_schema( schema_name )
14
+ end
15
+
16
+ # Returns the name of a shard usable for schema definition
17
+ # connections
18
+ #
19
+ def schema_shard_name_by_schema( schema_name )
20
+ shard_def = shard_definitions_by_schema( schema_name ).first
21
+
22
+ shard_def.nil? ? nil : shard_def.name.to_sym
23
+ end
24
+
25
+ # Returns a Shard Definition by the shard name
26
+ #
27
+ # @return [ShardDefinition]
28
+ #
29
+ def shard( name )
30
+ shard_collection.shard( name )
31
+ end
32
+
33
+ def add_shard( *args )
34
+ shard_collection.add_shard( *args )
35
+ end
36
+
37
+ def shard_definitions
38
+ shard_collection.shard_definitions
39
+ end
40
+
41
+ def remove_shard( shard_name )
42
+ shard_collection.remove_shard( shard_name )
43
+ end
44
+
45
+ private
46
+ def shard_collection
47
+ @shard_collection ||= ShardCollection.new
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,16 @@
1
+ module ActiveShard
2
+
3
+ # CODE CONVENTION VIOLATION
4
+ #
5
+ # For better visualization, the following code is indented based on class hierarchy rather
6
+ # than nesting.
7
+ #
8
+
9
+ class ActiveShardError < StandardError; end
10
+
11
+ class DefinitionError < ActiveShardError; end
12
+ class NameNotUniqueError < DefinitionError; end
13
+
14
+ class NoActiveShardError < ActiveShardError; end
15
+
16
+ end
@@ -0,0 +1,118 @@
1
+ module ActiveShard
2
+
3
+ # Maintains the state of current active shards per schema.
4
+ #
5
+ # Requests for the current active shard for a schema name will iterate
6
+ # up the scope stack until it finds an active shard (or nil).
7
+ #
8
+ # @example
9
+ # scope = Scope.new
10
+ # scope.push( :user_data => :db_1, :directories => :directory_1 )
11
+ #
12
+ # # updates current :user_data shard, :directories shard remains
13
+ # scope.push( :user_data => :db_3 )
14
+ #
15
+ # scope.active_shard_for_schema( :user_data )
16
+ # => :db_3
17
+ #
18
+ # scope.active_shard_for_schema( :directories )
19
+ # => :directory_1
20
+ #
21
+ class Scope
22
+
23
+ def initialize
24
+ @scope_crumbs = []
25
+ @current_shards = {}
26
+ end
27
+
28
+ # Push a new scope on onto the stack.
29
+ #
30
+ # The active_shards parameter should be a hash with schema names for
31
+ # keys and shard names for the corresponding values.
32
+ #
33
+ # eg: scope.push( :directory => :dir1, :user_data => :db1 )
34
+ #
35
+ # @param [Hash] active_shards
36
+ #
37
+ def push( active_shards )
38
+ scope_crumbs << active_shards
39
+
40
+ # shortcutting #build_current_shards for performance reasons
41
+ if active_shards.is_a?(Symbol)
42
+ # if active_shards is a Symbol, ALL schemas are using active shard
43
+ current_shards.keys.each do |schema|
44
+ current_shards[schema] = active_shards
45
+ end
46
+ current_shards[AnyShard] = active_shards
47
+
48
+ else
49
+ current_shards.merge!( normalize_keys( active_shards ) )
50
+ end
51
+ end
52
+
53
+ # Remove the last scope from the stack
54
+ #
55
+ def pop( pop_until=nil )
56
+ if pop_until.nil?
57
+ scope_crumbs.pop
58
+ else
59
+ (scope_crumbs.size - scope_crumbs.index(pop_until)).times do
60
+ scope_crumbs.pop
61
+ end
62
+ end
63
+
64
+ build_current_shards( scope_crumbs )
65
+ end
66
+
67
+ # Returns the name of the active shard by the provided schema name.
68
+ #
69
+ # @param [Symbol] schema_name name of schema
70
+ #
71
+ # @return [Symbol, nil] current active shard for schema
72
+ #
73
+ def active_shard_for_schema( schema_name )
74
+ current_shards[ schema_name.to_sym ] || current_shards[ AnyShard ]
75
+ end
76
+
77
+ private
78
+
79
+ # scope_crumbs and current_shards are two different data structures that
80
+ # essentially represent the same data. "current_shards" is used for performance
81
+ # when possible; "scope_crumbs" is used to generate current_shards when
82
+ # necessary.
83
+
84
+ attr_reader :scope_crumbs
85
+ attr_reader :current_shards
86
+
87
+ def build_current_shards( crumbs )
88
+ current_shards.clear
89
+
90
+ crumbs.each do |crumb|
91
+ case crumb
92
+ when Symbol
93
+
94
+ current_shards.keys.each do |schema|
95
+ current_shards[schema] = crumb
96
+ end
97
+ current_shards[AnyShard] = crumb
98
+ when Hash
99
+
100
+ crumb.each_pair { |schema, shard| current_shards[ schema.to_sym ] = shard.to_sym }
101
+ end
102
+ end
103
+
104
+ current_shards
105
+ end
106
+
107
+ def normalize_keys( hash )
108
+ ret = {}
109
+
110
+ hash.each_pair { |k, v| ret[ k.to_sym ] = v }
111
+
112
+ ret
113
+ end
114
+
115
+ class AnyShard; end
116
+
117
+ end
118
+ end
@@ -0,0 +1,66 @@
1
+ module ActiveShard
2
+
3
+ autoload :Scope, 'active_shard/scope'
4
+
5
+ # ScopeManager handles the passing of messages to a Scope based on
6
+ # the current thread.
7
+ #
8
+ # Allows consumers to operate on a Scope duck-typed object without
9
+ # handling the Thread local variable stuff.
10
+ #
11
+ class ScopeManager
12
+
13
+ # Initializes a scope manager
14
+ #
15
+ # @param [Hash] options
16
+ # @option options [Class,#new] :scope_class class to use when instantiating
17
+ # scope instances
18
+ #
19
+ def initialize( options={} )
20
+ @scope_class = options[:scope_class] if options[:scope_class]
21
+ end
22
+
23
+ # @see ActiveShard::Scope#push
24
+ #
25
+ def push( *args )
26
+ scope.push( *args )
27
+ end
28
+
29
+ # @see ActiveShard::Scope#pop
30
+ #
31
+ def pop( *args )
32
+ scope.pop( *args )
33
+ end
34
+
35
+ # @see ActiveShard::Scope#active_shard_for_schema
36
+ #
37
+ def active_shard_for_schema( *args )
38
+ scope.active_shard_for_schema( *args )
39
+ end
40
+
41
+ # Sets the class to use for maintaining Thread-local scopes
42
+ #
43
+ # Instances of klass must respond to:
44
+ # #push
45
+ # #pop
46
+ # #active_shard_for_schema
47
+ #
48
+ # @param [Class,#new] klass scope_class
49
+ #
50
+ def scope_class=( klass )
51
+ @scope_class = klass
52
+ end
53
+
54
+ # Returns the current scope_class
55
+ #
56
+ def scope_class
57
+ @scope_class ||= Scope
58
+ end
59
+
60
+ private
61
+ def scope
62
+ Thread.current[:active_shard_scope] ||= scope_class.new
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,137 @@
1
+ require 'active_shard/exceptions'
2
+
3
+ module ActiveShard
4
+
5
+ autoload :ShardDefinition, 'active_shard/shard_definition'
6
+
7
+ # Represents a group of shards. Most commonly used to group shards belonging to
8
+ # the same schema.
9
+ #
10
+ # Handles some basic caching of common use cases to (hopefully) increase performance
11
+ # in most situations.
12
+ #
13
+ class ShardCollection
14
+
15
+ # Initializes a shard group
16
+ #
17
+ # @param [Array<ShardDefinition>] shard_definitions
18
+ #
19
+ def initialize( shard_definitions=[] )
20
+ add_shards( shard_definitions )
21
+ end
22
+
23
+ # Returns a Shard Definition by the shard name
24
+ #
25
+ # @return [ShardDefinition]
26
+ #
27
+ def shard( name )
28
+ definitions_by_name[ name.to_sym ]
29
+ end
30
+
31
+ # Adds a list of shard definitions to this collection.
32
+ #
33
+ # @return [Array] a list of shards that were added
34
+ #
35
+ def add_shards( shard_definitions )
36
+ added_shards = []
37
+
38
+ shard_definitions.each do |definition|
39
+ begin
40
+ added_shards << add_shard( definition )
41
+ rescue NameNotUniqueError
42
+ # bury
43
+ end
44
+ end
45
+
46
+ added_shards
47
+ end
48
+
49
+ # Adds a shard definition to the collection
50
+ #
51
+ # @return [ShardDefinition] added shard definition
52
+ #
53
+ def add_shard( *args )
54
+ shard_def = ( args.first.is_a?(ShardDefinition) ? args.first : ShardDefinition.new( *args ) )
55
+
56
+ duplicate_exists = shard_name_exists?( shard_def.name )
57
+
58
+ raise NameNotUniqueError,
59
+ "Shard named '#{shard_def.name.to_s}' exists for schema '#{shard_def.schema.to_s}'" if duplicate_exists
60
+
61
+ definitions << shard_def
62
+ definitions_by_schema( shard_def.schema ) << shard_def
63
+ definitions_by_name[ shard_def.name.to_sym ] = shard_def
64
+
65
+ shard_def
66
+ end
67
+
68
+ # All shard definitions in collection
69
+ #
70
+ # @return [Array<ShardDefinition>] shard definitions
71
+ #
72
+ def shard_definitions
73
+ definitions.dup
74
+ end
75
+
76
+ # Returns a ShardCollection with definitions from this collection
77
+ # that match the provided schema name
78
+ #
79
+ # @param [Symbol] schema_name schema name
80
+ #
81
+ # @return [ShardCollection] collection containing applicable definitions
82
+ #
83
+ def by_schema( schema_name )
84
+ self.class.new( definitions_by_schema( schema_name ) )
85
+ end
86
+
87
+ # Removes a shard definition from the collection based on the
88
+ # provided shard name.
89
+ #
90
+ # @param [Symbol] shard_name
91
+ #
92
+ # @return [ShardDefinition,nil] the ShardDefinition removed, or
93
+ # nil if none was found
94
+ #
95
+ def remove_shard( shard_name )
96
+ shard_def = definitions_by_name.delete( shard_name.to_sym )
97
+ return nil if shard_def.nil?
98
+
99
+ definitions.delete( shard_def )
100
+ definitions_by_schema( shard_def.schema ).delete( shard_def )
101
+
102
+ shard_def
103
+ end
104
+
105
+ # Returns whether or not a Shard exists in this collection with the provided
106
+ # schema and shard names.
107
+ #
108
+ # @return [Boolean]
109
+ #
110
+ def shard_name_exists?( shard_name )
111
+ !( definitions_by_name[ shard_name.to_sym ].nil? )
112
+ end
113
+
114
+ # Returns a random shard name from the group
115
+ #
116
+ # @return [Symbol] a shard name
117
+ #
118
+ def any_shard
119
+ definitions[ rand( definitions.size ) ].name
120
+ end
121
+
122
+ private
123
+
124
+ def definitions
125
+ @definitions ||= []
126
+ end
127
+
128
+ def definitions_by_schema( schema_name )
129
+ ( @definitions_by_schema ||= {} )[ schema_name.nil? ? nil : schema_name.to_sym ] ||= []
130
+ end
131
+
132
+ def definitions_by_name
133
+ ( @definitions_by_name ||= {} )
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,86 @@
1
+ module ActiveShard
2
+
3
+ class ShardDefinition
4
+ attr_accessor :schema
5
+ attr_accessor :name
6
+ attr_writer :connection_spec
7
+
8
+ class << self
9
+
10
+ # Returns a hash with environments as the hash keys and
11
+ # a list of ShardDefinitions as the hash values
12
+ #
13
+ # @param [String] file_name path to Yaml file
14
+ #
15
+ # @return [Hash] hash of environments and lists of Definitions
16
+ #
17
+ def from_yaml_file( file_name )
18
+ definitions = {}
19
+
20
+ hash = YAML.load( ERB.new( File.open( file_name ).read() ).result )
21
+
22
+ hash.each_pair do |environment, schemas|
23
+ schemas.each_pair do |schema, shards|
24
+ shards.each_pair do |shard, spec|
25
+ ( definitions[ environment.to_sym ] ||= [] ) << self.new( shard.to_sym,
26
+ spec.merge( :schema => schema.to_sym ) )
27
+ end
28
+ end
29
+ end
30
+
31
+ definitions
32
+ end
33
+
34
+ end
35
+
36
+ # Returns a new ShardDefinition
37
+ #
38
+ # @param [String] name name of the shard
39
+ # @param [Hash] options
40
+ # @option options [String] :schema name of the schema
41
+ # @option options [String] :group group name of shard
42
+ # @option options all other options passed as connection spec
43
+ #
44
+ def initialize( name, options={} )
45
+ opts = options.dup
46
+
47
+ @name = name
48
+ @schema = opts.delete( :schema )
49
+ @connection_spec = symbolize_keys( opts )
50
+ end
51
+
52
+ def adapter_method
53
+ "#{connection_spec[:adapter]}_connection"
54
+ end
55
+
56
+ def connection_spec
57
+ @connection_spec ||= {}
58
+ end
59
+
60
+ # Returns true if our schema == schema_name and neither
61
+ # self#schema nor schema_name is nil. Returns false otherwise.
62
+ #
63
+ # @param [Symbol] schema_name
64
+ #
65
+ # @return [bool]
66
+ #
67
+ def belongs_to_schema?( schema_name )
68
+ return false if ( schema.nil? || schema_name.nil? )
69
+
70
+ ( schema.to_sym == schema_name.to_sym )
71
+ end
72
+
73
+ private
74
+
75
+ def symbolize_keys( hash )
76
+ ret = {}
77
+
78
+ hash.each_pair do |k,v|
79
+ ret[k.to_sym] = v
80
+ end
81
+
82
+ ret
83
+ end
84
+ end
85
+
86
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveShard
2
+
3
+ # Handles current and schema shard resolution using the current
4
+ # scope and
5
+ class ShardLookupHandler
6
+
7
+ # Initializes a shard lookup handler
8
+ #
9
+ # @param [Hash] options
10
+ # @option options [Scope,ScopeManager] :scope
11
+ # @option options [Config] :config
12
+ # scope instances
13
+ #
14
+ def initialize( options={} )
15
+ @scope = options[:scope]
16
+ @config = options[:config]
17
+ end
18
+
19
+ def lookup_active_shard( schema_name )
20
+ @scope.active_shard_for_schema( schema_name )
21
+ end
22
+
23
+ def lookup_schema_shard( schema_name )
24
+ @config.schema_shard_name_by_schema( schema_name )
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module ActiveShard
2
+ VERSION = '0.1.0.alpha'
3
+ end
@@ -0,0 +1,98 @@
1
+ module ActiveShard
2
+
3
+ autoload :Config, 'active_shard/config'
4
+ autoload :ScopeManager, 'active_shard/scope_manager'
5
+ autoload :ShardLookupHandler, 'active_shard/shard_lookup_handler'
6
+ autoload :ShardCollection, 'active_shard/shard_collection'
7
+
8
+ class << self
9
+
10
+ # Returns the current Config object for ActiveShard.
11
+ #
12
+ # @yield [c] yields the current config object to the block for setup
13
+ #
14
+ # @return [Config] current config
15
+ #
16
+ def config()
17
+ @config ||= Config.new
18
+
19
+ yield( @config ) if block_given?
20
+
21
+ @config
22
+ end
23
+
24
+ # Sets the current scope handling object.
25
+ #
26
+ # Scope handler must respond to the following methods:
27
+ # #push( scope ), #pop( scopes ), #active_shard_for_schema( schema )
28
+ #
29
+ def scope=( val )
30
+ @scope = val
31
+ end
32
+
33
+ # Returns current scope object
34
+ #
35
+ # @return [#push,#pop,#active_shard_for_schema] current scope object
36
+ #
37
+ def scope
38
+ @scope ||= ScopeManager.new
39
+ end
40
+
41
+ # Sets the active shards before yielding, and reverts them before returning.
42
+ #
43
+ # This method will also pop off any additional scopes that were added by the
44
+ # provided block if they were not already popped.
45
+ #
46
+ # @example
47
+ #
48
+ # ActiveShard.with( :users => :user_db1 ) do
49
+ # ActiveShard.with( :users => :user_db2 ) do
50
+ # ActiveShard.activate_shards( :users => :user_db3 )
51
+ # # 3 shard entries on scope stack
52
+ # end
53
+ # # 1 shard entry on scope stack
54
+ # end
55
+ #
56
+ # @param [Hash] scopes schemas (keys) and active shards (values)
57
+ #
58
+ # @return the return value from the provided block
59
+ #
60
+ def with( scopes={}, &block )
61
+ ret = nil
62
+
63
+ begin
64
+ activate_shards( scopes )
65
+
66
+ ret = block.call()
67
+ ensure
68
+ pop_to( scopes )
69
+ ret
70
+ end
71
+ end
72
+
73
+ # Pushes active shards onto the scope without a block.
74
+ #
75
+ def activate_shards( scopes={} )
76
+ scope.push( scopes )
77
+ end
78
+
79
+ def pop_to( scopes )
80
+ scope.pop( scopes )
81
+ end
82
+
83
+ def shards_by_schema( schema_name )
84
+ config.shards_by_schema( schema_name )
85
+ end
86
+
87
+ def logger
88
+ @logger
89
+ end
90
+
91
+ def logger=(val)
92
+ @logger = val
93
+ end
94
+
95
+ end
96
+ end
97
+
98
+ ActiveSupport.run_load_hooks(:active_shard, ActiveShard) if defined?(ActiveSupport)
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_shard
3
3
  version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.0.1
4
+ prerelease: 6
5
+ version: 0.1.0.alpha
6
6
  platform: ruby
7
7
  authors:
8
8
  - Brasten Sager
@@ -11,7 +11,7 @@ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
13
 
14
- date: 2011-08-12 00:00:00 Z
14
+ date: 2011-08-15 00:00:00 Z
15
15
  dependencies: []
16
16
 
17
17
  description: ActiveShard is a library that implements flexible sharding in ActiveRecord and Rails.
@@ -25,7 +25,24 @@ extensions: []
25
25
  extra_rdoc_files: []
26
26
 
27
27
  files:
28
- - README.rdoc
28
+ - CHANGELOG
29
+ - README.md
30
+ - lib/active_shard/active_record/connection_handler.rb
31
+ - lib/active_shard/active_record/railtie.rb
32
+ - lib/active_shard/active_record/schema_connection_adapter.rb
33
+ - lib/active_shard/active_record/schema_connection_pool.rb
34
+ - lib/active_shard/active_record/shard_support.rb
35
+ - lib/active_shard/active_record/sharded_base.rb
36
+ - lib/active_shard/active_record.rb
37
+ - lib/active_shard/config.rb
38
+ - lib/active_shard/exceptions.rb
39
+ - lib/active_shard/scope.rb
40
+ - lib/active_shard/scope_manager.rb
41
+ - lib/active_shard/shard_collection.rb
42
+ - lib/active_shard/shard_definition.rb
43
+ - lib/active_shard/shard_lookup_handler.rb
44
+ - lib/active_shard/version.rb
45
+ - lib/active_shard.rb
29
46
  homepage:
30
47
  licenses: []
31
48
 
data/README.rdoc DELETED
@@ -1 +0,0 @@
1
- Stub push. No code actually exists here. Will be pushed soon.