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 +8 -0
- data/README.md +245 -0
- data/lib/active_shard/active_record/connection_handler.rb +72 -0
- data/lib/active_shard/active_record/railtie.rb +56 -0
- data/lib/active_shard/active_record/schema_connection_adapter.rb +23 -0
- data/lib/active_shard/active_record/schema_connection_pool.rb +18 -0
- data/lib/active_shard/active_record/shard_support.rb +30 -0
- data/lib/active_shard/active_record/sharded_base.rb +18 -0
- data/lib/active_shard/active_record.rb +15 -0
- data/lib/active_shard/config.rb +52 -0
- data/lib/active_shard/exceptions.rb +16 -0
- data/lib/active_shard/scope.rb +118 -0
- data/lib/active_shard/scope_manager.rb +66 -0
- data/lib/active_shard/shard_collection.rb +137 -0
- data/lib/active_shard/shard_definition.rb +86 -0
- data/lib/active_shard/shard_lookup_handler.rb +28 -0
- data/lib/active_shard/version.rb +3 -0
- data/lib/active_shard.rb +98 -0
- metadata +21 -4
- data/README.rdoc +0 -1
data/CHANGELOG
ADDED
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
|
data/lib/active_shard.rb
ADDED
@@ -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.
|
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-
|
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
|
-
-
|
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.
|