Search code examples
node.jstimersocket.io

node.js scalability with event timers (setTimeout)


I'm building a turn-based text game with node.js and socket.io. Each turn has a timeout and after that the player loses the turn and it passes to the next player. I'm using the setTimeout function as I said in another question.

The problem is that I don't know how to scale that over multiple instances and maybe multiple servers. AIUI, if I set a timeout, I could only clear it in the same instance. So if a player loses his turn, for example, the timeout will be renewed with the other player turn, but this new player won't have access to the timer object to clear it, because it's running on the first player's instance.

I looked at Redis pub/sub feature (which I'll have to use anyway), but I didn't find anything about timed events or delayed publishing.

TL;DR, how can I keep an instance/server independent timer?


Solution

  • The solution I've found to this is to use some message system (Redis pub/sub in my case) to keep each player instance aware of the current status.

    Each player has a worker instance which handles his own turn (this includes the timer). When it finishes, either via a move by the player or via the timeout, it advances the turn counter and informs all instances via the pub/sub with the new turn number. All of the instances receive the message and compares the turn number with its own player number. If it matches, then the instance handles the turn and the cycle repeats.

    I'll try to provide an example (more of a pseudocode):

    // pub & sub are Redis publisher & subscriber clients, respectively
    
    function Game (totalPlayers, playerNumber) {
      this.turn = 0
      this.totalPlayers = totalPlayers
      this.playerNumber = playerNumber
    
      // Subscribe to Redis events
      sub.on('message', function (channel, message) {
        message = JSON.parse(message)
    
        switch(message.type) {
          case 'turn':
            this.onTurn(message.turn)
        }
      })
    
      sub.subscribe(this.channel, function() {
        this.checkStart()
      })
    }
    
    Game.prototype.checkStart = function () {
        // This checks if this instance  is for
        // the last player and, if so, starts the
        // main loop:
        if(this.playerNumber == this.totalPlayers - 1) {
          pub.publish(this.channel, JSON.stringify({type: 'turn', turn: 0})
        }
    }
    
    Game.prototype.onTurn = function(turn) {
      this.turn = turn
      if(this.turn == this.playerNumber) {
        this.timer = setTimeout(this.endTurn.bind(this), this.turnTime)
      }
    }
    
    Game.prototype.endTurn = function() {
      this.turn = (this.turn + 1) % this.totalPlayers
      pub.publish(this.channel, JSON.stringify({type: 'turn', turn: this.turn})
    }
    

    I had some problems with this approach and the main problem was the initial status, which wasn't quite right if the players connected almost at the same time. It's also a good idea to send information and make sure all instances are in sync.

    I hope that I made this clear if someone is running into the same problem.