lookout-data_fabric 1.5.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.
- checksums.yaml +15 -0
- data/CHANGELOG +79 -0
- data/README.rdoc +125 -0
- data/Rakefile +93 -0
- data/TESTING.rdoc +30 -0
- data/example30/Gemfile +5 -0
- data/example30/Gemfile.lock +79 -0
- data/example30/README +256 -0
- data/example30/Rakefile +52 -0
- data/example30/app/controllers/accounts_controller.rb +22 -0
- data/example30/app/controllers/application_controller.rb +39 -0
- data/example30/app/controllers/figments_controller.rb +8 -0
- data/example30/app/helpers/application_helper.rb +2 -0
- data/example30/app/models/account.rb +3 -0
- data/example30/app/models/figment.rb +4 -0
- data/example30/app/views/accounts/index.html.erb +47 -0
- data/example30/app/views/layouts/application.html.erb +8 -0
- data/example30/config.ru +4 -0
- data/example30/config/application.rb +42 -0
- data/example30/config/boot.rb +13 -0
- data/example30/config/database.yml +21 -0
- data/example30/config/environment.rb +5 -0
- data/example30/config/environments/development.rb +26 -0
- data/example30/config/environments/production.rb +49 -0
- data/example30/config/environments/test.rb +35 -0
- data/example30/config/initializers/backtrace_silencers.rb +7 -0
- data/example30/config/initializers/inflections.rb +10 -0
- data/example30/config/initializers/mime_types.rb +5 -0
- data/example30/config/initializers/secret_token.rb +7 -0
- data/example30/config/initializers/session_store.rb +8 -0
- data/example30/config/locales/en.yml +5 -0
- data/example30/config/routes.rb +65 -0
- data/example30/db/migrate/20080702154628_create_accounts.rb +14 -0
- data/example30/db/migrate/20080702154820_create_figments.rb +14 -0
- data/example30/db/seeds.rb +7 -0
- data/example30/script/rails +6 -0
- data/example30/test/fixtures/accounts.yml +7 -0
- data/example30/test/functional/accounts_controller_test.rb +12 -0
- data/example30/test/integration/account_figments_test.rb +97 -0
- data/example30/test/performance/browsing_test.rb +9 -0
- data/example30/test/test_helper.rb +13 -0
- data/lib/data_fabric.rb +107 -0
- data/lib/data_fabric/connection_proxy.rb +176 -0
- data/lib/data_fabric/extensions.rb +39 -0
- data/lib/data_fabric/version.rb +5 -0
- data/test/connection_test.rb +142 -0
- data/test/database.yml +24 -0
- data/test/database.yml.mysql +36 -0
- data/test/database_test.rb +51 -0
- data/test/shard_test.rb +58 -0
- data/test/test_helper.rb +28 -0
- data/test/thread_test.rb +83 -0
- metadata +172 -0
@@ -0,0 +1,65 @@
|
|
1
|
+
Example30::Application.routes.draw do
|
2
|
+
resources :figments
|
3
|
+
resources :accounts do
|
4
|
+
member do
|
5
|
+
get :choose
|
6
|
+
end
|
7
|
+
end
|
8
|
+
root :to => 'accounts#index'
|
9
|
+
# The priority is based upon order of creation:
|
10
|
+
# first created -> highest priority.
|
11
|
+
|
12
|
+
# Sample of regular route:
|
13
|
+
# match 'products/:id' => 'catalog#view'
|
14
|
+
# Keep in mind you can assign values other than :controller and :action
|
15
|
+
|
16
|
+
# Sample of named route:
|
17
|
+
# match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
|
18
|
+
# This route can be invoked with purchase_url(:id => product.id)
|
19
|
+
|
20
|
+
# Sample resource route (maps HTTP verbs to controller actions automatically):
|
21
|
+
# resources :products
|
22
|
+
|
23
|
+
# Sample resource route with options:
|
24
|
+
# resources :products do
|
25
|
+
# member do
|
26
|
+
# get 'short'
|
27
|
+
# post 'toggle'
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# collection do
|
31
|
+
# get 'sold'
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
|
35
|
+
# Sample resource route with sub-resources:
|
36
|
+
# resources :products do
|
37
|
+
# resources :comments, :sales
|
38
|
+
# resource :seller
|
39
|
+
# end
|
40
|
+
|
41
|
+
# Sample resource route with more complex sub-resources
|
42
|
+
# resources :products do
|
43
|
+
# resources :comments
|
44
|
+
# resources :sales do
|
45
|
+
# get 'recent', :on => :collection
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
|
49
|
+
# Sample resource route within a namespace:
|
50
|
+
# namespace :admin do
|
51
|
+
# # Directs /admin/products/* to Admin::ProductsController
|
52
|
+
# # (app/controllers/admin/products_controller.rb)
|
53
|
+
# resources :products
|
54
|
+
# end
|
55
|
+
|
56
|
+
# You can have the root of your site routed with "root"
|
57
|
+
# just remember to delete public/index.html.
|
58
|
+
# root :to => "welcome#index"
|
59
|
+
|
60
|
+
# See how all your routes lay out with "rake routes"
|
61
|
+
|
62
|
+
# This is a legacy wild controller route that's not recommended for RESTful applications.
|
63
|
+
# Note: This route will make all actions in every controller accessible via GET requests.
|
64
|
+
# match ':controller(/:action(/:id(.:format)))'
|
65
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
# This file should contain all the record creation needed to seed the database with its default values.
|
2
|
+
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
|
3
|
+
#
|
4
|
+
# Examples:
|
5
|
+
#
|
6
|
+
# cities = City.create([{ :name => 'Chicago' }, { :name => 'Copenhagen' }])
|
7
|
+
# Mayor.create(:name => 'Daley', :city => cities.first)
|
@@ -0,0 +1,6 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
|
3
|
+
|
4
|
+
APP_PATH = File.expand_path('../../config/application', __FILE__)
|
5
|
+
require File.expand_path('../../config/boot', __FILE__)
|
6
|
+
require 'rails/commands'
|
@@ -0,0 +1,97 @@
|
|
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(0) 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(1) 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(shard)
|
58
|
+
open_session do |sess|
|
59
|
+
DataFabric.activate_shard :shard => shard do
|
60
|
+
sess.extend(Operations)
|
61
|
+
yield sess if block_given?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module Operations
|
67
|
+
def goes_home
|
68
|
+
get accounts_path
|
69
|
+
assert_response :success
|
70
|
+
end
|
71
|
+
|
72
|
+
def creates_account(name, shard)
|
73
|
+
post accounts_path, :acct => { :name => name, :shard => shard }
|
74
|
+
assert_response :redirect
|
75
|
+
follow_redirect!
|
76
|
+
assert_response :success
|
77
|
+
assert_template "accounts/index"
|
78
|
+
Account.find_by_name(name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def selects_account(account)
|
82
|
+
get choose_account_path(account)
|
83
|
+
assert_response :redirect
|
84
|
+
follow_redirect!
|
85
|
+
assert_response :success
|
86
|
+
assert_template "accounts/index"
|
87
|
+
end
|
88
|
+
|
89
|
+
def creates_figment(value)
|
90
|
+
post figments_path, :figment => { :value => value }
|
91
|
+
assert_response :redirect
|
92
|
+
follow_redirect!
|
93
|
+
assert_response :success
|
94
|
+
assert_template "accounts/index"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
2
|
+
require File.expand_path('../../config/environment', __FILE__)
|
3
|
+
require 'rails/test_help'
|
4
|
+
|
5
|
+
class ActiveSupport::TestCase
|
6
|
+
# Setup all fixtures in test/fixtures/*.(yml|csv) for all tests in alphabetical order.
|
7
|
+
#
|
8
|
+
# Note: You'll currently still have to declare fixtures explicitly in integration tests
|
9
|
+
# -- they do not yet inherit this setting
|
10
|
+
fixtures :all
|
11
|
+
|
12
|
+
# Add more helper methods to be used by all tests here...
|
13
|
+
end
|
data/lib/data_fabric.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/version'
|
3
|
+
require 'active_record/connection_adapters/abstract/connection_pool'
|
4
|
+
require 'active_record/connection_adapters/abstract/connection_specification'
|
5
|
+
require 'data_fabric/version'
|
6
|
+
|
7
|
+
# DataFabric adds a new level of flexibility to ActiveRecord connection handling.
|
8
|
+
# You need to describe the topology for your database infrastructure in your model(s). As with ActiveRecord normally, different models can use different topologies.
|
9
|
+
#
|
10
|
+
# class MyHugeVolumeOfDataModel < ActiveRecord::Base
|
11
|
+
# data_fabric :replicated => true, :shard_by => :city
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# There are four supported modes of operation, depending on the options given to the data_fabric method. The plugin will look for connections in your config/database.yml with the following convention:
|
15
|
+
#
|
16
|
+
# No connection topology:
|
17
|
+
# #{environment} - this is the default, as with ActiveRecord, e.g. "production"
|
18
|
+
#
|
19
|
+
# data_fabric :replicated => true
|
20
|
+
# #{environment}_#{role} - no sharding, just replication, where role is "master" or "slave", e.g. "production_master"
|
21
|
+
#
|
22
|
+
# data_fabric :shard_by => :city
|
23
|
+
# #{group}_#{shard}_#{environment} - sharding, no replication, e.g. "city_austin_production"
|
24
|
+
#
|
25
|
+
# data_fabric :replicated => true, :shard_by => :city
|
26
|
+
# #{group}_#{shard}_#{environment}_#{role} - sharding with replication, e.g. "city_austin_production_master"
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# When marked as replicated, all write and transactional operations for the model go to the master, whereas read operations go to the slave.
|
30
|
+
#
|
31
|
+
# 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:
|
32
|
+
#
|
33
|
+
# class ApplicationController < ActionController::Base
|
34
|
+
# around_filter :select_shard
|
35
|
+
#
|
36
|
+
# private
|
37
|
+
# def select_shard(&action_block)
|
38
|
+
# DataFabric.activate_shard(:city => @current_user.city, &action_block)
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
module DataFabric
|
42
|
+
|
43
|
+
def self.logger
|
44
|
+
@logger ||= ActiveRecord::Base.logger || default_logger
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.logger=(log)
|
48
|
+
@logger = log
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.activate_shard(shards, &block)
|
52
|
+
ensure_setup
|
53
|
+
|
54
|
+
# Save the old shard settings to handle nested activation
|
55
|
+
old = Thread.current[:shards].dup
|
56
|
+
|
57
|
+
shards.each_pair do |key, value|
|
58
|
+
Thread.current[:shards][key.to_s] = value.to_s
|
59
|
+
end
|
60
|
+
if block_given?
|
61
|
+
begin
|
62
|
+
yield
|
63
|
+
ensure
|
64
|
+
Thread.current[:shards] = old
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# For cases where you can't pass a block to activate_shards, you can
|
70
|
+
# clean up the thread local settings by calling this method at the
|
71
|
+
# end of processing
|
72
|
+
def self.deactivate_shard(shards)
|
73
|
+
ensure_setup
|
74
|
+
shards.each do |key, value|
|
75
|
+
Thread.current[:shards].delete(key.to_s)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.active_shard(group)
|
80
|
+
raise ArgumentError, 'No shard has been activated' unless Thread.current[:shards]
|
81
|
+
|
82
|
+
shard = Thread.current[:shards][group.to_s]
|
83
|
+
raise ArgumentError, "No active shard for #{group}" unless shard
|
84
|
+
shard
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.shard_active_for?(group)
|
88
|
+
return true unless group
|
89
|
+
Thread.current[:shards] and Thread.current[:shards][group.to_s]
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.ensure_setup
|
93
|
+
Thread.current[:shards] = {} unless Thread.current[:shards]
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
def self.default_logger
|
98
|
+
devnull = RUBY_PLATFORM =~ /w32/ ? 'nul' : '/dev/null'
|
99
|
+
l = Logger.new(devnull)
|
100
|
+
l.level = Logger::INFO
|
101
|
+
l
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
require 'data_fabric/extensions'
|
107
|
+
ActiveRecord::Base.send(:include, DataFabric::Extensions)
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module DataFabric
|
2
|
+
module ActiveRecordConnectionMethods
|
3
|
+
def self.included(base)
|
4
|
+
unless base.method_defined? :reload_without_master
|
5
|
+
base.alias_method_chain :reload, :master
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def reload_with_master(*args, &block)
|
10
|
+
connection.with_master { reload_without_master }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class StringProxy
|
15
|
+
def initialize(&block)
|
16
|
+
@proc = block
|
17
|
+
end
|
18
|
+
def to_s
|
19
|
+
@proc.call
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
class PoolProxy
|
24
|
+
def initialize(proxy)
|
25
|
+
@proxy = proxy
|
26
|
+
end
|
27
|
+
|
28
|
+
def connection
|
29
|
+
@proxy
|
30
|
+
end
|
31
|
+
|
32
|
+
def spec
|
33
|
+
@proxy.current_pool.spec
|
34
|
+
end
|
35
|
+
|
36
|
+
def with_connection
|
37
|
+
yield @proxy
|
38
|
+
end
|
39
|
+
|
40
|
+
def connected?
|
41
|
+
@proxy.connected?
|
42
|
+
end
|
43
|
+
|
44
|
+
%w(disconnect! release_connection clear_reloadable_connections! clear_stale_cached_connections! verify_active_connections!).each do |name|
|
45
|
+
define_method(name.to_sym) do
|
46
|
+
@proxy.shard_pools.values.each do |pool|
|
47
|
+
pool.send(name.to_sym)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
%w(columns column_defaults columns_hash table_exists? primary_keys).each do |name|
|
53
|
+
define_method(name.to_sym) do |*args|
|
54
|
+
@proxy.current_pool.send(name.to_sym, *args)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_missing(name, *args)
|
59
|
+
DataFabric.logger.warn "Add '#{name}' to DataFabric::PoolProxy for performance"
|
60
|
+
@proxy.current_pool.send(name, *args)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class ConnectionProxy
|
65
|
+
cattr_accessor :shard_pools
|
66
|
+
|
67
|
+
def initialize(model_class, options)
|
68
|
+
@model_class = model_class
|
69
|
+
@replicated = options[:replicated]
|
70
|
+
@shard_group = options[:shard_by]
|
71
|
+
@prefix = options[:prefix]
|
72
|
+
set_role('slave') if @replicated
|
73
|
+
|
74
|
+
@model_class.send :include, ActiveRecordConnectionMethods if @replicated
|
75
|
+
end
|
76
|
+
|
77
|
+
delegate :insert, :update, :delete, :create_table, :rename_table, :drop_table, :add_column, :remove_column,
|
78
|
+
:change_column, :change_column_default, :rename_column, :add_index, :remove_index, :initialize_schema_information,
|
79
|
+
:dump_schema_information, :execute, :execute_ignore_duplicate, :to => :master
|
80
|
+
|
81
|
+
delegate :insert_many, :to => :master # ar-extensions bulk insert support
|
82
|
+
|
83
|
+
def transaction(start_db_transaction = true, &block)
|
84
|
+
with_master do
|
85
|
+
connection.transaction(start_db_transaction, &block)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def respond_to?(method)
|
90
|
+
super || connection.respond_to?(method)
|
91
|
+
end
|
92
|
+
|
93
|
+
def method_missing(method, *args, &block)
|
94
|
+
DataFabric.logger.debug { "Calling #{method} on #{connection}" }
|
95
|
+
connection.send(method, *args, &block)
|
96
|
+
end
|
97
|
+
|
98
|
+
def connection_name
|
99
|
+
connection_name_builder.join('_')
|
100
|
+
end
|
101
|
+
|
102
|
+
def with_master
|
103
|
+
# Allow nesting of with_master.
|
104
|
+
old_role = current_role
|
105
|
+
set_role('master')
|
106
|
+
yield
|
107
|
+
ensure
|
108
|
+
set_role(old_role)
|
109
|
+
end
|
110
|
+
|
111
|
+
def connected?
|
112
|
+
current_pool.connected?
|
113
|
+
end
|
114
|
+
|
115
|
+
def connection
|
116
|
+
current_pool.connection
|
117
|
+
end
|
118
|
+
|
119
|
+
def current_pool
|
120
|
+
name = connection_name
|
121
|
+
self.class.shard_pools[name] ||= begin
|
122
|
+
config = ActiveRecord::Base.configurations[name]
|
123
|
+
raise ArgumentError, "Unknown database config: #{name}" unless config
|
124
|
+
ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec_for(config))
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def spec_for(config)
|
131
|
+
config = config.symbolize_keys
|
132
|
+
adapter_method = "#{config[:adapter]}_connection"
|
133
|
+
initialize_adapter(config[:adapter])
|
134
|
+
ActiveRecord::Base::ConnectionSpecification.new(config, adapter_method)
|
135
|
+
end
|
136
|
+
|
137
|
+
def initialize_adapter(adapter)
|
138
|
+
begin
|
139
|
+
require 'rubygems'
|
140
|
+
gem "activerecord-#{adapter}-adapter"
|
141
|
+
require "active_record/connection_adapters/#{adapter}_adapter"
|
142
|
+
rescue LoadError
|
143
|
+
begin
|
144
|
+
require "active_record/connection_adapters/#{adapter}_adapter"
|
145
|
+
rescue LoadError
|
146
|
+
raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def connection_name_builder
|
152
|
+
@connection_name_builder ||= begin
|
153
|
+
clauses = []
|
154
|
+
clauses << @prefix if @prefix
|
155
|
+
clauses << @shard_group if @shard_group
|
156
|
+
clauses << StringProxy.new { DataFabric.active_shard(@shard_group) } if @shard_group
|
157
|
+
clauses << Rails.env
|
158
|
+
clauses << StringProxy.new { current_role } if @replicated
|
159
|
+
clauses
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def set_role(role)
|
164
|
+
Thread.current[:data_fabric_role] = role
|
165
|
+
end
|
166
|
+
|
167
|
+
def current_role
|
168
|
+
Thread.current[:data_fabric_role] || 'slave'
|
169
|
+
end
|
170
|
+
|
171
|
+
def master
|
172
|
+
with_master { return connection }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end
|