Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
35fa444
refactor: improve canister logging test helpers
maksymar Apr 14, 2026
db6f7ce
feat: charge cycles for log memory resize in update_settings
maksymar Apr 15, 2026
1c85187
refactor: simplify LogResizeNotEnoughCycles to tuple variant
maksymar Apr 15, 2026
316f234
test: improve resize test structure and add coverage
maksymar Apr 15, 2026
40eabed
refactor: remove LogResizeNotEnoughCycles, reuse InsufficientCyclesIn…
maksymar Apr 15, 2026
b54d97a
refactor: simplify resize tests to use realistic 100-byte messages
maksymar Apr 15, 2026
230624d
merge master
maksymar Apr 15, 2026
8fa3adb
refactor: compute repeat count from estimated record size
maksymar Apr 15, 2026
5a2ca1a
test: remove unreliable no_charge_without_limit test
maksymar Apr 15, 2026
061fdf3
docs: improve LOG_RESIZE_COST_PER_BYTE doc comment
maksymar Apr 15, 2026
98e37f7
refactor: add InsufficientCyclesInLogResize error variant
maksymar Apr 15, 2026
a9c27ab
test: improve log resize charging tests
maksymar Apr 15, 2026
81a9e94
test: replace magic number with named variable in resize test
maksymar Apr 15, 2026
f237b2a
typo
maksymar Apr 15, 2026
67048a1
fix: avoid recomputing freezing threshold in log resize cycle deduction
maksymar Apr 15, 2026
0b8371f
typo
maksymar Apr 15, 2026
e6f1c13
refactor: consolidate log_memory_limit validation into a single if/else
maksymar Apr 15, 2026
451e2de
fix: charge log resize based on pre-mutation bytes_used
maksymar Apr 15, 2026
2ab7012
.
maksymar Apr 15, 2026
06cdb77
cleanup
maksymar Apr 15, 2026
79c2064
fix: account for reservation_cycles in log resize affordability check
maksymar Apr 15, 2026
ee9ce82
fix: skip log resize charge when resize would be a no-op
maksymar Apr 15, 2026
fb8f721
refactor: unify would_resize and resize_impl no-op logic
maksymar Apr 15, 2026
af1e42a
refactor: replace InsufficientCyclesInLogResize with LogResizeNotEnou…
maksymar Apr 16, 2026
0cc3b3c
test: remove redundant test_would_resize_matches_resize
maksymar Apr 16, 2026
6dbf8b2
LOG_MEMORY_STORE_FEATURE_ENABLED false
maksymar Apr 16, 2026
16a973a
Merge branch 'master' into maksym/log-resize-charging
maksymar Apr 16, 2026
6a490ea
cleanup
maksymar Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 91 additions & 7 deletions rs/execution_environment/src/canister_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ pub(crate) mod types;
/// Maximum binary slice size allowed per single message download.
const MAX_SLICE_SIZE_BYTES: u64 = 2_000_000;

/// Instructions charged per byte of stored log data during log memory resize.
///
/// When the log memory limit changes, all existing records must be read from
/// the old ring buffer into heap memory, re-encoded, and written into a newly
/// allocated ring buffer. The cost is proportional to the bytes currently
/// stored (not the allocated capacity).
///
/// TODO(DSM-11): Consider moving this constant into `CyclesAccountManagerConfig`
/// alongside other per-byte fee parameters.
const LOG_RESIZE_COST_PER_BYTE: u64 = 32;

/// Contains validated cycles and memory usage:
/// - cycles for instructions that can be consumed safely;
/// - new memory usage (to compute the new freezing threshold);
Expand Down Expand Up @@ -323,6 +334,7 @@ impl CanisterManager {
/// - it cannot be lower than the current canister memory usage.
/// - there must be enough available subnet capacity for the change.
/// - there must be enough cycles for storage reservation.
/// - there must be enough cycles for resizing log storage.
/// - there must be enough cycles to avoid freezing the canister.
/// - compute allocation:
/// - there must be enough available compute capacity for the change.
Expand Down Expand Up @@ -354,6 +366,8 @@ impl CanisterManager {
cost_schedule: CanisterCyclesCostSchedule,
canister_reserved_balance: Cycles,
canister_reserved_balance_limit: Option<Cycles>,
canister_log_bytes_used: NumBytes,
log_resize_needed: bool,
) -> Result<ValidatedCanisterSettings, CanisterManagerError> {
self.validate_environment_variables(&settings)?;

Expand Down Expand Up @@ -501,20 +515,44 @@ impl CanisterManager {
});
}

let log_memory_limit = settings.log_memory_limit().or(Some(NumBytes::new(
DEFAULT_AGGREGATE_LOG_MEMORY_LIMIT as u64,
)));
if let Some(requested_limit) = log_memory_limit {
// User can setup a zero log memory limit to disable logging.
// But cannot set it higher than the maximum limit.
let log_memory_limit = if let Some(requested_limit) = settings.log_memory_limit() {
// User explicitly sets log_memory_limit: validate the limit
// and check the canister can afford the resize cost.
let max_limit = NumBytes::new(MAX_AGGREGATE_LOG_MEMORY_LIMIT as u64);
if requested_limit > max_limit {
return Err(CanisterManagerError::CanisterLogMemoryLimitIsTooHigh {
bytes: requested_limit,
limit: max_limit,
});
}
}
// Resizing reads all stored log records from the old ring buffer and
// rewrites them into a new one. Cost is proportional to bytes_used
// (actual stored data), not allocated capacity.
// Skip the charge when resize would be a no-op (e.g., capacity
// unchanged or limit set to 0 with an already-empty store).
if log_resize_needed {
let log_resize_instructions =
NumInstructions::new(canister_log_bytes_used.get() * LOG_RESIZE_COST_PER_BYTE);
let log_resize_cycles = self
.cycles_account_manager
.management_canister_cost(log_resize_instructions, subnet_size, cost_schedule)
.real();
if canister_cycles_balance < reservation_cycles + threshold + log_resize_cycles {
return Err(CanisterManagerError::LogResizeNotEnoughCycles {
available: canister_cycles_balance,
threshold: reservation_cycles + threshold,
requested: log_resize_cycles,
});
}
}
Some(requested_limit)
} else {
// User did not set log_memory_limit: default so that new canisters
// get a log memory store initialized via do_update_settings.
// For existing canisters this is a no-op (resize early-returns
// when capacity is unchanged). No cycles are charged.
Some(NumBytes::new(DEFAULT_AGGREGATE_LOG_MEMORY_LIMIT as u64))
};

