Skip to content

Services

Duncan edited this page Mar 25, 2020 · 4 revisions

We use a services model. Here's how it works:

Each service gets its own thread running a looping function. This function holds some portion of the state, and reads from a list of messages that result in alterations to the state as well as calls to other services or potentially other side effects such as printing to the console or sending data over sockets.

State

The service holds a part of the state of the server. For instance, the player state service holds a list of all the players and their properties. Nothing in the codebase can interact with that list of players outside of that service.

Messages

The services generally* run a function that waits to receive messages on a channel, then handles each message one at a time. These channels are unidirectional- meaning that those who sent the messages never receive any response to their message. The outcomes of these messages could be a change to state (i.e. change the position of a player), have side effects (i.e notify all players that this player has moved), or both. While some services have to interact with components in which failure is entirely possible (sending over the network) most services simply make calls to other services.

*The exception to this is the keep alive service that just uses a timer instead of reading messages, but generally follows the same event loop model

Reasoning

State isolation

This comes from the experience that shared, mutable state tends to be a constant source of bugs and contention issues. Keeping state easy to reason about by not allowing multiple points of contact will reduce the number of these issues. There is also some benefit to modularity- we can change how state is stored in a service without changing any other part of the code since they cannot rely upon a certain structure, only upon the channel messages. There are also performance implications, since we can safely take advantage of multithreading and run all of the services at once.

This doesn't come for free though- there have certainly been times where I felt like I needed multiple pieces of state- or I needed to coordinate two services together. Almost every time, this has been remedied by changing the model. When there is no other way around it, we can chain the services together to gather up all the needed state. Very important to note that if you send a message to service A, then a message to service B right after there is absolutely no guarantee that service A will process the message before service B, so coordination requires chaining.

Unidirectional Channels

In the context of the game server, downstream errors tend to be either tolerable or unrecoverable. This is an assumption that I've made that could possibly be challenged given new information, but so far I haven't found a real use case for backwards feedback. This assumption greatly simplifies the code- our services no longer have to worry about anything going wrong in the downstream, they are effectively simple functions that take a message, their own state, and output a set of state changes and messages of their own to send to other services. It also makes everything asynchronous by default, since the services can only communicate asynchronously.

Example Service

Here is the code for the player service. You should be able to identify the messages, how they alter the player state, and how they make calls to other services (the send_packet macro expands into a call to the messenger service)

Clone this wiki locally