Skip to content

Bad interaction with fake-zones. - static futures must be created in the root zone. #32556

@grouma

Description

@grouma

Creating an external tracking issue so that it can be referenced from package:test.

From an internal investigation:

This is a bug in the SDK and a bad interaction with the fake-zones.
Basically what happens is that the async-SDK caches some common futures to avoid allocating them all the time. Specifically, there is:

  /// A `Future<Null>` completed with `null`.
  static final _Future<Null> _nullFuture = new _Future<Null>.value(null);

  /// A `Future<bool>` completed with `false`.
  static final _Future<bool> _falseFuture = new _Future<bool>.value(false);

These are returned from internal classes whenever needed.
Furthermore, there are some optimizations in our Stream implementation. For example (but there are others):

      if (!identical(cancelFuture, Future._nullFuture)) {
        cancelFuture.whenComplete(() {
          result._completeError(error, stackTrace);
        });
      } else {
        result._completeError(error, stackTrace);
      }
    };

What happens now is that the first time the _nullFuture is used in the test, the current Zone is set to a custom zone that overwrites the scheduleMicrotask to redirect it to testing._FakeAsync.

However, scheduleMicrotask is used for every .then call, and with the fake-async scheduleMicrotask (which needs to be triggered by hand?) the listeners are never executed. This can be easily seen by adding one more scenario to Nate's document:

  tearDown(() async {
    var f = setUpSubscription.cancel();
    f.then((_) { print("null triggered"); });
    return f;
  });

This will never execute the .then.

The only reason it is sometimes working, is that it hits our optimizations in the stream class. There we don't even call .then but shortcut the execution since we recognize the instance and know that it will just return null.

This also explains most of Nate's scenarios: as soon as you actually call .then on the future you will get a timeout. This can be explicitly or implicitly with await. It only works, if the cancel() future is passed on directly (as an optimization as is the case in my example here), and it then hits our optimization.

For some reason the whole cancel() call must be delayed too, but I haven't tried to figure out why yet. It's probably related to some timing guarantees we give, thus calling .then on the future.

The solution is as simple as creating these static futures in the root-zone. Inside async/future.dart:

static final _Future<Null> _nullFuture = Zone.ROOT.run(() => new
      Future<Null>.value(null));

  /// A `Future<bool>` completed with `false`.
  static final _Future<bool> _falseFuture = Zone.ROOT.run(() => new
      Future<bool>.value(false));

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-core-librarySDK core library issues (core, async, ...); use area-vm or area-web for platform specific libraries.library-async

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions