Hello! I'm working on an online turn based multiplayer game as a hobby project. My game is implemented using Node.js and Javascript.
Currently I'm designing how implement the persistent storage so that games can recovered in case of server crash.
My idea is to use a in-process memory store for data access in the game server logic. When a store modification is being done, the in-process memory data store is always updated directly and the persistent storage changes are queued. I was thinking of something along the lines of this:
function Store(memoryStore, persistentStore) {
// Different queues for operations of different importance.
// If the different operations need to happen in order and be in consistent state, they must be placed in same queue
this.queue = new Queue(persistentStore);
// Could be something like game chat. Saving them at different time doesn't affect the atomicity
this.lessImportantQueue = new Queue(persistentStore);
this.actionHandler = new ActionHandler();
this.doOperation = function(action) {
// Generate applicable change using the memory store as source of truth
var actionResult = actionHandler.handleAction(memoryStore, action);
memoryStore.doOperation(actionResult);
this.queue.push('doOperation', [actionResult])
}
this.doLessImportantOperation = function() {
memoryStore.doLessImportantOperation();
this.lessImportantQueue.push('doLessImportantOperation', arguments)
}
this.getSomeData = function() {
return memoryStore.getSomeData();
}
}
function ActionHandler {
// Generates operation using the memory storage as source of truth
this.handleAction = function(memoryStore, action) {
return action.complete(memoryStore);
}
}
// Stores game state altering operations
function Queue(store) {
this.items = [];
this.push = function(operation, args) {
this.items.push([operation, args]);
}
// Can be called directly, after certain queue size or at an interval
this.synchronize = function() {
var p = Promise.resolve();
this.items.forEach(function(op){
p = p.then(function(){ return store[op[0]].apply(persistentStore, op[1]); });
});
this.items = [];
}
}
// Special type of store that has synchronous interface
function MemoryStore() {
this.getState = function() {
...
}
this.getSomeData = function() {
return ...
}
this.doOperation = function(actionResult) {
...
}
}
// Uses external persistent storage
function PersistentStore() {
// Get state as an object that can be used for creating a memory store
this.getState = function() {
...
}
this.getSomeData = function() {
...
return ...
}
this.doOperation = function(actionResult) {
...
return Promise.resolve();
}
}
The Store class is used as top level layer for fetching and storing data.
Contrary to the example, I also considered being able to use a remote data stores for primary data access. The good side with that would be that the server wouldn't need to hold the whole game state in memory, consistent game state could be read from different processes and the server would be quite much stateless. The two latter benefits are highly questionable. The downsides I found were
1) either all server code involving data fetching would need to be asynchronous or the whole state would need to be fetched in advance.
2) data fetching would be a lot slower from remote database, even one like Redis
Example:
this.doOperation = function(action) {
// Generate applicable change using a store as source of truth
actionHandler.handleAction(mainStore, action).then(function(actionResult) {
// Main store can either memory store with asynchronous wrapper or a persistent store
return mainStore.doOperation(actionResult);
});
}
As the game server is normally a single process, store like Redis should not be needed as primary data store. Deciding between use of the memory store (synchronous) or possible use of both memory store (asynchronous wrapper) and persistent storages (asynchronous) has big implications for the codebase. With choice to use asynchronous stores, essentially lots of code would be asynchronous in places where one wouldn't expect asynchronous code.
Every persistent store function call would be an atomic operation. Thefore the game state would stay consistent if the game server would shut down, whether some of the operations are still being completed or not. This structure would allow changing the frequency of storage which would mainly effect performance. Buffering the operations would allow for less round trips to the database. The performance would not likely be problem with any of the solutions I introduced, but would allow more games to be run in one server.
I'm currently thinking about following questions:
1) Does the choice of the primary and secondary database make sense here?
2) Does it make sense to exclusively use synchronous in-process memory database for game data processing instead of database such as Redis or another type of database like RDBMS or noSQL database?
3) Is the plan for the storing data and queueing reasonable? I haven't seen literature nor good design patterns for this yet. Having to repeat the queue push for every operation seems kind of unnecessary. I'm also thinking if I'm overengineering.
TL;DR: Should a turn based game store its state in memory and use asynchronous periodic backup or should game data be stored remotely directly at cost of lots of codebase being filled with callbacks/promises?
Feel free to also note if my post doesn't provide enough information for allowing discussion.