14

I'm using setTimeout in Node.js and it seems to behave differently from client-side setTimeout in that it returns an object instead of a number. I want to store this in redis, but since redis only stores strings, I need to convert the object to a string. However, using JSON.stringify throws a circular reference error. How can I store this object in redis if I want to be able to fetch it from redis and call clearTimeout on it?

user730569
  • 3,940
  • 9
  • 42
  • 68
  • I don't think the `setTimeout` call has anything to do with the circular reference error. http://stackoverflow.com/questions/1493453/example-of-a-circular-reference-in-javascript – Trevor Jul 02 '12 at 20:40
  • @Trevor `setTimeout` creates this object: `{ _idleTimeout: 1000000000, _idlePrev: { _idleNext: [Circular], _idlePrev: [Circular], ontimeout: [Function] }, _idleNext: { _idleNext: [Circular], _idlePrev: [Circular], ontimeout: [Function] }, _onTimeout: [Function], _idleStart: Mon, 02 Jul 2012 20:28:18 GMT }` – user730569 Jul 02 '12 at 20:42
  • 2
    `_idleNext` and `_idlePrev` keys seem to be circular references... – user730569 Jul 02 '12 at 20:42
  • I gather from your comments that what you really need is using redis to scale socket.io across instances. I recommend you look into using socket.io's `RedisStore` -- see e.g. [this answer of mine](http://stackoverflow.com/questions/9267292/examples-in-using-redisstore-in-socket-io/9275798#9275798). – Linus Thiel Jul 03 '12 at 11:09
  • @LinusGThiel Yes, I'm already using Socket.io's `RedisStore`. The problem lies in storing the return value of the `setTimeout` method though. – user730569 Jul 03 '12 at 15:20
  • 1
    Ok, I see. Can you tell us some more about what you need the timeouts for? I.e., what are you actually trying to accomplish? Perhaps, there's a better way. – Linus Thiel Jul 03 '12 at 15:25
  • @LinusGThiel It's to check to see if a user has accidentally disconnected from the websocket connection for greater than X seconds (internet timeout for example). Which is different from going from page to page where you disconnect briefly. So I create a timer on the `disconnect` event but if they return within X seconds I grab the timer from storage and clear it. Otherwise, the timer emits an "accidental disconnect" event after those X seconds. – user730569 Jul 03 '12 at 15:31
  • I get the feeling you need to think over this a little bit. The user disconnects. If s/he reconnects, you don't want to do anything. Otherwise, you want to emit an "accidental disconnect" to where, exactly? – Linus Thiel Jul 03 '12 at 15:46
  • @LinusGThiel To the other sockets connected to the same channel. – user730569 Jul 03 '12 at 15:52
  • In that case, you should be able to just `emit` to the channel on that one instance which handled the user initially. I'm pretty sure RedisStore will handle the propagation to the other instances. – Linus Thiel Jul 03 '12 at 15:59
  • @LinusGThiel Oh I see... is that the redis `publish/subscribe` stuff? – user730569 Jul 03 '12 at 16:02
  • Right, so with the default `MemoryStore`, if you do e.g. `io.sockets.of('foo').emit('foo', 'bar')`, that will be sent to all clients connected *to that instance*. Using `RedisStore`, the message will be sent to all clients *on all instances*, leveraging redis' "`publish/subscribe` stuff". – Linus Thiel Jul 03 '12 at 16:05
  • @LinusGThiel ohhh I had no idea it uses that in the background for rooms. Very cool. What about with channels? I'm using `socket.join("channel name");` – user730569 Jul 03 '12 at 16:06
  • I'm pretty sure socket.io "rooms" and "channels" are the same thing -- i.e., doing `var socket = io.connect(); socket.join('foo');` will join the room `'foo'`, and `var socket = io.connect('/foo')` will also join the room `'foo'`. – Linus Thiel Jul 03 '12 at 16:09
  • @LinusGThiel Just realized something. Emitting on the channel to sockets connected to all instances will work when propogating the "accidental disconnect" event. However, if I do not store the object returned from `setTimeout` in redis, how will I be able to grab that object and clear the timeout when the client reconnects? Is there a way to make sure the client reconnects to the same server on the same instance? Otherwise the newly connected server won't be able to find the timer object on the previous server if it's in memory with a different instance and/or different node process, right? – user730569 Nov 28 '12 at 02:52

4 Answers4

2

You cannot store the object in Redis. The setTimeout method returns a Handler (object reference).

One idea would be to create your own associative array in memory, and store the index in Redis. For example:

var nextTimerIndex = 0;
var timerMap = {};

var timer = setTimeout(function(timerIndex) {
    console.log('Ding!');

    // Free timer reference!
    delete timerMap[timerIndex];
}, 5 * 1000, nextTimerIndex);