Ok(ValidatedCanisterSettings::new(
settings.controllers(),
Expand Down Expand Up @@ -557,6 +595,8 @@ impl CanisterManager {
cost_schedule,
Cycles::zero(),
None,
NumBytes::new(0),
true, // New canister: resize always needed.
)
}

Expand Down Expand Up @@ -634,6 +674,15 @@ impl CanisterManager {

validate_controller(canister, &sender)?;

let log_resize_needed = settings
.log_memory_limit()
.map(|limit| {
canister
.system_state
.log_memory_store
.would_resize(limit.get() as usize)
})
.unwrap_or(false);
Comment thread
maksymar marked this conversation as resolved.
let validated_settings = self.validate_canister_settings(
settings,
canister.memory_usage(),
Expand All @@ -649,11 +698,14 @@ impl CanisterManager {
cost_schedule,
canister.system_state.reserved_balance(),
canister.system_state.reserved_balance_limit(),
NumBytes::new(canister.system_state.log_memory_store.bytes_used() as u64),
log_resize_needed,
)?;

let old_usage = canister.memory_usage();
let old_mem = canister.memory_allocation().allocated_bytes(old_usage);
let old_compute_allocation = canister.compute_allocation().as_percent();
let old_log_bytes_used = canister.system_state.log_memory_store.bytes_used() as u64;

self.do_update_settings(&validated_settings, canister);

Expand Down Expand Up @@ -684,6 +736,38 @@ impl CanisterManager {
);
}

// Deduct cycles and account instructions for log resize.
// Validation was done in validate_canister_settings; this is the actual deduction.
// Use consume_with_threshold with zero threshold instead of
// consume_cycles_for_management_canister_instructions to avoid
// recomputing the freezing threshold from post-mutation memory usage,
// which could fail despite validation having passed.
if log_resize_needed {
// Use pre-mutation bytes_used so that downsize operations (which drop
// old records) are charged for the actual work of reading/rewriting
// the original buffer, matching what validation checked.
let log_resize_instructions =
NumInstructions::new(old_log_bytes_used * LOG_RESIZE_COST_PER_BYTE);
let log_resize_cycles = self.cycles_account_manager.management_canister_cost(
log_resize_instructions,
subnet_size,
cost_schedule,
);
let reveal_top_up = canister.system_state.controllers.contains(&sender);
self.cycles_account_manager
.consume_with_threshold(
&mut canister.system_state,
log_resize_cycles,
Cycles::zero(),
reveal_top_up,
)
.expect(
"Consuming cycles for log resize should succeed because \
the canister settings have been validated.",
);
Comment thread
maksymar marked this conversation as resolved.
round_limits.instructions -= as_round_instructions(log_resize_instructions);
}
Comment thread
maksymar marked this conversation as resolved.

canister.system_state.bump_canister_version();
let new_controllers = match validated_settings.controllers() {
Some(_) => Some(canister.system_state.controllers.iter().copied().collect()),
Expand Down
21 changes: 21 additions & 0 deletions rs/execution_environment/src/canister_manager/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,11 @@ pub(crate) enum CanisterManagerError {
available: Cycles,
required: Cycles,
},
LogResizeNotEnoughCycles {
available: Cycles,
threshold: Cycles,
requested: Cycles,
},
ReservedCyclesLimitExceededInMemoryAllocation {
memory_allocation: MemoryAllocation,
requested: Cycles,
Expand Down Expand Up @@ -609,6 +614,10 @@ impl AsErrorHelp for CanisterManagerError {
suggestion: "Top up the canister with more cycles.".to_string(),
doc_link: doc_ref("insufficient-cycles-in-memory-grow-1"),
},
CanisterManagerError::LogResizeNotEnoughCycles { .. } => ErrorHelp::UserError {
suggestion: "Top up the canister with more cycles.".to_string(),
doc_link: doc_ref("log-resize-not-enough-cycles"),
},
CanisterManagerError::ReservedCyclesLimitExceededInMemoryAllocation { .. } => {
ErrorHelp::UserError {
suggestion: "Try increasing this canister's reserved cycles limit or moving \
Expand Down Expand Up @@ -991,6 +1000,18 @@ impl From<CanisterManagerError> for UserError {
required - available
),
),
LogResizeNotEnoughCycles {
available,
threshold,
requested,
} => Self::new(
ErrorCode::CanisterOutOfCycles,
format!(
"Cannot resize canister log memory due to insufficient cycles. \
At least {} additional cycles are required.{additional_help}",
(threshold + requested) - available
),
),
ReservedCyclesLimitExceededInMemoryAllocation {
memory_allocation,
requested,
Expand Down
Loading
Loading