boardgameio / boardgame.io

State Management and Multiplayer Networking for Turn-Based Games
https://boardgame.io
MIT License
9.93k stars 707 forks source link

How Game state is loaded from DB? #540

Closed pociej closed 4 years ago

pociej commented 4 years ago

Hi, sorry its rather support question than issue. I'm plying a bit with multiplayer with mongo. Very strange thing happens on client i see G in some unexpected state, i checked the mongo DB to check which document was loaded, and surprise, there is no document witch marching state, so it seems that G state was modified somehow before was load client side and im trying to find out how thats possible ?

I see even more surprising behaviour :

1) I do some moves, in DB i see related documents everything is ok 2) Lets say game is now in state X, i go to mongo, remove all the documents i collection related to current game, i refresh the browser and still see game in state X even though there are no data in database, does it mean there is some caching mechanism ?

delucis commented 4 years ago

@pociej Everything is handled by the server instance:

Creating a game

  1. A user asks the Server to create a game (via the Lobby API).

  2. The Server creates a game instance, using your game’s setup function, and stores metadata and initial game state in your Database.

Joining a game

  1. A user asks to join a specific game ID and player ID (via the Lobby API).

  2. The Server updates the metadata for that game to add the player (if possible) and returns credentials that will be used to verify that that user has the right to play as that player ID. The Server stores the updated metadata in the Database.

Playing a game

  1. A user chooses to play a game from the React lobby, the Client is started, opening a socket.io connection to the Server.

  2. At this point, the Client dispatches the SYNC action, requesting the latest game state from the Server. The Server retrieves this from the Database and returns it.

  3. When a user makes a move, the MOVE action is sent over WebSockets to the Server.

  4. The Server processes the action and sends back the updated game state, then it also persists that updated state to the Database.

A couple of important notes

Possible causes of unexpected state

If you get state you don’t expect when refreshing a game (i.e. the Server is providing something unexpected), but the database document doesn’t match that state, two possibilities come to mind:

  1. The Server is failing to store the state in the Database (sometimes? always?) Because of the caching, the Server’s own state will usually be up-to-date, because it just keeps the object in memory. The Database on the other hand might hang, be misconfigured, or the game state might be in a form that can’t be correctly serialized. I ran into trouble for example at one point because I had an array of arrays, which Firestore doesn’t support, so the state was never persisted even though the game appeared to progress as expected.

  2. Something other than the Server is writing to the Database documents. Any direct writes to the game state objects, bypassing the Server, won’t be picked up by the caching layer and therefore risk not being passed on to a user.

pociej commented 4 years ago

@delucis thx for reply. Anyways i dont see most important thing in this answer. Is there some caching ? And if so how to disable it? I want to be able to remove document from database directly and make sure that server is notified so next time i open client i see the state up to date with DB.

delucis commented 4 years ago

Sorry perhaps my reply was a little long, see under the notes section:

The Mongo and Firestore database implementations use lru-cache for in-memory caching between the Server and the Database. This avoids reading from the database every time something is needed and is safe because for now the Server design does not expect anything but its one instance to be writing to the Database. This is why you see what you do in 2) above; restarting the server would remove those deleted documents from the cache.

There isn’t currently a way to completely disable this, but you can pass cacheSize to the database constructor:

const { Mongo } = require('boardgame.io/server')

const db = new Mongo({
  url: 'mongodb://...',
  dbname: 'bgio',
  cacheSize: 2
})

Unfortunately, setting this to 0 will actually create an infinite cache (see the lru-cache docs), so you’re stuck setting it to some low number. Given each game has 2 cache “entries” (one for the state, one for the metadata), setting it to 1 would invalidate the cache most of the time. (Obviously this still isn’t ideal in your case but maybe OK for prototyping.)

That said, you might want to fork the Server code to add a delete endpoint, rather than having a separate path to deleting database documents. That endpoint could use the database implementation’s remove method ensuring the cache is updated as expected.

pociej commented 4 years ago

Oh thats me who should say sorry. Indeed your reply was long and i lost important part, sorry. I would rather add a way to make sure server observes DB so we have real data in client than creating separate endpoint. Thx a lot for clarification.