rails_is_forked 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "rspec", "~> 2.3.0"
10
+ gem "bundler", "~> 1.0.0"
11
+ gem "jeweler", "~> 1.5.2"
12
+ gem "rcov", ">= 0"
13
+ gem "activerecord", ">= 3.0.3"
14
+ gem "pg"
15
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Kurt Stephens
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,103 @@
1
+ = rails_is_forked
2
+
3
+ Rails does not clear ConnectionPool and reestablish connections in Process.fork children:
4
+
5
+ puts "Parent: #{$$}: #{ActiveRecord::Base.connection.object_id}"
6
+ fork { puts "Child: #{$$}: #{ActiveRecord::Base.connection.object_id}" }
7
+ puts "Parent after fork: #{$$}: #{ActiveRecord::Base.connection.object_id}"
8
+ Thread.new { puts "Thread: #{ActiveRecord::Base.connection.object_id}" }
9
+
10
+ Sample Output:
11
+
12
+ Parent: 27282: 153210860
13
+ Child: 29777: 153210860
14
+ Parent after fork: 27282: 153210860
15
+ Thread: 153076390
16
+
17
+ Note that Threads get their own connections, but subprocesses do not.
18
+
19
+ A ConnectionPool exists for each ActiveRecord::Base subclass that has been sent #establish_connection.
20
+
21
+ puts "Parent #{$$}: #{ActiveRecord::Base.connection_pool.object_id}"
22
+ fork { puts "Child: #{$$}: #{ActiveRecord::Base.connection_pool.object_id}" }
23
+ Thread.new { puts "Thread: #{ActiveRecord::Base.connection_pool.object_id}" }
24
+
25
+ Sample Output:
26
+
27
+ Parent 30003: 102371950
28
+ Child: 30053: 102371950
29
+ Thread: 102371950
30
+
31
+ Note that ConnectionPools are only unique per process.
32
+
33
+ Using rails_is_forked:
34
+
35
+ require 'rails_is_forked/rails'
36
+ puts "Parent #{$$}: #{ActiveRecord::Base.connection.object_id}"
37
+ fork { puts "Child: #{$$}: #{ActiveRecord::Base.connection.object_id}" }
38
+ puts "Parent after fork: #{$$}: #{ActiveRecord::Base.connection.object_id}"
39
+ Thread.new { puts "Thread: #{ActiveRecord::Base.connection.object_id}" }
40
+
41
+ Sample Output:
42
+
43
+ Parent 31583: 90260550
44
+ Child: 31627: 152197900
45
+ Parent after fork: 31583: 90260550
46
+ Thread: 151738880
47
+
48
+ == Strategy
49
+
50
+ The naive solution is #disconnect! all database connections at the beginning of each
51
+ forked child process. However the child will likely send "termination commands" to a database handle that is still active in the parent.
52
+
53
+ The database handle's FD is shared between the parent process and its children; its resources are not likely to be reclaimed in the database server until all processes close() their FDs.
54
+
55
+ Another "portable" solution is to #disconnect! all the connections in the parent process *before* forking the children. This doesn't require digging deep into each and every database adapter. However this is likely to cause problems if the parent is in an active transaction when the child is forked.
56
+
57
+ At the risk of leaking memory and FDs in long running child processes, a safe solution is to "forget", not #disconnect!, all connections at the beginning of the forked child processes. The GC should eventually reap the ActiverRecord connection and the underlying handle, but may leave the FD still open, until the child process dies, because ActiveRecord connections and database handles do not have finalizers.
58
+
59
+ If a database handle has a finalizer and it sends "termination commands", forgetting connections in the child could still affect parent processes.
60
+
61
+ The correct solution is: close the FDs in the database handles and forget the connections in the child.
62
+
63
+ However, most database connection adapters and APIs do not provide mechanisms for:
64
+
65
+ 1) closing the low-level file descriptor (FD),
66
+ 2) resetting the database client library handle (ex: gem pg) without sending a "termination command" along the FD, as this is usually done in the database client code (see libpq sources for "X"),
67
+ 3) automatically reconnecting the database handle after the FD has been closed.
68
+
69
+ Database client handles should:
70
+
71
+ 1) have a notion of the "owning process id" of the handle.
72
+ 2) send "termination commands" only if the current process is the owning process.
73
+ 3) should have a "close" method, that simply closes its FD, but does not send "termination commands".
74
+ 4) should call "close" or "disconnect" on finalization, depending on if the handle is owned by the current process or not.
75
+
76
+ ActiveRecord connection objects should disconnect database handles on finalization.
77
+
78
+ For long-running child processes, it's probably best for the parent process to forcefully #disconnect! (and remove from their ConnectionPools) all its connections, before forking children and outside a transaction, to insure there are no connections leaked in the children. This can be done by calling ActiveRecord::Base.connection_handler.clear_all_connections! before Process.fork.
79
+
80
+ == Functionality
81
+
82
+ * RailsIsForked::Rails
83
+ ** Calls ActiveRecord::ConnectionAdapters::ConnectionPool#disconnect! for all instances in forked children. See ActiveRecord::Base.connection_handler.connection_pools.
84
+
85
+ == See Also
86
+
87
+ * http://github.com/kstephens/ruby_is_forked
88
+
89
+ == Contributing to rails_is_forked
90
+
91
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
92
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
93
+ * Fork the project
94
+ * Start a feature/bugfix branch
95
+ * Commit and push until you are happy with your contribution
96
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
97
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
98
+
99
+ == Copyright
100
+
101
+ Copyright (c) 2011 Kurt Stephens. See LICENSE.txt for
102
+ further details.
103
+
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "rails_is_forked"
16
+ gem.homepage = "http://github.com/kstephens/rails_is_forked"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Deal with resources (DB connections, etc) in forked Rails processes.}
19
+ gem.description = %Q{See http://github.com/kstephens/rails_is_forked}
20
+ gem.email = "ks.github@kurtstephens.com"
21
+ gem.authors = ["Kurt Stephens"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
25
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
26
+ end
27
+ Jeweler::RubygemsDotOrgTasks.new
28
+
29
+ require 'rspec/core'
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |spec|
32
+ spec.rspec_opts = [ '-f', 'd' ]
33
+ spec.pattern = FileList['spec/**/*_spec.rb']
34
+ end
35
+
36
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
37
+ spec.pattern = 'spec/**/*_spec.rb'
38
+ spec.rcov = true
39
+ end
40
+
41
+ task :default => :spec
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "rails_is_forked #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
@@ -0,0 +1,5 @@
1
+ require 'ruby_is_forked/fork_callback'
2
+ module RailsIsForked
3
+ ForkCallback = ::RubyIsForked::ForkCallback # alias
4
+ end
5
+ require 'rails_is_forked/rails'
@@ -0,0 +1,57 @@
1
+ require 'ruby_is_forked/fork_callback'
2
+
3
+ require 'active_record/connection_adapters/abstract/connection_pool'
4
+
5
+ module RailsIsForked
6
+ module ConnectionPoolDisconnectOnFork
7
+ @@once = false
8
+
9
+ def self.included target
10
+ super
11
+
12
+ return if @@once
13
+ @@once = true
14
+
15
+ if true
16
+ # Register callback to call forget connections in child processes.
17
+ proc = RubyIsForked::ForkCallback.add_callback_in_child! do | child_pid |
18
+ ActiveRecord::Base.connection_handler.connection_pools.each_value do | pool |
19
+ # Naive solution:
20
+ # pool.disconnect!
21
+ #
22
+ # This causes the following error in the parent:
23
+ # PGError: server closed the connection unexpectedly
24
+ # This probably means the server terminated abnormally
25
+ # before or while processing the request.
26
+
27
+ # Simplest solution:
28
+ pool.forget_all_connections!
29
+ end
30
+ end
31
+ else
32
+ # Naive solution:
33
+ # Register callback to disconnect connections before forking child processes.
34
+ #
35
+ # This cause the parent to fail if fork occurs with a transaction:
36
+ # Failure/Error: ActiveRecord::Base.transaction do
37
+ # not connected
38
+ #
39
+ proc = RubyIsForked::ForkCallback.add_callback_before_child! do | child_pid |
40
+ ActiveRecord::Base.connection_handler.clear_all_connections!
41
+ end
42
+ end
43
+ # $stderr.puts "Registered callback #{proc}"
44
+ end
45
+
46
+ # Forgets all reserved connections and live connections,
47
+ # without calling #disconnect! on each of them.
48
+ # See also #disconnect!
49
+ def forget_all_connections!
50
+ @reserved_connections = {}
51
+ @connections = []
52
+ end
53
+
54
+ ::ActiveRecord::ConnectionAdapters::ConnectionPool.send(:include, self)
55
+ end
56
+ end
57
+
@@ -0,0 +1,72 @@
1
+ unless ENV['RAILS_DATABASE_YML']
2
+ $stderr.puts "#{__FILE__}: enable test with: export RAILS_DATABASE_YML=.../database.yml"
3
+ else
4
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
5
+ require 'rubygems'
6
+ gem 'activerecord'
7
+ require 'active_record'
8
+ require 'rails_is_forked/rails'
9
+
10
+ describe "RailsIsForked::Rails" do
11
+
12
+ before(:all) do
13
+ spec = ENV["RAILS_DATABASE_YML"]
14
+ spec = YAML.load_file(spec)
15
+ spec = spec[ENV['RAILS_ENV'] || 'development']
16
+ case spec['adapter']
17
+ when 'postgresql'
18
+ gem 'pg'; require 'pg'
19
+ end
20
+ ActiveRecord::Base.establish_connection(spec)
21
+ end
22
+
23
+ it "should make forked children acquire new db connections." do
24
+ parent_connection = ActiveRecord::Base.connection
25
+ parent_connection_obj = parent_connection.instance_variable_get('@connection')
26
+ read_pipe, write_pipe = IO.pipe
27
+ Process.fork do
28
+ read_pipe.close
29
+ child_connection = ActiveRecord::Base.connection
30
+ child_connection_obj = child_connection.instance_variable_get('@connection')
31
+ write_pipe.write(Marshal.dump([child_connection.object_id, child_connection_obj.object_id]))
32
+ write_pipe.close
33
+ end
34
+ write_pipe.close
35
+ child_connection_object_id, child_connection_obj_object_id = Marshal.load(read_pipe.read)
36
+ read_pipe.close
37
+ child_connection_object_id.should_not == parent_connection.object_id
38
+ child_connection_obj_object_id.should_not == parent_connection_obj.object_id
39
+
40
+ if true # not disconnect! before fork
41
+ ActiveRecord::Base.connection.object_id.should == parent_connection.object_id
42
+ ActiveRecord::Base.connection.instance_variable_get('@connection').object_id.should == parent_connection_obj.object_id
43
+ end
44
+ end
45
+
46
+ it "should handle forks within a transaction." do
47
+ ActiveRecord::Base.transaction do
48
+ parent_connection = ActiveRecord::Base.connection
49
+ parent_connection_obj = parent_connection.instance_variable_get('@connection')
50
+ read_pipe, write_pipe = IO.pipe
51
+ Process.fork do
52
+ read_pipe.close
53
+ child_connection = ActiveRecord::Base.connection
54
+ child_connection_obj = child_connection.instance_variable_get('@connection')
55
+ write_pipe.write(Marshal.dump([child_connection.object_id, child_connection_obj.object_id]))
56
+ write_pipe.close
57
+ end
58
+ write_pipe.close
59
+ child_connection_object_id, child_connection_obj_object_id = Marshal.load(read_pipe.read)
60
+ read_pipe.close
61
+ child_connection_object_id.should_not == parent_connection.object_id
62
+ child_connection_obj_object_id.should_not == parent_connection_obj.object_id
63
+
64
+ ActiveRecord::Base.connection.object_id.should == parent_connection.object_id
65
+ ActiveRecord::Base.connection.instance_variable_get('@connection').object_id.should == parent_connection_obj.object_id
66
+ end
67
+ end
68
+
69
+ end # describe
70
+
71
+ end # unless
72
+
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "RailsIsForked" do
4
+ it "Has no tests" do
5
+ 1.should == 1
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ # require 'rails_is_forked'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails_is_forked
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Kurt Stephens
9
+ - Robert Fletcher
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2012-05-01 00:00:00.000000000 Z
14
+ dependencies: []
15
+ description: Handles issues with forked Rails processes - DB connections, etc.
16
+ email: lobatifricha@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/rails_is_forked/rails.rb
22
+ - lib/rails_is_forked.rb
23
+ - spec/rails_is_forked_spec.rb
24
+ - spec/rails_is_forked/rails_spec.rb
25
+ - spec/spec_helper.rb
26
+ - README.rdoc
27
+ - Gemfile
28
+ - LICENSE.txt
29
+ - Rakefile
30
+ homepage: https://github.com/mockdeep/rails_is_forked
31
+ licenses: []
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ requirements: []
49
+ rubyforge_project:
50
+ rubygems_version: 1.8.25
51
+ signing_key:
52
+ specification_version: 3
53
+ summary: Handles issues with forked Rails processes - DB connections, etc.
54
+ test_files: []