Home > Software engineering >  Wrong order of execution async handler
Wrong order of execution async handler

Time:10-14

I have small app that execute requests to external service.

  final app = Alfred();
  app.post('/ctr', (req, res) async { // complex tender request
    var data = await req.body;
    await complexTendexAPIRequest(data as Map<String, dynamic>);
    print('Hello World');
    await res.json({'data': 'ok'});
  }); 

Handler code:

complexTendexAPIRequest(Map<String, dynamic> data) async {
  print('Request: $data');
  try {
      final response = await http.post(
        Uri.parse(COMPLEX_URL),
        headers: {'Content-Type': 'application/json', 'Authorization': 'bearer $ACCESS_TOKEN'},
        body: json.encode(data)
      );

      if(response.statusCode == 200) {
        var res = json.decode(response.body);
        int latestId = res['id'];
        String url = 'https://api.ru/v2/complex/status?id=$latestId';
        stdout.write('Waiting for "complete" status from API: ');
        Timer.periodic(Duration(seconds: 1), (timer) async {
          final response = await http.get(
            Uri.parse(url),
            headers: {'Content-Type': 'application/json', 'Authorization': 'bearer $ACCESS_TOKEN'}
          );

          var data = json.decode(response.body);
          if(data['status'] == 'completed') {
            timer.cancel();
            stdout.write('[DONE]');
            stdout.write('\nFetching result: ');
            String url = "https://api.ru/v2/complex/results?id=$latestId";
            final response = await http.get(
              Uri.parse(url),
              headers: {'Content-Type': 'application/json', 'Authorization': 'bearer $ACCESS_TOKEN'}
            );
            stdout.write('[DONE]');
            var data = prettyJson(json.decode(response.body));
            await File('result.json').writeAsString(data.toString());
            print("\nCreating dump of result: [DONE]");
          }
          
        });
        
      }
      else {
        print('[ERROR] Wrong status code for complex request. StatusCode: ${response.statusCode}');
      }
  } 
  on SocketException catch(e) {
    print('No Internet connection: $e');
  } on TimeoutException catch(e) {
    print('TenderAPI Timeout: $e');
  } on Exception catch(e) {
    print('Some unknown Exception: $e');
  }

}

But output is very strange it's look like it's do not waiting complexTendexAPIRequest completion and go forward:

Waiting for "complete" status from API: Hello World
[DONE]
Fetching result: [DONE]
Creating dump of result: [DONE]

But should be:

Waiting for "complete" status from API: [DONE]
Fetching result: [DONE]
Creating dump of result: [DONE]
Hello World

I suppose that reason can be in Timer.periodic but how to fix it to get expected order and execution of:

    print('Hello World');
    await res.json({'data': 'ok'});

only after complexTendexAPIRequest completed.

upd: I rewrote code to while loop: https://gist.github.com/bubnenkoff/fd6b4f0d7aeae7007680e7902fbdc1e9 it's seems that it's ok.

Alfred https://github.com/rknell/alfred

CodePudding user response:

The problem is the Timer.periodic, as others have pointed out.

You do:

  Timer.periodic(Duration(seconds: 1), (timer) async {
    // do something ...
  });

That sets up a timer, then immediately continues execution. The timer triggers every second, calls the async callback (which returns a future that no-one ever waits for) and which does something which just might take longer than a second.

You can convert this to a normal loop, basically:

  while (true) {
    // do something ...
    if (data['status'] == 'completed') {
      // ...
      break;
    } else {
      // You can choose your own delay here, doesn't have
      // to be the same one every time.
      await Future.delayed(const Duration(seconds: 1));
    }
  }

If you still want it to be timer driven, with fixed ticks, consider rewriting this as:

await for (var _ in Stream.periodic(const Duration(seconds: 1))) {
  // do something ...
  // Change `timer.cancel();` to a `break;` at the end of the block.
}

Here you create a stream which fires an event every second. Then you use an await for loop to wait for each steam event. If the thing you do inside the loop is asynchronous (does an await) then you are even ensured that the next stream event is delayed until the loop body is done, so you won't have two fetches running at the same time. And if the code throws, the error will be caught by the surrounding try/catch, which I assume was intended, rather than being an uncaught error ending up in the Future that no-one listens to.

If you want to retain the Timer.periodic code, you can, but you need to do something extra to synchronize it with the async/await code around it (which only really understands futures and streams, not timers). For example:

var timerDone = Completer();
Timer.periodic(const Duration(seconds: 1), (timer) async {
  try {
    // do something ...
    // Add `timerDone.complete();` next to `timer.cancel()`.
  } catch (e, s) {
    timer.cancel();
    timerDone.completeError(e, s);
  }
});
await timerDone.future;

This code uses a Completer to complete a future and effectively bridge the gap between timers and futures that can be awaited (one of the listed uses of Completer in the documentation). You may still risk the timer running concurrently if one step takes longer than a second.

Also, you can possibly use the retry package, if it's the same thing you want to retry until it works.

  • Related