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