Skip to content
This repository has been archived by the owner on Apr 22, 2023. It is now read-only.

nextTick documentation and warning message are confusing #6718

Closed
davepacheco opened this issue Dec 16, 2013 · 19 comments
Closed

nextTick documentation and warning message are confusing #6718

davepacheco opened this issue Dec 16, 2013 · 19 comments

Comments

@davepacheco
Copy link

A user of node-vasync reported this warning message emitted by Node on the latest v0.10:

Trace: (node) warning: Recursive process.nextTick detected. This will break in the next version of node. Please use setImmediate for recursive deferral.
    at maxTickWarn (node.js:377:17)
    at process.nextTick (node.js:480:9)
    at onwrite (_stream_writable.js:260:15)
    at WritableState.onwrite (_stream_writable.js:97:5)
    at WriteStream.Socket._write (net.js:651:5)
    at doWrite (_stream_writable.js:221:10)
    at writeOrBuffer (_stream_writable.js:211:5)
    at WriteStream.Writable.write (_stream_writable.js:180:11)
    at WriteStream.Socket.write (net.js:613:40)
    at Console.log (console.js:53:16)
    at WorkQueue.worker (/Users/linda/jpc2/jpc-report-db/scripts/zuora_verify_usage_load:102:11)
    at /Users/linda/jpc2/jpc-report-db/node_modules/vasync/lib/vasync.js:317:6
    at process._tickCallback (node.js:415:13)

This is a classic misuse of process.nextTick: the caller is using a vasync Queue with a synchronous worker function, and the Queue dispatches each new task using process.nextTick. As a result, once the first task completes, all subsequent ones are dispatched and completed on the same tick. If that number is more than 1000, we get this warning (though the program is arguably operating correctly, as this is not an infinite loop).

While this is sort of a programmer error, I think there are two concrete Node changes that would improve the situation.

First is the warning message. There's no recursion going on here. It's just a loop. The code refers to "depth", but it's not stack depth -- it's just a loop counter (as the code comment says). I get the sense in which it feels recursive, in that the function is triggering a call back into itself, but it's confusing to say that recursion is the problem because the real problem (which is only potentially detected) is event loop starvation, and that's not normally a result of recursion. I think this would be a clearer error message:

warning: infinite loop suspected after 1000 nextTick handlers were processed in the same tick.  Remaining handlers have been deferred, but future versions of Node will continue executing handlers, which may result in an infinite loop.  Please use setImmediate for deferral instead of nextTick.

That makes it much clearer what actually happened, what Node did about it, why that's still a problem for the developer, and what to do about it.

The second improvement is for the documentation for process.nextTick, which I think leads people down the wrong path. There appear to be two common use cases for nextTick():

  1. It's used within a subsystem to defer execution of a known function until the current "tick" completes, typically so that the caller can finish updating the state of an object and process the rest of the event later.
  2. It's used to defer execution of an arbitrary callback (i.e. to some other module).

I take as given that case 1 is useful, but case 2 is toxic because in case 2 there's no way to be sure to avoid the recursion problem. Users must always use setImmediate() for case 2. Since I think case 2 is overwhelmingly more common than case 1, I think the docs should strongly downplay any use of process.nextTick in favor of setImmediate. Specifically:

  • The first sentence says: "On the next loop around the event loop call this callback." That sounds too much like what people want when they really want setImmediate. nextTick should be qualified, with something like: "Call this callback the next time nextTick handlers are processed. This is normally at the start of the next loop around the event loop, but it may be much sooner if nextTick handlers are currently being processed. When nextTick handlers are processed, they are processed until none remain. As a result, calling nextTick from a nextTick handler extends the loop. Repeatedly doing so can thus starve the event loop, preventing any other events (e.g., network or filesystem I/O) from being processed until all handlers are processed."
  • The example of a "hazardous" API should not be using nextTick at all. If the API function is being called in a nextTick() handler, the program may run into the recursion problem.
  • The process.nextTick docs should explicitly refer users to setImmediate().

I'd be happy to draft these changes, if this sounds good.

@othiym23
Copy link

Since we can't swap the names of setImmediate and process.nextTick, this is probably the best / least confusing thing we can do. So much 👍. I can review doc patches if you like, @davepacheco.

@gagle
Copy link

gagle commented Dec 17, 2013

nextTick and setImmediate are one of the things of node.js (along with the streams implementation) that cause more confusion and are harder to learn, basically because the documentation leads to a bad understanding of the problem.

nextTick vs setImmediate, visual explanation

@rlidwka
Copy link

rlidwka commented Dec 17, 2013

