Sunday, August 29, 2010

Surviving Restart

‹prev | My Chain | next›

Today, I would like to see if I can restart my (fab) game without the usual assorted problems. Before the switch to a permanent store (naturally CouchDB) this would have been impossible. At this point, I only need to restore the in-code setTimeouts, which are used to idle timeout players. Every other attribute should already be in CouchDB.

First up, I return the idle timeout epoch milliseconds from the idle_timeout method:
  timeout: 30*60*1000,
timeouts: { },

idle_watch: function(id) {
if (this.timeouts[id])
clearTimeout(this.timeouts[id]);

var self = this;
this.timeouts[id] = setTimeout(function() {
Logger.info("timeout " + id +"!");
self.drop_player(id);
}, self.timeout);

return (new Date((new Date()).getTime() + self.timeout)).getTime();
}
The actual setTimeout is specified in milliseconds that it will run. I hope to use the epoch seconds returned from idle_timeout to re-establish timeouts. The difference between the epoch milliseconds and the current epoch milliseconds is the remaining number of milliseconds in the timeout:
node> new Date((new Date()).getTime() + 30*60*1000).getTime();
1283131853957
node> new Date(1283131853957);
Mon, 30 Aug 2010 01:30:53 GMT
node> 1283131853957 - (new Date).getTime();
1533424
The only time the idle_timeout gets updated is during update_player. That method is also responsible for storing updated player status in the backend. I can use that to store the timeout time:
  update_player_status: function(status) {
var self = this;
this.get(status.id, function(player) {
Logger.debug("[players.update_player_status] " + inspect(player));
if (player) {
Logger.info("players.update_player_status: " + status.id);
player.status = status;
player.timeout = self.idle_watch(status.id);
db.saveDoc(player);

}
else {
Logger.warn("[players.update_player_status] unknown player: " + status.id + "!");
}
});
}
With that being stored in CouchDB:



I can add a bit of code to the players init() method to attempt to restore player timeout:
  init: function() {
var self = this;

// Now, in epoch milliseconds
var now = (new Date()).getTime();

// Grab all players from CouchDB
self.all(function(players) {
players.forEach(function(player){
// Difference between recorded time and now
var timeout = player.timeout - now;

// If time left before timeout, begin the wait again
if (timeout > 0) {
Logger.info("[init_timeouts] " + player._id + " " + timeout);
self.idle_watch(player._id, timeout);
}
// Otherwise drop the player
else {
Logger.info("[init_timeouts] dropping: " + player._id);
self.drop_player(player._id);
}
});
});


// Ensure that the faye server has fully established by waiting
// half a second before subscribing to channels
setTimeout(function(){ self.init_subscriptions(); }, 500);

return self;
}
That seems to work just fine unless a player needs to be deleted. In that can, the drop_player() method needs to broadcast via faye that the player is no longer in the room. The problem is that the faye client is not immediately available.

To get around this, I move the restore-players functionality into an init_players() method to be invoked after the faye client is ready:
  init: function() {
var self = this;

// Ensure that the faye server has fully established by waiting
// half a second before subscribing to channels
setTimeout(function(){
self.init_subscriptions();
self.init_players();
}, 500);

return self;
}
That is all well and good, but does it work?

video

That is pretty freaking cool! I move my player about in the room a little while, then stop the node.js server with a Ctrl+C. After waiting a few seconds, I restart the server and see that the timeout is restored:
cstrom@whitefall:~/repos/my_fab_game$ ./game.js
[INFO] Starting up...
[INFO] [init_timeouts] bob 1793694
...
Most importantly, I can continue playing as if nothing happened.


Day #210

No comments:

Post a Comment