read_from_slave 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,96 @@
1
+ Read_from_slave
2
+
3
+ Read_from_slave for Rails enables database reads from a slave database, while writes continue to go to the master
4
+
5
+ Read_from_slave will work with Rails 2.2 and above.
6
+
7
+ Installation
8
+
9
+ sudo gem install sdsykes-read_from_slave
10
+
11
+ Configuration
12
+
13
+ In config/environments/production.rb (for instance)
14
+
15
+ config.gem "sdsykes-read_from_slave", :lib=>"read_from_slave"
16
+
17
+ In config/database.yml
18
+
19
+ production:
20
+ adapter: mysql
21
+ database: mydatabase
22
+ username: myuser
23
+ password: mypassword
24
+ host: my.main.database.server.com
25
+ port: 3306
26
+
27
+ slave_for_mydatabase:
28
+ adapter: mysql
29
+ database: mydatabase
30
+ username: myuser
31
+ password: mypassword
32
+ socket: /var/lib/mysql/mysql.sock
33
+
34
+ Just use the regular YAML format to specify your slave database, it could equally well be on
35
+ another server as the local example given above.
36
+
37
+ Phusion Passenger
38
+
39
+ Note that if you are using Passenger, you need to make sure that the slave database is reconnected
40
+ if there is any chance that it was accessed before the spawner forks. This could be because
41
+ database was accessed during the generation of routes, or perhaps if you are using the has_many_polymorphs
42
+ gem.
43
+
44
+ The safest thing to do is to have something like this in your production.rb or environment.rb:
45
+
46
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
47
+ if forked
48
+ # We're in smart spawning mode.
49
+ ActiveRecord::Base.establish_slave_connections
50
+ else
51
+ # We're in conservative spawning mode. We don't need to do anything.
52
+ end
53
+ end
54
+
55
+ Documentation
56
+
57
+ http://rdoc.info/projects/sdsykes/read_from_slave
58
+
59
+ Tests
60
+
61
+ Clone the git repository, and you can run the read_from_slave tests or entire ActiveRecord test suite to prove that read_from_slave works correctly.
62
+
63
+ $ rake test
64
+ ...snip..
65
+ Finished in 0.046365 seconds.
66
+
67
+ 7 tests, 7 assertions, 0 failures, 0 errors
68
+
69
+ $ rake test_with_active_record
70
+ ...snip...
71
+ Finished in 51.904306 seconds.
72
+
73
+ 2057 tests, 6685 assertions, 0 failures, 0 errors
74
+
75
+ Todo
76
+
77
+ * Support a pool of multiple slaves
78
+
79
+ References
80
+
81
+ "Masochism":http://github.com/technoweenie/masochism/tree/master
82
+ not thread safe
83
+ won't work with apps that talk to multiple (master) databases
84
+
85
+ "Acts as readonlyable":http://rubyforge.org/projects/acts-as-with-ro/
86
+ old, not suitable for Rails 2.x
87
+
88
+ "master_slave_adapter":http://github.com/mauricio/master_slave_adapter/tree/master
89
+ similar to read_from_slave, but adapter based approach
90
+
91
+ "multi_db":http://github.com/schoefmax/multi_db/tree/master
92
+ another one, proxy connection approach
93
+ looks like it won't work with apps that talk to multiple (master) databases
94
+ more complex than read_from_slave
95
+
96
+ (c) 2009 Stephen Sykes
data/README.textile ADDED
@@ -0,0 +1,114 @@
1
+ h1. Read_from_slave
2
+
3
+ h4. Read_from_slave for Rails enables database reads from a slave database, while writes continue to go to the master
4
+
5
+ Read_from_slave will work with Rails 2.2 and above.
6
+
7
+ h2. Installation
8
+
9
+ <pre>
10
+ <code>
11
+ sudo gem install sdsykes-read_from_slave
12
+ </code>
13
+ </pre>
14
+
15
+ h2. Configuration
16
+
17
+ In config/environments/production.rb (for instance)
18
+
19
+ <pre>
20
+ <code>
21
+ config.gem "sdsykes-read_from_slave", :lib=>"read_from_slave"
22
+ </code>
23
+ </pre>
24
+
25
+ In config/database.yml
26
+
27
+ <pre>
28
+ <code>
29
+ production:
30
+ adapter: mysql
31
+ database: mydatabase
32
+ username: myuser
33
+ password: mypassword
34
+ host: my.main.database.server.com
35
+ port: 3306
36
+
37
+ slave_for_mydatabase:
38
+ adapter: mysql
39
+ database: mydatabase
40
+ username: myuser
41
+ password: mypassword
42
+ socket: /var/lib/mysql/mysql.sock
43
+ </code>
44
+ </pre>
45
+
46
+ Just use the regular YAML format to specify your slave database, it could equally well be on
47
+ another server as the local example given above.
48
+
49
+ h2. Phusion Passenger
50
+
51
+ Note that if you are using Passenger, you need to make sure that the slave database is reconnected
52
+ if there is any chance that it was accessed before the spawner forks. This could be because
53
+ database was accessed during the generation of routes, or perhaps if you are using the has_many_polymorphs
54
+ gem.
55
+
56
+ The safest thing to do is to have something like this in your production.rb or environment.rb:
57
+
58
+ <pre>
59
+ <code>
60
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
61
+ if forked
62
+ # We're in smart spawning mode.
63
+ ActiveRecord::Base.establish_slave_connections
64
+ else
65
+ # We're in conservative spawning mode. We don't need to do anything.
66
+ end
67
+ end
68
+ </code>
69
+ </pre>
70
+
71
+ h2. Documentation
72
+
73
+ "http://rdoc.info/projects/sdsykes/read_from_slave":http://rdoc.info/projects/sdsykes/read_from_slave
74
+
75
+ h2. Tests
76
+
77
+ Clone the git repository, and you can run the read_from_slave tests or entire ActiveRecord test suite to prove that read_from_slave works correctly.
78
+
79
+ <pre>
80
+ <code>
81
+ $ rake test
82
+ ...snip..
83
+ Finished in 0.046365 seconds.
84
+
85
+ 7 tests, 7 assertions, 0 failures, 0 errors
86
+
87
+ $ rake test_with_active_record
88
+ ...snip...
89
+ Finished in 51.904306 seconds.
90
+
91
+ 2057 tests, 6685 assertions, 0 failures, 0 errors
92
+ </code>
93
+ </pre>
94
+
95
+ h2. Todo
96
+
97
+ * Support a pool of multiple slaves
98
+
99
+ h2. References
100
+
101
+ * "Masochism":http://github.com/technoweenie/masochism/tree/master
102
+ ** not thread safe
103
+ ** won't work with apps that talk to multiple (master) databases
104
+ * "Acts as readonlyable":http://rubyforge.org/projects/acts-as-with-ro/
105
+ ** old, not suitable for Rails 2.x
106
+ * "master_slave_adapter":http://github.com/mauricio/master_slave_adapter/tree/master
107
+ ** similar to read_from_slave, but adapter based approach
108
+ * "multi_db":http://github.com/schoefmax/multi_db/tree/master
109
+ ** another one, proxy connection approach
110
+ ** looks like it won't work with apps that talk to multiple (master) databases
111
+ ** more complex than read_from_slave
112
+
113
+
114
+ (c) 2009 Stephen Sykes
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'test/helper'
4
+
5
+ task :default => [:test_read_from_slave]
6
+
7
+ task :test => :default
8
+
9
+ Rake::TestTask.new(:test_with_active_record) do |t|
10
+ t.libs << ReadFromSlave::ActiveRecordTest::AR_TEST_SUITE
11
+ t.libs << ReadFromSlave::ActiveRecordTest.connection
12
+ t.test_files = ReadFromSlave::ActiveRecordTest.test_files
13
+ t.ruby_opts = ["-r #{File.join(File.dirname(__FILE__), 'test', 'active_record_setup')}"]
14
+ t.verbose = true
15
+ end
16
+
17
+ Rake::TestTask.new(:test_read_from_slave) do |t|
18
+ t.libs << 'lib'
19
+ t.test_files = ReadFromSlave::Test.test_files
20
+ t.verbose = true
21
+ end
22
+
23
+ begin
24
+ require 'jeweler'
25
+ Jeweler::Tasks.new do |s|
26
+ s.name = "read_from_slave"
27
+ s.summary = "Read_from_slave - Utilise your slave databases with rails"
28
+ s.email = "sdsykes@gmail.com"
29
+ s.homepage = "http://github.com/sdsykes/read_from_slave"
30
+ s.description = "Read_from_slave for Rails enables database reads from a slave database, while writes continue to go to the master"
31
+ s.authors = ["Stephen Sykes"]
32
+ s.files = FileList["[A-Z]*", "{lib,test}/**/*"]
33
+ end
34
+ Jeweler::GemcutterTasks.new
35
+ rescue LoadError
36
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://
37
+ gems.github.com"
38
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 3
3
+ :patch: 0
4
+ :major: 0
@@ -0,0 +1,151 @@
1
+ # Read_from_slave for Rails enables database reads from a slave database, while writes continue
2
+ # to go to the master
3
+ # To use read_from_slave you must install the gem, configure the gem in your environment file,
4
+ # and setup your database.yml file with an entry for your slave database.
5
+ #
6
+ # === Configuration
7
+ # In config/environments/production.rb (for instance)
8
+ #
9
+ # config.gem "sdsykes-read_from_slave", :lib=>"read_from_slave"
10
+ #
11
+ # In config/database.yml
12
+ #
13
+ # production:
14
+ # adapter: mysql
15
+ # database: mydatabase
16
+ # username: myuser
17
+ # password: mypassword
18
+ # host: my.main.database.server.com
19
+ # port: 3306
20
+ #
21
+ # slave_for_mydatabase:
22
+ # adapter: mysql
23
+ # database: mydatabase
24
+ # username: myuser
25
+ # password: mypassword
26
+ # socket: /var/lib/mysql/mysql.sock
27
+ #
28
+ # Note that if you have multiple databases you can also configure multiple slaves - use the
29
+ # database name after slave_for_ in the configuration.
30
+ #
31
+ # === References
32
+ # * "Masochism":http://github.com/technoweenie/masochism/tree/master
33
+ # ** not thread safe
34
+ # ** won't work with apps that talk to multiple (master) databases
35
+ # * "Acts as readonlyable":http://rubyforge.org/projects/acts-as-with-ro/
36
+ # ** old, not suitable for Rails 2.x
37
+ # * "master_slave_adapter":http://github.com/mauricio/master_slave_adapter/tree/master
38
+ # ** similar to read_from_slave, but adapter based approach
39
+ # * "multi_db":http://github.com/schoefmax/multi_db/tree/master
40
+ # ** another one, proxy connection approach
41
+ # ** looks like it won't work with apps that talk to multiple (master) databases
42
+ # ** more complex than read_from_slave
43
+ #
44
+ module ReadFromSlave
45
+ class << self
46
+ def install!
47
+ base = ActiveRecord::Base
48
+ base.send(:include, InstanceMethods)
49
+ base.alias_method_chain :reload, :read_from_slave
50
+ base.extend(SingletonMethods)
51
+ base.class_eval do
52
+ class << self
53
+ alias_method_chain :find_by_sql, :read_from_slave
54
+ alias_method_chain :connection, :read_from_slave
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ module InstanceMethods
61
+ def reload_with_read_from_slave(options = nil)
62
+ Thread.current[:read_from_slave] = :reload
63
+ reload_without_read_from_slave(options)
64
+ end
65
+ end
66
+
67
+ module SingletonMethods
68
+
69
+ @@slave_models = {}
70
+
71
+ def find_by_sql_with_read_from_slave(sql)
72
+ Thread.current[:read_from_slave] = (Thread.current[:read_from_slave] != :reload)
73
+ find_by_sql_without_read_from_slave(sql)
74
+ ensure
75
+ Thread.current[:read_from_slave] = false
76
+ end
77
+
78
+ def connection_with_read_from_slave
79
+ normal_connection = connection_without_read_from_slave
80
+ if Thread.current[:read_from_slave] && normal_connection.open_transactions == 0
81
+ Thread.current[:read_from_slave_uses] = :slave # for testing use
82
+ slave_connection
83
+ else
84
+ Thread.current[:read_from_slave_uses] = :master
85
+ normal_connection
86
+ end
87
+ end
88
+
89
+ # Returns a connection to the slave database, or to the regular database if
90
+ # no slave is configured
91
+ #
92
+ def slave_connection
93
+ (@slave_model || slave_model).connection_without_read_from_slave
94
+ end
95
+
96
+
97
+ # Returns an AR model class that has a connection to the appropriate slave db
98
+ #
99
+ def slave_model
100
+ db_name = master_database_name
101
+ if slave_config_for(db_name)
102
+ unless @@slave_models[db_name]
103
+ slave_model_name = "ReadFromSlaveFor_#{db_name}"
104
+ @@slave_models[db_name] = eval %{
105
+ class #{slave_model_name} < ActiveRecord::Base
106
+ self.abstract_class = true
107
+ establish_slave_connection_for('#{db_name}')
108
+ end
109
+ #{slave_model_name}
110
+ }
111
+ end
112
+ @slave_model = @@slave_models[db_name]
113
+ else
114
+ @slave_model = self
115
+ end
116
+ end
117
+
118
+ # Returns the name of the database in use, as given in the database.yml file
119
+ #
120
+ def master_database_name
121
+ connection_without_read_from_slave.instance_variable_get(:@config)[:database]
122
+ end
123
+
124
+ # Returns the config for the associated slave database for this master,
125
+ # as given in the database.yml file
126
+ #
127
+ def slave_config_for(master)
128
+ configurations["slave_for_#{master}"]
129
+ end
130
+
131
+ # Establishes a connection to the slave database that is configured for
132
+ # the database name provided
133
+ #
134
+ def establish_slave_connection_for(master)
135
+ conn_spec = slave_config_for(master)
136
+ establish_connection(conn_spec) if conn_spec
137
+ end
138
+
139
+ # Re-establishes connections to all the slave databases that
140
+ # have been used so far. Use this in your
141
+ # PhusionPassenger.on_event(:starting_worker_process) block if required.
142
+ #
143
+ def establish_slave_connections
144
+ @@slave_models.each do |db_name, model|
145
+ model.establish_slave_connection_for(db_name)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ ReadFromSlave.install!
@@ -0,0 +1,3 @@
1
+ require File.join(File.dirname(__FILE__), 'helper')
2
+
3
+ ReadFromSlave::ActiveRecordTest.setup
data/test/helper.rb ADDED
@@ -0,0 +1,96 @@
1
+ require File.join(File.dirname(__FILE__), 'setup')
2
+ require 'active_support/test_case'
3
+
4
+ module ReadFromSlave
5
+ class Test
6
+ class << self
7
+
8
+ def setup
9
+ setup_constants
10
+ make_sqlite_config
11
+ make_sqlite_connection
12
+ load_models
13
+ load(SCHEMA_ROOT + "/schema.rb")
14
+ require 'test/unit'
15
+ end
16
+
17
+ def test_files
18
+ glob("#{File.dirname(__FILE__)}/**/*_test.rb")
19
+ end
20
+
21
+ def test_model_files
22
+ %w{course}
23
+ end
24
+
25
+ private
26
+
27
+ def setup_constants
28
+ set_constant('TEST_ROOT') {File.expand_path(File.dirname(__FILE__))}
29
+ set_constant('SCHEMA_ROOT') {TEST_ROOT + "/schema"}
30
+ end
31
+
32
+ def make_sqlite_config
33
+ ActiveRecord::Base.configurations = {
34
+ 'rfs' => {
35
+ :adapter => 'sqlite3',
36
+ :database => 'test_db',
37
+ :timeout => 5000
38
+ },
39
+ 'slave_for_test_db' => {
40
+ :adapter => 'sqlite3',
41
+ :database => 'test_db',
42
+ :timeout => 5000
43
+ }
44
+ }
45
+ end
46
+
47
+ def load_models
48
+ test_model_files.each {|f| require File.join(File.dirname(__FILE__), "models", f)}
49
+ end
50
+
51
+ def make_sqlite_connection
52
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['rfs'])
53
+ end
54
+
55
+ def set_constant(constant)
56
+ Object.const_set(constant, yield) unless Object.const_defined?(constant)
57
+ end
58
+
59
+ def glob(pattern)
60
+ Dir.glob(pattern)
61
+ end
62
+ end
63
+ end
64
+
65
+ class ActiveRecordTest < Test
66
+ class << self
67
+ def setup
68
+ setup_constants
69
+ end
70
+
71
+ def test_files
72
+ glob("#{AR_TEST_SUITE}/cases/**/*_test.rb").sort
73
+ end
74
+
75
+ def connection
76
+ File.join(AR_TEST_SUITE, 'connections', 'native_mysql')
77
+ end
78
+
79
+ private
80
+
81
+ def setup_constants
82
+ set_constant('MYSQL_DB_USER') {'rails'}
83
+ set_constant('AR_TEST_SUITE') {find_active_record_test_suite}
84
+ end
85
+
86
+ def find_active_record_test_suite
87
+ ts = ($:).grep(/activerecord/).last.split('/')
88
+ ts.pop
89
+ ts << 'test'
90
+ ts.join('/')
91
+ end
92
+ end
93
+
94
+ AR_TEST_SUITE = find_active_record_test_suite
95
+ end
96
+ end
@@ -0,0 +1,2 @@
1
+ class Course < ActiveRecord::Base
2
+ end
@@ -0,0 +1,45 @@
1
+ require File.join(File.dirname(__FILE__), "helper")
2
+
3
+ ReadFromSlave::Test.setup
4
+
5
+ class ReadFromSlaveTest < ActiveSupport::TestCase
6
+ test "slave connection should be different from normal connection" do
7
+ assert_not_equal Course.connection_without_read_from_slave, Course.slave_connection
8
+ end
9
+
10
+ test "should be able to write and read from database" do
11
+ Course.create(:name=>"Saw playing")
12
+ x = Course.find(:first)
13
+ assert_equal "Saw playing", x.name
14
+ end
15
+
16
+ test "should write to master" do
17
+ Course.create(:name=>"Saw playing")
18
+ assert_equal :master, Thread.current[:read_from_slave_uses]
19
+ end
20
+
21
+ test "should read from slave" do
22
+ Course.create(:name=>"Saw playing")
23
+ x = Course.find(:first)
24
+ assert_equal :slave, Thread.current[:read_from_slave_uses]
25
+ end
26
+
27
+ test "should reload from master" do
28
+ Course.create(:name=>"Saw playing")
29
+ x = Course.find(:first)
30
+ x.reload
31
+ assert_equal :master, Thread.current[:read_from_slave_uses]
32
+ end
33
+
34
+ test "should get new slave connection when calling establish_slave_connections" do
35
+ conn = Course.slave_connection
36
+ ActiveRecord::Base.establish_slave_connections
37
+ assert_not_equal conn, Course.slave_connection
38
+ end
39
+
40
+ test "should not get new master connection when calling establish_slave_connections" do
41
+ conn = Course.connection_without_read_from_slave
42
+ ActiveRecord::Base.establish_slave_connections
43
+ assert_equal conn, Course.connection_without_read_from_slave
44
+ end
45
+ end
@@ -0,0 +1,5 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table :courses, :force => true do |t|
3
+ t.column :name, :string, :null => false
4
+ end
5
+ end
data/test/setup.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'activerecord'
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'read_from_slave')
data/test/test.rb ADDED
@@ -0,0 +1 @@
1
+ # todo
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: read_from_slave
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Stephen Sykes
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-10 00:00:00 +03:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Read_from_slave for Rails enables database reads from a slave database, while writes continue to go to the master
17
+ email: sdsykes@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ - README.textile
25
+ files:
26
+ - README
27
+ - README.textile
28
+ - Rakefile
29
+ - VERSION.yml
30
+ - lib/read_from_slave.rb
31
+ - test/active_record_setup.rb
32
+ - test/helper.rb
33
+ - test/models/course.rb
34
+ - test/read_from_slave_test.rb
35
+ - test/schema/schema.rb
36
+ - test/setup.rb
37
+ - test/test.rb
38
+ has_rdoc: true
39
+ homepage: http://github.com/sdsykes/read_from_slave
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --charset=UTF-8
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: "0"
52
+ version:
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ version:
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.5
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Read_from_slave - Utilise your slave databases with rails
66
+ test_files:
67
+ - test/active_record_setup.rb
68
+ - test/helper.rb
69
+ - test/models/course.rb
70
+ - test/read_from_slave_test.rb
71
+ - test/schema/schema.rb
72
+ - test/setup.rb
73
+ - test/test.rb