Dart - Detect non-completing futures - dart

I have a Dart console app that is calling into a third-party library.
When my console app calls the third-party library the call to the method returns however my CLI app then 'hangs' for 10 seconds or so before finally shutting down.
I suspect that the library has some type of resource that it has created but has not closed/completed.
My best guess is that it is a non-completed future.
So I'm looking for ways to detect resources that haven't been freed.
My first port of call would be looking for a technique to detect futures that haven't been completed but solutions for other resource types would be useful.
I'm currently using a runZoneGuarded, passing in a ZoneSpecification to hook calls.
Edit: with some experimentation, I've found I can detect timers and cancel them. In a simple experiment, I've found that a non-cancelled timer will cause the app to hang. If I cancel the timers (during my checkLeaks method) the app will shut down, however, this isn't enough in my real-world app so I'm still looking for ways to detect other resources.
Here is the experimental code I have:
#! /usr/bin/env dcli
import 'dart:async';
import 'package:dcli/dcli.dart';
import 'package:onepub/src/pub/global_packages.dart';
import 'package:onepub/src/pub/system_cache.dart';
import 'package:onepub/src/version/version.g.dart';
import 'package:pub_semver/pub_semver.dart';
void main(List<String> arguments) async {
print(orange('OnePub version: $packageVersion '));
print('');
print(globals);
// await globals.repairActivatedPackages();
await runZonedGuarded(() async {
Timer(Duration(seconds: 20), () => print('timer done'));
unawaited(Future.delayed(Duration(seconds: 20)));
var completer = Completer();
unawaited(
Future.delayed(Duration(seconds: 20), () => completer.complete()));
// await globals.activateHosted(
// 'dcli_unit_tester',
// VersionConstraint.any,
// null, // all executables
// overwriteBinStubs: true,
// url: null, // hostedUrl,
// );
print('end activate');
}, (error, stackTrace) {
print('Uncaught error: $error');
}, zoneSpecification: buildZoneSpec());
print('end');
checkLeaks();
// await entrypoint(arguments, CommandSet.ONEPUB, 'onepub');
}
late final SystemCache cache = SystemCache(isOffline: false);
GlobalPackages? _globals;
GlobalPackages get globals => _globals ??= GlobalPackages(cache);
List<void Function()> actions = [];
List<Source<Timer>> timers = [];
int testCounter = 0;
int timerCount = 0;
int periodicCallbacksCount = 0;
int microtasksCount = 0;
ZoneSpecification buildZoneSpec() {
return ZoneSpecification(
createTimer: (source, parent, zone, duration, f) {
timerCount += 1;
final result = parent.createTimer(zone, duration, f);
timers.add(Source(result));
return result;
},
createPeriodicTimer: (source, parent, zone, period, f) {
periodicCallbacksCount += 1;
final result = parent.createPeriodicTimer(zone, period, f);
timers.add(Source(result));
return result;
},
scheduleMicrotask: (source, parent, zone, f) {
microtasksCount += 1;
actions.add(f);
final result = parent.scheduleMicrotask(zone, f);
return result;
},
);
}
void checkLeaks() {
print(actions.length);
print(timers.length);
print('testCounter $testCounter');
print('timerCount $timerCount');
print('periodicCallbacksCount $periodicCallbacksCount');
print('microtasksCount $microtasksCount');
for (var timer in timers) {
if (timer.source.isActive) {
print('Active Timer: ${timer.st}');
timer.source.cancel();
}
}
}
class Source<T> {
Source(this.source) {
st = StackTrace.current;
}
T source;
late StackTrace st;
}
I'm my real-world testing I can see that I do have hanging timers caused by HTTP connections. As I originally guessed this does seem to point to some other problem with the HTTP connections not being closed down correctly.
Active Timer: #0 new Source (file:///home/bsutton/git/onepub/onepub/bin/onepub.dart:105:21)
#1 buildZoneSpec.<anonymous closure> (file:///home/bsutton/git/onepub/onepub/bin/onepub.dart:68:18)
#2 _CustomZone.createTimer (dart:async/zone.dart:1388:19)
#3 new Timer (dart:async/timer.dart:54:10)
#4 _HttpClientConnection.startTimer (dart:_http/http_impl.dart:2320:18)
#5 _ConnectionTarget.returnConnection (dart:_http/http_impl.dart:2381:16)
#6 _HttpClient._returnConnection (dart:_http/http_impl.dart:2800:41)
#7 _HttpClientConnection.send.<anonymous closure>.<anonymous closure>.<anonymous closure> (dart:_http/http_impl.dart:2171:25)
#8 _rootRunUnary (dart:async/zone.dart:1434:47)

