This is my current thoughts on the issue David started to fix in
https://github.com/meteor/meteor/commit/dcd26415fe3b526d72137dfc67091ac5ea4108dc
Here's the issue: You're in a method invocation, and you want to do a
write asynchronously (using Meteor.setTimeout and friends).
If you're thinking carefully about it (which many users may not be)
there are two cases you might want:
(A) This is really a "write later" situation, and you don't want to
block the method's "all committed" message on the write. (Of course,
you might run into the issue that the write never happens because the
process crashes / is shut down by the runner / etc, but hey, welcome
to software engineering.)
(B) The write is asynchronous for some good reason, but you don't
want the method to send the "all committed" message until the write in
the method is done. This is something Tinytest does: there are
asynchronous calls all over the place, and it doesn't want
tinytest/run to quiesce until all test results are committed.
Of course, there's also:
(C) You aren't thinking particularly carefully about the fact that
the write is happening asynchronously.
Now, in the current Meteor implementation, the trigger to send "all
committed" is that the method body returns and all writes created at
that point finish. So if you want to implement (B), you need to use
Futures manually to make sure you don't return from the method until
after all writes have been *started*, at the very least.
Before David's commit, Meteor.setTimeout (and similar functions)
explicitly removed the write fence from timeout callbacks. So (A)
situations "work" by making all timer callbacks never block the
method. (B) situations required avoiding Meteor.setTimeout; there was
a hacky Tinytest.setTimeout that explicitly preserved the
CurrentWriteFence.
David's commit effectively (though not technically) changes the
semantics such that timer callbacks preserve the CurrentWriteFence,
until the point where the fence fires, at which point they are mutated
to have no write fence. (The actual implementation involves setting an
onAllCommitted timeout on Meteor's one _CurrentWriteFence
instantiation which "retires" the fence, which means essentially that
all calls on it becomes no-ops.) This means that (A) cases continue
to "just work", and (B) cases work (like in Tinytest) as long as you
are doing the "wait on the callbacks using Futures" thing correctly.
However, as several people noted, it means that if you set shortish
timeouts and *don't* wait with Futures, then whether or not the write
is included in the write fence is nondeterministic. Additionally, it's
*silently* nondeterministic, in that nobody is going to notice that
sometimes their writes end up in the fence and sometimes they don't.
I do think that the current code is an improvement on last week's
code, and that it's fine to release 0.4.1 with this change... just
that we may want to make further changes in the future.
I suggest two possible ways we can go from here, together or separately:
(1)
Keep the new implementation of setTimeout which preserves the write
fence, but get rid of the concept of retiring write fences.
Add a new function called (something better than)
Meteor.dontWaitForWrites, with an implementation like
Meteor.dontWaitForWrites = function (f) {
return function () { Meteor._CurrentWriteFence.withValue(null, f); };
}
This way, users who do (A) will receive an error about trying to add a
write after the fence fired (the error message should be improved
too), but the answer if they are really trying to do this is to wrap
their callback in dontWaitForWrites:
Meteor.setTimeout(Meteor.dontWaitForWrites(function (f) {
// do some write
}, 60*1000);
(NOTE: This implementation DOES NOT WORK, because the wrapping of
dontWaitForWrites and the bindEnvironment inside setTimeout are in the
wrong order! But we could make something work.)
Users who do (B) will have things Just Work, as long as they are
waiting properly (otherwise they'll get the exception too).
Users who do (C) will be forced to think a little about what they are
really trying to do, and either put in some waits or inject
dontWaitForWrites.
(2)
Another approach is to allow methods to override the meaning of when
methods end. Inside a method, you can run something like
this.methodMayDoMoreWrites(); // with a better name
This prevents the fence from being armed as soon as the method body
returns. (The result value is still returned immediately, though.)
Then, later, from inside the callback (which should be run inside the
write fence!) you call
this.methodDoneWriting();
to arm the fence.
I *think* you still want to have dontWaitForWrites to handle the (A) case.
But this means that the (B) case now, instead of futzing around with
Futures, can just call methodMayDoMoreWrites, return, and let the
callbacks later call methodDoneWriting.
(ie, this doesn't affect the unpredictability of the current code; it
just makes writing (B) easier.)
Thoughts?