Skip to content

fix: lower the consumed cycles gauge on a full refund - #11274

Merged
mraszyk merged 2 commits into
masterfrom
mraszyk/fix-full-refund-consumed-cycles-gauge
Aug 24, 2026
Merged

fix: lower the consumed cycles gauge on a full refund#11274
mraszyk merged 2 commits into
masterfrom
mraszyk/fix-full-refund-consumed-cycles-gauge

Conversation

@mraszyk

@mraszyk mraszyk commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

SystemState::observe_consumed_cycles_with_use_case() maintains two kinds of metrics: the gauges (consumed_cycles and consumed_cycles_by_use_cases), which are raised by the prepayment and lowered by the refund; and the monotonic counters (consumed_cycles_by_use_cases_as_counters), which for the refundable use cases (Instructions and RequestAndResponseTransmission) are only bumped at refund time, by prepayment - refund.

The early return conflated the two: for a refund equal to its prepayment it skipped the update altogether, so the gauges kept the prepayment even though nothing was consumed in the end. The inflated value also outlives the canister, as CanisterManager::delete_canister() folds consumed_cycles_by_use_cases into the subnet level metrics.

Skip only if there is nothing to record at all, i.e. if both the prepayment and the refund are zero. Non-refundable use cases are unaffected, as a zero prepayment implies a zero refund for them, and NominalCycles saturates on subtraction, so a refund against a freshly created entry cannot underflow.

Tests: the new full_refund_resets_consumed_cycles refunds a prepayment in full and checks that the balance and both gauges are back to where they started and that the counter did not move. It covers both refundable use cases under both cost schedules. The gauges are kept in nominal cycles, which the cost schedule does not affect, so a full refund under Free hits the same skipped update; it is in fact the more interesting case, as Free waives the real amount and hence leaves the metrics as the only observable of the prepayment. The assertion on the balance after the prepayment (which drops by the real amount under Normal and stays put under Free) pins down that difference. Each of the four combinations fails without the fix.

`SystemState::observe_consumed_cycles_with_use_case()` maintains two kinds of
metrics: the gauges (`consumed_cycles` and `consumed_cycles_by_use_cases`),
which are raised by the prepayment and lowered by the refund; and the monotonic
counters (`consumed_cycles_by_use_cases_as_counters`), which for the refundable
use cases (`Instructions` and `RequestAndResponseTransmission`) are only bumped
at refund time, by `prepayment - refund`.

The early return conflated the two: for a refund equal to its prepayment it
skipped the update altogether, so the gauges kept the prepayment even though
nothing was consumed in the end. The inflated value also outlives the canister,
as `CanisterManager::delete_canister()` folds `consumed_cycles_by_use_cases`
into the subnet level metrics.

Skip only if there is nothing to record at all, i.e. if both the prepayment and
the refund are zero. Non-refundable use cases are unaffected, as a zero
prepayment implies a zero refund for them, and `NominalCycles` saturates on
subtraction, so a refund against a freshly created entry cannot underflow.

Tests: the new `full_refund_resets_consumed_cycles` refunds a prepayment in
full and checks that the balance and both gauges are back to where they started
and that the counter did not move. It covers both refundable use cases under
both cost schedules. The gauges are kept in nominal cycles, which the cost
schedule does not affect, so a full refund under `Free` hits the same skipped
update; it is in fact the more interesting case, as `Free` waives the real
amount and hence leaves the metrics as the only observable of the prepayment.
The assertion on the balance after the prepayment (which drops by the real
amount under `Normal` and stays put under `Free`) pins down that difference.
Each of the four combinations fails without the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes consumed-cycle gauges remaining inflated after a full refund.

Changes:

  • Updates metric handling to process equal prepayment/refund amounts.
  • Adds coverage for both refundable use cases and cost schedules.

Reviewed changes

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

File Description
rs/replicated_state/src/canister_state/system_state.rs Corrects the metric-update early return.
rs/replicated_state/src/canister_state/tests.rs Tests full-refund balance, gauges, and counters.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@mraszyk
mraszyk marked this pull request as ready for review August 24, 2026 08:44
@mraszyk
mraszyk requested a review from a team as a code owner August 24, 2026 08:44
@zeropath-ai

zeropath-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown

No security or compliance issues detected. Reviewed everything up to 594a004.

Security Overview
Detected Code Changes
Change Type Relevant files
Bug Fix ► rs/replicated_state/src/canister_state/system_state.rs
      Skip only if there is nothing to record at all. Note that a refund equal to its prepayment still has to lower the gauge by the refunded amount, even though it contributes nothing to the monotonic counter.
Enhancement ► rs/replicated_state/src/canister_state/tests.rs
      Add test: full refund resets consumed cycles
► rs/replicated_state/src/canister_state/tests.rs
      Extend imports to include CyclesUseCaseRefundableKind and related types
► rs/replicated_state/src/canister_state/tests.rs
      Implement test logic for full refund behavior across CanisterCyclesCostSchedule variants

Comment thread rs/replicated_state/src/canister_state/tests.rs
…cles

Address review feedback: prepay for the same use case once more without ever
refunding it, so the full refund is expected to bring the gauges back down to
that outstanding prepayment rather than all the way to zero. This pins down
that the refund subtracts the refunded amount instead of dropping or clearing
the use case entry, which a zero baseline cannot distinguish.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mraszyk
mraszyk enabled auto-merge August 24, 2026 12:52
@mraszyk
mraszyk added this pull request to the merge queue Aug 24, 2026
Merged via the queue into master with commit 369f5bf Aug 24, 2026
68 of 70 checks passed
@mraszyk
mraszyk deleted the mraszyk/fix-full-refund-consumed-cycles-gauge branch August 24, 2026 14:27
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.

3 participants