Ticket Created
over 3 years ago

WERETECH-12052

Conflict/race between makeWebRequest and Storage.setValue called from onUpdate on a watchface.

Hi, I was able to reproduce the following problem (both in simulator and in a Descent MK1 using SDK 3.2.5 and 4.0.6):

If the function setValue is called from onUpdate while makeWebRequest is still waiting for the webserver to return data,

the callback function is "lost" (and not called).

The following code reproduce the problem 100% of the time.



using Toybox.Application;
using Toybox.Background;
using Toybox.System;
using Toybox.WatchUi;
using Toybox.Time;
using Toybox.Communications;
using Toybox.Application.Storage;

(:background)
class PiServiceDelegate extends Toybox.System.ServiceDelegate {
  function initialize() {
    System.ServiceDelegate.initialize();
  }
  function onTemporalEvent() {
    // That URI takes about 3 seconds to answer
    var uri = "https://wb.elaine.fi/pi_slow";
    System.println("Before query");
    var options = {
      :methods => Communications.HTTP_REQUEST_METHOD_GET,
      :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
    };
    Communications.makeWebRequest(uri, null, options,
                                  method(:pi_received));
  }
  function pi_received(code, data) {
    System.println(code);
    System.println(data);
    if (code == 200) {
      Background.exit(data);
    }
  }
}

(:background)
class PiWatch extends Application.AppBase {
  public function initialize() {
    AppBase.initialize();
  }
  function onBackgroundData(data) {
    System.println("onBackgroundData: " + data);
  }
  function getServiceDelegate() {
    return [new PiServiceDelegate()];
  }
  function getInitialView() {
    Background.registerForTemporalEvent(new Time.Duration(5 * 60));
    return [new PiWatchView(), new PiWatchDelegate()];
  }
}

class PiWatchView extends WatchUi.WatchFace {
  public function initialize() {
    WatchFace.initialize();
  }
  public function onUpdate(dc) {
    System.println("onUpdate");
    // On simulator, onUpdate is ran every seconds
    // So we will call Storage.setValue between the http request
    // and the result
    // On watch same problem exists
    Storage.setValue("foo", "bar");
    // Note that problem does not exists if Storage.setValue
    // is called in onPartialUpdate
  }
}

class PiWatchDelegate extends WatchUi.WatchFaceDelegate {
  public function initialize() {
    WatchFaceDelegate.initialize();
  }
}


Here is the output:


onUpdate
onUpdate
onUpdate
Background: Before query
onUpdate
onUpdate
onUpdate
Error: Symbol Not Found Error
Details: Failed invoking <symbol>
Stack:
onUpdate
onUpdate
onUpdate



If I comment the setValue line, I get the expected result:

