ElixirSeattle/tanx

Architecture question

Closed this issue · 3 comments

Hi! Both this project and the talk at ElixirConf 2018 are fantastic!

I'm current trying to dig deeper into OTP and was wondering if you could give me an overview of the tanx app architecture.

I've found it to be difficult to see working, open source projects that deal extensively with OTP. If you know some, please let me know!

It's difficult to put this in words, but I would love to know more about:

  1. naming conventions: why did you name things the way you did? Were you following a guideline?

  2. OTP architecture: how did you decide on the final OTP architecture? Same as above, were you following a guideline?

  3. I've found it difficult to find non-trivial OTP usage (like i believe you demonstrate here) in the books I've read so far. It seems to be a topic that is lightly brushed upon in the end of books, but not discussed in greater detail.

Maybe the answers this yields would help you write a book of your own! I'd buy it!

Thanks again for doing this and open sourcing the project. Great learning opportunity.

To be more clear, I'm referring to the apps/tanx app.

Hi,

Sooo... to be frank, I wouldn't put this app on too much of a pedestal as it is. This was literally the first real app that the four of us wrote in Elixir, and we were just feeling our way around how to use genservers in a way that didn't get us into too much trouble. Especially if you look at some of the early history in git, it was a mess. Think of it this way: Originally, we had separate processes for every object in the game (tanks, shots, explosions, what have you). Because I originally had the rather mistaken idea that Elixir processes should be used wherever we would think of using objects in an OO language like Ruby. (Spoiler alert: that's not the right way to think about processes.) As a result, we had a lot of unnecessary complexity in our code that was just about keeping track of all those processes and orchestrating communication among them. (And there was no supervision either. It was a mess, and inefficient as all heck to boot.)

The current code is really just one generation removed from that steaming pile of turd. I did a rewrite of the apps/tanx app a few months before the ElixirConf talk, mostly because it would otherwise not have been feasible to implement migration of those processes from one node to another. Now I think it's a bit more sane: each game is a single supervision tree, with a small, fixed number of processes and a simple coordination pattern of messages. But... yeah, I'm sure in another year or two I'll look back at even the current iteration and say, "turd".

Also, apologies for the lack of documentation and type specs. My only excuse is, this is the result of the mad scramble to "just get the demo working" by a conference date.

Anyway, I don't have much of an answer to give for your 3 specific questions. Naming conventions? Largely borrowed from what I'm used to—Ruby, and actually probably more from my earlier background in Java. (I hope it's not too nauseating to hear that.) Guidelines? Nope. Beyond the basics like "use supervision". Actually, I'd say foundational OO guidelines such as single responsibility principle still largely apply to OTP apps, and did influence the architecture here. Just reach back into your software engineering training. Things really aren't that different.

I guess the one thing I'd say from a OTP specific standpoint is, learning to work with actors is really important. There are some actor-related patterns that I only began to understand while working on Tanx. You'll often have different "kinds" of actors based on the messaging/interaction patterns. Each tanx game, for example, has two primary genservers. The main "game" genserver serves as the "entrypoint" or "interaction point". Its job is to handle calls (for example from web clients) and respond quickly. So it never performs a long computation, and never blocks (for long) inside any handle_* callback. The second genserver is what I call the "updater". It performs all the frame-by-frame updates, collision detection, all the math and potentially long computations.

Now, because I had this requirement for the main genserver to respond quickly to all messages, this dictates how the two processes are designed and how they interact. For example, the state of the game is kept in the main process because it always has to be available immediately if a client asks for it. So, how do we update it for a new frame? One might normally expect the main process to simply call the updater, passing it the current state and receiving the new state in a response. Except, we can't have the main process block while waiting for a lengthy call to return. So the interaction is reversed. The updater genserver wakes up, calls the main process to get the current game state, does its lengthy calculation, and then calls the main process again to set the new state. All to preserve the property that the main process can always respond quickly to a message.

So, bits and pieces. But I'm learning just like everyone else.

Regarding books, yeah, I hear you. I learned a lot from Sasa Juric's book Elixir in Action, but there isn't much out there about the next step of how to design nontrivial apps with OTP. I'm actually quite hopeful about (though I haven't yet read) the new pragprog book Designing Elixir Systems with OTP by James Edward Gray and Bruce Tate. I'd love to hear a review if anyone has picked it up yet.

Very interesting. Thanks for the insight.

There aren't many open source projects that use OTP extensively. As you mentioned, same with books. I did pick up the book you suggested, there is some content at the moment but the bulk of OTP stuff is yet to be written (the latter part of the book). Hopefully it will be great!

Thanks again for the project!