setImmediate runs once per event iteration. nextTick runs multiple times per iteration. That's all the difference there is.

If your code depends on what fires first in series of deeply nested callbacks, you're doing it wrong.

@tjfontaine
Copy link

@rlidwka amen.

@davepacheco if you want to draft the changes that would be great, otherwise in the short term I'll land a rewording of the error message in master.

@tjfontaine
Copy link

and by master, I meant v0.10 since the warning doesn't exist in master

davepacheco pushed a commit to davepacheco/node that referenced this issue Dec 17, 2013
davepacheco pushed a commit to davepacheco/node that referenced this issue Dec 17, 2013
@davepacheco
Copy link
Author

@othiym23 and others: Thanks. I submitted a PR with suggested changes for the docs.

@sam-github
Copy link

/to @rlidwka It will soon not be true that setImmediate runs once per iteration, see fa46483

I can't think of a reason anymore to use .nextTick(), other than writing code with subtle timing and ordering dependencies, which has never been a good idea.

@rlidwka
Copy link

rlidwka commented Dec 17, 2013

@sam-github , oh wow, I didn't know that they landed it.

Well, now there is almost no difference between them at all. Time to deprecate something. :)

@tjfontaine
Copy link

I am still of the opinion that fix should have gone back to v0.10 but neither here nor there, it was a deficiency in my initial implementation that was not caught.

If you could have been with me at lunch you would have heard me going on at length about how excited I was to deprecate nextTick.

I did a quick test swapping nextTick out for setImmediate and there were no test failures aside from tests that are actually designed to check the implementation details of nextTick.

I haven't done any benchmarking -- though @trevnorris suggests there may be some impact, I doubt any impact is insurmountable.

The path forward as I see it,

  • land documentation improvements on the v0.10 branch
  • util.deprecate(process.nextTick)
  • replace internal uses of nextTick with setImmediate
  • collect performance data
    • IFF negative impacts are found that cannot be fixed otherwise, abstract nextTick into an internal mechanism and leave the public facing API deprecated.

@sam-github
Copy link

I'm not sure actually deprecating nextTick is very friendly, it was the recommended way to some things for a long time, how about making nextTick and setImmediate identical?

@rlidwka
Copy link

rlidwka commented Dec 17, 2013

how about making nextTick and setImmediate identical?

If you want them to be identical, they should exist in the same object and have the same name. Let's move nextTick from process to global and rename it to setImmediate, I'm fine with that. :)

@tjfontaine
Copy link

This decision making process is actually particularly hard:

On the one hand I am relatively in favor of changing the implementation to be setImmediate as I did in my test.

However that seems particularly impolite to swap out the implementation under the user without actually notifying them.

It seems like it's most polite to leave the implementation there, deprecate it, and mark it as receiving no further updates.

There is a semantic change happening here, and while I'm not fond of nextTicks semantics it's important to note that it does behave differently, albeit in a now discouraged way.

@Mithgol
Copy link

Mithgol commented Dec 18, 2013

Wait, what?

I was under the impression that

  • nextTick is allowed to run before I/O and calling nextTick inside nextTick prevents I/O from happening (starving the event loop);
  • setImmediate is “less immediate” and runs after I/O and does not starve the event loop.

If setImmediate is changed to match nextTick, users can still use setTimeout with zero timeout to get its previous behaviour.

If nextTick is changed to match setImmediate, then there's suddenly no way to postpone the task but be sure that no I/O happens before the task runs.

@trevnorris
Copy link

@sam-github

I can't think of a reason anymore to use .nextTick(), other than writing code with subtle timing and ordering dependencies, which has never been a good idea.

Performance. Allowing a consistent async API, but still running the async callback before other I/O has had a chance to process. And, um, performance :)

I'm sure @tjfontaine and I are going to have a fun discussion about the deprecation of process.nextTick(). While I agree the naming is unfortunate, the functionality is necessary. Take the following:

Contrived example, but think the concept is applicable. Say we want to be able to queue up a bunch of I/O work to be done in the following way.

function runner(arg, cb) {
  var err = checkArg(arg);
  if (err)
    setImmediate(cb, err);
  continueNormally(arg, cb);
}

Problem is now if we run runner() multiple times all the requests will have already gone out. Where as in the following case we can mitigate some of that pain:

function runner(arg, cb) {
  var err = checkArg(arg);
  if (err)
    // Yes, I know the anon function semantics aren't optimal, but for
    // the sake of the example.
    process.nextTick(function() { cb(err); });
  continueNormally(arg, cb);
}

Now, from the callback we can do the following easily:

function cb(err) {
  if (err)
    cancelRunners();
}