In general, it's impossible to find things that doesn't happen.
There is no way to find all futures in the program.
With a zone, you might be able to intercept all the callbacks being "registered" in the zone, but you can't know which of them must be called. A future can have both value handlers and an error handlers, and at most one of them will ever be called. So, just because a callback on a future isn't called, it doesn't mean the future didn't complete.
A future most likely won't keep the isolate alive, though.
An incompleted future will just be garbage collected if nothing important is hanging on to it.
The most likely culprits for keeping an isolate alive are timers and receive ports.
(The VM internal implementation of timers, and I/O, and sockets, all use receive ports, so it's really just the ports.)
Again, there is no way to find all open ports programmatically.
You need a debugger with memory inspection tools for that.
I'd recommend using the developer tools to look for instances of ReceivePort or RawReceivePort that are not being garbage collected, and see whether they are still alive.
Also be careful with runZonedGuarded.
Since runZonedGuarded introduces a new error zone (because it introduces an uncaught error handler in the new zone), an error future created inside the zone will not be seen to complete outside the zone.
That means that the code:
await runZonedGuarded(() async {
will not work if the body throws. The error of the future is handled by the zone instead of the await, so the await just sees a future which never completes.

Related

In Dart, why waiting for a trivial ("empty") StreamController stream immediately exits program?

This is the backbone of a simple program that uses a StreamController to listen on some input stream and outputs some other data on its own stream as a reaction:
import 'dart:async';
main() async {
var c = StreamController(
onListen: (){},
onPause: (){},
onResume: (){},
onCancel: (){});
print("start");
await for (var data in c.stream) {
print("loop");
}
print("after loop");
}
Output:
$ dart cont.dart
start
$ dart
Why does this code exit immediately at the await for line without executing neither print("loop") nor print("after loop") ?
Note: in the original program, onListen() will receive the input stream and subscribe to it. The loop actually works until calling subscription.cancel() on the input stream, when it also suddenly exits, without any chance to clean up.
I was surprised by this, but then again all of async/await code is converted to .then()s and it seems everything can't be perfectly translated.
It seems this is part of the answer: You need to close the stream in another task to get out of the await for. Here is a previous related question. Since we don't close it, lines after await for do not execute. Here's an example that avoids this:
import 'dart:async';
main() async {
var c = StreamController(
onListen: () {},
onPause: () {},
onResume: () {},
onCancel: () {});
print("start");
await Future.wait(<Future>[producer(c), consumer(c)]);
print("after loop");
}
Future producer(StreamController c) async {
// this makes it print "loop" as well
// c.add("val!");
await c.close();
}
Future consumer(StreamController c) async {
await for (var data in c.stream) {
print("loop");
}
}
which prints:
start
after loop
So, moral of the story is,
Never leave a StreamController unattended or weird things will happen.
Some async functions that you wait on will break your code, even bypassing your try/finally block!
I thought I knew async/await inside out, but this second point scares me now.

Service Workers and IndexedDB

In a simple JavaScript Service Worker I want to intercept a request and read a value from IndexedDB before the event.respondWith
But the asynchronous nature of IndexDB does not seem to allow this.
Since the indexedDB.open is asynchronous, we have to await it which is fine. However, the callback (onsuccess) happens later so the function will exit immediately after the await on open.
The only way I have found to get it to work reliably is to add:
var wait = ms => new Promise((r, j) => setTimeout(r, ms));
await wait(50)
at the end of my readDB function to force a wait until the onsuccess has completed.
This is completely stupid!
And please don't even try to tell me about promises. They DO NOT WORK in this circumstance.
Does anyone know how we are supposed to use this properly?
Sample readDB is here (all error checking removed for clarity). Note, we cannot use await inside the onsuccess so the two inner IndexedDB calls are not awaited!
async function readDB(dbname, storeName, id) {
var result;
var request = await indexedDB.open(dbname, 1); //indexedDB.open is an asynchronous function
request.onsuccess = function (event) {
let db = event.target.result;
var transaction = db.transaction([storeName], "readonly"); //This is also asynchronous and needs await
var store = transaction.objectStore(storeName);
var objectStoreRequest = store.get(id); //This is also asynchronous and needs await
objectStoreRequest.onsuccess = function (event) {
result = objectStoreRequest.result;
};
};
//Without this wait, this function returns BEFORE the onsuccess has completed
console.warn('ABOUT TO WAIT');
var wait = ms => new Promise((r, j) => setTimeout(r, ms));
await wait(50)
console.warn('WAIT DONE');
return result;
}
And please don't even try to tell me about promises. They DO NOT WORK in this circumstance.
...
...
...
I mean, they do, though. Assuming that you're okay putting the promise-based IndexedDB lookups inside of event.respondWith() rather than before event.respondWith(), at least. (If you're trying to do this before calling event.respondWith(), to figure out whether or not you want to respond at all, you're correct in that it's not possible, since the decision as to whether or not to call event.respondWith() needs to be made synchronously.)
It's not easy to wrap IndexedDB in a promise-based interface, but https://github.com/jakearchibald/idb has already done the hard work, and it works quite well inside of a service worker. Moreover, https://github.com/jakearchibald/idb-keyval makes it even easier to do this sort of thing if you just need a single key/value pair, rather than the full IndexedDB feature set.
Here's an example, assuming you're okay with idb-keyval:
importScripts('https://cdn.jsdelivr.net/npm/idb-keyval#3/dist/idb-keyval-iife.min.js');
// Call idbKeyval.set() to save data to your datastore in the `install` handler,
// in the context of your `window`, etc.
self.addEventListener('fetch', event => {
// Optionally, add in some *synchronous* criteria here that examines event.request
// and only calls event.respondWith() if this `fetch` handler can respond.
event.respondWith(async function() {
const id = someLogicToCalculateAnId();
const value = await idbKeyval.get(id);
// You now can use `value` however you want.
const response = generateResponseFromValue(value);
return response;
}())
});

Dart Unit Test - Always passing

All,
Here is a unit test for checking the size of a collection
main() {
test("Resource Manager Image Load", () {
ResourceManager rm = new ResourceManager();
int WRONG_SIZE = 1000000;
rm.loadImageManifest("data/rm/test_images.yaml").then((_){
print("Length="+ rm.images.length.toString()); // PRINTS '6' - WHICH IS CORRECT
expect(rm.images, hasLength(WRONG_SIZE));
});
});
}
I am running this from a browser (client-side Dart libraries are in use) and it ALWAYS passes, no matter what the value of WRONG_SIZE.
Help appreciated.
In such simple cases you can just return the future. The unit test framework recognizes it and waits for the future to complete. This also works for setUp/tearDown.
main() {
test("Resource Manager Image Load", () {
ResourceManager rm = new ResourceManager();
int WRONG_SIZE = 1000000;
return rm.loadImageManifest("data/rm/test_images.yaml").then((_) {
//^^^^
print("Length="+ rm.images.length.toString()); // PRINTS '6' - WHICH IS CORRECT
expect(rm.images, hasLength(WRONG_SIZE));
});
});
}
The problem is that your code returns a Future, and your test completes before the code in the Future has finished, so there's nothing to cause it to fail.
Check out the Asynchronous Tests section on the Dart site. There are methods like expectAsync that allow the future to be passed to the test framework so that it can wait for them to complete and handle the result correctly.
Here's an example (note the expect call is now inside the function passed to expectAsync)
test('callback is executed once', () {
// wrap the callback of an asynchronous call with [expectAsync] if
// the callback takes 0 arguments...
var timer = Timer.run(expectAsync(() {
int x = 2 + 3;
expect(x, equals(5));
}));
});

Listening to the Stream created from List in Dart

I've modified little bit example from tutorial https://www.dartlang.org/docs/tutorials/streams/ by adding item after subscription:
import 'dart:async';
main() {
var data = new List<int>();
var stream = new Stream.fromIterable(data); // create the stream
// subscribe to the streams events
stream.listen((value) { //
print("Received: $value"); // onData handler
}); //
data.add(1);
}
And after running this program I've got:
Uncaught Error: Concurrent modification during iteration: _GrowableList len:1.
Stack Trace:
#0 ListIterator.moveNext (dart:_collection-dev/iterable.dart:315)
#1 _IterablePendingEvents.handleNext (dart:async/stream_impl.dart:532)
#2 _PendingEvents.schedule.<anonymous closure> (dart:async/stream_impl.dart:661)
#3 _asyncRunCallback (dart:async/schedule_microtask.dart:18)
#4 _createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:11)
#5 _Timer._createTimerHandler._handleTimeout (timer_impl.dart:151)
#6 _Timer._createTimerHandler.<anonymous closure> (timer_impl.dart:166)
#7 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:93)
Putting data.add(1) before adding listener works as expected.
I've checked documentation about Stream and didn't found what I am doing wrong. I was expecting that listener will be fired in best case and just not fired in worst case, but not exception.
Is it expected behavior? If yes, please describe why.
The exception comes from you trying to modify the list while it is iterated over. This is unspecified behaviour in Dart (*), and the used implementation simply chose to throw an exception. While it is obfuscated by the asynchronous stuff happening in Stream.fromIterable, it basically is the same as if you tried to do this:
var data = [1,2,3];
for(var d in data) {
print(d);
data.add(d+10);
}
If you wrapped your data.add in another async call, for example with Timer.run(() => data.add(2)), it would "work". By that, I mean it wouldn't throw an exception.
Received: 2 still would not be printed. The stream will only send the elements that where already in the list at the time new Stream.fromIterable was called. After that, the stream is closed (onDone will be called), and modifications to the original list will not be sent to your listener.
(*) Source: iterator.dart in SDK 1.1.3 -- "If the object iterated over is changed during the iteration, the behavior is unspecified." Why the text on api.dartlang.org is different is beyond me.
EDIT
To answer the question in the comment: One way would be to use a StreamController.
// or new StreamController<int>.broadcast(), if you want to listen to the stream more than once
StreamController s = new StreamController<int>();
// produce periodic errors
new Timer.periodic(new Duration(seconds: 5), (Timer t) {
s.isClosed ? t.cancel() : s.addError("I AM ERROR");
});
// add some elements before subscribing
s.add(6);
s.add(9);
// this will close the stream eventually
new Timer(new Duration(seconds: 20), () => s.close());
// start listening to the stream
s.stream.listen((v) => print(v),
onError: (err) => print("An error occured: $err"),
onDone: () => print("The stream was closed"));
// add another element before the next event loop iteration
Timer.run(() => s.add(4711));
// periodically add an element
new Timer.periodic(new Duration(seconds: 3), (Timer t) {
s.isClosed ? t.cancel() : s.add(0);
});
// one more (will be sent before 4711)
s.add(4);
The List can't be modified while it is iterated over.
You need an iterable that doesn't have this limitation (e.g. custom implementation) for your example.

WinJS.xhr Timeout Loses Requests?

What I'm trying to do (though I fully suspect there's a better way to do it) is to send HTTP requests to a range of hosts on my network. I can hit every host by calling WinJS.xhr in a loop. However, it takes too long to complete the range.
Inspecting in Fiddler shows that a dozen or so requests are sent at a time, wait to time out, and then move on to the next dozen or so. So I figured I'd try to reduce the timeout for each request. For my needs, if the host doesn't respond in 500 ms, it's not going to respond.
Following the documentation, I tried wrapping the call to WinJS.xhr in a call to WinJS.Promise.timeout with a small enough setting, but there was no change. Changing the promise timeout didn't really affect the actual request.
A little more searching led me to a suggestion whereby I could modify the XMLHttpRequest object that WinJS.xhr uses and set the timeout on that. This worked like a charm in terms of blasting out requests at a faster rate. However, there seems to be a side-effect.
Watching the requests in Fiddler, about a dozen or so fire off very quickly and then the whole thing ends. The "next dozen or so" never come. Sometimes (based on the semi-randomness of asynchronous calls) the first dozen or so that shows up in fiddler includes 9-10 from the low and of the range and 2-3 from the top end of the range, or close to it.
Is there something else I can try, or some other way to accomplish the end goal here? (Within the scope of this question the end goal is to send a large number of requests in a reasonable amount of time, but any suggestions on a better overall way to scan for a particular service on a network is also welcome.)
Can you write out the code you're using for timeout, i wrote something like this but it wasn't working, so I'm curious as to how you're doing it:
var timeoutFired = function () {
console.log("derp");
};
var options = {
url: "http://somesite.com",
responseType: "document",
customRequestInitializer: function (req) {
req.timeout = 1;
req.ontimeout = timeoutFired;
//do something with the XmlHttpRequest object req
}
};
WinJS.xhr(options).
....
Here are some alternatives that you may find helpful, not sure how/why timeout wasn't working but I tried to write out a custom timeout function:
(function (global) {
var options = {
url: "http://something.com",
responseType: "document",
};
var request = WinJS.xhr(options).then(
function (xmlHttpRequest) {
console.log("completed");
},
function (xmlHttpRequest) {
//error or cancel() will throw err
console.log("error"+ xmlHttpRequest.message);
},
function (xmlHttpRequest) {
console.log("progress")
});
function waitTime() {
return new WinJS.Promise(
function (complete, error, progress) {
var seconds = 0;
var interval = window.setInterval(
function () {
seconds++;
progress(seconds);
//prob should be called milliseconds
if (seconds > 5) {
window.clearInterval(interval);
complete();
}
}, 100);
});
};
waitTime().done(
function () {
console.log("complete");
request.cancel();
},
function () {
console.log("error")
},
function (seconds) {
console.log("progress:" + seconds)
});
});
Another cool little trick is using promise.any (vs .join) which fires off when one OR the other finishes first, so taking that into account you can write something like this:
(function (global) {
var options = {
url: "http://url.com",
responseType: "document",
};
var request = {
runRequest: function () {
return WinJS.xhr(options).then(
function (xmlHttpRequest) {
console.log("completed");
},
function (xmlHttpRequest) {
//error or cancel() will throw err
console.log("error" + xmlHttpRequest.message);
},
function (xmlHttpRequest) {
console.log("progress")
});
}
};
WinJS.Promise.any([WinJS.Promise.timeout(500), request.runRequest()]).done(
function () {
console.log("any complete");
});
})();

Resources