Search code examples
nginxrabbitmqsinatrapassengerserver-sent-events

Passenger, Sinatra, Nginx, RabbitMQ and SSE (how Passenger starts processes and where Ruby threads fit in the picture)


I have a problem with my current setup which is not working as expected and prevents me from going further to have a server-sent events (SSE) enabled web site. My main question can be found below in bold but boils down to "How can I launch an extra thread from a Sinatra web app in a Passenger setup ?".

I use Passenger 5.0.21 and Sinatra 1.4.6. The app is written as a classical Sinatra application, not modular, but that can be changed if necessary.

I have put the directive passenger_min_instances 3 in the Nginx configuration to get a minimum of 3 web app instances launched. I have two puts in the config.ru file of my Sinatra app so when the thread is launched I get feedback inside /var/log/nginx/passenger.log and also when the thread receives messages through its RabbitMQ queue :

...
Thread.new {
  puts " [* #{Thread.current.inspect}] Waiting for logs. To exit press CTRL+C"

  begin
    q.subscribe(:block => true) do |delivery_info, properties, body|
      puts " [x #{Thread.current.inspect}] #{body}"
    end
  rescue Interrupt => _
    ch.close
    conn.close
  end
}

run Sinatra::Application

I expected this code to be run n times, n being the number of processes launched by Passenger. It looks like it is not the case.

Furthermore, my app.rb contains a lot of stuff which can be reduced to :

puts "(CLASS)... Inside thread #{Thread.current.inspect}"

configure do
  puts "(CONFIGURE)... Inside thread #{Thread.current.inspect}"
end

get '/debug' do
  puts "(DEBUG)... Inside thread #{Thread.current.inspect}"
end

When I restart Nginx and make a first HTTP GET access to the URL /debug, the processes are instantiated and one of the processes serves the request. What do I get in /var/log/nginx/passenger.log ?

