Skip to content

feat: add optional on_complete hook to canister endpoints#703

Open
hpeebles wants to merge 7 commits intodfinity:mainfrom
hpeebles:on-complete
Open

feat: add optional on_complete hook to canister endpoints#703
hpeebles wants to merge 7 commits intodfinity:mainfrom
hpeebles:on-complete

Conversation

@hpeebles
Copy link
Copy Markdown
Contributor

@hpeebles hpeebles commented Mar 26, 2026

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:

#[update(on_complete = "record_execution")]
fn send_message(args: SendMessageArgs) -> SendMessageResponse {
    ...
}

fn record_execution(args: OnExecutionCompleteArgs) {
    let instructions = ic_cdk::api::instruction_counter();
    
    // Record request/response sizes + instructions used in metrics
    ...
}

The hook's args currently look like this:

/// 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,
}

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:

  • The title of this PR complies with Conventional Commits.
  • I have edited the CHANGELOG accordingly.
  • I have made corresponding changes to the documentation.

@hpeebles hpeebles requested a review from a team as a code owner March 26, 2026 09:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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::OnExecutionCompleteArgs to 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_complete callback.
  • 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.

Comment on lines 273 to 275
let arg_bytes = #cratename::api::msg_arg_data();
#on_complete_arg_len
let ( #( #arg_tuple, )* ) = #decode_with_ident(arg_bytes); }
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);
}

Copilot uses AI. Check for mistakes.
#arg_decode
let result = #function_call;
#return_encode
#on_complete_ident
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
#on_complete_ident
let _ = ::std::panic::catch_unwind(::std::panic::AssertUnwindSafe(|| {
#on_complete_ident
}));

Copilot uses AI. Check for mistakes.
Comment thread ic-cdk/src/api.rs
Comment on lines +602 to +611
/// 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,
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants