Ruby - Send message to a Websocket - ruby-on-rails

How can I send data to a WebSocket using Ruby in a Background Process?
Background
I already have a separate ruby file running a Websocket server using the websocket-eventmachine-server gem. However, within my Rails application, I want to send data to the websocket in a background task.
Here is my WebSocket server:
EM.run do
trap('TERM') { stop }
trap('INT') { stop }
WebSocket::EventMachine::Server.start(host: options[:host], port: options[:port]) do |ws|
ws.onopen do
puts 'Client connected'
end
ws.onmessage do |msg, type|
ws.send msg, type: type
end
ws.onclose do
puts 'Client disconnected'
end
end
def stop
puts 'Terminating WebSocket Server'
EventMachine.stop
end
end
However, in my background process (I'm using Sidekiq), I'm not sure how to connect to the WebSocket and send data to it.
Here's my Sidekiq worker:
class MyWorker
include Sidekiq::Worker
def perform(command)
100.times do |i|
# Send 'I am on #{i}' to the Websocket
end
end
end
I was hoping to be able to do something like EventMachine::WebSocket.send 'My message!' but I don't see an API for that or something similar. What is the correct way to send data to a WebSocket in Ruby?

Accepted Answer:
Af your keeping your current websocket server:
You can use Iodine as a simple websocket client for testing. It runs background tasks using it's own reactor pattern based code and has a websocket client (I'm biased, I'm the author).
You could do something like this:
require 'iodine/http'
Iodine.protocol = :timers
# force Iodine to start immediately
Iodine.force_start!
options = {}
options[:on_message] = Proc.new {|data| puts data}
100.times do |i|
options[:on_open] = Proc.new {write "I am number #{i}"}
Iodine.run do
Iodine::Http.ws_connect('ws://localhost:3000', options)
end
end
P.S.
I would recommend using a framework, such as Plezi, for your websockets (I'm the author). Some frameworks let you run their code within a Rails/Sinatra app (Plezi does that and I think Faye, although not strictly a framework, does that too).
Using EM directly is quite hardcore and there are a lot of things to manage when dealing with Websockets, which a good framework helps you manage.
EDIT 3:
Iodine WebSocket client connections are (re)supported starting with Iodine 0.7.17, including TLS connections when OpenSSL >= 1.1.0.
The following code is an updated version of the original answer:
require 'iodine'
class MyClient
def on_open connection
connection.subscribe :updates
puts "Connected"
end
def on_message connection, data
puts data
end
def on_close connection
# auto-reconnect after 250ms.
puts "Connection lost, re-connecting in 250ms"
Iodine.run_after(250) { MyClient.connect }
end
def self.connect
Iodine.connect(url: "ws://localhost:3000/path", handler: MyClient.new)
end
end
Iodine.threads = 1
Iodine.defer { MyClient.connect if Iodine.master? }
Thread.new { Iodine.start }
100.times {|i| Iodine.publish :updates, "I am number #{i}" }
EDIT 2:
This answer in now outdated, since Iodine 0.2.x doesn't include a client any longer. Use Iodine 0.1.x or a different gem for websocket clients.

websocket-eventmachine-server is a websockets server.
If you want to connect to a websocket server using ruby, you can do it with some gems, like
https://github.com/igrigorik/em-websocket: Both server and client, also based on eventmachine.
ruby-websocket-client: Client only

Related

Trigger/Subscribe to websocket-rails event from inside a rails runner

I have a Rails Application with websocket-rails gem.
Inside my application there is a Daemon that I launch with rails runner MyDaemon.start
I'm using websocket-rails Synchronization, so my config/initializers/websocket_rails.rb looks like this:
WebsocketRails.setup do |config|
config.log_internal_events = false
config.standalone = false
config.synchronize = true
end
Inside MyDaemon, using synchronization, I can trigger event that will reach both my WebsocketRails::BaseController and my javascript WebSocketRails.
What I'm trying to do is to find a way to bind to events from my MyDaemon.
I've tried to implement a plain WebSocket client using both faye-websocket-ruby and websocket-client-simple, but after banging my head on my keyboard for some time, I figured out that there is some kind of "handshake" process using connection_id from the client_connected message. Basically none of the solutions provided in this other so question works for me.
I need to understand if inside my MyDaemon I can subscribe directly to some WebsocketRails callback, even inside an EventMachine, or how should I implement a Websocket Client in Ruby itself.
My last attempt to have a ruby client can be found in this gist, and this is a sample output:
ruby client.rb ws://localhost:3000/websocket
[:open, {"upgrade"=>"websocket", "connection"=>"Upgrade", "sec-websocket-accept"=>"zNTdGvxFKJeP+1PyGf27T4x2PGo="}]
JSON message is
[["client_connected", {"id"=>nil, "channel"=>nil, "user_id"=>nil, "data"=>{"connection_id"=>"4b7b91001befb160d17b"}, "success"=>nil, "result"=>nil, "token"=>nil, "server_token"=>nil}]]
client id is 4b7b91001befb160d17b
[:message, "[[\"client_connected\",{\"id\":null,\"channel\":null,\"user_id\":null,\"data\":{\"connection_id\":\"4b7b91001befb160d17b\"},\"success\":null,\"result\":null,\"token\":null,\"server_token\":null}]]"]
JSON message is
[["websocket_rails.ping", {"id"=>nil, "channel"=>nil, "user_id"=>nil, "data"=>{}, "success"=>nil, "result"=>nil, "token"=>nil, "server_token"=>nil}]]
Sending ["pong",{}]
[:message, "[[\"websocket_rails.ping\",{\"id\":null,\"channel\":null,\"user_id\":null,\"data\":{},\"success\":null,\"result\":null,\"token\":null,\"server_token\":null}]]"]
[:close, 1006, ""]
While the log of websocket-rails is:
I [2015-06-27 02:08:45.250] [ConnectionManager] Connection opened: #<Connection::2b3dddaf3ec4ed5e3550>
I [2015-06-27 02:08:45.251] [Dispatcher] Started Event: client_connected
I [2015-06-27 02:08:45.251] [Dispatcher] Name: client_connected
I [2015-06-27 02:08:45.251] [Dispatcher] Data: {"connection_id"=>"2b3dddaf3ec4ed5e3550"}
I [2015-06-27 02:08:45.251] [Dispatcher] Connection: #<Connection::2b3dddaf3ec4ed5e3550>
I [2015-06-27 02:08:45.251] [Dispatcher] Event client_connected Finished in 0.000174623 seconds
I [2015-06-27 02:09:05.252] [ConnectionManager] Connection closed: #<Connection::2b3dddaf3ec4ed5e3550>
I [2015-06-27 02:09:05.252] [Dispatcher] Started Event: client_disconnected
I [2015-06-27 02:09:05.252] [Dispatcher] Name: client_disconnected
I [2015-06-27 02:09:05.252] [Dispatcher] Connection: #<Connection::2b3dddaf3ec4ed5e3550>
I [2015-06-27 02:09:05.253] [Dispatcher] Event client_disconnected Finished in 0.000236669 seconds
Probably I'm missing somethig very stupid, so I'm here to ask your help!
You can use Iodine as a websocket client (I'm the author):
require 'iodine/http'
# prevents the Iodine's server from running
Iodine.protocol = :timer
# starts Iodine while the script is still running
Iodine.force_start!
options = {}
options[:on_open] = Proc.new {puts 'Connection Open'; write "Hello World!" }
options[:on_close] = Proc.new {puts 'Connection Closed'}
options[:on_message] = Proc.new {|data| puts "I got: #{data}" }
# connect to an echo server for demo. Use the blocking method:
websocket = Iodine::Http::WebsocketClient.connect "wss://echo.websocket.org/", options
websocket << "sending data"
sleep 0.5
websocket.close
As an aside note, reading around I noticed that the websocket-rails gem isn't being updated all that much. See this question
As an alternative, you can run websockets inside your Rails app by using the Plezi framework (I'm the author).
It's quite easy to use both frameworks at the same time on the same server. This way you can use your Rails model's code inside your Plezi Websocket controller.
Because Plezi will manage the websockets and Rails will probably render the 404 Not Found page, Plezi's routes will take precedence... but as long as your routes don't override each other, you're golden.
Notice that to allow both apps to run together, Plezi will force you to use Iodine server as your Rack server. To avoid this you can use the Placebo API and run Plezi on a different process.
You can read more the framework's README file.

websocket-rails how to close socket connection?

I'm switching over from ActionController:Live to websocket-rails, and I was just wondering how to close the connection on the server side once the user closes the browser window?
With ActionController:Live I used to have:
def stream
response.headers['Content-Type'] = 'text/event-stream'
#redis_sub = RedisStream.new_redis_client
# Subscribing to user's stream by session token
#redis_sub.subscribe([ token ]) do |on|
on.message do |channel, msg|
## Did stuff
response.stream.write(msg)
end
end
rescue IOError
"\n\nIOError in controller"
rescue ClientDisconnected
puts "\n\nClient has disconnected\n\n"
ensure
#redis_sub.quit
response.stream.close
end
And this worked fine, now I'm trying to do the same thing as this but using websockets, and I was wondering how to close the connection and quit out of my redis subscription.
connection.close! from within the websocket controller.
https://github.com/websocket-rails/websocket-rails/issues/219
To deepen a little in #TheNastyOne answer, you can close socket a connection from dispatcher on client side like this:
// Open connection.
var dispatcher = new WebSocketRails('localhost:3000/websocket');
// Close connection.
dispatcher.disconnect();
Here lives the issue that describe both methods.

Redis + ActionController::Live threads not dying

Background: We've built a chat feature in to one of our existing Rails applications. We're using the new ActionController::Live module and running Puma (with Nginx in production), and subscribing to messages through Redis. We're using EventSource client side to establish the connection asynchronously.
Problem Summary: Threads are never dying when the connection is terminated.
For example, should the user navigate away, close the browser, or even go to a different page within the application, a new thread is spawned (as expected), but the old one continues to live.
The problem as I presently see it is that when any of these situations occur, the server has no way of knowing whether the connection on the browser's end is terminated, until something attempts to write to this broken stream, which would never happen once the browser has moved away from the original page.
This problem seems to be documented on github, and similar questions are asked on StackOverflow here (pretty well exact same question) and here (regarding getting number of active threads).
The only solution I've been able to come up with, based on these posts, is to implement a type of thread / connection poker. Attempting to write to a broken connection generates an IOError which I can catch and properly close the connection, allowing the thread to die. This is the controller code for that solution:
def events
response.headers["Content-Type"] = "text/event-stream"
stream_error = false; # used by flusher thread to determine when to stop
redis = Redis.new
# Subscribe to our events
redis.subscribe("message.create", "message.user_list_update") do |on|
on.message do |event, data| # when message is received, write to stream
response.stream.write("messageType: '#{event}', data: #{data}\n\n")
end
# This is the monitor / connection poker thread
# Periodically poke the connection by attempting to write to the stream
flusher_thread = Thread.new do
while !stream_error
$redis.publish "message.create", "flusher_test"
sleep 2.seconds
end
end
end
rescue IOError
logger.info "Stream closed"
stream_error = true;
ensure
logger.info "Events action is quitting redis and closing stream!"
redis.quit
response.stream.close
end
(Note: the events method seems to get blocked on the subscribe method invocation. Everything else (the streaming) works properly so I assume this is normal.)
(Other note: the flusher thread concept makes more sense as a single long-running background process, a bit like a garbage thread collector. The problem with my implementation above is that a new thread is spawned for each connection, which is pointless. Anyone attempting to implement this concept should do it more like a single process, not so much as I've outlined. I'll update this post when I successfully re-implement this as a single background process.)
The downside of this solution is that we've only delayed or lessened the problem, not completely solved it. We still have 2 threads per user, in addition to other requests such as ajax, which seems terrible from a scaling perspective; it seems completely unattainable and impractical for a larger system with many possible concurrent connections.
I feel like I am missing something vital; I find it somewhat difficult to believe that Rails has a feature that is so obviously broken without implementing a custom connection-checker like I have done.
Question: How do we allow the connections / threads to die without implementing something corny such as a 'connection poker', or garbage thread collector?
As always let me know if I've left anything out.
Update
Just to add a bit of extra info: Huetsch over at github posted this comment pointing out that SSE is based on TCP, which normally sends a FIN packet when the connection is closed, letting the other end (server in this case) know that its safe to close the connection. Huetsch points out that either the browser is not sending that packet (perhaps a bug in the EventSource library?), or Rails is not catching it or doing anything with it (definitely a bug in Rails, if that's the case). The search continues...
Another Update
Using Wireshark, I can indeed see FIN packets being sent. Admittedly, I am not very knowledgeable or experienced with protocol level stuff, however from what I can tell, I definitely detect a FIN packet being sent from the browser when I establish the SSE connection using EventSource from the browser, and NO packet sent if I remove that connection (meaning no SSE). Though I'm not terribly up on my TCP knowledge, this seems to indicate to me that the connection is indeed being properly terminated by the client; perhaps this indicates a bug in Puma or Rails.
Yet another update
#JamesBoutcher / boutcheratwest(github) pointed me to a discussion on the redis website regarding this issue, specifically in regards to the fact that the .(p)subscribe method never shuts down. The poster on that site pointed out the same thing that we've discovered here, that the Rails environment is never notified when the client-side connection is closed, and therefore is unable to execute the .(p)unsubscribe method. He inquires about a timeout for the .(p)subscribe method, which I think would work as well, though I'm not sure which method (the connection poker I've described above, or his timeout suggestion) would be a better solution. Ideally, for the connection poker solution, I'd like to find a way to determine whether the connection is closed on the other end without writing to the stream. As it is right now, as you can see, I have to implement client-side code to handle my "poking" message separately, which I believe is obtrusive and goofy as heck.
A solution I just did (borrowing a lot from #teeg) which seems to work okay (haven't failure tested it, tho)
config/initializers/redis.rb
$redis = Redis.new(:host => "xxxx.com", :port => 6379)
heartbeat_thread = Thread.new do
while true
$redis.publish("heartbeat","thump")
sleep 30.seconds
end
end
at_exit do
# not sure this is needed, but just in case
heartbeat_thread.kill
$redis.quit
end
And then in my controller:
def events
response.headers["Content-Type"] = "text/event-stream"
redis = Redis.new(:host => "xxxxxxx.com", :port => 6379)
logger.info "New stream starting, connecting to redis"
redis.subscribe(['parse.new','heartbeat']) do |on|
on.message do |event, data|
if event == 'parse.new'
response.stream.write("event: parse\ndata: #{data}\n\n")
elsif event == 'heartbeat'
response.stream.write("event: heartbeat\ndata: heartbeat\n\n")
end
end
end
rescue IOError
logger.info "Stream closed"
ensure
logger.info "Stopping stream thread"
redis.quit
response.stream.close
end
I'm currently making an app that revolves around ActionController:Live, EventSource and Puma and for those that are encountering problems closing streams and such, instead of rescuing an IOError, in Rails 4.2 you need to rescue ClientDisconnected. Example:
def stream
#Begin is not required
twitter_client = Twitter::Streaming::Client.new(config_params) do |obj|
# Do something
end
rescue ClientDisconnected
# Do something when disconnected
ensure
# Do something else to ensure the stream is closed
end
I found this handy tip from this forum post (all the way at the bottom): http://railscasts.com/episodes/401-actioncontroller-live?view=comments
Here's a potentially simpler solution which does not use a heartbeat. After much research and experimentation, here's the code I'm using with sinatra + sinatra sse gem (which should be easily adapted to Rails 4):
class EventServer < Sinatra::Base
include Sinatra::SSE
set :connections, []
.
.
.
get '/channel/:channel' do
.
.
.
sse_stream do |out|
settings.connections << out
out.callback {
puts 'Client disconnected from sse';
settings.connections.delete(out);
}
redis.subscribe(channel) do |on|
on.subscribe do |channel, subscriptions|
puts "Subscribed to redis ##{channel}\n"
end
on.message do |channel, message|
puts "Message from redis ##{channel}: #{message}\n"
message = JSON.parse(message)
.
.
.
if settings.connections.include?(out)
out.push(message)
else
puts 'closing orphaned redis connection'
redis.unsubscribe
end
end
end
end
end
The redis connection blocks on.message and only accepts (p)subscribe/(p)unsubscribe commands. Once you unsubscribe, the redis connection is no longer blocked and can be released by the web server object which was instantiated by the initial sse request. It automatically clears when you receive a message on redis and sse connection to the browser no longer exists in the collection array.
Building on #James Boutcher, I used the following in clustered Puma with 2 workers, so that I have only 1 thread created for the heartbeat in config/initializers/redis.rb:
config/puma.rb
on_worker_boot do |index|
puts "worker nb #{index.to_s} booting"
create_heartbeat if index.to_i==0
end
def create_heartbeat
puts "creating heartbeat"
$redis||=Redis.new
heartbeat = Thread.new do
ActiveRecord::Base.connection_pool.release_connection
begin
while true
hash={event: "heartbeat",data: "heartbeat"}
$redis.publish("heartbeat",hash.to_json)
sleep 20.seconds
end
ensure
#no db connection anyway
end
end
end
Here you are solution with timeout that will exit blocking Redis.(p)subscribe call and kill unused connection tread.
class Stream::FixedController < StreamController
def events
# Rails reserve a db connection from connection pool for
# each request, lets put it back into connection pool.
ActiveRecord::Base.clear_active_connections!
# Last time of any (except heartbeat) activity on stream
# it mean last time of any message was send from server to client
# or time of setting new connection
#last_active = Time.zone.now
# Redis (p)subscribe is blocking request so we need do some trick
# to prevent it freeze request forever.
redis.psubscribe("messages:*", 'heartbeat') do |on|
on.pmessage do |pattern, event, data|
# capture heartbeat from Redis pub/sub
if event == 'heartbeat'
# calculate idle time (in secounds) for this stream connection
idle_time = (Time.zone.now - #last_active).to_i
# Now we need to relase connection with Redis.(p)subscribe
# chanel to allow go of any Exception (like connection closed)
if idle_time > 4.minutes
# unsubscribe from Redis because of idle time was to long
# that's all - fix in (almost)one line :)
redis.punsubscribe
end
else
# save time of this (last) activity
#last_active = Time.zone.now
end
# write to stream - even heartbeat - it's sometimes chance to
# capture dissconection error before idle_time
response.stream.write("event: #{event}\ndata: #{data}\n\n")
end
end
# blicking end (no chance to get below this line without unsubscribe)
rescue IOError
Logs::Stream.info "Stream closed"
rescue ClientDisconnected
Logs::Stream.info "ClientDisconnected"
rescue ActionController::Live::ClientDisconnected
Logs::Stream.info "Live::ClientDisconnected"
ensure
Logs::Stream.info "Stream ensure close"
redis.quit
response.stream.close
end
end
You have to use reds.(p)unsubscribe to end this blocking call. No exception can break this.
My simple app with information about this fix: https://github.com/piotr-kedziak/redis-subscribe-stream-puma-fix
Instead of sending a heartbeat to all the clients, it might be easier to just set a watchdog for each connection. [Thanks to #NeilJewers]
class Stream::FixedController < StreamController
def events
# Rails reserve a db connection from connection pool for
# each request, lets put it back into connection pool.
ActiveRecord::Base.clear_active_connections!
redis = Redis.new
watchdog = Doberman::WatchDog.new(:timeout => 20.seconds)
watchdog.start
# Redis (p)subscribe is blocking request so we need do some trick
# to prevent it freeze request forever.
redis.psubscribe("messages:*") do |on|
on.pmessage do |pattern, event, data|
begin
# write to stream - even heartbeat - it's sometimes chance to
response.stream.write("event: #{event}\ndata: #{data}\n\n")
watchdog.ping
rescue Doberman::WatchDog::Timeout => e
raise ClientDisconnected if response.stream.closed?
watchdog.ping
end
end
end
rescue IOError
rescue ClientDisconnected
ensure
response.stream.close
redis.quit
watchdog.stop
end
end
If you can tolerate a small chance of missing a message you can use subscribe_with_timeout:
sse = SSE.new(response.stream)
sse.write("hi", event: "hello")
redis = Redis.new(reconnect_attempts: 0)
loop do
begin
redis.subscribe_with_timeout(5 * 60, 'mycoolchannel') do |on|
on.message do |channel, message|
sse.write(message, event: 'message_posted')
end
end
rescue Redis::TimeoutError
sse.write("ping", event: "ping")
end
end
This code subscribes to a Redis channel, waits for 5 minutes, then closes connection to Redis and subscribes again.

Can I use a Request / Reply - RPC pattern in Rails 3 with AMQP?

For reasons similar to the ones in this discussion, I'm experimenting with messaging in lieu of REST for a synchronous RPC call from one Rails 3 application to another. Both apps are running on thin.
The "server" application has a config/initializers/amqp.rb file based on the Request / Reply pattern in the rubyamqp.info documentation:
require "amqp"
EventMachine.next_tick do
connection = AMQP.connect ENV['CLOUDAMQP_URL'] || 'amqp://guest:guest#localhost'
channel = AMQP::Channel.new(connection)
requests_queue = channel.queue("amqpgem.examples.services.time", :exclusive => true, :auto_delete => true)
requests_queue.subscribe(:ack => true) do |metadata, payload|
puts "[requests] Got a request #{metadata.message_id}. Sending a reply..."
channel.default_exchange.publish(Time.now.to_s,
:routing_key => metadata.reply_to,
:correlation_id => metadata.message_id,
:mandatory => true)
metadata.ack
end
Signal.trap("INT") { connection.close { EventMachine.stop } }
end
In the 'client' application, I'd like to render the results of a synchronous call to the 'server' in a view. I realize this is a bit outside the comfort zone of an inherently asynchronous library like the amqp gem, but I'm wondering if there's a way to make it work. Here is my client config/initializers/amqp.rb:
require 'amqp'
EventMachine.next_tick do
AMQP.connection = AMQP.connect 'amqp://guest:guest#localhost'
Signal.trap("INT") { AMQP.connection.close { EventMachine.stop } }
end
Here is the controller:
require "amqp"
class WelcomeController < ApplicationController
def index
puts "[request] Sending a request..."
WelcomeController.channel.default_exchange.publish("get.time",
:routing_key => "amqpgem.examples.services.time",
:message_id => Kernel.rand(10101010).to_s,
:reply_to => WelcomeController.replies_queue.name)
WelcomeController.replies_queue.subscribe do |metadata, payload|
puts "[response] Response for #{metadata.correlation_id}: #{payload.inspect}"
#message = payload.inspect
end
end
def self.channel
#channel ||= AMQP::Channel.new(AMQP.connection)
end
def self.replies_queue
#replies_queue ||= channel.queue("reply", :exclusive => true, :auto_delete => true)
end
end
When I start both applications on different ports and visit the welcome#index view.
#message is nil in the view, since the result has not yet returned. The result arrives a few milliseconds after the view is rendered and is displayed on the console:
$ thin start
>> Using rack adapter
>> Thin web server (v1.5.0 codename Knife)
>> Maximum connections set to 1024
>> Listening on 0.0.0.0:3000, CTRL+C to stop
[request] Sending a request...
[response] Response for 3877031: "2012-11-27 22:04:28 -0600"
No surprise here: subscribe is clearly not meant for synchronous calls. What is surprising is that I can't find a synchronous alternative in the AMQP gem source code or in any documentation online. Is there an alternative to subscribe that will give me the RPC behavior I want? Given that there are other parts of the system in which I'd want to use legitimately asynchronous calls, the bunny gem didn't seem like the right tool for the job. Should I give it another look?
edit in response to Sam Stokes
Thanks to Sam for the pointer to throw :async / async.callback. I hadn't seen this technique before and this is exactly the kind of thing I was trying to learn with this experiment in the first place. send_response.finish is gone in Rails 3, but I was able to get his example to work for at least one request with a minor change:
render :text => #message
rendered_response = response.prepare!
Subsequent requests fail with !! Unexpected error while processing request: deadlock; recursive locking. This may have been what Sam was getting at with the comment about getting ActionController to allow concurrent requests, but the cited gist only works for Rails 2. Adding config.allow_concurrency = true in development.rb gets rid of this error in Rails 3, but leads to This queue already has default consumer. from AMQP.
I think this yak is sufficiently shaven. ;-)
While interesting, this is clearly overkill for simple RPC. Something like this Sinatra streaming example seems a more appropriate use case for client interaction with replies. Tenderlove also has a blog post about an upcoming way to stream events in Rails 4 that could work with AMQP.
As Sam points out in his discussion of the HTTP alternative, REST / HTTP makes perfect sense for the RPC portion of my system that involves two Rails apps. There are other parts of the system involving more classic asynchronous event publishing to Clojure apps. For these, the Rails app need only publish events in fire-and-forget fashion, so AMQP will work fine there using my original code without the reply queue.
You can get the behaviour you want - have the client make a simple HTTP request, to which your web app responds asynchronously - but you need more tricks. You need to use Thin's support for asynchronous responses:
require "amqp"
class WelcomeController < ApplicationController
def index
puts "[request] Sending a request..."
WelcomeController.channel.default_exchange.publish("get.time",
:routing_key => "amqpgem.examples.services.time",
:message_id => Kernel.rand(10101010).to_s,
:reply_to => WelcomeController.replies_queue.name)
WelcomeController.replies_queue.subscribe do |metadata, payload|
puts "[response] Response for #{metadata.correlation_id}: #{payload.inspect}"
#message = payload.inspect
# Trigger Rails response rendering now we have the message.
# Tested in Rails 2.3; may or may not work in Rails 3.x.
rendered_response = send_response.finish
# Pass the response to Thin and make it complete the request.
# env['async.callback'] expects a Rack-style response triple:
# [status, headers, body]
request.env['async.callback'].call(rendered_response)
end
# This unwinds the call stack, skipping the normal Rails response
# rendering, all the way back up to Thin, which catches it and
# interprets as "I'll give you the response later by calling
# env['async.callback']".
throw :async
end
def self.channel
#channel ||= AMQP::Channel.new(AMQP.connection)
end
def self.replies_queue
#replies_queue ||= channel.queue("reply", :exclusive => true, :auto_delete => true)
end
end
As far as the client is concerned, the result is indistinguishable from your web app blocking on a synchronous call before returning the response; but now your web app can process many such requests concurrently.
CAUTION!
Async Rails is an advanced technique; you need to know what you're doing. Some parts of Rails do not take kindly to having their call stack abruptly dismantled. The throw will bypass any Rack middlewares that don't know to catch and rethrow it (here is a rather old partial solution). ActiveSupport's development-mode class reloading will reload your app's classes after the throw, without waiting for the response, which can cause very confusing breakage if your callback refers to a class that has since been reloaded. You'll also need to ask ActionController nicely to allow concurrent requests.
Request/response
You're also going to need to match up requests and responses. As it stands, if Request 1 arrives, and then Request 2 arrives before Request 1 gets a response, then it's undefined which request would receive Response 1 (messages on a queue are distributed round-robin between the consumers subscribed to the queue).
You could do this by inspecting the correlation_id (which you'll have to explicitly set, by the way - RabbitMQ won't do it for you!) and re-enqueuing the message if it's not the response you were waiting for. My approach was to create a persistent Publisher object which would keep track of open requests, listen for all responses, and lookup the appropriate callback to invoke based on the correlation_id.
Alternative: just use HTTP
You're really solving two different (and tricky!) problems here: persuading Rails/thin to process requests asynchronously, and implementing request-response semantics on top of AMQP's publish-subscribe model. Given you said this is for calling between two Rails apps, why not just use HTTP, which already has the request-response semantics you need? That way you only have to solve the first problem. You can still get concurrent request processing if you use a non-blocking HTTP client library, such as em-http-request.

ruby interprocess communication

I have a Rails project and two ruby mini-daemons running in the background. What's the best way to communicate between them?
Communication like below should be possible:
Rails -> Process 1 -> Process 2 -> Rails
Some requests would be sync, other async.
Queues (something like AMQ, or custom Redis based) or RPC HTTP calls?
Check DRb as well.
I implemented a system via RabbitMq + the bunny gem.
Update:
After reading http://blog.brightbox.co.uk/posts/queues-and-callbacks I decided to try out RabbitMQ. There are two gems amqp (async, eventmachine based) or bunny (sync). amqp is great, but if you're using Rails with passenger it can do some weird things.
The system works like this, the daemons listen on a queue for messages:
# The incoming data should be a JSON encoded hash that looks like:
# { "method" => method_to_call, "opts" => [ Array of opts for method ],
# "output" => "a queue where to send the result (optional)" }
# If output is specified it will publish the JSON encoded response there.
def listen_on(queue_name, class)
BUNNY.start
bunny = BUNNY.queue(queue_name)
bunny.subscribe do |msg|
msg = JSON.parse(msg[:payload])
result = class.new.send(msg["method"], *msg["opts"])
if msg["output"]
BUNNY.queue(msg["output"]).publish(result.to_json)
end
end
So once a message is received it calls a method from a class. One thing to note is that it would have been ideal to use bunny for Rails and amqp in the daemons. But I like to use one gem pe service.

Resources