For Developers
View SourceDevelopment
Textgroup development requires Erlang/OTP and Rebar3 to be
in the $PATH
. The source code repository is found on GitHub.
Building Textgroup
rebar3 compile
Testing Textgroup
rebar3 check
Running Textgroup
rebar3 shell
Creating Textgroup Documentation
rebar3 ex_doc
Creating a Textgroup Release
rebar3 release
See the operator documentation for hints on how to deploy and run such a release.
Design Hints
The Textgroup service uses the supervision tree shown below: The main supervisor starts a worker child (for integrating with systemd) and two supervisor childs, one for supervising a fixed-size pool of five TCP connection acceptors, and another one for supervising dynamically created connection handlers, one per client (there's six of them, in this example).
This is a straightforward structure, except that the acceptor processes work
in a somewhat non-ideomatic way. However, don't let the
implementation confuse you: Maybe just view it as a blackbox for the
moment. Once everything else seems clear, here's an explanation of what's going
on in the textgroup_acceptor
module:
Each acceptor process blocks in
gen_tcp:accept/2
while waiting for a new connection. The problem is: While waiting, the process is unresponsive to system messages. Basically, OTP processes are supposed to only ever wait for Erlang messages, to handle those in callback functions, and to return to waiting for the next Erlang message. Asgen_tcp
(quite against the usual OTP semantics) doesn't offer a non-blocking way to accept connections (whereas there is a non-blocking way to receive data from the socket), the acceptor processes callgen_tcp:accept/2
with a timeout, so they can check for system messages every few seconds. One alternative is to spawn simple (non-OTP) processes just for blocking ingen_tcp:accept/1
, and then wake a proper OTP process for handling the new connection, basically implementing the non-blocking mechanism to accept connections thatgen_tcp
doesn't provide. Another option would be usingprim_inet:async_accept/2
, which does offer this functionality. However, that's not a documented interface. In the future, a nicer solution might become available based on the newsocket
backend, which provides a non-blockingaccept/2
variant.The
textgroup_acceptor
is built as a special process. It could just as well be implemented as a generic server with the same behavior. The only reason it wasn't done this way is that mostgen_server
features would remain unused. Matter of taste.When a new connection is accepted, the acceptor asks
textgroup_client_sup
to spawn a new process for handling the client. An alternative would be to not split the tasks of accepting and handling connections into separate processes: You could spawn a pool of client handler processes that wait for new connections, maybe using the same workaround as thetextgroup_acceptor
to remain responsive. Those handlers would spawn a fresh worker immediately after accepting a connection, handle the connection, and then terminate. This is suggested in Learn You Some Erlang and Erlang and OTP in Action, for example. It would also be consistent with the usual Erlang pattern to create a process for each concurrent activity (processing a client connection from begin to end) rather than each task (accepting connections in one process and then handling them in another). However, for Textgroup, it seemed preferable to have a clear separation of the fixed-size acceptor pool on the one hand and the client handler processes on the other: The advantage is a one-to-one mapping of clients and (fully responsive) handler processes. This allows for askingtextgroup_client_sup
for a list of clients and communication with them without delays. This design would also allow more complex applications to easily close/change the listener socket without disconnecting existing clients.
All that said, real-world projects will often just use an existing application (such as Ranch) for accepting connections.