Dart - Detect non-completing futures
18:29 08 Jul 2022

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 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 actions = [];
List> 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 {
  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. (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... (dart:_http/http_impl.dart:2171:25)
#8      _rootRunUnary (dart:async/zone.dart:1434:47)
dart memory-leaks future