feat: add optional on_complete hook to canister endpoints#703
feat: add optional on_complete hook to canister endpoints#703hpeebles wants to merge 7 commits intodfinity:mainfrom
on_complete hook to canister endpoints#703Conversation
There was a problem hiding this comment.
Pull request overview
Adds an optional on_complete hook to ic-cdk canister endpoint macros so canister developers can run a callback after an endpoint call finishes (e.g., for metrics), passing basic request/response size metadata.
Changes:
- Introduces
api::OnExecutionCompleteArgsto carry endpoint name, request arg size, and response size. - Extends the endpoint macro attribute parsing/codegen to optionally create/populate these args and invoke an
on_completecallback. - Adds a macro-generation test and updates the changelog entry.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
ic-cdk/src/api.rs |
Adds the public OnExecutionCompleteArgs type and constructor used by macro-generated wrappers. |
ic-cdk/CHANGELOG.md |
Documents the new optional on_complete hook under unreleased. |
ic-cdk-macros/src/export.rs |
Adds on_complete attribute support, injects codegen to populate arg/return byte lengths, invokes the hook, and adds a test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let arg_bytes = #cratename::api::msg_arg_data(); | ||
| #on_complete_arg_len | ||
| let ( #( #arg_tuple, )* ) = #decode_with_ident(arg_bytes); } |
There was a problem hiding this comment.
The generated token stream for the multi-argument decode_with branch appears to include an extra } at the end of the quote! block (... = #decode_with_ident(arg_bytes); }). This will produce syntactically invalid generated code for endpoints using a custom decoder with 2+ args. Remove the stray brace and consider adding a test that covers decode_with with multiple arguments (ideally combined with on_complete as well).
| let arg_bytes = #cratename::api::msg_arg_data(); | |
| #on_complete_arg_len | |
| let ( #( #arg_tuple, )* ) = #decode_with_ident(arg_bytes); } | |
| let arg_bytes = #cratename::api::msg_arg_data(); | |
| #on_complete_arg_len | |
| let ( #( #arg_tuple, )* ) = #decode_with_ident(arg_bytes); | |
| } |
| #arg_decode | ||
| let result = #function_call; | ||
| #return_encode | ||
| #on_complete_ident |
There was a problem hiding this comment.
on_complete is invoked after the reply is constructed/sent (msg_reply happens in return_encode, then #on_complete_ident runs). If the hook traps/panics, the canister method will trap and the reply may never be delivered, which is a risky default for a metrics-style callback. Consider isolating failures (e.g., best-effort execution that can’t abort the method) and/or explicitly documenting that the hook must not trap.
| #on_complete_ident | |
| let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| { | |
| #on_complete_ident | |
| })); |
| /// The arguments passed to the `on_complete` callback exposed by canister endpoint macros | ||
| #[derive(Debug)] | ||
| pub struct OnExecutionCompleteArgs { | ||
| /// The name of the canister endpoint | ||
| pub endpoint_name: &'static str, | ||
| /// The number of bytes in the request arg | ||
| pub arg_bytes_len: usize, | ||
| /// The number of bytes returned in the response | ||
| pub return_bytes_len: usize, | ||
| } |
There was a problem hiding this comment.
This is a new public args struct intended to be consumed by user code. With public fields and no #[non_exhaustive], adding additional fields later (e.g., instructions used) will be a breaking change for downstream pattern matches/struct literals. Consider marking the struct #[non_exhaustive] and/or keeping fields private with accessor methods to make future extension non-breaking.
Description
This allows canister developers to implement a function which will be executed once a call to a canister endpoint has completed. We have been using this within a new project we are working on to track metrics for each canister endpoint.
Example usage:
The hook's args currently look like this:
I purposely didn't include the instructions since that can optionally be looked up within the callback function.
How Has This Been Tested?
I have added a test which checks the macro generates the expected code.
Also, we have annotated all update and lifecycle endpoints with this new callback within a new project we are working on and have given it lots of usage.
Checklist: