diff options
Diffstat (limited to 'toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js')
-rw-r--r-- | toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js | 100 |
1 files changed, 100 insertions, 0 deletions
diff --git a/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js new file mode 100644 index 000000000..e32c37224 --- /dev/null +++ b/toolkit/components/osfile/tests/xpcshell/test_osfile_kill.js @@ -0,0 +1,100 @@ +"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(); +} |