mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-09 21:00:42 +02:00
OS.File requests like OS.File.stat would potentially be lost on b2g during auto-shutdown/auto-killing of the worker thread. This patch corrects the queue management so races with killing are not possible. A test is added that fails without the fix and passes with it. See the comments in the test and test for further details. Also, the bug is nice.
100 lines
4.6 KiB
JavaScript
100 lines
4.6 KiB
JavaScript
"use strict";
|
|
|
|
Components.utils.import("resource://gre/modules/osfile.jsm");
|
|
|
|
// We want the actual global to get at the internals since Scheduler is not
|
|
// exported.
|
|
var AsyncFrontGlobal = Components.utils.import(
|
|
"resource://gre/modules/osfile/osfile_async_front.jsm",
|
|
null);
|
|
var Scheduler = AsyncFrontGlobal.Scheduler;
|
|
|
|
/**
|
|
* Verify that Scheduler.kill() interacts with other OS.File requests correctly,
|
|
* and that no requests are lost. This is relevant because on B2G we
|
|
* auto-kill the worker periodically, making it very possible for valid requests
|
|
* to be interleaved with the automatic kill().
|
|
*
|
|
* This test is being created with the fix for Bug 1125989 where `kill` queue
|
|
* management was found to be buggy. It is a glass-box test that explicitly
|
|
* re-creates the observed failure situation; it is not guaranteed to prevent
|
|
* all future regressions. The following is a detailed explanation of the test
|
|
* for your benefit if this test ever breaks or you are wondering what was the
|
|
* point of all this. You might want to skim the code below first.
|
|
*
|
|
* OS.File maintains a `queue` of operations to be performed. This queue is
|
|
* nominally implemented as a chain of promises. Every time a new job is
|
|
* OS.File.push()ed, it effectively becomes the new `queue` promise. (An
|
|
* extra promise is interposed with a rejection handler to avoid the rejection
|
|
* cascading, but that does not matter for our purposes.)
|
|
*
|
|
* The flaw in `kill` was that it would wait for the `queue` to complete before
|
|
* replacing `queue`. As a result, another OS.File operation could use `push`
|
|
* (by way of OS.File.post()) to also use .then() on the same `queue` promise.
|
|
* Accordingly, assuming that promise was not yet resolved (due to a pending
|
|
* OS.File request), when it was resolved, both the task scheduled in `kill`
|
|
* and in `post` would be triggered. Both of those tasks would run until
|
|
* encountering a call to worker.post().
|
|
*
|
|
* Re-creating this race is not entirely trivial because of the large number of
|
|
* promises used by the code causing control flow to repeatedly be deferred. In
|
|
* a slightly simpler world we could run the follwing in the same turn of the
|
|
* event loop and trigger the problem.
|
|
* - any OS.File request
|
|
* - Scheduler.kill()
|
|
* - any OS.File request
|
|
*
|
|
* However, we need the Scheduler.kill task to reach the point where it is
|
|
* waiting on the same `queue` that another task has been scheduled against.
|
|
* Since the `kill` task yields on the `killQueue` promise prior to yielding
|
|
* on `queue`, however, some turns of the event loop are required. Happily,
|
|
* for us, as discussed above, the problem triggers when we have two promises
|
|
* scheduled on the `queue`, so we can just wait to schedule the second OS.File
|
|
* request on the queue. (Note that because of the additional then() added to
|
|
* eat rejections, there is an important difference between the value of
|
|
* `queue` and the value returned by the first OS.File request.)
|
|
*/
|
|
add_task(function* test_kill_race() {
|
|
// Ensure the worker has been created and that SET_DEBUG has taken effect.
|
|
// We have chosen OS.File.exists for our tests because it does not trigger
|
|
// a rejection and we absolutely do not care what the operation is other
|
|
// than it does not invoke a native fast-path.
|
|
yield OS.File.exists('foo.foo');
|
|
|
|
do_print('issuing first request');
|
|
let firstRequest = OS.File.exists('foo.bar');
|
|
let secondRequest;
|
|
let secondResolved = false;
|
|
|
|
// As noted in our big block comment, we want to wait to schedule the
|
|
// second request so that it races `kill`'s call to `worker.post`. Having
|
|
// ourselves wait on the same promise, `queue`, and registering ourselves
|
|
// before we issue the kill request means we will get run before the `kill`
|
|
// task resumes and allow us to precisely create the desired race.
|
|
Scheduler.queue.then(function() {
|
|
do_print('issuing second request');
|
|
secondRequest = OS.File.exists('foo.baz');
|
|
secondRequest.then(function() {
|
|
secondResolved = true;
|
|
});
|
|
});
|
|
|
|
do_print('issuing kill request');
|
|
let killRequest = Scheduler.kill({ reset: true, shutdown: false });
|
|
|
|
// Wait on the killRequest so that we can schedule a new OS.File request
|
|
// after it completes...
|
|
yield killRequest;
|
|
// ...because our ordering guarantee ensures that there is at most one
|
|
// worker (and this usage here should not be vulnerable even with the
|
|
// bug present), so when this completes the secondRequest has either been
|
|
// resolved or lost.
|
|
yield OS.File.exists('foo.goz');
|
|
|
|
ok(secondResolved,
|
|
'The second request was resolved so we avoided the bug. Victory!');
|
|
});
|
|
|
|
function run_test() {
|
|
run_next_test();
|
|
}
|