I am working on a simple web application that allows people to join a virtual room. Each room has a video player that synchronises the state of the video to everyone connected and there is a live chat next to it. All communication is done with Phoenix (channels).
One of my requirements is that each room expires after 24 hours of inactivity. The timeout feature of GenServers seems appropriate here.
My question is how should I structure the room component/application?
Current structure
The Dynamic Supervisor will spawn a Supervisor per room (resolved using :global and a name) that supervises two GenServers (Chat.Server
and Player.Server
).
I have a working prototype with the following structure (inspiration from Dave Thomas):
lib
├── room
│ ├── application.ex
│ ├── chat
│ │ ├── impl.ex
│ │ ├── message.ex
│ │ ├── server.ex
│ │ └── user.ex
│ ├── chat.ex
│ ├── dynamic_supervisor.ex
│ ├── player
│ │ ├── impl.ex
│ │ └── server.ex
│ ├── player.ex
│ └── supervisor.ex
└── room.ex
With this structure lib/room.ex
is the public API. All this module does (should) is forwarding calls to the Chat
and Player
modules. This feels nice: I could easily refactor those modules (Chat
and Player
) into their own components/applications.
HOWEVER! I have two problems (1) where do I put the GenServer timeout? It is not really the chat itself or the player that expires. It is the room. The room is just a supervisor though, not a GenServer and (2) there is a slight relation between the chat and the player. When all users leave, the player should pause the video. This is currently handled directly in lib/room.ex
module and looks like this:
alias Room.{Chat, Player}
###
def leave(name, user_id) do
users_left = name
|> Chat.leave(user_id)
|> Chat.count_users()
if users_left == 0 do
Player.pause(name)
end
name
end
This logic feels a bit dirty to me and I don’t like it there.
Alternative structure
One alternative is to combine the state of the two Chat
and Player
GenServers into a single Room
GenServer. Something like this:
lib
├── room
│ ├── application.ex
│ ├── chat
│ │ ├── impl.ex
│ │ ├── message.ex
│ │ └── user.ex
│ ├── chat.ex
│ ├── dynamic_supervisor.ex
│ ├── impl.ex # only for `leave`?
│ ├── player
│ │ ├── impl.ex
│ ├── player.ex
│ ├── server.ex
└── room.ex
The implementation of Chat
and Player
is still unknown to Room
. This also solves problem (1) with the timeout and kind of (2) since I won’t have that logic in the public API.
It still feels a bit wrong though: One advantage of current solution is that an error in the chat GenServer doesn’t bring the player GenServer down. Now an error in chat will also take the player down and restart the entire room basically.
I plan to open source this project but I would prefer to have solved this problem (and clean up) first. If it’s much easier to help with the current source code, I can make the repo public.