data_fabric 1.0.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/CHANGELOG +1 -0
- data/Manifest +79 -0
- data/README.rdoc +108 -0
- data/Rakefile +69 -0
- data/TESTING.rdoc +34 -0
- data/TODO +1 -0
- data/data_fabric.gemspec +166 -0
- data/example/Rakefile +58 -0
- data/example/app/controllers/accounts_controller.rb +22 -0
- data/example/app/controllers/application.rb +39 -0
- data/example/app/controllers/figments_controller.rb +8 -0
- data/example/app/helpers/accounts_helper.rb +2 -0
- data/example/app/helpers/application_helper.rb +3 -0
- data/example/app/helpers/figments_helper.rb +2 -0
- data/example/app/models/account.rb +3 -0
- data/example/app/models/figment.rb +4 -0
- data/example/app/views/accounts/index.html.erb +47 -0
- data/example/app/views/layouts/application.html.erb +8 -0
- data/example/config/boot.rb +109 -0
- data/example/config/database.yml +21 -0
- data/example/config/environment.rb +67 -0
- data/example/config/environments/development.rb +17 -0
- data/example/config/environments/production.rb +22 -0
- data/example/config/environments/test.rb +22 -0
- data/example/config/initializers/inflections.rb +10 -0
- data/example/config/initializers/mime_types.rb +5 -0
- data/example/config/initializers/new_rails_defaults.rb +15 -0
- data/example/config/routes.rb +45 -0
- data/example/db/development.sqlite3 +0 -0
- data/example/db/migrate/20080702154628_create_accounts.rb +14 -0
- data/example/db/migrate/20080702154820_create_figments.rb +14 -0
- data/example/db/s0_development.sqlite3 +0 -0
- data/example/db/s0_test.sqlite3 +0 -0
- data/example/db/s1_development.sqlite3 +0 -0
- data/example/db/s1_test.sqlite3 +0 -0
- data/example/db/schema.rb +28 -0
- data/example/db/test.sqlite3 +0 -0
- data/example/public/404.html +30 -0
- data/example/public/422.html +30 -0
- data/example/public/500.html +30 -0
- data/example/public/dispatch.cgi +10 -0
- data/example/public/dispatch.fcgi +24 -0
- data/example/public/dispatch.rb +10 -0
- data/example/public/favicon.ico +0 -0
- data/example/public/images/rails.png +0 -0
- data/example/public/javascripts/application.js +2 -0
- data/example/public/javascripts/controls.js +963 -0
- data/example/public/javascripts/dragdrop.js +972 -0
- data/example/public/javascripts/effects.js +1120 -0
- data/example/public/javascripts/prototype.js +4225 -0
- data/example/public/robots.txt +5 -0
- data/example/script/about +3 -0
- data/example/script/console +3 -0
- data/example/script/dbconsole +3 -0
- data/example/script/destroy +3 -0
- data/example/script/generate +3 -0
- data/example/script/performance/benchmarker +3 -0
- data/example/script/performance/profiler +3 -0
- data/example/script/performance/request +3 -0
- data/example/script/plugin +3 -0
- data/example/script/process/inspector +3 -0
- data/example/script/process/reaper +3 -0
- data/example/script/process/spawner +3 -0
- data/example/script/runner +3 -0
- data/example/script/server +3 -0
- data/example/test/fixtures/accounts.yml +7 -0
- data/example/test/functional/accounts_controller_test.rb +12 -0
- data/example/test/integration/account_figments_test.rb +95 -0
- data/example/test/test_helper.rb +41 -0
- data/example/vendor/plugins/data_fabric/init.rb +1 -0
- data/example/vendor/plugins/data_fabric/lib/data_fabric.rb +231 -0
- data/init.rb +1 -0
- data/lib/data_fabric/version.rb +5 -0
- data/lib/data_fabric.rb +231 -0
- data/test/connection_test.rb +103 -0
- data/test/database.yml +27 -0
- data/test/database_test.rb +39 -0
- data/test/shard_test.rb +24 -0
- data/test/test_helper.rb +17 -0
- data/test/thread_test.rb +91 -0
- metadata +164 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AccountFigmentsTest < ActionController::IntegrationTest
|
4
|
+
|
5
|
+
def test_create_account_and_figments
|
6
|
+
conn0 = db_connection "shard_0_test"
|
7
|
+
conn1 = db_connection "shard_1_test"
|
8
|
+
conn0.clear 'figments'
|
9
|
+
conn1.clear 'figments'
|
10
|
+
assert_equal 0, conn0.count_for('figments')
|
11
|
+
assert_equal 0, conn1.count_for('figments')
|
12
|
+
|
13
|
+
new_session do |user|
|
14
|
+
user.goes_home
|
15
|
+
mike = user.creates_account('mike', '0')
|
16
|
+
user.selects_account(mike)
|
17
|
+
before = mike.figments.size
|
18
|
+
user.creates_figment(14)
|
19
|
+
mike.figments.reload
|
20
|
+
assert_equal before + 1, mike.figments.size
|
21
|
+
assert_equal 14, mike.figments.first.value
|
22
|
+
end
|
23
|
+
|
24
|
+
# Bypass data_fabric and verify the figment went to shard 0.
|
25
|
+
assert_equal 1, conn0.count_for('figments')
|
26
|
+
assert_equal 0, conn1.count_for('figments')
|
27
|
+
|
28
|
+
new_session do |user|
|
29
|
+
user.goes_home
|
30
|
+
bob = user.creates_account('bob', '1')
|
31
|
+
user.selects_account(bob)
|
32
|
+
before = bob.figments.size
|
33
|
+
user.creates_figment(66)
|
34
|
+
bob.figments.reload
|
35
|
+
assert_equal before + 1, bob.figments.size
|
36
|
+
assert_equal 66, bob.figments.first.value
|
37
|
+
end
|
38
|
+
|
39
|
+
# Bypass data_fabric and verify the figment went to shard 1.
|
40
|
+
assert_equal 1, conn0.count_for('figments')
|
41
|
+
assert_equal 1, conn1.count_for('figments')
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def db_connection(conf)
|
47
|
+
conn = ActiveRecord::Base.sqlite3_connection(HashWithIndifferentAccess.new(ActiveRecord::Base.configurations[conf]))
|
48
|
+
def conn.count_for(table)
|
49
|
+
Integer(execute("select count(*) as c from #{table}")[0]['c'])
|
50
|
+
end
|
51
|
+
def conn.clear(table)
|
52
|
+
execute("delete from #{table}")
|
53
|
+
end
|
54
|
+
conn
|
55
|
+
end
|
56
|
+
|
57
|
+
def new_session
|
58
|
+
open_session do |sess|
|
59
|
+
sess.extend(Operations)
|
60
|
+
yield sess if block_given?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
module Operations
|
65
|
+
def goes_home
|
66
|
+
get accounts_path
|
67
|
+
assert_response :success
|
68
|
+
end
|
69
|
+
|
70
|
+
def creates_account(name, shard)
|
71
|
+
post accounts_path, :acct => { :name => name, :shard => shard }
|
72
|
+
assert_response :redirect
|
73
|
+
follow_redirect!
|
74
|
+
assert_response :success
|
75
|
+
assert_template "accounts/index"
|
76
|
+
Account.find_by_name(name)
|
77
|
+
end
|
78
|
+
|
79
|
+
def selects_account(account)
|
80
|
+
get choose_account_path(account)
|
81
|
+
assert_response :redirect
|
82
|
+
follow_redirect!
|
83
|
+
assert_response :success
|
84
|
+
assert_template "accounts/index"
|
85
|
+
end
|
86
|
+
|
87
|
+
def creates_figment(value)
|
88
|
+
post figments_path, :figment => { :value => value }
|
89
|
+
assert_response :redirect
|
90
|
+
follow_redirect!
|
91
|
+
assert_response :success
|
92
|
+
assert_template "accounts/index"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
|
3
|
+
require 'test_help'
|
4
|
+
|
5
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
6
|
+
ActiveRecord::Base.logger.level = Logger::DEBUG
|
7
|
+
|
8
|
+
class Test::Unit::TestCase
|
9
|
+
# Transactional fixtures accelerate your tests by wrapping each test method
|
10
|
+
# in a transaction that's rolled back on completion. This ensures that the
|
11
|
+
# test database remains unchanged so your fixtures don't have to be reloaded
|
12
|
+
# between every test method. Fewer database queries means faster tests.
|
13
|
+
#
|
14
|
+
# Read Mike Clark's excellent walkthrough at
|
15
|
+
# http://clarkware.com/cgi/blosxom/2005/10/24#Rails10FastTesting
|
16
|
+
#
|
17
|
+
# Every Active Record database supports transactions except MyISAM tables
|
18
|
+
# in MySQL. Turn off transactional fixtures in this case; however, if you
|
19
|
+
# don't care one way or the other, switching from MyISAM to InnoDB tables
|
20
|
+
# is recommended.
|
21
|
+
#
|
22
|
+
# The only drawback to using transactional fixtures is when you actually
|
23
|
+
# need to test transactions. Since your test is bracketed by a transaction,
|
24
|
+
# any transactions started in your code will be automatically rolled back.
|
25
|
+
self.use_transactional_fixtures = true
|
26
|
+
|
27
|
+
# Instantiated fixtures are slow, but give you @david where otherwise you
|
28
|
+
# would need people(:david). If you don't want to migrate your existing
|
29
|
+
# test cases which use the @david style and don't mind the speed hit (each
|
30
|
+
# instantiated fixtures translates to a database query per test method),
|
31
|
+
# then set this back to true.
|
32
|
+
self.use_instantiated_fixtures = false
|
33
|
+
|
34
|
+
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
|
35
|
+
#
|
36
|
+
# Note: You'll currently still have to declare fixtures explicitly in integration tests
|
37
|
+
# -- they do not yet inherit this setting
|
38
|
+
fixtures :all
|
39
|
+
|
40
|
+
# Add more helper methods to be used by all tests here...
|
41
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
DataFabric.init
|
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/version'
|
3
|
+
|
4
|
+
# DataFabric adds a new level of flexibility to ActiveRecord connection handling.
|
5
|
+
# You need to describe the topology for your database infrastructure in your model(s). As with ActiveRecord normally, different models can use different topologies.
|
6
|
+
#
|
7
|
+
# class MyHugeVolumeOfDataModel < ActiveRecord::Base
|
8
|
+
# connection_topology :replicated => true, :shard_by => :city
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# There are four supported modes of operation, depending on the options given to the connection_topology method. The plugin will look for connections in your config/database.yml with the following convention:
|
12
|
+
#
|
13
|
+
# No connection topology:
|
14
|
+
# #{environment} - this is the default, as with ActiveRecord, e.g. "production"
|
15
|
+
#
|
16
|
+
# connection_topology :replicated => true
|
17
|
+
# #{environment}_#{role} - no sharding, just replication, where role is "master" or "slave", e.g. "production_master"
|
18
|
+
#
|
19
|
+
# connection_topology :shard_by => :city
|
20
|
+
# #{group}_#{shard}_#{environment} - sharding, no replication, e.g. "city_austin_production"
|
21
|
+
#
|
22
|
+
# connection_topology :replicated => true, :shard_by => :city
|
23
|
+
# #{group}_#{shard}_#{environment}_#{role} - sharding with replication, e.g. "city_austin_production_master"
|
24
|
+
#
|
25
|
+
#
|
26
|
+
# When marked as replicated, all write and transactional operations for the model go to the master, whereas read operations go to the slave.
|
27
|
+
#
|
28
|
+
# Since sharding is an application-level concern, your application must set the shard to use based on the current request or environment. The current shard for a group is set on a thread local variable. For example, you can set the shard in an ActionController around_filter based on the user as follows:
|
29
|
+
#
|
30
|
+
# class ApplicationController < ActionController::Base
|
31
|
+
# around_filter :select_shard
|
32
|
+
#
|
33
|
+
# private
|
34
|
+
# def select_shard(&action_block)
|
35
|
+
# DataFabric.activate_shard(:city => @current_user.city, &action_block)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
module DataFabric
|
39
|
+
VERSION = "1.0.0"
|
40
|
+
|
41
|
+
def self.logger
|
42
|
+
ActiveRecord::Base.logger
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.init
|
46
|
+
logger.info "Loading data_fabric #{VERSION} with ActiveRecord #{ActiveRecord::VERSION::STRING}"
|
47
|
+
ActiveRecord::Base.send(:include, self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.activate_shard(shards, &block)
|
51
|
+
ensure_setup
|
52
|
+
shards.each do |key, value|
|
53
|
+
Thread.current[:shards][key.to_s] = value.to_s
|
54
|
+
end
|
55
|
+
if block_given?
|
56
|
+
begin
|
57
|
+
yield
|
58
|
+
ensure
|
59
|
+
shards.each do |key, value|
|
60
|
+
Thread.current[:shards].delete(key.to_s)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# For cases where you can't pass a block to activate_shards, you can
|
67
|
+
# clean up the thread local settings by calling this method at the
|
68
|
+
# end of processing
|
69
|
+
def self.deactivate_shard(shards)
|
70
|
+
ensure_setup
|
71
|
+
shards.each do |key, value|
|
72
|
+
Thread.current[:shards].delete(key.to_s)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.active_shard(group)
|
77
|
+
raise ArgumentError, 'No shard has been activated' unless Thread.current[:shards]
|
78
|
+
|
79
|
+
returning(Thread.current[:shards][group.to_s]) do |shard|
|
80
|
+
raise ArgumentError, "No active shard for #{group}" unless shard
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.included(model)
|
85
|
+
# Wire up ActiveRecord::Base
|
86
|
+
model.extend ClassMethods
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.ensure_setup
|
90
|
+
Thread.current[:shards] = {} unless Thread.current[:shards]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Class methods injected into ActiveRecord::Base
|
94
|
+
module ClassMethods
|
95
|
+
def connection_topology(options)
|
96
|
+
proxy = DataFabric::ConnectionProxy.new(self, options)
|
97
|
+
ActiveRecord::Base.active_connections[name] = proxy
|
98
|
+
|
99
|
+
raise ArgumentError, "data_fabric does not support ActiveRecord's allow_concurrency = true" if allow_concurrency
|
100
|
+
DataFabric.logger.info "Creating data_fabric proxy for class #{name}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
class StringProxy
|
105
|
+
def initialize(&block)
|
106
|
+
@proc = block
|
107
|
+
end
|
108
|
+
def to_s
|
109
|
+
@proc.call
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class ConnectionProxy
|
114
|
+
def initialize(model_class, options)
|
115
|
+
@model_class = model_class
|
116
|
+
@replicated = options[:replicated]
|
117
|
+
@shard_group = options[:shard_by]
|
118
|
+
@prefix = options[:prefix]
|
119
|
+
@current_role = 'slave' if @replicated
|
120
|
+
@current_connection_name_builder = connection_name_builder
|
121
|
+
@cached_connection = nil
|
122
|
+
@current_connection_name = nil
|
123
|
+
@role_changed = false
|
124
|
+
|
125
|
+
@model_class.send :include, ActiveRecordConnectionMethods if @replicated
|
126
|
+
end
|
127
|
+
|
128
|
+
delegate :insert, :update, :delete, :create_table, :rename_table, :drop_table, :add_column, :remove_column,
|
129
|
+
:change_column, :change_column_default, :rename_column, :add_index, :remove_index, :initialize_schema_information,
|
130
|
+
:dump_schema_information, :to => :master
|
131
|
+
|
132
|
+
def transaction(start_db_transaction = true, &block)
|
133
|
+
with_master { raw_connection.transaction(start_db_transaction, &block) }
|
134
|
+
end
|
135
|
+
|
136
|
+
def method_missing(method, *args, &block)
|
137
|
+
unless @cached_connection and !@role_changed
|
138
|
+
raw_connection
|
139
|
+
@role_changed = false
|
140
|
+
end
|
141
|
+
if logger.debug?
|
142
|
+
logger.debug("Calling #{method} on #{@cached_connection}")
|
143
|
+
end
|
144
|
+
@cached_connection.send(method, *args, &block)
|
145
|
+
end
|
146
|
+
|
147
|
+
def connection_name
|
148
|
+
@current_connection_name_builder.join('_')
|
149
|
+
end
|
150
|
+
|
151
|
+
def disconnect!
|
152
|
+
@cached_connection.disconnect! if @cached_connection
|
153
|
+
@cached_connection = nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def verify!(arg)
|
157
|
+
@cached_connection.verify!(0) if @cached_connection
|
158
|
+
end
|
159
|
+
|
160
|
+
def with_master
|
161
|
+
set_role('master')
|
162
|
+
yield
|
163
|
+
ensure
|
164
|
+
set_role('slave')
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def connection_name_builder
|
170
|
+
clauses = []
|
171
|
+
clauses << @prefix if @prefix
|
172
|
+
clauses << @shard_group if @shard_group
|
173
|
+
clauses << StringProxy.new { DataFabric.active_shard(@shard_group) } if @shard_group
|
174
|
+
clauses << RAILS_ENV
|
175
|
+
clauses << StringProxy.new { @current_role } if @replicated
|
176
|
+
clauses
|
177
|
+
end
|
178
|
+
|
179
|
+
def raw_connection
|
180
|
+
conn_name = connection_name
|
181
|
+
unless already_connected_to? conn_name
|
182
|
+
@cached_connection = begin
|
183
|
+
config = ActiveRecord::Base.configurations[conn_name]
|
184
|
+
raise ArgumentError, "Unknown database config: #{conn_name}, have #{ActiveRecord::Base.configurations.inspect}" unless config
|
185
|
+
@model_class.establish_connection config
|
186
|
+
if logger.debug?
|
187
|
+
logger.debug "Switching from #{@current_connection_name} to #{conn_name}"
|
188
|
+
end
|
189
|
+
@current_connection_name = conn_name
|
190
|
+
conn = @model_class.connection
|
191
|
+
conn.verify! 0
|
192
|
+
conn
|
193
|
+
end
|
194
|
+
@model_class.active_connections[@model_class.name] = self
|
195
|
+
end
|
196
|
+
@cached_connection
|
197
|
+
end
|
198
|
+
|
199
|
+
def already_connected_to?(conn_name)
|
200
|
+
conn_name == @current_connection_name and @cached_connection
|
201
|
+
end
|
202
|
+
|
203
|
+
def set_role(role)
|
204
|
+
if @replicated and @current_role != role
|
205
|
+
@current_role = role
|
206
|
+
@role_changed = true
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def master
|
211
|
+
set_role('master')
|
212
|
+
return raw_connection
|
213
|
+
ensure
|
214
|
+
set_role('slave')
|
215
|
+
end
|
216
|
+
|
217
|
+
def logger
|
218
|
+
DataFabric.logger
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
module ActiveRecordConnectionMethods
|
223
|
+
def self.included(base)
|
224
|
+
base.alias_method_chain :reload, :master
|
225
|
+
end
|
226
|
+
|
227
|
+
def reload_with_master(*args, &block)
|
228
|
+
connection.with_master { reload_without_master }
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|