Skip to content

feat: charge cycles for log memory resize in update_settings#9883

Merged
maksymar merged 28 commits intomasterfrom
maksym/log-resize-charging
Apr 16, 2026
Merged

feat: charge cycles for log memory resize in update_settings#9883
maksymar merged 28 commits intomasterfrom
maksym/log-resize-charging

Conversation

@maksymar
Copy link
Copy Markdown
Contributor

@maksymar maksymar commented Apr 15, 2026

This PR adds cycle charging for log memory resize in update_settings.

When update_settings changes log_memory_limit, the log memory store rebuilds its ring buffer by reading and rewriting all stored records. This work is now charged proportionally to stored data.

Implementation follows a two-step pattern:

  1. validate_canister_settings: pre-check that the canister can afford the resize cost without dropping below the freezing threshold
  2. update_settings: deduct cycles and account instructions in the round budget after the resize completes

Only triggers when the user explicitly sets log_memory_limit and the resize would actually do work (capacity change), not when the default is applied during canister creation.

Changes:

  • Add LogResizeNotEnoughCycles error variant mapped to ErrorCode::CanisterOutOfCycles with a log-resize-specific message (no public API change).
  • Add would_resize() to predict whether a resize is a no-op, used both for the charge gate and as the early-return guard inside resize_impl.
  • Add tests for cycle deduction, rejection on insufficient balance, minimal charge on empty buffers, and no extra charge when the feature is disabled.

Rename fetch_canister_logs to fetch_canister_logs_query to clarify
it uses the query call path, distinguishing it from the replicated
inter-canister call path.
@github-actions github-actions bot added the feat label Apr 15, 2026
@maksymar maksymar changed the base branch from master to maksym/log-test-refactor April 15, 2026 06:37
@maksymar maksymar changed the base branch from maksym/log-test-refactor to master April 15, 2026 07:25
@maksymar maksymar changed the base branch from master to maksym/log-test-refactor April 15, 2026 07:25
@maksymar maksymar marked this pull request as ready for review April 15, 2026 07:35
@maksymar maksymar requested a review from a team as a code owner April 15, 2026 07:35
When update_settings changes log_memory_limit, the log memory store
rebuilds its ring buffer by reading and rewriting all stored records.
This work is now charged proportionally to the amount of stored data
(bytes_used), converted to cycles via management_canister_cost.

Implementation follows a two-step pattern:
1. validate_canister_settings: pre-check that the canister can afford
   the resize cost without dropping below the freezing threshold
2. update_settings: deduct cycles and account instructions in the
   round budget after the resize completes

Only triggers when the user explicitly sets log_memory_limit, not
when the default is applied during canister creation.

Add LogResizeNotEnoughCycles error variant, inter-canister log fetch
test helpers, expanded resize preservation tests, and two new tests
verifying cycle deduction and rejection on insufficient balance.
@maksymar maksymar force-pushed the maksym/log-resize-charging branch from dcabf57 to db6f7ce Compare April 15, 2026 07:46
@maksymar maksymar marked this pull request as draft April 15, 2026 07:51
Change LogResizeNotEnoughCycles from a struct variant to a tuple
wrapping CanisterOutOfCyclesError, matching the CanisterSnapshotNotEnoughCycles
pattern. Use InsufficientCyclesInMemoryAllocation for the pre-check in
validate_canister_settings (which has no canister_id), and the clean
.map_err(CanisterManagerError::LogResizeNotEnoughCycles) for the
deduction step in update_settings.
Clean up existing resize tests to separate concerns:
- Remove balance assertions from preserve tests (charging is tested
  separately)

Add three new tests:
- test_canister_log_resize_empty_buffer_minimal_charge: verifies
  near-zero cost when buffer has no records
- test_canister_log_resize_no_charge_without_limit: verifies no
  resize charge when update_settings doesn't set log_memory_limit
- test_canister_log_resize_no_extra_charge_feature_disabled: verifies
  no resize charge when the log memory store feature is disabled
…MemoryAllocation

