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 +15 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +103 -0
- data/Rakefile +51 -0
- data/lib/rails_is_forked.rb +5 -0
- data/lib/rails_is_forked/rails.rb +57 -0
- data/spec/rails_is_forked/rails_spec.rb +72 -0
- data/spec/rails_is_forked_spec.rb +7 -0
- data/spec/spec_helper.rb +12 -0
- metadata +54 -0
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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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,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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|