Reputation: 410552
I'm working on an IRC bot application written in Erlang. It has two key modules: irc_bot
and irc_proto
, each running as a gen_server
process (there is one process instance for each). irc_bot
connects to an IRC server using gen_tcp
, receiving TCP messages and responding accordingly. The job of irc_proto
is to expose functions that, in turn, format responses according to the IRC protolocol. (For example, there is a function, irc_proto:say(To, Msg)
which turns Msg
into an appropriately-formatted PRIVMSG
response.)
Right now, this application is not OTP compliant. When irc_bot
starts, it opens up a TCP connection, then passes the TCP socket (returned by gen_tcp:connect()
) to irc_proto:start_link()
. irc_proto
stores that as part of its state and uses that socket to send responses back.
Eventually (in the near future!), this application will be OTP-compliant, and will have some sort of supervisor (maybe more, but obviously at least a top-level supervisor). And therein lies a problem: If the supervisor is responsible for starting both irc_bot
and irc_proto
, the supervisor will not have the TCP socket (returned by gen_tcp:connect()
), so it cannot pass that to irc_proto
.
I'm not sure what the best approach is here. Do I need a couple layers of supervisors (or something like that) so I can (somehow) pass the TCP socket to irc_proto
? Or is sharing that socket just a poor design? Should irc_bot
actually expose some a function, e.g. irc_bot:send()
, that irc_proto
actually uses to send back a response after it has formatted the response? Does OTP offer any guidelines or suggestions on how to structure this application?
Upvotes: 2
Views: 617
Reputation: 13154
There are two ways:
gen_tcp:send/2
doesn't have an ownership restriction, for example) and have them send directly to it. Having two processes receive from a single socket can become a bit more of a sticky case than this, though it is indeed possible.The socket will close when the controlling process that owns it closes, though, and a socket can have only one owner. To make sure you're not running a half-broken case you need links/monitors across processes that will share a socket, the easiest case being links so they all die at once and... at that point you are effectively right back at the case where only one process handles the socket on behalf of several underlying ones, but with less readable code.
HOWEVER
I prefer to isolate things conceptually in a way that lets me have each service speak at most two protocols -- one "up" the service chain and one "down" the service chain. In this case "up" might mean knowing how to speak IRC, and "down" might mean knowing how to speak to a channel handler.
If I think about the bot this way the question of "who owns the socket?" goes away because the answer is obvious: the one that knows how to speak IRC and nobody else.
Heading this direction I would write a gen_server module that knows how to speak IRC (or, maybe more likely, a gen_fsm -- I would have to look at the protocol to decide), a gen_server module that behaves as a bot in-channel (a channel handler), and a module of functions that can do whatever string/binary processing you need.
So one IRC talker per server, and below that one channel handling process per channel you join. The string handling would, in this case, not be processes unless you chose to spawn a process per message you want to handle (which is totally possible, but probably not an approach needed in the case of IRC channels).
Thus far that's potential modules like
irc_talker.erl
chan_hand.erl
message_munger.erl
This is three modules now, though... Oh my! As for making that into an OTP application... You will probably wind up with a "supersup" that represents the whole application and manages the server connection supervisors. The server connection supervisors are the ones that spawn and manage the IRC talkers (which each represent a server connection) and a channel supervisor. Either a master channel sup would manage all the channel connections, or a channel sup per server would be spawned -- in either case the channel handlers are all free to have their own non-shared state so if anything crazy comes across a channel that crashes one the others aren't affected.
As for modules that winds up looking more like:
ircbot.erl
irc_sup.erl
irc_talker.erl
chan_sup.erl
chan_hand.erl
message_munger.erl
Hopefully that came across correctly in prose. Here is a horrible non-diagram.
Supersup
ServerSups
IRCTalkers ChannelSups
ChannelHandlers
Imagine you can see lines of communication among them and what they would need to say to get your job done. Make sure you say those things and that things blow up in a way that makes recovery obvious and repeatable.
The concrete advantage is writing pure-functions in the message-munging module only. The (probably never-to-be-realized) advantage of this is that you could replace the IRC talker with any other protocol (so long as that new talker conforms to the "downward" inner protocol of the application) and use your same bot code to talk to whatever other text chat service you feel like.
Upvotes: 4