Skip to content

Commit

Permalink
Automatic Fork Safety
Browse files Browse the repository at this point in the history
Forking servers are a popular deployment method in Ruby.
One big footgun is that uppon fork, all file descriptors
are inherited, so if you create some FDs before fork and hold on them
you may end up with the same connection being used across many process
creating havoc.

As such it's a good idea for libraries that do keep persistent
connections do check the PID before re-using them.

The alternative is for libraries to provide some kind of callback that
the application has to call before fork, but that's easilly missed
by users and the PID check is very cheap.
  • Loading branch information
byroot committed Feb 2, 2023
1 parent 433f334 commit 136ca97
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 0 deletions.
6 changes: 6 additions & 0 deletions lib/excon/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ def logger=(logger)
# @option params [Class] :instrumentor Responds to #instrument as in ActiveSupport::Notifications
# @option params [String] :instrumentor_name Name prefix for #instrument events. Defaults to 'excon'
def initialize(params = {})
@pid = Process.pid
@data = Excon.defaults.dup
# merge does not deep-dup, so make sure headers is not the original
@data[:headers] = @data[:headers].dup
Expand Down Expand Up @@ -480,6 +481,11 @@ def sockets
@_excon_sockets ||= {}
@_excon_sockets.compare_by_identity

if @pid != Process.pid
@_excon_sockets.clear # GC will take care of closing sockets
@pid = Process.pid
end

if @data[:thread_safe_sockets]
# In a multi-threaded world, if the same connection is used by multiple
# threads at the same time to connect to the same destination, they may
Expand Down
16 changes: 16 additions & 0 deletions tests/connection_tests.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@
response = connection.request(path: '/bar', method: 'get')
response.body == 'bar'
end

if ::Process.respond_to?(:fork)
connection_id = connection.send(:socket).object_id
test("fork safety") do
read, write = IO.pipe
pid = fork do
connection_id = connection.send(:socket).object_id
write.write(Marshal.dump(connection_id))
write.close
exit!(0)
end
Process.waitpid(pid)
child_connection_id = Marshal.load(read)
child_connection_id != connection_id
end
end
end
end

Expand Down

0 comments on commit 136ca97

Please sign in to comment.