onUpdate
onUpdate
onUpdate
onUpdate
Background: Before query
onUpdate
onUpdate
onUpdate
Background: 200
Background: {pi=>3.140000}
onBackgroundData: {pi=>3.140000}
onUpdate
onUpdate
onUpdate
  • So, I did the test you suggest (test for having storage and register on onExitSleep).

    Same issue. (tested with sdk 4.0.7)

    output: 
    we have storage
    onUpdate 8
    we have storage
    onUpdate 9
    onExitSleep
    we have storage
    onUpdate 10
    we have storage
    [...]
    we have storage
    onUpdate 15
    Background: Before query
    we have storage
    onUpdate 16
    we have storage
    onUpdate 17
    we have storage
    onUpdate 18
    we have storage
    onUpdate 19

    Error: Symbol Not Found Error
    Details: Failed invoking <symbol>
    Stack:
    we have storage
    onUpdate 20
    we have storage



    source: 

    using Toybox.Application;
    using Toybox.Background;
    using Toybox.System;
    using Toybox.WatchUi;
    using Toybox.Time;
    using Toybox.Communications;
    using Toybox.Application.Storage;

    (:background)
    class PiServiceDelegate extends Toybox.System.ServiceDelegate {
      function initialize() {
        System.ServiceDelegate.initialize();
      }
      function onTemporalEvent() {
        // That URI takes about 3 seconds to answer
        var uri = "">https://wb.elaine.fi/pi_slow";
        System.println("Before query");
        var options = {
          :methods => Communications.HTTP_REQUEST_METHOD_GET,
          :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
        };
        Communications.makeWebRequest(uri, null, options,
                                      method(:pi_received));
      }
      function pi_received(code, data) {
        System.println(code);
        System.println(data);
      }
    }

    (:background)
    class PiWatch extends Application.AppBase {
      public function initialize() {
        AppBase.initialize();
      }
      function getServiceDelegate() {
        return [new PiServiceDelegate()];
      }
      function getInitialView() {
        return [new PiWatchView(), new PiWatchDelegate()];
      }
    }

    class PiWatchView extends WatchUi.WatchFace {
      var i = 1;
      public function initialize() {
        WatchFace.initialize();
      }
      public function onUpdate(dc) {
        i += 1;
        if (Toybox.Application has :Storage) {
          System.println("we have storage");
          Storage.setValue("foo", "bar");
        }
        System.println("onUpdate " + i.format("%d"));
      }
      public function onExitSleep() {
        System.println("onExitSleep");
        Background.registerForTemporalEvent(new Time.Duration(5 * 60));
      }
    }

    class PiWatchDelegate extends WatchUi.WatchFaceDelegate {
      public function initialize() {
        WatchFaceDelegate.initialize();
      }
    }

  • As I've said, setValue is expensive, and really not something you want to do all the time in onUpdate, as it will also impact your battery life,  Only call setValue when you need to - do it in onBackgroundDate instead for example.  The most it will be called then will be every 5 minutes.  Or, as I mentioned before you only really need to do it in onStop for the main app,

  • on sim it's to short probably to get watchdog

    Have test "has  for storage" to exclude error in onUpdate?

    Move

    Background.registerForTemporalEvent(new Time.Duration(5 * 60));

    to PiWatchVie.onShow or batter to onExitSleep()

  • So, now that the onPartialUpdate/budget exceeded case is cleared, I tested the following:

    Long loop on onUpdate, just enough to not trigger the watchdog.

    In that version, we do not call setValue, and there is no problem.

    using Toybox.Application;
    using Toybox.Background;
    using Toybox.System;
    using Toybox.WatchUi;
    using Toybox.Time;
    using Toybox.Communications;
    using Toybox.Application.Storage;

    (:background)
    class PiServiceDelegate extends Toybox.System.ServiceDelegate {
      function initialize() {
        System.ServiceDelegate.initialize();
      }
      function onTemporalEvent() {
        // That URI takes about 3 seconds to answer
        var uri = "">https://wb.elaine.fi/pi_slow";
        System.println("Before query");
        var options = {
          :methods => Communications.HTTP_REQUEST_METHOD_GET,
          :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON
        };
        Communications.makeWebRequest(uri, null, options,
                                      method(:pi_received));
      }
      function pi_received(code, data) {
        System.println(code);
        System.println(data);
      }
    }

    (:background)
    class PiWatch extends Application.AppBase {
      public function initialize() {
        AppBase.initialize();
      }
      function getServiceDelegate() {
        return [new PiServiceDelegate()];
      }
      function getInitialView() {
        Background.registerForTemporalEvent(new Time.Duration(5 * 60));
        return [new PiWatchView(), new PiWatchDelegate()];
      }
    }

    class PiWatchView extends WatchUi.WatchFace {
      var i = 1;
      public function initialize() {
        WatchFace.initialize();
      }
      public function onUpdate(dc) {
        i += 1;
        // 6000 is sweet spot, 7000 triggers the Watchdog
        for(var j = 0; j < 6000; j++) {
          var k = Math.sqrt(j);
        }
      }
    }

    class PiWatchDelegate extends WatchUi.WatchFaceDelegate {
      public function initialize() {
        WatchFaceDelegate.initialize();
      }
    }
  • The order that these calls happen can vary, and it's something you need to account for in your code.  onAppUpdate/onAppInstall will happen before getInitialView, and even if you don't override them, getserviceDelate will be called.  And then there's the thing in your appBase (initialize(), onStart. onStop, etc,) that are called when the main app or the background service runs.