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
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ DataFabric.init
@@ -0,0 +1,5 @@
1
+ module DataFabric
2
+ module Version
3
+ STRING = "1.0.0"
4
+ end
5
+ end
@@ -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
@@ -0,0 +1,103 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'flexmock/test_unit'
3
+
4
+ class PrefixModel < ActiveRecord::Base
5
+ connection_topology :prefix => 'prefix'
6
+ end
7
+
8
+ class ShardModel < ActiveRecord::Base
9
+ connection_topology :shard_by => :city
10
+ end
11
+
12
+ class TheWholeEnchilada < ActiveRecord::Base
13
+ connection_topology :prefix => 'fiveruns', :replicated => true, :shard_by => :city
14
+ end
15
+
16
+ class AdapterMock < ActiveRecord::ConnectionAdapters::AbstractAdapter
17
+ # Minimum required to perform a find with no results
18
+ def columns(table_name, name=nil)
19
+ []
20
+ end
21
+ def select(sql, name=nil)
22
+ []
23
+ end
24
+ def execute(sql, name=nil)
25
+ 0
26
+ end
27
+
28
+ def name
29
+ 'fake-db'
30
+ end
31
+
32
+ def method_missing(name, *args)
33
+ raise ArgumentError, "#{self.class.name} missing '#{name}': #{args.inspect}"
34
+ end
35
+ end
36
+
37
+ class RawConnection
38
+ def method_missing(name, *args)
39
+ puts "#{self.class.name} missing '#{name}': #{args.inspect}"
40
+ end
41
+ end
42
+
43
+ class ConnectionTest < Test::Unit::TestCase
44
+
45
+ def test_should_install_into_arbase
46
+ assert PrefixModel.methods.include?('connection_topology')
47
+ end
48
+
49
+ def test_prefix_connection_name
50
+ setup_configuration_for PrefixModel, 'prefix_test'
51
+ assert_equal 'prefix_test', PrefixModel.connection.connection_name
52
+ end
53
+
54
+ def test_shard_connection_name
55
+ setup_configuration_for ShardModel, 'city_austin_test'
56
+ # ensure unset means error
57
+ assert_raises ArgumentError do
58
+ ShardModel.connection.connection_name
59
+ end
60
+ DataFabric.activate_shard(:city => 'austin', :category => 'art') do
61
+ assert_equal 'city_austin_test', ShardModel.connection.connection_name
62
+ end
63
+ # ensure it got unset
64
+ assert_raises ArgumentError do
65
+ ShardModel.connection.connection_name
66
+ end
67
+ end
68
+
69
+ def test_enchilada
70
+ setup_configuration_for TheWholeEnchilada, 'fiveruns_city_dallas_test_slave'
71
+ setup_configuration_for TheWholeEnchilada, 'fiveruns_city_dallas_test_master'
72
+ DataFabric.activate_shard :city => :dallas do
73
+ assert_equal 'fiveruns_city_dallas_test_slave', TheWholeEnchilada.connection.connection_name
74
+
75
+ # Should use the slave
76
+ assert_raises ActiveRecord::RecordNotFound do
77
+ TheWholeEnchilada.find(1)
78
+ end
79
+
80
+ # Should use the master
81
+ mmmm = TheWholeEnchilada.new
82
+ mmmm.instance_variable_set(:@attributes, { 'id' => 1 })
83
+ assert_raises ActiveRecord::RecordNotFound do
84
+ mmmm.reload
85
+ end
86
+ # ...but immediately set it back to default to the slave
87
+ assert_equal 'fiveruns_city_dallas_test_slave', TheWholeEnchilada.connection.connection_name
88
+
89
+ # Should use the master
90
+ TheWholeEnchilada.transaction do
91
+ mmmm.save!
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def setup_configuration_for(clazz, name)
99
+ flexmock(clazz).should_receive(:mysql_connection).and_return(AdapterMock.new(RawConnection.new))
100
+ ActiveRecord::Base.configurations ||= HashWithIndifferentAccess.new
101
+ ActiveRecord::Base.configurations[name] = HashWithIndifferentAccess.new({ :adapter => 'mysql', :database => name, :host => 'localhost'})
102
+ end
103
+ end
data/test/database.yml ADDED
@@ -0,0 +1,27 @@
1
+ fiveruns_city_austin_test_master:
2
+ adapter: mysql
3
+ host: localhost
4
+ database: vr_austin_master
5
+ username: root
6
+ password:
7
+
8
+ fiveruns_city_austin_test_slave:
9
+ adapter: mysql
10
+ host: localhost
11
+ database: vr_austin_slave
12
+ username: root
13
+ password:
14
+
15
+ fiveruns_city_dallas_test_master:
16
+ adapter: mysql
17
+ host: localhost
18
+ database: vr_dallas_master
19
+ username: root
20
+ password:
21
+
22
+ fiveruns_city_dallas_test_slave:
23
+ adapter: mysql
24
+ host: localhost
25
+ database: vr_dallas_slave
26
+ username: root
27
+ password:
@@ -0,0 +1,39 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'flexmock/test_unit'
3
+ require 'erb'
4
+
5
+ class TheWholeBurrito < ActiveRecord::Base
6
+ connection_topology :prefix => 'fiveruns', :replicated => true, :shard_by => :city
7
+ end
8
+
9
+ class DatabaseTest < Test::Unit::TestCase
10
+
11
+ def setup
12
+ filename = File.join(File.dirname(__FILE__), "database.yml")
13
+ ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(filename)).result)
14
+ end
15
+
16
+ def test_live_burrito
17
+ DataFabric.activate_shard :city => :dallas do
18
+ assert_equal 'fiveruns_city_dallas_test_slave', TheWholeBurrito.connection.connection_name
19
+
20
+ # Should use the slave
21
+ burrito = TheWholeBurrito.find(1)
22
+ assert_equal 'vr_dallas_slave', burrito.name
23
+
24
+ # Should use the master
25
+ burrito.reload
26
+ assert_equal 'vr_dallas_master', burrito.name
27
+
28
+ # ...but immediately set it back to default to the slave
29
+ assert_equal 'fiveruns_city_dallas_test_slave', TheWholeBurrito.connection.connection_name
30
+
31
+ # Should use the master
32
+ TheWholeBurrito.transaction do
33
+ burrito = TheWholeBurrito.find(1)
34
+ assert_equal 'vr_dallas_master', burrito.name
35
+ burrito.save!
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ShardTest < Test::Unit::TestCase
4
+
5
+ def test_activation_should_persist_in_thread
6
+ DataFabric.activate_shard(:city => 'austin')
7
+ assert_equal 'austin', DataFabric.active_shard(:city)
8
+ end
9
+
10
+ def test_activation_in_one_thread_does_not_change_another
11
+ assert_raises ArgumentError do
12
+ DataFabric.active_shard(:city)
13
+ end
14
+ DataFabric.activate_shard(:city => 'austin')
15
+
16
+ Thread.new do
17
+ assert_raises ArgumentError do
18
+ DataFabric.active_shard(:city)
19
+ end
20
+ DataFabric.activate_shard(:city => 'dallas')
21
+ assert_equal 'dallas', DataFabric.active_shard(:city)
22
+ end.join
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ ENV['RAILS_ENV']='test'
2
+ RAILS_ENV='test'
3
+
4
+ require 'rubygems'
5
+ require 'test/unit'
6
+
7
+ # Bootstrap AR
8
+ gem 'activerecord', '=2.0.2'
9
+ require 'active_record'
10
+ require 'active_record/version'
11
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
12
+ ActiveRecord::Base.logger.level = Logger::WARN
13
+ ActiveRecord::Base.allow_concurrency = false
14
+
15
+ # Bootstrap DF
16
+ Dependencies.load_paths << File.join(File.dirname(__FILE__), '../lib')
17
+ require 'init'