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.
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