Use the existing InsufficientCyclesInMemoryAllocation error for both
the validation pre-check and the deduction step. This keeps the error
uniform with other memory-related settings failures and avoids adding
a new error variant.
Replace 0-byte messages with 100-byte messages across all resize tests
for realistic log record sizes. Use repeat counts that fill the buffer
in a single ingress call (20K for 2 MiB, 3K for 256 KiB). Remove
intermediate variables and multi-call fill loops. Replace magic cycles
constant with a named fetch_cycles variable. Remove unused
LogMemoryStore import.
Comment thread rs/config/src/execution_environment.rs Outdated
Base automatically changed from maksym/log-test-refactor to master April 15, 2026 12:01
Replace hardcoded repeat counts (20_000, 3_000) with records_to_fill()
helper that calculates the count from the buffer size and
LogMemoryStore::estimate_record_size(). This keeps the fill logic
correct if record overhead changes.
The test could not reliably detect the user_set_log_memory_limit
guard removal because the resize charge after subnet scaling is too
small relative to the base update_settings cost. The guard is simple
code verified by review, and the remaining tests (deducts_cycles,
rejected, empty_buffer) cover all charging logic.
Clarify what the resize operation does and add a TODO about moving
the constant into CyclesAccountManagerConfig.
Replace misuse of InsufficientCyclesInMemoryAllocation for log resize
cycle checks with a dedicated error variant that reports accurate
context (available/threshold/requested cycles instead of memory
allocation).
- Assert error code and message in rejection test instead of just
  checking is_err().
- Increase buffer size to 2 MiB in deduction test to maximize the
  observable cost.
Extract log_resize_cost_per_byte variable to clarify the constant's
origin and link it back to LOG_RESIZE_COST_PER_BYTE in canister_manager.

This comment was marked as outdated.

Use consume_with_threshold with zero threshold instead of
consume_cycles_for_management_canister_instructions to prevent
the cycle deduction from failing after settings have already been
mutated. The validation in validate_canister_settings already
confirms affordability, so the post-mutation deduction is safe.
Group the limit check and cycle affordability check under the
user-set branch, and move the default for canister creation into
an explicit else branch with a comment explaining the no-op behavior
for existing canisters.
Capture bytes_used before do_update_settings so that downsize
operations are charged for the actual work of reading/rewriting
the original buffer, not the reduced post-resize size.

This comment was marked as outdated.

The log-resize check validated against the pre-mutation main balance,
but do_update_settings() calls reserve_cycles() before the resize
charge is deducted. This could cause the consume_with_threshold()
expect to panic when reservation_cycles + threshold + log_resize_cycles
exceeded the balance, even though the individual checks passed.

This comment was marked as outdated.

The resize cost was charged even when resize_impl would do no work
(e.g., capacity unchanged or limit set to 0 with an already-empty
store). Add LogMemoryStore::would_resize() to mirror the early-return
logic and use it to gate both the affordability check and the actual
cycle deduction.
Split the conflated feature-disabled and limit-zero branches in
would_resize into separate explicit checks. Have resize_impl use
would_resize directly as its early-return guard, eliminating
duplicated decision logic that could diverge.

Add test_would_resize_matches_resize covering all edge cases:
unallocated, allocated-empty, allocated-with-data, minimum-capacity
clamping, and feature-disabled states.
…ghCycles

Remove the dedicated ErrorCode::InsufficientCyclesInLogResize (542) public
API variant and instead use an internal LogResizeNotEnoughCycles error that
maps to the existing ErrorCode::CanisterOutOfCycles. This avoids a public
API change, protobuf additions, and upgrade coordination while still
providing a log-resize-specific error message for debuggability.
Copy link
Copy Markdown
Contributor

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rs/execution_environment/tests/canister_logging.rs
Comment thread rs/execution_environment/src/canister_manager.rs
Since resize_impl delegates to would_resize as its first check
(fb8f721), this test only asserts that would_resize agrees with
itself. The invariant is structurally guaranteed by the code.
@maksymar maksymar marked this pull request as ready for review April 16, 2026 09:13
@maksymar maksymar added this pull request to the merge queue Apr 16, 2026
Merged via the queue into master with commit 227397c Apr 16, 2026
37 checks passed
@maksymar maksymar deleted the maksym/log-resize-charging branch April 16, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants