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.
Files changed (81) hide show
  1. data/CHANGELOG +1 -0
  2. data/Manifest +79 -0
  3. data/README.rdoc +108 -0
  4. data/Rakefile +69 -0
  5. data/TESTING.rdoc +34 -0
  6. data/TODO +1 -0
  7. data/data_fabric.gemspec +166 -0
  8. data/example/Rakefile +58 -0
  9. data/example/app/controllers/accounts_controller.rb +22 -0
  10. data/example/app/controllers/application.rb +39 -0
  11. data/example/app/controllers/figments_controller.rb +8 -0
  12. data/example/app/helpers/accounts_helper.rb +2 -0
  13. data/example/app/helpers/application_helper.rb +3 -0
  14. data/example/app/helpers/figments_helper.rb +2 -0
  15. data/example/app/models/account.rb +3 -0
  16. data/example/app/models/figment.rb +4 -0
  17. data/example/app/views/accounts/index.html.erb +47 -0
  18. data/example/app/views/layouts/application.html.erb +8 -0
  19. data/example/config/boot.rb +109 -0
  20. data/example/config/database.yml +21 -0
  21. data/example/config/environment.rb +67 -0
  22. data/example/config/environments/development.rb +17 -0
  23. data/example/config/environments/production.rb +22 -0
  24. data/example/config/environments/test.rb +22 -0
  25. data/example/config/initializers/inflections.rb +10 -0
  26. data/example/config/initializers/mime_types.rb +5 -0
  27. data/example/config/initializers/new_rails_defaults.rb +15 -0
  28. data/example/config/routes.rb +45 -0
  29. data/example/db/development.sqlite3 +0 -0
  30. data/example/db/migrate/20080702154628_create_accounts.rb +14 -0
  31. data/example/db/migrate/20080702154820_create_figments.rb +14 -0
  32. data/example/db/s0_development.sqlite3 +0 -0
  33. data/example/db/s0_test.sqlite3 +0 -0
  34. data/example/db/s1_development.sqlite3 +0 -0
  35. data/example/db/s1_test.sqlite3 +0 -0
  36. data/example/db/schema.rb +28 -0
  37. data/example/db/test.sqlite3 +0 -0
  38. data/example/public/404.html +30 -0
  39. data/example/public/422.html +30 -0
  40. data/example/public/500.html +30 -0
  41. data/example/public/dispatch.cgi +10 -0
  42. data/example/public/dispatch.fcgi +24 -0
  43. data/example/public/dispatch.rb +10 -0
  44. data/example/public/favicon.ico +0 -0
  45. data/example/public/images/rails.png +0 -0
  46. data/example/public/javascripts/application.js +2 -0
  47. data/example/public/javascripts/controls.js +963 -0
  48. data/example/public/javascripts/dragdrop.js +972 -0
  49. data/example/public/javascripts/effects.js +1120 -0
  50. data/example/public/javascripts/prototype.js +4225 -0
  51. data/example/public/robots.txt +5 -0
  52. data/example/script/about +3 -0
  53. data/example/script/console +3 -0
  54. data/example/script/dbconsole +3 -0
  55. data/example/script/destroy +3 -0
  56. data/example/script/generate +3 -0
  57. data/example/script/performance/benchmarker +3 -0
  58. data/example/script/performance/profiler +3 -0
  59. data/example/script/performance/request +3 -0
  60. data/example/script/plugin +3 -0
  61. data/example/script/process/inspector +3 -0
  62. data/example/script/process/reaper +3 -0
  63. data/example/script/process/spawner +3 -0
  64. data/example/script/runner +3 -0
  65. data/example/script/server +3 -0
  66. data/example/test/fixtures/accounts.yml +7 -0
  67. data/example/test/functional/accounts_controller_test.rb +12 -0
  68. data/example/test/integration/account_figments_test.rb +95 -0
  69. data/example/test/test_helper.rb +41 -0
  70. data/example/vendor/plugins/data_fabric/init.rb +1 -0
  71. data/example/vendor/plugins/data_fabric/lib/data_fabric.rb +231 -0
  72. data/init.rb +1 -0
  73. data/lib/data_fabric/version.rb +5 -0
  74. data/lib/data_fabric.rb +231 -0
  75. data/test/connection_test.rb +103 -0
  76. data/test/database.yml +27 -0
  77. data/test/database_test.rb +39 -0
  78. data/test/shard_test.rb +24 -0
  79. data/test/test_helper.rb +17 -0
  80. data/test/thread_test.rb +91 -0
  81. metadata +164 -0
@@ -0,0 +1,5 @@
1
+ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
2
+ #
3
+ # To ban all spiders from the entire site uncomment the next two lines:
4
+ # User-Agent: *
5
+ # Disallow: /
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/about'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/console'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/dbconsole'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/destroy'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/generate'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/performance/benchmarker'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/performance/profiler'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/performance/request'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/plugin'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/process/inspector'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/process/reaper'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../../config/boot'
3
+ require 'commands/process/spawner'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/runner'
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../config/boot'
3
+ require 'commands/server'
@@ -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,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