Try/Finally preventing a future upgrade?

I was recently reading the Motoko try/finally docs and saw this note.

Does this mean that trapping in a finally block could prevent a future upgrade? That seems like a pretty big hole in the implementation if so?

4 Likes

Indeed, trapping in finally is a bad thing. There is no such thing as a tower of cleanups that magically removes all traces of history from the canister state. The canister has one shot at it. I don’t think this is different from the Drop-trait based cleanup as employed in the Rust CDK. So, yes, you should audit your finally blocks very well as suggested by the RTFM :slight_smile:

Yes, but preventing a canister from upgrading is a far worse potential outcome than failing to reset/clean up state in the finally block.

My worry is that this is a language feature, so developers assume it’s safe and skip over the consequences. I’d err on the side that most people don’t fully read the docs, and assume this would be just like the finally keyword in any other language (no understanding of secondary trap behavior).

I don’t understand. Can someone explain in more detail please?

Is there a difference in terms of future upgradability between
a) using try-catch without finally and accidentally trapping inside the catch block or after the catch block, and
b) using try-catch-finally and accidentally trapping inside the finally block?

And if try-catch-finally is used and and the finally block accidentally traps: Does the effect on future upgradability depend on how the finally block was entered, i.e. whether it was entered via a trap in the try block or without a trap in the try block?

I think the answer is yes, there’s a difference.

Motoko refuses to upgrade if there is some pending call that needs to complete, indicated by the non-empty ness of an internal callback table.

Without a finally clause, Motoko uses the ICP cleanup facility to remove a faulting callback from the table.

With a finally clause, Motoko runs the finally chain before removing the table entry. If the finally chain traps, the table entry is not removed. Removing the entry upfront would not help as the trap would still revert the removal.

However, even if the non-empty ness of the callback table prevents upgrades, I believe that stopping the canister first will allow the upgrade to take place, even if the callback table is non-empty.

If you read the doc above, it even states that you can stop the canister to get out of the stuck state.

1 Like

Just for clarity, if you have outstanding awaits on other canisters you have to wait until they come back before your canister will stop. Are these callbacks different in that they would allow your canister to stop?

Obviously they may be different in that one is internal and one is external, but I’m curious if there is more to it than that.

Ok, good to know! So then in the case of a trap in the finally block, would the developer receive a specific error when trying to upgrade their canister or would the error be opaque? Would I receive something like the outstanding callbacks error?

So then for clarity, the steps would be

  1. Trap occurs in finally block (developer is unaware)
  2. Developer tries to upgrade canister, receives error (opaque or specific?).
  3. Developer troubleshoots (somehow comes to realize there was an error in the finally block and they need to upgrade while the canister is stopped).
  4. Developer stops the canister
  5. Developer performs the upgrade.
  6. Developer restarts the canister

At this point, is the canister is in a healthy state (ability to upgrade without stopping the canister)?

Yes, that’s the recipe I think should work.

But if we haven’t tested this already, we probably should @ggreif.

The callback is triggered in reaction to some reply or reject from an inter-canister call.

I expect, but don’t know for sure, that the protocol will consider the inner call completed,
whether or not the callback traps.

I also expect, but don’t know for sure, that the outer call is considered completed if the callback traps.

The thing that is preventing the upgrade is a Motoko specific internal check on the callback table. The table won’t be cleaned up properly if a finally block traps after it’s guarded callback has trapped.