'Ruby spawned process listening on parent server port

I am running a puma server ruby application on fedora 32. In my server I have certain calls which will spawn new long running processes for various reasons. I came across an issue where my spawned processes were running and listening on the same port as my server. This lead to issues with restarting my server on deploys as the server could not start because of processes listening on the desired port. How could this be possible? From my understanding when I spawn a process it should have completely different memory to the parent process, and share no file descriptors. My spawn command is simply

my_pid = Process.spawn(my_cmd, %i[out err] => log_file)

Ruby version 2.7.0

Edit: something I had overlooked in my deploy process and my original problem description, server restart is not an actual tear down and restart of a new process, but via signalling USR2 to the puma server (as described here)



Solution 1:[1]

I believe I have found what is causing this. Seems to be an issue with puma restart process, which I was using. By restarting the server with a USR2 signal, it changes the flags on the open fd for the socket.

[me@home puma_testing]$ cat /proc/511620/fdinfo/5
pos:    0
flags:  02000002
mnt_id: 10
[me@home puma_testing]$ kill -s USR2 511620
[me@home puma_testing]$ cat /proc/511620/fdinfo/5
pos:    0
flags:  02
mnt_id: 10

This was tested on fedora 32 using a very simple puma and sinatra setup like so: puma.rb

# frozen_string_literal: true

rackup File.join(File.dirname(File.realpath(__FILE__)), './server.ru')

# https://www.rubydoc.info/gems/puma/Puma/DSL#prune_bundler-instance_method
# This allows us to install new gems with just a phased-restart. Otherwise you
# need to take the master process down each time.
prune_bundler

port 11111

environment 'production'

pidfile File.join(File.dirname(File.realpath(__FILE__)), '../', 'server.pid')

tag 'test'

And server.ru like so

require 'sinatra'

class App < Sinatra::Base
  get "/" do
    "Hello World!"
  end

  get "/spawn" do
    spawn "sleep 500"
  end
end

run App

Ran using bundler bundle exec puma -C puma.rb. Note you can use /spawn get request to test spawning a new process before and after restart to see if it is listening on the socket with lsof -itcp:11111

Solution 2:[2]

A quick workaround / solution will be to call fork, close Puma's socket within the forked process and then call exec, which replaces the running process... however, this workaround is limited to Unix systems. On windows you can probably achieve something similar using a more complicated approach.

Sadly, I am not sure how to close Puma's listening socket. Perhaps this will helps, but more likely than not there's some other trick to this.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Nick Hyland
Solution 2 Myst