Responding to HTTP Requests in Eventmachine - ruby

I have a very simple server for use in integration tests, built using eventmachine:
EM.run do
EM::start_server(server, port, HttpRecipient)
end
I can receive HTTP requests and parse them like so:
class HttpRecipient < EM::Connection
def initialize
##stored = ''
end
# Data is received in chunks, so here we wait until we've got a full web request before
# calling spool.
def receive_data(data)
##stored << data
begin
spool(##stored)
EM.stop
rescue WEBrick::HTTPStatus::BadRequest
#Not received a complete request yet
end
end
def spool(data)
#Parse the request
req = WEBrick::HTTPRequest.new(WEBrick::Config::HTTP)
req.parse(StringIO.new(##stored))
#Send a response, e.g. HTTP OK
end
end
The question is, how do I send a response? Eventmachine provides a send_data for sending responses, but that doesn't understand http. Similarly there is the em-http-request
module for sending requests, but it's not obvious that this is capable of generating responses.
I can generate HTTP messages manually and then send them using send_data, but I wonder if there is a clean way to use an existing http library, or the functionality built in to eventmachine?

If you want something easy then use Thin or Rainbows. It uses Eventmachine inside and provides Rack interface support.
# config.ru
http_server = proc do |env|
response = "Hello World!"
[200, {"Connection" => "close", "Content-Length" => response.bytesize.to_s}, [response]]
end
run http_server
And then
>> thin start -R config.ru
UPD.
If you need server to run in parallel you could run it in a Thread
require 'thin'
class ThreadedServer
def initialize(*args)
#server = Thin::Server.new(*args)
end
def start
#thread = Thread.start do
#server.start
end
end
def stop
#server.stop
if #thread
#thread.join
#thread = nil
end
end
end
http_server = proc do |env|
response = "Hello World!"
[200, {"Connection" => "close", "Content-Length" => response.bytesize.to_s}, [response]]
end
server = ThreadedServer.new http_server
server.start
# Some job with server
server.stop
# Server is down

Related

Faye Websocket Ruby not working as expected

I am trying to use haproxy to load balance my websocket rack application.
I publish message in channel rates using redis-cli and this succeeds puts "sent" if ws.send(msg)
The client does receive the 'Welcome! from server' message so I know the initial handshake is done.
But, the client never receives the published message in channel 'rates'.
web_socket.rb
require 'faye/websocket'
module WebSocket
class App
KEEPALIVE_TIME = 15 # in seconds
def initialize(app)
#app = app
#mutex = Mutex.new
#clients = []
# #redis = Redis.new(host: 'rds', port: 6739)
Thread.new do
#redis_sub = Redis.new(host: 'rds', port: 6379)
#redis_sub.subscribe('rates') do |on|
on.message do |channel, msg|
p [msg,#clients.length]
#mutex.synchronize do
#clients.each do |ws|
# ws.ping 'Mic check, one, two' do
p ws
puts "sent" if ws.send(msg)
# end
end
end
end
end
end
end
def call(env)
if Faye::WebSocket.websocket?(env)
# WebSockets logic goes here
ws = Faye::WebSocket.new(env, nil) # {ping: KEEPALIVE_TIME }
ws.on :open do |event|
p [:open, ENV['APPID'], ws.object_id]
ws.ping 'Mic check, one, two' do
# fires when pong is received
puts "Welcome sent" if ws.send('Welcome! from server')
#mutex.synchronize do
#clients << ws
end
p [#clients.length, ' Client Connected']
end
end
ws.on :close do |event|
p [:close, ENV['APPID'], ws.object_id, event.code, event.reason]
#mutex.synchronize do
#clients.delete(ws)
end
p #clients.length
ws = nil
end
ws.on :message do |event|
p [:message, event.data]
# #clients.each {|client| client.send(event.data) }
end
# Return async Rack response
ws.rack_response
else
#app.call(env)
end
end
end
end
My haproxy.cfg
frontend http
bind *:8080
mode http
timeout client 1000s
use_backend all
backend all
mode http
timeout server 1000s
timeout tunnel 1000s
timeout connect 1000s
server s1 app1:8080
server s2 app2:8080
server s3 app3:8080
server s4 app4:8080
Chrome dev tools
Please help me!!!
EDIT:
I have tried Thread running in Middleware is using old version of parent's instance variable but this does not work.
As mentioned earlier. The below code succeeds
puts "sent" if ws.send(msg)
Okay, After a lot of searching and testing.. I found that the issue was with not setting a ping. during websocket initialization in the server.
Change this
ws = Faye::WebSocket.new(env, nil) # {ping: KEEPALIVE_TIME }
to
ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME })
My KEEPALIVE_TIME is 0.5 because I am making a stock application where rates change very quickly. You can keep it per your needs.

Sinatra + Fibers + EventMachine

I would like to know to to pause a Root Fiber in ruby (if possible).
I have this Sinatra app, and I am making async calls to an external API with EventMachine.
I don't want to respond to the client until the api responds me.
For example, sleeping the Root Fiber in Sinatra until the EventMachine callback wake it up.
Thanks.
get '/some/route/' do
fib = Fiber.current
req = EM::SomeNonBlokingLib.request
req.callback do |response|
fib.resume(response)
end
req.errback do |err|
fib.resume(err)
end
Fiber.yield
end
EDIT
In your case you should spawn a Fiber for each request. So. Firstly create Rack config file and add some magick:
# config.ru
BOOT_PATH = File.expand_path('../http.rb', __FILE__)
require BOOT_PATH
class FiberSpawn
def initialize(app)
#app = app
end
def call(env)
fib = Fiber.new do
res = #app.call(env)
env['async.callback'].call(res)
end
EM.next_tick{ fib.resume }
throw :async
end
end
use FiberSpawn
run Http
Then your http Sinatra application:
# http.rb
require 'sinatra'
require 'fiber'
class Http < Sinatra::Base
get '/' do
f = Fiber.current
EM.add_timer(1) do
f.resume("Hello World")
end
Fiber.yield
end
end
Now you could run it under thin for example:
> thin start -R config.ru
Then if you will visit locakhost:3000 you'll see your Hello World message

Ctrl+C not killing Sinatra + EM::WebSocket servers

I'm building a Ruby app that runs both an EM::WebSocket server as well as a Sinatra server. Individually, I believe both of these are equipped to handle a SIGINT. However, when running both in the same app, the app continues when I press Ctrl+C. My assumption is that one of them is capturing the SIGINT, preventing the other from capturing it as well. I'm not sure how to go about fixing it, though.
Here's the code in a nutshell:
require 'thin'
require 'sinatra/base'
require 'em-websocket'
EventMachine.run do
class Web::Server < Sinatra::Base
get('/') { erb :index }
run!(port: 3000)
end
EM::WebSocket.start(port: 3001) do |ws|
# connect/disconnect handlers
end
end
I had the same issue. The key for me seemed to be to start Thin in the reactor loop with signals: false:
Thin::Server.start(
App, '0.0.0.0', 3000,
signals: false
)
This is complete code for a simple chat server:
require 'thin'
require 'sinatra/base'
require 'em-websocket'
class App < Sinatra::Base
# threaded - False: Will take requests on the reactor thread
# True: Will queue request for background thread
configure do
set :threaded, false
end
get '/' do
erb :index
end
end
EventMachine.run do
# hit Control + C to stop
Signal.trap("INT") {
puts "Shutting down"
EventMachine.stop
}
Signal.trap("TERM") {
puts "Shutting down"
EventMachine.stop
}
#clients = []
EM::WebSocket.start(:host => '0.0.0.0', :port => '3001') do |ws|
ws.onopen do |handshake|
#clients << ws
ws.send "Connected to #{handshake.path}."
end
ws.onclose do
ws.send "Closed."
#clients.delete ws
end
ws.onmessage do |msg|
puts "Received message: #{msg}"
#clients.each do |socket|
socket.send msg
end
end
end
Thin::Server.start(
App, '0.0.0.0', 3000,
signals: false
)
end
I downgrade thin to version 1.5.1 and it just works. Wired.

ruby 1.9.3 simple GET request to unicorn through socket

Now I try to connect to my socket cretaed by unicorn with this code
require 'socket'
def foo
socket = UNIXSocket.new("path_to_socket/tmp/unicorn.sock")
data = "GET /time HTTP/1.1\n"
data << "Connection: Close\n"
data << "User-Agent: Mozilla/5.0\n"
data << "Accept: */*\n"
data << "Content-Type: application/x-www-form-urlencoded\n"
data << "\n\r\n\r"
socket.puts(data)
while(line = socket.gets) do
puts line
end
end
foo
But always get a "HTTP/1.1 400 Bad Request"
Please, can any body say what I'm do wrong???
Use net/http...
require "net/http"
require "socket"
sock = Net::BufferedIO.new(UNIXSocket.new("path_to_socket/tmp/unicorn.sock"))
request = Net::HTTP::Get.new("/time")
request.exec(sock, "1.1", "/time")
begin
response = Net::HTTPResponse.read_new(sock)
end while response.kind_of?(Net::HTTPContinue)
response.reading_body(sock, request.response_body_permitted?) { }
response.body
response.code
This is very helpful but please note that the Net::HTTP#exec method is marked for internal use only. Likely because it doesn't do resource management, etc.
The following work adapts the suggested strategy to override Net::HTTP#connect (to connect to a socket). I like to use the HTTParty gem for handling my HTTP requests. So the strategy here makes use of a custom ConnectionAdaptor for HTTParty. Now I can just change the ::default_params= call on my including class, to control whether we're using a Unix or a TCP/HTTP socket.
###########################################################
# net/socket_http.rb
###########################################################
module Net
# Overrides the connect method to simply connect to a unix domain socket.
class SocketHttp < HTTP
attr_reader :socket_path
# URI should be a relative URI giving the path on the HTTP server.
# socket_path is the filesystem path to the socket the server is listening to.
def initialize(uri, socket_path)
#socket_path = socket_path
super(uri)
end
# Create the socket object.
def connect
#socket = Net::BufferedIO.new UNIXSocket.new socket_path
on_connect
end
# Override to prevent errors concatenating relative URI objects.
def addr_port
File.basename(socket_path)
end
end
end
###########################################################
# sock_party.rb, a ConnectionAdapter class
###########################################################
require "net/http"
require "socket"
class SockParty < HTTParty::ConnectionAdapter
# Override the base class connection method.
# Only difference is that we'll create a Net::SocketHttp rather than a Net::HTTP.
# Relies on :socket_path in the
def connection
http = Net::SocketHttp.new(uri, options[:socket_path])
if options[:timeout] && (options[:timeout].is_a?(Integer) || options[:timeout].is_a?(Float))
http.open_timeout = options[:timeout]
http.read_timeout = options[:timeout]
end
if options[:debug_output]
http.set_debug_output(options[:debug_output])
end
if options[:ciphers]
http.ciphers = options[:ciphers]
end
return http
end
end
###########################################################
# class MockSockParty, a really *nix-y HTTParty
###########################################################
class MockSockParty
include HTTParty
self.default_options = {connection_adapter: SockParty, socket_path: '/tmp/thin.sock'}
def party_hard
self.class.get('/client').body
end
end
###########################################################
# sock_party_spec.rb
###########################################################
require 'spec_helper'
describe SockParty do
it "should party until its socks fall off." do
puts MockSockParty.new.party_hard
end
end

Streaming on Ruby Thin server

I tried the following ruby code...
self.response.headers["Cache-Control"] ||= "no-cache"
self.response.headers["Transfer-Encoding"] = "chunked"
self.response.headers['Last-Modified'] = Time.now.ctime.to_s
self.response_body = Rack::Chunked::Body.new(Enumerator.new do |y|
10.times do
sleep 1
y << "Hello World\n"
end
end)
This works great in Unicron server but can't stream using Thin server. I tried 1.5.0 and 2.0.0.pre too, this is not working in thin.
I tried the following rack code,
class DeferredBody
def each(block)
#server_block = block
end
def send(data)
#server_block.call data
end
end
class RackStreamApp
def self.call(env)
Thread.new do
sleep 2 # simulate waiting for some event
body = DeferredBody.new
response = [200, {'Content-Type' => 'text/plain'}, body]
env['async.callback'].call response
body.send 'Hello, '
sleep 2
body.send 'World'
end
[-1, {}, []] # or throw :async
end
end
The above code streams "Hello, World" if we use Unicorn Server, but the code doesn't stream using Thin server 1.5.0 ( I tried 2.0.0-pre too)
Is there anything I can do to stream data using the thin server?

Resources