A TCP server implementation with tracking of startup and shutdown
Race-free server startup and shutdown can be a tricky task. The following example illustrates, how a TCP server can be started and interrupted properly.
For startup it makes use of yield and Eventbox::CompletionProc#raise to complete MyServer.new
either successfully or with the forwarded exception raised by TCPServer.new
.
For the shutdown it makes use of Eventbox::Action#raise to send a Stop
signal to the blocking accept
method.
The Stop
instance carries the Eventbox::CompletionProc which is used to signal that the shutdown has finished by returning from MyServer#stop
.
require "eventbox"
require "socket"
class MyServer < Eventbox
yield_call def init(bind, port, result)
@count = 0
@server = start_serving(bind, port, result) # Start an action to handle incomming connections
end
action def start_serving(bind, port, init_done)
serv = TCPServer.new(bind, port)
rescue => err
init_done.raise err # complete MyServer.new with an exception
else
init_done.yield # complete MyServer.new without exception
loop do # accept all connection requests until Stop is received
begin
# enable interruption by the Stop class for the duration of the `accept` call
conn = Thread.handle_interrupt(Stop => :on_blocking) do
serv.accept # wait for the next connection request come in
end
rescue Stop => st
serv.close
st.stopped.yield # let MyServer#stop return
break # and exit the action
else
MyConnection.new(conn, self) # Handle each client by its own instance
end
end
end
# A simple example for a shared resource to be used by several threads
sync_call def count
@count += 1 # atomically increment the counter
end
yield_call def stop(result)
# Don't return from `stop` externally, but wait until the server is down
@server.raise(Stop.new(result))
end
class Stop < RuntimeError
def initialize(stopped)
@stopped = stopped
end
attr_reader :stopped
end
end
# Each call to `MyConnection.new` starts a new thread to do the communication.
class MyConnection < Eventbox
action def init(conn, server)
conn.write "Hello #{server.count}"
ensure
conn.close # Don't wait for an answer but just close the client connection
end
end
The server can now be started like so.
s = MyServer.new('localhost', 12345) # Open a TCP socket
10.times.map do # run 10 client connections in parallel
Thread.new do
TCPSocket.new('localhost', 12345).read
end
end.each { |th| p th.value } # and print their responses
s.stop # shutdown the server socket
It prints some output like this:
"Hello 2"
"Hello 1"
"Hello 7"
"Hello 8"
"Hello 3"
"Hello 9"
"Hello 5"
"Hello 6"
"Hello 4"
"Hello 10"