Motoko function doesn't complete

I’m trying to run a function to clean some teams involving looping over the manager records, updating them and then saving. It works locally. However on live the function doesn’t get to the end, I confirm the following via logs when the function runs:

I have 313 managers to loop over:

image

Here is the function:



  private func cleanManagerTeams(managers : [T.Manager], allPlayers : [DTOs.PlayerDTO]) : async [T.Manager] {
    let managerBuffer = Buffer.fromArray<T.Manager>([]);

    await logStatus("Cleaning " # Nat.toText(Array.size(managers)) # " teams.");
    for (manager in Iter.fromArray(managers)) {
      var captainRemoved = false;
      let playerIdBuffer = Buffer.fromArray<T.PlayerId>([]);
      for (playerId in Iter.fromArray(manager.playerIds)) {
        
        let activePlayer = Array.find<DTOs.PlayerDTO>(allPlayers, func(player: DTOs.PlayerDTO){
          player.id == playerId
        });

        if(Option.isSome(activePlayer)){
          playerIdBuffer.add(playerId);
        };

        if(Option.isNull(activePlayer)){
          if(playerId == manager.captainId){
            captainRemoved := true;
          };
          playerIdBuffer.add(0);
        };
      };

      var captainId = manager.captainId;
      if(captainRemoved){
        let highestValuedPlayer = Array.foldLeft<T.PlayerId, ?DTOs.PlayerDTO>(
          Buffer.toArray(playerIdBuffer),
          null,
          func(highest, id) : ?DTOs.PlayerDTO {
            if (id == 0) { return highest };
            let player = Array.find<DTOs.PlayerDTO>(allPlayers, func(p) { p.id == id });
            switch (highest, player) {
              case (null, ?p) {
                ?p;
              };
              case (?h, ?p) {
                if (p.valueQuarterMillions > h.valueQuarterMillions) {
                  ?p;
                } else {
                  ?h;
                };
              };
              case (_, null) {
                highest;
              };
            };
          },
        );
        switch(highestValuedPlayer){
          case (?foundPlayer){
            captainId := foundPlayer.id;
          };
          case (null){}
        };
      };

      var updatedPlayerIds = Buffer.toArray(playerIdBuffer);

      let allTeamPlayers = Array.filter<DTOs.PlayerDTO>(allPlayers, func(player: DTOs.PlayerDTO){
        Array.find<T.PlayerId>(updatedPlayerIds, func(playerId){
          playerId == player.id
        }) != null;
      });

      let allPlayerValues = Array.map<DTOs.PlayerDTO, Nat16>(allTeamPlayers, func (player: DTOs.PlayerDTO) : Nat16 { return player.valueQuarterMillions; });

      let currentTeamValue = Array.foldLeft<Nat16, Nat16>(allPlayerValues, 0, func(sumSoFar, x) = sumSoFar + x);

      var arrstr = "";
      for(id in Iter.fromArray(manager.playerIds)){
        arrstr := arrstr # Nat16.toText(id) # ",";
      };
      
      await logStatus("Team value for " # manager.principalId # "is " # Nat16.toText(currentTeamValue) # ", " # arrstr);
      if(currentTeamValue > 1200){
        await logStatus("Team " # manager.principalId # " has a team over 300m.");
      };

      var bankBalance: Nat16 = 0;
      var testBalance: Int = 1200 - Int16.toInt(Int16.fromNat16(currentTeamValue));
      if(testBalance >= 0){
        bankBalance := Nat16.fromIntWrap(testBalance);
      };

      if(testBalance < 0){
        updatedPlayerIds := [];
        bankBalance := 1200;
      };
      
      let updatedManager : T.Manager = {
        principalId = manager.principalId;
        username = manager.username;
        termsAccepted = manager.termsAccepted;
        favouriteClubId = manager.favouriteClubId;
        createDate = manager.createDate;
        history = manager.history;
        profilePicture = manager.profilePicture;
        profilePictureType = manager.profilePictureType;
        transfersAvailable = manager.transfersAvailable;
        monthlyBonusesAvailable = manager.monthlyBonusesAvailable;
        bankQuarterMillions = bankBalance;
        playerIds = updatedPlayerIds;
        captainId = captainId;
        goalGetterGameweek = manager.goalGetterGameweek;
        goalGetterPlayerId = manager.goalGetterPlayerId;
        passMasterGameweek = manager.passMasterGameweek;
        passMasterPlayerId = manager.passMasterPlayerId;
        noEntryGameweek = manager.noEntryGameweek;
        noEntryPlayerId = manager.noEntryPlayerId;
        teamBoostGameweek = manager.teamBoostGameweek;
        teamBoostClubId = manager.teamBoostClubId;
        safeHandsGameweek = manager.safeHandsGameweek;
        safeHandsPlayerId = manager.safeHandsPlayerId;
        captainFantasticGameweek = manager.captainFantasticGameweek;
        captainFantasticPlayerId = manager.captainFantasticPlayerId;
        countrymenGameweek = manager.countrymenGameweek;
        countrymenCountryId = manager.countrymenCountryId;
        prospectsGameweek = manager.prospectsGameweek;
        braceBonusGameweek = manager.braceBonusGameweek;
        hatTrickHeroGameweek = manager.hatTrickHeroGameweek;
        transferWindowGameweek = manager.transferWindowGameweek;
        ownedPrivateLeagues = manager.ownedPrivateLeagues;
        privateLeagueMemberships = manager.privateLeagueMemberships;
      };
      managerBuffer.add(updatedManager);
    };
    return Buffer.toArray(managerBuffer);
  };

As you can see I check by logging the team value that it loops through everyone and I get 313 log records:

After this function completes its execution I have another log statement immediately after, which only executes on local, for some reason this function doesn’t complete after the final loop where it logs the team value.

The complete file is here:

Any help appreciated.

1 Like

Since it bugs out on live, which has more managers and it happens just before the function completes, it makes me think returning the buffer of 300+ managers is too large, is there a limit for calls returned between functions inside an actor?

More debugging, so I call this:


    public func cleanFantasyTeams() : async (){
      for (canisterId in Iter.fromList(uniqueManagerCanisterIds)) {
        let manager_canister = actor (canisterId) : actor {
          cleanFantasyTeams : () -> async ();
        };

        await manager_canister.cleanFantasyTeams();

        logStatus("Clean fantasy teams end from manager composite.");
      };
    };

which calls out to the manager canister:


  public shared ({ caller }) func cleanFantasyTeams() : async () {

    assert not Principal.isAnonymous(caller);
    let principalId = Principal.toText(caller);
    assert principalId == Environment.BACKEND_CANISTER_ID;

    let openfpl_backend_canister = actor (Environment.BACKEND_CANISTER_ID) : actor {
      getActivePlayers : () -> async [DTOs.PlayerDTO];
    };
    let allPlayers : [DTOs.PlayerDTO] = await openfpl_backend_canister.getActivePlayers();

    if(Array.size(allPlayers) == 0){
      return;
    };

    for (index in Iter.range(0, 11)) {
      switch (index) {
        case 0 {
          managerGroup1 := await cleanManagerTeams(managerGroup1, allPlayers);
        };
        case 1 {
          managerGroup2 := await cleanManagerTeams(managerGroup2, allPlayers);
        };
        case 2 {
          managerGroup3 := await cleanManagerTeams(managerGroup3, allPlayers);
        };
        case 3 {
          managerGroup4 := await cleanManagerTeams(managerGroup4, allPlayers);
        };
        case 4 {
          managerGroup5 := await cleanManagerTeams(managerGroup5, allPlayers);
        };
        case 5 {
          managerGroup6 := await cleanManagerTeams(managerGroup6, allPlayers);
        };
        case 6 {
          managerGroup7 := await cleanManagerTeams(managerGroup7, allPlayers);
        };
        case 7 {
          managerGroup8 := await cleanManagerTeams(managerGroup8, allPlayers);
        };
        case 8 {
          managerGroup9 := await cleanManagerTeams(managerGroup9, allPlayers);
        };
        case 9 {
          managerGroup10 := await cleanManagerTeams(managerGroup10, allPlayers);
        };
        case 10 {
          managerGroup11 := await cleanManagerTeams(managerGroup11, allPlayers);
        };
        case 11 {
          managerGroup12 := await cleanManagerTeams(managerGroup12, allPlayers);
        };
        case _ {

        };
      };
    };
  };


  private func cleanManagerTeams(managers : [T.Manager], allPlayers : [DTOs.PlayerDTO]) : async [T.Manager] {
    let managerBuffer = Buffer.fromArray<T.Manager>([]);

    await logStatus("Cleaning " # Nat.toText(Array.size(managers)) # " teams.");
    for (manager in Iter.fromArray(managers)) {
      var captainRemoved = false;
      let playerIdBuffer = Buffer.fromArray<T.PlayerId>([]);
      for (playerId in Iter.fromArray(manager.playerIds)) {
        
        let activePlayer = Array.find<DTOs.PlayerDTO>(allPlayers, func(player: DTOs.PlayerDTO){
          player.id == playerId
        });

        if(Option.isSome(activePlayer)){
          playerIdBuffer.add(playerId);
        };

        if(Option.isNull(activePlayer)){
          if(playerId == manager.captainId){
            captainRemoved := true;
          };
          playerIdBuffer.add(0);
        };
      };

      var captainId = manager.captainId;
      if(captainRemoved){
        let highestValuedPlayer = Array.foldLeft<T.PlayerId, ?DTOs.PlayerDTO>(
          Buffer.toArray(playerIdBuffer),
          null,
          func(highest, id) : ?DTOs.PlayerDTO {
            if (id == 0) { return highest };
            let player = Array.find<DTOs.PlayerDTO>(allPlayers, func(p) { p.id == id });
            switch (highest, player) {
              case (null, ?p) {
                ?p;
              };
              case (?h, ?p) {
                if (p.valueQuarterMillions > h.valueQuarterMillions) {
                  ?p;
                } else {
                  ?h;
                };
              };
              case (_, null) {
                highest;
              };
            };
          },
        );
        switch(highestValuedPlayer){
          case (?foundPlayer){
            captainId := foundPlayer.id;
          };
          case (null){}
        };
      };

      var updatedPlayerIds = Buffer.toArray(playerIdBuffer);

      let allTeamPlayers = Array.filter<DTOs.PlayerDTO>(allPlayers, func(player: DTOs.PlayerDTO){
        Array.find<T.PlayerId>(updatedPlayerIds, func(playerId){
          playerId == player.id
        }) != null;
      });

      let allPlayerValues = Array.map<DTOs.PlayerDTO, Nat16>(allTeamPlayers, func (player: DTOs.PlayerDTO) : Nat16 { return player.valueQuarterMillions; });

      let currentTeamValue = Array.foldLeft<Nat16, Nat16>(allPlayerValues, 0, func(sumSoFar, x) = sumSoFar + x);

      var arrstr = "";
      for(id in Iter.fromArray(manager.playerIds)){
        arrstr := arrstr # Nat16.toText(id) # ",";
      };
      
      await logStatus("Team value for " # manager.principalId # "is " # Nat16.toText(currentTeamValue) # ", " # arrstr);
      if(currentTeamValue > 1200){
        await logStatus("Team " # manager.principalId # " has a team over 300m.");
      };

      var bankBalance: Nat16 = 0;
      var testBalance: Int = 1200 - Int16.toInt(Int16.fromNat16(currentTeamValue));
      if(testBalance >= 0){
        bankBalance := Nat16.fromIntWrap(testBalance);
      };

      if(testBalance < 0){
        updatedPlayerIds := [];
        bankBalance := 1200;
      };
        await logStatus("Adding updated " # manager.principalId # " to manager array.");
      
      let updatedManager : T.Manager = {
        principalId = manager.principalId;
        username = manager.username;
        termsAccepted = manager.termsAccepted;
        favouriteClubId = manager.favouriteClubId;
        createDate = manager.createDate;
        history = manager.history;
        profilePicture = manager.profilePicture;
        profilePictureType = manager.profilePictureType;
        transfersAvailable = manager.transfersAvailable;
        monthlyBonusesAvailable = manager.monthlyBonusesAvailable;
        bankQuarterMillions = bankBalance;
        playerIds = updatedPlayerIds;
        captainId = captainId;
        goalGetterGameweek = manager.goalGetterGameweek;
        goalGetterPlayerId = manager.goalGetterPlayerId;
        passMasterGameweek = manager.passMasterGameweek;
        passMasterPlayerId = manager.passMasterPlayerId;
        noEntryGameweek = manager.noEntryGameweek;
        noEntryPlayerId = manager.noEntryPlayerId;
        teamBoostGameweek = manager.teamBoostGameweek;
        teamBoostClubId = manager.teamBoostClubId;
        safeHandsGameweek = manager.safeHandsGameweek;
        safeHandsPlayerId = manager.safeHandsPlayerId;
        captainFantasticGameweek = manager.captainFantasticGameweek;
        captainFantasticPlayerId = manager.captainFantasticPlayerId;
        countrymenGameweek = manager.countrymenGameweek;
        countrymenCountryId = manager.countrymenCountryId;
        prospectsGameweek = manager.prospectsGameweek;
        braceBonusGameweek = manager.braceBonusGameweek;
        hatTrickHeroGameweek = manager.hatTrickHeroGameweek;
        transferWindowGameweek = manager.transferWindowGameweek;
        ownedPrivateLeagues = manager.ownedPrivateLeagues;
        privateLeagueMemberships = manager.privateLeagueMemberships;
      };
      managerBuffer.add(updatedManager);
    };
        
    await logStatus("All snapshots have been taken and the function is returning ." # Nat.toText(Array.size(Buffer.toArray(managerBuffer))));
    return Buffer.toArray(managerBuffer);
  };

As you can see I’ve done a lot of logging but from this screenshot you can see it reaches the end of the cleanManagerTeams function:

But it doesn’t log the statement after the call to manager_canister.cleanFantasyTeams, but I’m not sure if/where it has trapped to not print:

logStatus("Clean fantasy teams end from manager composite.");

If I’m not mistaken, in Motoko you must at least use “ignore” when calling async methods (assuming logStatus is async).

I get this when I use ignore on it:

As I said, locally this function completes. Could it be something to do with subnets, I’m assuming locally my canisters are on the same subnet but live they may not be.

For some reason on live the call to the manager canister just doesn’t return to continue the fix.

What happens if you change it to

    ignore logStatus("Clean fantasy teams end from manager composite.");

Yes that works syntax wise.

So the logging is the issue? or is it any await call to a different canister?

So in the manager canister I can update the log which calls out to the backend (the backend the function isn’t returning to) to use ignore:


  private func logStatus(statusMessage: Text) : async (){
    let openfpl_backend_canister = actor (Environment.BACKEND_CANISTER_ID) : actor {
      logStatus : (DTOs.LogStatusDTO) -> async ();
    };
    ignore openfpl_backend_canister.logStatus({ message = statusMessage; });
  };

But within this calculation I make other await calls, some again to the backend canister to get information like player info:

 let openfpl_backend_canister = actor (Environment.BACKEND_CANISTER_ID) : actor {
      getActivePlayers : () -> async [DTOs.PlayerDTO];
    };
    let allPlayers : [DTOs.PlayerDTO] = await openfpl_backend_canister.getActivePlayers();

I’m not 100% sure when I should be using ignore and when I should await the result of something I need.

For logging I always used ignore in Motoko when doing an async call as you don’t need to be sure if it trapped or not (even if logging failed, your main canister should not fail).

For everything else that needs to be confirmed, even without an explicit return value, I await’ed the call inside a try … catch block to treat any trap/exception and revert the transaction if needed.

I tried ignoring the log statement when calling back to the canister I get the same problem. I call the last line of clearManagerTeams:

But it still doesn’t return the array and move onto the next group:

Do you have any idea what I could do to debug between the end of the loop and the start of the next value? I feel like I’m missing something obvious as on local it goes through each of the 12 manager groups without issue.

I would try to log the length of managerGroup# before and after calls and the index value.

But if it works locally, I guess the local data isn’t the same as in production, right? I would try to dump the production data and load it locally to debug.

Yeah I do that on the last line of the log

await logStatus("All snapshots have been taken and the function is returning ." # Nat.toText(Array.size(Buffer.toArray(managerBuffer))));

Ignore the full stop before the number, that is 313 managers.

Is there a reason for knowing the number, because there is some data size limit on function to function array size transfers?

It would trap if going over the 2MB response size limit.

Are you testing locally with the same data present in production?

I’m not transferring 2MB out of the canister, it’s setting a stable variable in the canister. But that’s 2MB for any response. Let’s say all 313 had a profile picture, I’d be trying to update around 150 MB of data.

So I just need to update individually, but I can hold the 150mb in memory inside the function? I’d need to for the stable managerGroup1 update…

So this is refactored to not transfer any large data outside of the function that is running:



  public shared ({ caller }) func cleanFantasyTeams() : async () {

    assert not Principal.isAnonymous(caller);
    let principalId = Principal.toText(caller);
    assert principalId == Environment.BACKEND_CANISTER_ID;

    let openfpl_backend_canister = actor (Environment.BACKEND_CANISTER_ID) : actor {
      getActivePlayers : () -> async [DTOs.PlayerDTO];
    };
    let allPlayers : [DTOs.PlayerDTO] = await openfpl_backend_canister.getActivePlayers();

    if(Array.size(allPlayers) == 0){
      return;
    };

    for (index in Iter.range(0, 11)) {
      await cleanManagerTeams(index, allPlayers);
    };
  };


  private func cleanManagerTeams(managerGroup: Int, allPlayers : [DTOs.PlayerDTO]) : async () {
    let managerBuffer = Buffer.fromArray<T.Manager>([]);

    var managers: [T.Manager] = [];

    switch (managerGroup) {
      case 0 {
        managers := managerGroup1;
      };
      case 1 {
        managers := managerGroup2;
      };
      case 2 {
        managers := managerGroup3;
      };
      case 3 {
        managers := managerGroup4;
      };
      case 4 {
        managers := managerGroup5;
      };
      case 5 {
        managers := managerGroup6;
      };
      case 6 {
        managers := managerGroup7;
      };
      case 7 {
        managers := managerGroup8;
      };
      case 8 {
        managers := managerGroup9;
      };
      case 9 {
        managers := managerGroup10;
      };
      case 10 {
        managers := managerGroup11;
      };
      case 11 {
        managers := managerGroup12;
      };
      case _ {

      };
    };

    await logStatus("Cleaning " # Nat.toText(Array.size(managers)) # " teams.");
    for (manager in Iter.fromArray(managers)) {
      var captainRemoved = false;
      let playerIdBuffer = Buffer.fromArray<T.PlayerId>([]);
      for (playerId in Iter.fromArray(manager.playerIds)) {
        
        let activePlayer = Array.find<DTOs.PlayerDTO>(allPlayers, func(player: DTOs.PlayerDTO){
          player.id == playerId
        });

        if(Option.isSome(activePlayer)){
          playerIdBuffer.add(playerId);
        };

        if(Option.isNull(activePlayer)){
          if(playerId == manager.captainId){
            captainRemoved := true;
          };
          playerIdBuffer.add(0);
        };
      };

      var captainId = manager.captainId;
      if(captainRemoved){
        let highestValuedPlayer = Array.foldLeft<T.PlayerId, ?DTOs.PlayerDTO>(
          Buffer.toArray(playerIdBuffer),
          null,
          func(highest, id) : ?DTOs.PlayerDTO {
            if (id == 0) { return highest };
            let player = Array.find<DTOs.PlayerDTO>(allPlayers, func(p) { p.id == id });
            switch (highest, player) {
              case (null, ?p) {
                ?p;
              };
              case (?h, ?p) {
                if (p.valueQuarterMillions > h.valueQuarterMillions) {
                  ?p;
                } else {
                  ?h;
                };
              };
              case (_, null) {
                highest;
              };
            };
          },
        );
        switch(highestValuedPlayer){
          case (?foundPlayer){
            captainId := foundPlayer.id;
          };
          case (null){}
        };
      };

      var updatedPlayerIds = Buffer.toArray(playerIdBuffer);

      let allTeamPlayers = Array.filter<DTOs.PlayerDTO>(allPlayers, func(player: DTOs.PlayerDTO){
        Array.find<T.PlayerId>(updatedPlayerIds, func(playerId){
          playerId == player.id
        }) != null;
      });

      let allPlayerValues = Array.map<DTOs.PlayerDTO, Nat16>(allTeamPlayers, func (player: DTOs.PlayerDTO) : Nat16 { return player.valueQuarterMillions; });

      let currentTeamValue = Array.foldLeft<Nat16, Nat16>(allPlayerValues, 0, func(sumSoFar, x) = sumSoFar + x);

      var arrstr = "";
      for(id in Iter.fromArray(manager.playerIds)){
        arrstr := arrstr # Nat16.toText(id) # ",";
      };
      
      await logStatus("Team value for " # manager.principalId # "is " # Nat16.toText(currentTeamValue) # ", " # arrstr);
      if(currentTeamValue > 1200){
        await logStatus("Team " # manager.principalId # " has a team over 300m.");
      };

      var bankBalance: Nat16 = 0;
      var testBalance: Int = 1200 - Int16.toInt(Int16.fromNat16(currentTeamValue));
      if(testBalance >= 0){
        bankBalance := Nat16.fromIntWrap(testBalance);
      };

      if(testBalance < 0){
        updatedPlayerIds := [];
        bankBalance := 1200;
      };
        await logStatus("Adding updated " # manager.principalId # " to manager array.");
      
      let updatedManager : T.Manager = {
        principalId = manager.principalId;
        username = manager.username;
        termsAccepted = manager.termsAccepted;
        favouriteClubId = manager.favouriteClubId;
        createDate = manager.createDate;
        history = manager.history;
        profilePicture = manager.profilePicture;
        profilePictureType = manager.profilePictureType;
        transfersAvailable = manager.transfersAvailable;
        monthlyBonusesAvailable = manager.monthlyBonusesAvailable;
        bankQuarterMillions = bankBalance;
        playerIds = updatedPlayerIds;
        captainId = captainId;
        goalGetterGameweek = manager.goalGetterGameweek;
        goalGetterPlayerId = manager.goalGetterPlayerId;
        passMasterGameweek = manager.passMasterGameweek;
        passMasterPlayerId = manager.passMasterPlayerId;
        noEntryGameweek = manager.noEntryGameweek;
        noEntryPlayerId = manager.noEntryPlayerId;
        teamBoostGameweek = manager.teamBoostGameweek;
        teamBoostClubId = manager.teamBoostClubId;
        safeHandsGameweek = manager.safeHandsGameweek;
        safeHandsPlayerId = manager.safeHandsPlayerId;
        captainFantasticGameweek = manager.captainFantasticGameweek;
        captainFantasticPlayerId = manager.captainFantasticPlayerId;
        countrymenGameweek = manager.countrymenGameweek;
        countrymenCountryId = manager.countrymenCountryId;
        prospectsGameweek = manager.prospectsGameweek;
        braceBonusGameweek = manager.braceBonusGameweek;
        hatTrickHeroGameweek = manager.hatTrickHeroGameweek;
        transferWindowGameweek = manager.transferWindowGameweek;
        ownedPrivateLeagues = manager.ownedPrivateLeagues;
        privateLeagueMemberships = manager.privateLeagueMemberships;
      };
      managerBuffer.add(updatedManager);
    };
        
    await logStatus("All snapshots have been taken and the function is returning ." # Nat.toText(Array.size(Buffer.toArray(managerBuffer))));
    
    switch (managerGroup) {
      case 0 {
        managerGroup1 := Buffer.toArray(managerBuffer);
      };
      case 1 {
        managerGroup2 := Buffer.toArray(managerBuffer);
      };
      case 2 {
        managerGroup3 := Buffer.toArray(managerBuffer);
      };
      case 3 {
        managerGroup4 := Buffer.toArray(managerBuffer);
      };
      case 4 {
        managerGroup5 := Buffer.toArray(managerBuffer);
      };
      case 5 {
        managerGroup6 := Buffer.toArray(managerBuffer);
      };
      case 6 {
        managerGroup7 := Buffer.toArray(managerBuffer);
      };
      case 7 {
        managerGroup8 := Buffer.toArray(managerBuffer);
      };
      case 8 {
        managerGroup9 := Buffer.toArray(managerBuffer);
      };
      case 9 {
        managerGroup10 := Buffer.toArray(managerBuffer);
      };
      case 10 {
        managerGroup11 := Buffer.toArray(managerBuffer);
      };
      case 11 {
        managerGroup12 := Buffer.toArray(managerBuffer);
      };
      case _ {

      };
    };

will see how it goes

If you don’t await it is likely you’ll run out of cycles…I don’t think that is the issue.

Hey,

So I found the issue, bad code. I was transferring the managers between functions and I should just have done it in the function. Refactored and all working.

2 Likes