Sunday, March 4, 2012

Complete is not Broken

‹prev | My Chain | next›

I ran into a weird behavior in exceptional Dart Futures yesterday. In the simplest case, the following works:
main() {
  var completer = new Completer();
  var future = completer.future;

  future.handleException((e) {
    print("Handled: $e");
    return true;
  });

  completer.completeException("That ain't gonna work");
}
(try.dartlang.org)

In this case, the completer completes with an exception, not with the normal successful complete(). But, since the future associated with the completer injects a handleException() method, the exception is handled. More specifically since the injected handleException() returns true, the exception is handled.

If I remove the true return, I see both the "Handled" print() statement as well as the exception bubbling all the way up:
Handled: That ain't gonna work
Exception: That ain't gonna work
Stack Trace:  0. Function: 'FutureImpl._complete@924b4b8' url: 'bootstrap_impl' line:3124 col:9
 1. Function: 'FutureImpl._setException@924b4b8' url: 'bootstrap_impl' line:3146 col:14
 2. Function: 'CompleterImpl.completeException' url: 'bootstrap_impl' line:3207 col:30
 3. Function: '::main' url: 'file:///home/cstrom/repos/dart-book/book/includes/no_more_callback_hell/main.dart' line:12 col:30
(try.dartlang does not seem to care about the return value—it always considers handleException() to have handled the exception)

My problem is that, although this behaves as expected in a simple case, it does not seem to work as expected in a more complex case. In my Hipster MVC library, my syncing layer that is responsible for coordinating in-browser data with permanent storage can have different behavior injected. So the HipsterSync.call() class method first needs a conditional to return a Future from the appropriate behavior (_defultSync in this case):
class HipsterSync {
  // ...
  static Future<Dynamic> call(method, model) {
    if (_injected_sync == null) {
      return _defaultSync(method, model);
    }
    else { /* ... */ }
  }
  // ...
}
The default syncing behavior then creates the completer and invokes completeException() when a non-200 response is received from the backend:
class HipsterSync {
  // ...
  static Future<Dynamic> _defaultSync(method, model) {
    var request = new XMLHttpRequest(),
        completer = new Completer();

    request.
      on.
      load.
      add((event) {
        var req = event.target;

        if (req.status > 299) {
          completer.
            completeException("That ain't gonna work: ${req.status}");
        }
        else { /* ... */ }
      });
    // ...

    return completer.future;
  }
}
Finally, the HipsterModel class is invoking HipsterSync.call() and trying to do the right thing with handleException():
class HipsterCollection implements Collection {
  // ...
  create(attrs) {
    Future after_save = _buildModel(attrs).save();

    after_save.
      then((saved_model) {
        this.add(saved_model);
      });

    after_save.
      handleException(bool (e) {
        print("Exception handled: ${e.type}");
        return true;
      });
  }
  // ...
}
When I generate a 409, however, that handleException() does not kick in. Instead, I see the exception bubbling all the way up to the Dart console:
POST http://localhost:3000/comics 409 (Conflict)
Exception: That ain't gonna work: 409
Stack Trace:  0. Function: 'FutureImpl._complete@924b4b8' url: 'bootstrap_impl' line:3124 col:9
 1. Function: 'FutureImpl._setException@924b4b8' url: 'bootstrap_impl' line:3146 col:14
 2. Function: 'CompleterImpl.completeException' url: 'bootstrap_impl' line:3207 col:30
 3. Function: 'HipsterSync.function' url: 'http://localhost:3000/scripts/HipsterSync.dart' line:44 col:30
 4. Function: 'EventListenerListImplementation.function' url: 'dart:htmlimpl' line:23163 col:35
Seemingly, the problem lies somewhere in between the simplest use-case and my complex Hipster MVC. So I start expanding from this simple case to more and more complex cases. After a time, I have worked through no less than six test cases on increasing complexity—all of them working. In my last test case, I have a static method from one class produce a completer exception that another class handles. Both are in separate libraries.

They say you can't prove a negative, but I seem to have just done so.

So I go back to my code and, indeed, the bug was mine. I eventually realize that the Future in HipsterCollection is not coming from HipsterSync as I thought. Rather it comes from HipsterModel:
class HipsterCollection implements Collection {
  // ...
  create(attrs) {
    Future after_save = _buildModel(attrs).save();
    // ...
  }
}
In hindsight this seems perfectly obvious.

When I first refactored into Completers and Futures, I did take into account that HipsterModel required changes as well:
class HipsterModel {
  // ...
  Future<HipsterModel> save() {
    Completer completer = new Completer();

    HipsterSync.
      call('post', this).
      then((attrs) {
        this.attributes = attrs;
        on.load.dispatch(new ModelEvent('save', this));
        completer.complete(this);
      });

    return completer.future;
  }
}
What is missing from that implementation is concern about an exception from HipsterSync.call(). The only thing that I do is wait patiently for a then() that never comes. So, just as I do in the HipsterCollection, I grab the Future returned by HipsterSync.call() and inject then() and handleException() behavior:
class HipsterModel {
  // ...
  Future<HipsterModel> save() {
    Completer completer = new Completer();

    Future after_call = HipsterSync.call('post', this);

    after_call.
      then((attrs) {
        this.attributes = attrs;
        on.load.dispatch(new ModelEvent('save', this));
        completer.complete(this);
      });

    after_call.
      handleException((e) {
        completer.completeException(e);
        return true;
      });

    return completer.future;
  }
}
It feels a bit much having to completeException() / handleException() all the way from HipsterSync, through HipsterModel and on up to HipsterCollection. Then again, the model has need to perform error handling before notifying the collection that something went wrong, so I see no real way around this.

At any rate, the Future exception is now captured in the model, re-raisd to the collection in such a way that the collection can handle it:


It is difficult when working with new languages to judge whether a mistake like this lies with the language (e.g. too much complexity) or myself. In this case, my understanding of the technique was solid, but I muffed the implementation—was that Dart's fault or my own? I think in this case, it was my own. I think the architecture that I have chosen necessitates this type of propagation on occasion and I need to be cognizant of this. Re-examining my ultimate solution, I do not see anywhere that Dart could have eased my burden. Then again, perhaps I simply lack imagination.


Day #315

No comments:

Post a Comment