Test Ruby TCPSocket server - ruby

I have a small application, serving connections(like a chat). It catches the connection, grabs login from it, then listens to the data and broadcasts it to each connection, except sender.
The problem is i'm not a very advanced tester and do not know, how this can be tested.
# Handle each connection
def serve(io)
io.puts("LOGIN\n")
# Listen for identifier
user = io.gets.chomp
...
# Add connection to the list
#mutex.synchronize { #chatters[user] = io }
# Get and broadcast input until connection returns nil
loop do
incoming = io.gets
broadcast(incoming, io)
end
end
#Send message out to everyone, but sender
def broadcast(message="", sender)
# Mutex for safety - GServer uses threads
#mutex.synchronize do
#chatters.each do |chatter|
socket = chatter[1]
# Do not send to sender
if sock != sender
sock.print(message)
end
end
end
end

If you just want to do unit testing, you could use RSpec mocks (or some other mocking framework) to stub your methods and ensure that the logic works the way you expect. If you actually want to drive an integration test, that's a lot more work, and will require that you create a separate reader and writer for the socket so that you can actually test each piece of the conversation independently for expected behavior.
Someone else has apparently blogged about a similar issue to yours. Perhaps that example will help you.
If your question is more about the test cases you should write, instead of about how to test sockets, then you may want to rewrite your question so that answers will be more on-target.

Related

Presence not picking up user leave events?

I need to perform some actions when the user leaves a channel (in most cases where they close the tab voluntarily, but there may also be a connection loss/timeout etc.)
According to posts like https://elixirforum.com/t/phoenix-presence-run-some-code-when-user-leaves-the-channel/17739 and How to detect if a user left a Phoenix channel due to a network disconnect?, intercepting the "presence_diff" event from Presence seems to be a foolproof way to go, as it should also covers the cases where the connection terminates abnormally.
Strangely, the presence_diff event seems to only be triggered when I track the user via Presence.track, but not when the user leaves.
Meanwhile, adding a terminate(reason, socket) callback in my channel correctly catches the leave event.
I wonder what could be wrong in my configuration. Or did I not understand the use of Presence correctly?
Example code:
def join("participant:" <> participant_id, _payload, socket) do
if socket.assigns.participant_id == participant_id do
send(self(), :after_participant_join)
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
def handle_info(:after_participant_join, socket) do
experiment_id = socket.assigns.experiment_id
Presence.track(socket, experiment_id, %{
# keys to track
})
# Broadcast something
# broadcast(socket, ...)
{:noreply, socket}
end
intercept(["presence_diff"])
def handle_out("presence_diff", payload, socket) do
# Only gets triggered at Presence.track, but not when the connection is closed.
IO.puts("presence_diff triggered, payload is #{inspect(payload)}")
leaves = payload.leaves
for {experiment_id, meta} <- leaves do
IO.puts("Leave information: #{meta}")
# Do stuffs
end
end
# This works, however.
def terminate(reason, socket) do
IO.puts("terminated. #{inspect(reason)}")
# Do stuffs.
end
OK I think I know what happened: Each "participant:" <> participant_id topic is, as its name suggests, only subscribed to by one participant. Therefore, when that participant quits, the process also dies and nobody is able to act on the presence_diff message.
A separate process is still needed. One can call MyApp.Endpoint.subscribe from that process to subscribe to the "participant:" <> participant_id topic and act on the presence_diff messages.
Or one can set up an external monitor. See How to detect if a user left a Phoenix channel due to a network disconnect?

WebSocket and Redis results in hanging connection from pubsub and/or brpop

I am issuing a Redis subscribe within WebSocket (WS). When I receive the WS open, I thread the request and then instantiate the Redis client. Within the open, I thread for Redis and issue the subscription.
This all works fine, until I receive an unexpected WS close. At that point, the thread running the Redis subscription is gone. If I issue an unsubscribe, I get a hang. If I don't unsubscribe, I have left a phantom subscription that causes me trouble next go round.
Is there some way to delete a subscription once the thread that issued it has terminated? I have noted that the Redis instance has a mon variable for that terminated thread. Sample Ruby code is:
class Backend
include MInit
def initialize(app)
setup
#app = app
end
def run!(env)
if Faye::WebSocket.websocket?(env)
ws = Faye::WebSocket.new(env, [], ping: KEEPALIVE_TIME)
ws_thread = Thread.fork(env) do
credis = Redis.new(host: #redis_uri.host, port: #redis_uri.port, password: #redis_uri.password)
ws.on :open do |event|
channel = URI.parse(event.target.url).path[1..URI.parse(event.target.url).path.length]
redis_thread = Thread.fork do
credis.subscribe(channel) do |on|
on.message do |message_channel, message|
sent = ws.send(message)
end
on.unsubscribe do |message_channel|
puts "Unsubscribe on channel:#{channel};"
end
end
end
end
ws.on :message do |event|
handoff(ws: ws, event: event)
end
ws.on :close do |event|
# Hang occurs here
unsubscribed = credis.unsubscribe(channel)
end
ws.on :error do |event|
ws.close
end
# Return async Rack response
ws.rack_response
end
end
else
#app.call(env)
end
private
def handoff(ws: nil, event: nil, source: nil, message: nil)
# processing
end
end
The fix is rather simple once I really understood the problem. The Redis thread actually still exists. But, Redis is hanging anyway because it is waiting for the thread to get control. To do that, the WS.close code needs to cede control by using EM.next_tick within the WS.close as follows:
ws.on :close do |event|
EM.next_tick do
# Hang occurs here
unsubscribed = credis.unsubscribe(channel)
end
end
This is more of a long comment to offer a workaround rather than a solution.
If it were my application, I would re-consider the design.
Openning a new Redis connection and a new thread for every websocket client is a rather big commitment of resources.
Just to clarify, each connection to Redis requires a TCP/IP socket (which is a limited resource) and memory. Threads should cost about 2Mib per thread for their reserved stack memory... so that 1K Redis connections and threads can incur about a 2Gib cost in memory.
This is in addition to the fact that the Redis server itself has a limited amount of connections it can usually accept (although, this is usually a question of price rather than a hard limit, since they're designed to scale).
It should be quite simple to re-adjust the design so a single thread and connection are serving all websocket clients, which would also allow for an easier subscribe/unsubscribe management.
This can either be performed using an internal per-process broadcasting system (such as implemented by plezi.io) or by using the Redis subscribe/punsubscribe commands.

How to connect to multiple WebSockets with Ruby?

Using faye-websocket and EventMachine the code looks very similar to faye-websocket's client example:
require 'faye/websocket'
require 'eventmachine'
def setup_socket(url)
EM.run {
ws = Faye::WebSocket::Client.new(url)
ws.on :open do ... end
ws.on :message do ... end
ws.on :close do ... end
}
end
I'd like to have multiple connections open parallely. I can't simply call setup_socket multiple times as the execution won't exit the EM.run clause. I've tried to run setup_socket multiple times in separate threads as:
urls.each do |url|
Thread.new { setup_socket(url) }
end
But it doesn't seem to do anyhting as the puts statements don't reach the output.
I'm not restricted to use faye-websocket but it seemed most people use this library. If possible I'd like to avoid multithreading. I'd also not like to lose the possiblity to make changes (e.g. add a new websocket) over time. Therefore moving the iteration of URLs inside the EM.run clause is not desired but instead starting multiple EMs would be more beneficial. I found an example for starting multiple servers via EM in a very clean way. I'm looking for something similar.
How can I connect to multiple WebSockets at the same time?
Here's one way to do it.
First, you have to accept that the EM thread needs to be running. Without this thread you won't be able to process any current connections. So you just can't get around that.
Then, in order to add new URLs to the EM thread you then need some way to communicate from the main thread to the EM thread, so you can tell it to launch a new connection. This can be done with EventMachine::Channel.
So what we can build now is something like this:
#channel = EventMachine::Channel.new
Thread.new {
EventMachine.run {
#channel.subscribe { |url|
ws = Faye::...new(url)
...
}
}
}
Then in the main thread, any time you want to add a new URL to the event loop, you just use this:
def setup_socket(url)
#channel.push(url)
end
Here's another way to do it... Use Iodine's native websocket support (or the Plezi framework) instead of em-websocket...
...I'm biased (I'm the author), but I think they make it a lot easier. Also, Plezi offers automatic scaling with Redis, so it's easy to grow.
Here's an example using Plezi, where each Controller acts like a channel, with it's own URL and Websocket callback (although I think Plezi's Auto-Dispatch is easier than the lower level on_message callback). This code can be placed in a config.ru file:
require 'plezi'
# Once controller / channel for all members of the "Red" group
class RedGroup
def index # HTTP index for the /red URL
"return the RedGroup client using `:render`".freeze
end
# handle websocket messages
def on_message data
# in this example, we'll send the data to all the members of the other group.
BlueGroup.broadcast :handle_message, data
end
# This is the method activated by the "broadcast" message
def handle_message data
write data # write the data to the client.
end
end
# the blue group controller / channel
class BlueGroup
def index # HTTP index for the /blue URL
"return the BlueGroup client using `:render`".freeze
end
# handle websocket messages
def on_message data
# in this example, we'll send the data to all the members of the other group.
RedGroup.broadcast :handle_message, data
end
# This is the method activated by the "broadcast" message
def handle_message data
write data
end
end
# the routes
Plezi.route '/red', RedGroup
Plezi.route '/blue', BlueGroup
# Set the Rack application
run Plezi.app
P.S.
I wrote this answer also because em-websocket might fail or hog resources in some cases. I'm not sure about the details, but it was noted both on the websocket-shootout benchmark and the AnyCable Websocket Benchmarks.

Mocking a network connection

Just how about do I mock a network connection?
Suppose I am writing a client-server topology; the client is already in place, but I want to test that when a network connection is received on the server that an event is fired; or that a function is called in response to a network connection being received.
I'm currently using EventMachine, and I'd like to figure out how to unit test these interactions to make sure that mocked network connections/messages will be handled correctly without having to do integration testing and actually having to write a clien tthat would send the appropriate messages to the test interface. I hope this makes sense?
Basically, I want to be able to have a test for the Server to make sure it responds to mocked messages correctly without having to ever open a real internet connection or write a dedicated client just for testing - I would prefer to be able to be able to say 'This is the message I want to receive, now pretend i've received it from a real client and handle it'
I solved this simply by instantiating an instance of the connection class in EventMachine and tehn calling the method directly - not sure why I didn't think of that in the first place!
describe 'Network manager' do
it 'should call the ChangeStatus handler when it receives the ChangeStatus packet' do
# Arrange
connection = TcpConnection.new
# Set up the packet handler
packet_handler = double 'TcpPacketHandler'
# Inject dependency
connection.packet_handler = packet_handler
# Create the message
data = [ 15, # message code + payload size
0, # message code
4, # protocol version
12, # size of string
]
data.push 'hello, world'.bytes.to_a
reader, writer = IO.pipe
writer.write data
writer.close
puts data
# Assert
expect(packet_handler).to receive(:handle_message).with(data[1], anything()).once
message = reader.read
# Act
connection.receive_data message
end
end

How can I properly handle persistent TCP socket connections (to simulate an HTTP server)?

So, I'm trying to simulate some basic HTTP persistent connections using sockets and Ruby - for a college class.
The point is to build a server - able to handle multiple clients - that receives a file path and gives back the file content - just like an HTTP GET.
The current server implementation loops listening for clients, fires a new thread when there's an incoming connection and reads the file paths from this socket. It's very dumb, but it works fine when working with non-presistent connections - one request per connection.
But they should be persistent.
Which means the client shouldn't worry about closing the connection. In the non-persistent version the servers echoes the response and close the connection - goodbye client, farewell.
But being persistent means the server thread should loop and wait for more incoming requests until... well until there's no more requests. How does the server knows that? It doesn't! Some sort of timeout is needed. I tried to do that with Ruby's Timeout, but it didn't work.
Googling for some solutions - besides being thoroughly advised to avoid using Timeout module - I've seen a lot of posts about the IO.select method, that should handle this socket waiting issue way better than using threads and stuff (which really sounds cool, considering how Ruby threads (don't) work). I'm trying to understand here how IO.select works, but still wasn't able to make it work in the current scenario.
So I aske basically two things:
how can I efficiently work this timeout issue on the server-side, either using some thread based solution, low-level socket options or some IO.select magic?
how can the client side know that the server has closed its side of the connection?
Here's the current code for the server:
require 'date'
module Sockettp
class Server
def initialize(dir, port = Sockettp::DEFAULT_PORT)
#dir = dir
#port = port
end
def start
puts "Starting Sockettp server..."
puts "Serving #{#dir.yellow} on port #{#port.to_s.green}"
Socket.tcp_server_loop(#port) do |socket, client_addrinfo|
handle socket, client_addrinfo
end
end
private
def handle(socket, addrinfo)
Thread.new(socket) do |client|
log "New client connected"
begin
loop do
if client.eof?
puts "#{'-' * 100} end connection"
break
end
input = client.gets.chomp
body = content_for(input)
response = {}
if body
response.merge!({
status: 200,
body: body
})
else
response.merge!({
status: 404,
body: Sockettp::STATUSES[404]
})
end
log "#{addrinfo.ip_address} #{input} -- #{response[:status]} #{Sockettp::STATUSES[response[:status]]}".send(response[:status] == 200 ? :green : :red)
client.puts(response.to_json)
end
ensure
socket.close
end
end
end
def content_for(path)
path = File.join(#dir, path)
return File.read(path) if File.file?(path)
return Dir["#{path}/*"] if File.directory?(path)
end
def log(msg)
puts "#{Thread.current} -- #{DateTime.now.to_s} -- #{msg}"
end
end
end
Update
I was able to simulate the timeout behaviour using the IO.select method, but the implementation doesn't feel good when combining with a couple of threads for accepting new connections and another couple for handling requests. The concurrency makes the situation mad and unstable, and I'm probably not sticking with it unless I can figure out a better way of using this solution.
Update 2
Seems like Timeout is still the best way to handle this. I'm sticking with it till find a better option.
I still don't know how to deal with zombie client connections.
Solution
I endend up using IO.select (got inspired when looking at the webrick code). You cha check the final version here (lib/http/server/client_handler.rb)
You should implement something like heartbeat packets.Client side should send special packets to after few secs/mins to ensure that server doesn't time out the connection on the client end.You just avoid doing anything in this call.

Resources