So each request can be flagged to not go out before the transaction takes place.

For all those who want them to be identical, here's the complete patch for you to try out: https://gist.github.com/trevnorris/8011423

@jasnell
Copy link
Member

jasnell commented May 28, 2015

@trevnorris ... any reason to keep this one open?

@sam-github
Copy link

Maybe we can switch nextTick and setImmediate? Seriously, we should think about it for 3.x.

And I have not yet managed to find any coherent explanation from anyone (much less our docs) under what conditions one should choose one or the other.

@trevnorris your example makes interesting reading, but what does it have to do with performance? make all your calls to checkArgs(), then if they all succeed, make all the calls to continueNormally()... it might be fun to use the nextTick queue like that, but it sure doesn't look necessary.

Can you point to a place in node where swapping nextTick for setImmediate would cause a performance regression?

Right now, my best summary of when to use one vs the other would be:

  • before 0.12: setImmediate was really slow compared to nextTick, so always use nextTick
  • 0.12 and later, setImmediate is as fast, and nextTick gained the feature that it can recurse infinitely, starving your cpu, so always use setImmediate

This isn't great for people writing code that needs portability across a range of node versions.

Anyhow, they are fundamental APIs, and it should be possible to make some kind of brief and sensible statement about them, perhaps: "always use setImmediate, nextTick is legacy but still kept around for backwards compatbility, because it is used by some for its unique queueing semantics, and because years ago it used to be faster".

@trevnorris
Copy link

@sam-github setImmediate is fast running calls in parallel, but still slow running them in sequence. Making it run just as fast as nextTick in every case isn't technically feasible.

My example isn't about performance. It's about usability. Specifically when you want the callback to be asynchronous but want it to be called before the event loop proceeds. For example:

var server = require('net').createServer().listen(8080, listening);

server.on('connection', function onConnection(c) {
  // ...
});

var foo = 'bar';
function listening() {
   console.log(foo);
}

Internally, binding and listening to a port is synchronous on at least Linux. Though to maintain the "asynchronous" nature we still allow a callback to be called. If the listening callback was then placed in setImmediate() then it's theoretically possible to have onConnection() called first. But if you called the callback immediately then foo will not have yet been defined.

@sam-github
Copy link

I think you need to rewrite your example as:

var listened; // edit: was listening, oops
var server = require('net').createServer().listen(8080, function onListening() {
  listened = true;
});
server.on('connection', function onConnection(c) {
  assert(listened);
});

But point taken, we expect onListening before onConnection. If I understand, you are saying a sequence such as the following is possible:

. sync socket()/bind()/listen()
. tcp SYN incoming
. non-blocking accept... but it returns a sock instead of blocking because SYN has arrived?
. the sock is emitted immediately as a connection event, not in a setImmediate/nextTick?

because if that was so, setImmediate could work if the wrap of the sock as a connection and emit of such as an event was itself delayed by a setImmediate.

I take your point about nextTick always going to be faster under some conditions. If you are doing nextTicks one after the other, they will occur synchronously, with no interleaving epoll's, so will be "faster".

This comes at a price, however. Taking your example further, with the now infinite nextTick depth, the following code, while appearing async, is actually a synchronous busy-loop in the next tick handler:

// create 10000 ephemeral tcp sockets, safely async... or is it?
var servers = []
make();
function make() {
  if(servers.length>=1000) return;
  require('net').createServer().listen(0, make)
}

nextTick and setImmediate obviously do slightly different things, with slightly different behaviours, I'm not totally convinced ATM that having both of them at the same time, though, is better than having only one of them.

One issue is that nextTick and setImmediate both have determinate ordering - they queue in order of call, guaranteed. Very useful. But if you intermix the two, you have no such guarantees. And if you use nextTick, you don't have any particular guarantee that node will EVER return to the event loop. I've debugged subtle problems with this, where nextTick was used, as in the sync listen, to create "seeming asynchrony", but since the epoll loop never actually "ticks over", if your caller assumes that its getting real asynchrony, that deriving from epoll, you can actually get a busy loop.

I don't really want to convince you that nextTick should be banished from node (though I have yet to convinced that it shouldn't be at least discouraged), but I do think a clear statement about when to use the two should be in the docs, not just subtle examples of how to blow your foot off with them in github conversations.

@trevnorris
Copy link

@sam-github The two simplest places I think nextTick() should be used are in constructors (the 'listen' event is an anomaly since that is technically when the construction happens of the internal resources, not on createServer()) and when emitting an error. Not because it's the better thing to do in every case, but there are simply edge cases where it works better. And it's simpler to state it that way in documentation rather than make long edge case explanations.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests