lookout-data_fabric 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +15 -0
  2. data/CHANGELOG +79 -0
  3. data/README.rdoc +125 -0
  4. data/Rakefile +93 -0
  5. data/TESTING.rdoc +30 -0
  6. data/example30/Gemfile +5 -0
  7. data/example30/Gemfile.lock +79 -0
  8. data/example30/README +256 -0
  9. data/example30/Rakefile +52 -0
  10. data/example30/app/controllers/accounts_controller.rb +22 -0
  11. data/example30/app/controllers/application_controller.rb +39 -0
  12. data/example30/app/controllers/figments_controller.rb +8 -0
  13. data/example30/app/helpers/application_helper.rb +2 -0
  14. data/example30/app/models/account.rb +3 -0
  15. data/example30/app/models/figment.rb +4 -0
  16. data/example30/app/views/accounts/index.html.erb +47 -0
  17. data/example30/app/views/layouts/application.html.erb +8 -0
  18. data/example30/config.ru +4 -0
  19. data/example30/config/application.rb +42 -0
  20. data/example30/config/boot.rb +13 -0
  21. data/example30/config/database.yml +21 -0
  22. data/example30/config/environment.rb +5 -0
  23. data/example30/config/environments/development.rb +26 -0
  24. data/example30/config/environments/production.rb +49 -0
  25. data/example30/config/environments/test.rb +35 -0
  26. data/example30/config/initializers/backtrace_silencers.rb +7 -0
  27. data/example30/config/initializers/inflections.rb +10 -0
  28. data/example30/config/initializers/mime_types.rb +5 -0
  29. data/example30/config/initializers/secret_token.rb +7 -0
  30. data/example30/config/initializers/session_store.rb +8 -0
  31. data/example30/config/locales/en.yml +5 -0
  32. data/example30/config/routes.rb +65 -0
  33. data/example30/db/migrate/20080702154628_create_accounts.rb +14 -0
  34. data/example30/db/migrate/20080702154820_create_figments.rb +14 -0
  35. data/example30/db/seeds.rb +7 -0
  36. data/example30/script/rails +6 -0
  37. data/example30/test/fixtures/accounts.yml +7 -0
  38. data/example30/test/functional/accounts_controller_test.rb +12 -0
  39. data/example30/test/integration/account_figments_test.rb +97 -0
  40. data/example30/test/performance/browsing_test.rb +9 -0
  41. data/example30/test/test_helper.rb +13 -0
  42. data/lib/data_fabric.rb +107 -0
  43. data/lib/data_fabric/connection_proxy.rb +176 -0
  44. data/lib/data_fabric/extensions.rb +39 -0
  45. data/lib/data_fabric/version.rb +5 -0
  46. data/test/connection_test.rb +142 -0
  47. data/test/database.yml +24 -0
  48. data/test/database.yml.mysql +36 -0
  49. data/test/database_test.rb +51 -0
  50. data/test/shard_test.rb +58 -0
  51. data/test/test_helper.rb +28 -0
  52. data/test/thread_test.rb +83 -0
  53. metadata +172 -0
@@ -0,0 +1,5 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ hello: "Hello world"
@@ -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,14 @@
1
+ class CreateAccounts < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :accounts do |t|
4
+ t.string :name
5
+ t.string :shard
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :accounts
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ class CreateFigments < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :figments do |t|
4
+ t.integer :account_id
5
+ t.integer :value
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :figments
13
+ end
14
+ 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,7 @@
1
+ # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
2
+
3
+ <% %w(fiveruns google yahoo microsoft).each_with_index do |name, idx| %>
4
+ <%= name %>:
5
+ name: <%= name %>
6
+ shard: <%= idx % 2 %>
7
+ <% end %>
@@ -0,0 +1,12 @@
1
+ require 'test_helper'
2
+
3
+ class AccountsControllerTest < ActionController::TestCase
4
+ fixtures :accounts
5
+
6
+ def test_index
7
+ get :index
8
+ assert_response :success
9
+ assert assigns(:accounts)
10
+ assert_equal 4, assigns(:accounts).size
11
+ end
12
+ end
@@ -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,9 @@
1
+ require 'test_helper'
2
+ require 'rails/performance_test_help'
3
+
4
+ # Profiling results for each test method are written to tmp/performance.
5
+ class BrowsingTest < ActionDispatch::PerformanceTest
6
+ def test_homepage
7
+ get '/'
8
+ end
9
+ 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
@@ -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