(CLASS)... Inside thread #<Thread:0x007fb29f4ca258 run>
(CONFIGURE)... Inside thread #<Thread:0x007fb29f4ca258 run>
[* #<Thread:[email protected]:68 run>] Waiting for logs. To exit press CTRL+C
(DEBUG)... Inside thread #<Thread:0x007fb29f4ca8e8@/usr/lib/ruby/vendor_ruby/phusion_passenger
192.168.0.11 - test [30/Dec/2015:10:09:08 +0100] "GET /debug HTTP/1.1" 200 2184 0.0138

Both messages starting with CLASS and CONFIGURE are printed inside the same thread. I expected this to occur at process instantiation time like it did but it occured only one time making me think that Passenger fires only one process. However I can see 3 processes with passenger-status --verbose. Another thread is created (in config.ru) to receive RabbitMQ messages.

As you can see the first process has processed 1 request (shortened for clarity) :

$ passenger-status --verbose
----------- General information -----------
Max pool size : 6
App groups    : 1
Processes     : 3
Requests in top-level queue : 0

----------- Application groups -----------
/home/hydro/web2/public:
  App root: /home/hydro/web2
  Requests in queue: 0
  * PID: 1116    Sessions: 0       Processed: 1       Uptime: 2m 19s
    CPU: 0%      Memory  : 18M     Last used: 2m 19s ago
  * PID: 1123    Sessions: 0       Processed: 0       Uptime: 2m 19s
    CPU: 0%      Memory  : 3M      Last used: 2m 19s ago
  * PID: 1130    Sessions: 0       Processed: 0       Uptime: 2m 19s
    CPU: 0%      Memory  : 2M      Last used: 2m 19s ago

The ruby test program which publishes a RabbitMQ message for the subscribers to receive sometimes works and sometimes not. Maybe Passenger shuts down the running process even it has not seen a request in a given time. Nothing appears in the log. No feedback from the subscriber thread, no message from Passenger itself.

If I refresh the page I get the DEBUG message and the GET /debug trace. passenger-status --verbose shows that the first process has served two requests now.

I have seen during my different tests that I have to fire a lot of requests to make Passenger serve requests with the other 2 processes or even start new processes to a maximum of 6. Let's do it from another machine in the same LAN with
root@backup:~# ab -A test:test -kc 1000 -n 10000 https://192.168.0.10:445/debug. Passenger has started the maximum of 6 processes to handle the requests but I can't see anything in the passenger.log file except for DEBUG messages and GET /debug traces as if no other processes had been started.

$ passenger-status --verbose
----------- General information -----------
Max pool size : 6
App groups    : 1
Processes     : 6
Requests in top-level queue : 0

----------- Application groups -----------
/home/hydro/web2/public:
  App root: /home/hydro/web2
  Requests in queue: 0
  * PID: 1116    Sessions: 0       Processed: 664     Uptime: 16m 29s
    CPU: 0%      Memory  : 28M     Last used: 32s ago
  * PID: 1123    Sessions: 0       Processed: 625     Uptime: 16m 29s
    CPU: 0%      Memory  : 27M     Last used: 32s ago
  * PID: 1130    Sessions: 0       Processed: 614     Uptime: 16m 29s
    CPU: 0%      Memory  : 27M     Last used: 32s ago
  * PID: 2105    Sessions: 0       Processed: 106     Uptime: 33s
    CPU: 0%      Memory  : 23M     Last used: 32s ago
  * PID: 2112    Sessions: 0       Processed: 103     Uptime: 33s
    CPU: 0%      Memory  : 22M     Last used: 32s ago
  * PID: 2119    Sessions: 0       Processed: 92      Uptime: 33s
    CPU: 0%      Memory  : 21M     Last used: 32s ago

So the main question is : how can I launch a (RabbitMQ subscriber) thread from a Sinatra web application process everytime the process is started ?

I want to be able to send data to my web app processes so they can send it back to the web client using SSE. I would like to have two threads per web app process : the main thread used by Sinatra and my extra thread to do some RabbitMQ stuff. There is also an Oracle database and an Erlang back-end but I don't think they are relevant here.

I am also wondering how Passenger handles process instantiation in the case of a Sinatra web app. Multiple Ruby environment ? How could it be that it looks like the class is instantiated only once if multiple processes are started ? Is the file config.ru (and even app.rb) processed only once even when launching multiple processes ? I have read a lot of things on the web but could not figure this out.

More generally, what is the proper way of doing SSE with Ruby, Nginx, Passenger and Sinatra.

Details concerning Nginx have been put below for clarity.

Nginx is configured as a reverse-proxy standing in front of Passenger and the web application is configure under server and location / with SSL and HTTP basic authentication and the following directives :

location / {
  proxy_buffering off;
  proxy_cache off;

  proxy_pass_request_headers on;
  passenger_set_header Host $http_host;
  passenger_set_header X-Real-IP  $remote_addr;
  passenger_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  passenger_set_header X-Forwarded-Proto $scheme;
  passenger_set_header X-Remote-User $remote_user;
  passenger_set_header Host $http_host;
  passenger_min_instances 3;
  proxy_redirect off;
  passenger_enabled on;
  passenger_ruby /home/hydro/.rbenv/versions/2.3.0/bin/ruby;
  passenger_load_shell_envvars on;
  passenger_nodejs /usr/bin/nodejs;
  passenger_friendly_error_pages on;
}

Solution

  • I think your current architecture is wrong. Your Sinatra app shouldn't be mixed with extra threads, or kept alive simply so that it can send push to your clients - you should have a separate push server dedicated to pushing out messages, and let your HTTP API do what it does best - sleep until it receives a request.

    You mention you are using nginx, so I'd really recommend compiling in this module:

    https://github.com/wandenberg/nginx-push-stream-module

    Now you may be able to get rid of your RabbitMQ queue - any process that needs to push a message to one of your push subscribers simply needs to send an HTTP request to this module's RESTful API:

    Example curl request:

    curl -s -v -X POST 'http://localhost/pub?id=my_channel_1' -d 'Hello World!'
    

    Of course by default, this module will only listen to request from localhost for security reasons.