Merge branch 'master' into 10231-keep-cache-runtime-constraints
[arvados.git] / services / api / app / middlewares / rack_socket.rb
1 require 'rack'
2 require 'faye/websocket'
3 require 'eventmachine'
4
5 # A Rack middleware to handle inbound websocket connection requests and hand
6 # them over to the faye websocket library.
7 class RackSocket
8
9   DEFAULT_ENDPOINT  = '/websocket'
10
11   # Stop EventMachine on signal, this should give it a chance to to unwind any
12   # open connections.
13   def die_gracefully_on_signal
14     Signal.trap("INT") { EM.stop }
15     Signal.trap("TERM") { EM.stop }
16   end
17
18   # Create a new RackSocket handler
19   # +app+  The next layer of the Rack stack.
20   #
21   # Accepts options:
22   # +:handler+ (Required) A class to handle new connections.  #initialize will
23   # call handler.new to create the actual handler instance object.  When a new
24   # websocket connection is established, #on_connect on the handler instance
25   # object will be called with the new connection.
26   #
27   # +:mount+ The HTTP request path that will be recognized for websocket
28   # connect requests, defaults to '/websocket'.
29   #
30   # +:websocket_only+  If true, the server will only handle websocket requests,
31   # and all other requests will result in an error.  If false, unhandled
32   # non-websocket requests will be passed along on to 'app' in the usual Rack
33   # way.
34   def initialize(app = nil, options = nil)
35     @app = app if app.respond_to?(:call)
36     @options = [app, options].grep(Hash).first || {}
37     @endpoint = @options[:mount] || DEFAULT_ENDPOINT
38     @websocket_only = @options[:websocket_only] || false
39
40     # from https://gist.github.com/eatenbyagrue/1338545#file-eventmachine-rb
41     if defined?(PhusionPassenger)
42       PhusionPassenger.on_event(:starting_worker_process) do |forked|
43         # for passenger, we need to avoid orphaned threads
44         if forked && EM.reactor_running?
45           EM.stop
46         end
47         Thread.new {
48           EM.run
49         }
50         die_gracefully_on_signal
51       end
52     else
53       # faciliates debugging
54       Thread.abort_on_exception = true
55       # just spawn a thread and start it up
56       Thread.new {
57         EM.run
58       }
59     end
60
61     # Create actual handler instance object from handler class.
62     @handler = @options[:handler].new
63   end
64
65   # Handle websocket connection request, or pass on to the next middleware
66   # supplied in +app+ initialize (unless +:websocket_only+ option is true, in
67   # which case return an error response.)
68   # +env+ the Rack environment with information about the request.
69   def call env
70     request = Rack::Request.new(env)
71     if request.path_info == @endpoint and Faye::WebSocket.websocket?(env)
72       if @handler.overloaded?
73         return [503, {"Content-Type" => "text/plain"}, ["Too many connections, try again later."]]
74       end
75
76       ws = Faye::WebSocket.new(env, nil, :ping => 30)
77
78       # Notify handler about new connection
79       @handler.on_connect ws
80
81       # Return async Rack response
82       ws.rack_response
83     elsif not @websocket_only
84       @app.call env
85     else
86       [406, {"Content-Type" => "text/plain"}, ["Only websocket connections are permitted on this port."]]
87     end
88   end
89
90 end