// Store index in Redis...

// Then, store the timer object for later reference
timerMap[nextTimerIndex++] = timer;

// ...
// To clear the timeout
clearTimeout(timerMap[myTimerIndex]);
legege
  • 421
  • 4
  • 12
1

I was attempting to do the same thing as the OP. My solution was to set the timeout with a conditional check on a new key inside the timeout in my disconnect handler:

redis.hset("userDisconnecting:" + userId, "disconnect", 1);

setTimeout(function() {
    redis.hget("userDisconnecting:" + userId, "disconnect",
     function(err, result) {
        if (result.toString() === "1") {
           //do stuff, like notify other clients of the disconnect.
        }
    });
}, 10000);

Then, when the client connects again, I set that key to 0, so the stuff that needs to fire on true disconnect doesn't happen:

redis.hset("userDisconnecting:" + userId, "disconnect", 0);

The timeouts themselves aren't persistent across server restarts, but you could solve that by kicking off a sweeper method on startup. Connected clients would come back "online" pretty quickly.

Mike Atlas
  • 8,193
  • 4
  • 46
  • 62
1

In the newer versions of node, you can use the Id of the Timeout object instead of the object itself to end the loop.

   redisClient.set('time', JSON.stringify(10))
   let timeoutObject = setInterval(async function(){
      let time = await JSON.parse(redisClient.get('time'))
      if(time === 0){
       let intervalId = await JSON.parse(redisClient.get('intervalId'))
       clearInterval(intervalId)
      }
       time -= 1
       redisClient.set('time', JSON.stringify(time))
    }, 1000)
    
    let intervalId = timeoutObject[Symbol.toPrimitive]()
    redisClient.set('intervalId', JSON.stringify(intervalId))

This is just an example of a timer built with setInterval and redis combined. As you can see, you can grab the Id of the Timeout Object and store that to end setInterval's execution instead of trying to store the whole object.

Here is the link to the node docs: https://nodejs.org/api/timers.html#timers_timeout_symbol_toprimitive

Goose9192
  • 107
  • 1
  • 6
0

This code is used when the timeouts need not be persistent across server restarts

var timeouts = {};

app.get('/', function (req, res) {
  var index = timeouts.length;
  timeouts[index] = setTimeout(console.log, 1000000, req.user.name);

  redis.set('timeout:' + req.user.name, index, function (err, reply) {
    res.end();
  });
});

app.get('/clear', function (req, res) {
  redis.get('timeout:' + req.user.name, function (err, index) {
   clearTimeout(timeouts[index]);
   delete timeouts[index];
   redis.delete('timeout:' + req.user.name);
   res.end();
  });
});

If you need timeouts to be persistent across server restarts, then you might need to store _idleStart and _idleTimeout values for every timer in the redis, and load them up everytime you server restarts

app.get('/', function (req, res) {
  var timeout = setTimeout(console.log, 1000000, req.user.name);
  var time = timeout._idleStart.getTime() + timeout._idleTimeout;

  redis.set('timeout:' + req.user.name, time, function (err, reply) {
    res.end();
  });
});

app.get('/clear', function (req, res) {
  redis.delete('timeout:' + req.user.name);
  res.end();
});

// Load timeouts on server start
// *I know this is not the correct redis command*
// *It's not accurate, only approx*
redis.get('timeout:*', function (err, vals) {
  vals.forEach(function (val) {
    var time = val - new Date().getTime();
    setTimeout(console.log, time, username)
  });
});
Pavan Kumar Sunkara
  • 3,025
  • 21
  • 30
  • You're leaking timer object reference. – legege Jul 02 '12 at 22:00
  • @user730569 That won't work... the timer will be gone after a restart. You need to find a way to reschedule them after a restart. You can persist the start time, and recalculate the timeout value on restart. – legege Jul 02 '12 at 22:15
  • @legege I didn't mean server restarts... what I really mean is across instances, so anything in memory isn't an option – user730569 Jul 02 '12 at 22:27
  • 1
    @user730569 Across instances? Is the timer started on the n instances? Should it be canceled everywhere? Please provide a better context in your question. – legege Jul 02 '12 at 22:34
  • @legege Ok so I'm using socket.io storage, and the documentation claims that in order to scale across multiple instances, I need to move away from memory store and towards redis. – user730569 Jul 02 '12 at 22:53
  • @user730569 Ok, and your timer? – legege Jul 03 '12 at 00:28
  • @legege to be honest, I'm really not sure. I've never scaled an app past an instance so I have no idea. Your in memory solution will definitely work for me for now though. – user730569 Jul 03 '12 at 01:29