diff --git a/crates/icp-cli/src/commands/canister/create.rs b/crates/icp-cli/src/commands/canister/create.rs index 4ac16b72..a473ec81 100644 --- a/crates/icp-cli/src/commands/canister/create.rs +++ b/crates/icp-cli/src/commands/canister/create.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; use candid::{Nat, Principal}; use clap::{ArgGroup, Args, Parser}; use icp::context::Context; -use icp::parsers::{CyclesAmount, MemoryAmount}; +use icp::parsers::{CyclesAmount, DurationAmount, MemoryAmount}; use icp::{Canister, context::CanisterSelection, prelude::*}; use icp_canister_interfaces::cycles_ledger::CanisterSettingsArg; @@ -21,9 +21,11 @@ pub(crate) struct CanisterSettings { #[arg(long)] pub(crate) memory_allocation: Option, - /// Optional freezing threshold in seconds. Controls how long a canister can be inactive before being frozen. + /// Optional freezing threshold. Controls how long a canister can be inactive before being frozen. + /// Supports duration suffixes: s (seconds), m (minutes), h (hours), d (days), w (weeks). + /// A bare number is treated as seconds. #[arg(long)] - pub(crate) freezing_threshold: Option, + pub(crate) freezing_threshold: Option, /// Optional upper limit on cycles reserved for future resource payments. /// Memory allocations that would push the reserved balance above this limit will fail. @@ -96,8 +98,9 @@ impl CreateArgs { freezing_threshold: self .settings .freezing_threshold - .or(default.settings.freezing_threshold) - .map(Nat::from), + .clone() + .or(default.settings.freezing_threshold.clone()) + .map(|d| Nat::from(d.get())), controllers: if self.controller.is_empty() { None } else { @@ -127,7 +130,11 @@ impl CreateArgs { pub(crate) fn canister_settings(&self) -> CanisterSettingsArg { CanisterSettingsArg { - freezing_threshold: self.settings.freezing_threshold.map(Nat::from), + freezing_threshold: self + .settings + .freezing_threshold + .clone() + .map(|d| Nat::from(d.get())), controllers: if self.controller.is_empty() { None } else { diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index 4d153fda..2bd7d8ce 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -6,7 +6,7 @@ use ic_agent::export::Principal; use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; use icp::ProjectLoadError; use icp::context::{CanisterSelection, Context, TermWriter}; -use icp::parsers::{CyclesAmount, MemoryAmount}; +use icp::parsers::{CyclesAmount, DurationAmount, MemoryAmount}; use std::collections::{HashMap, HashSet}; use std::io::Write; @@ -100,8 +100,11 @@ pub(crate) struct UpdateArgs { #[arg(long)] memory_allocation: Option, - #[arg(long, value_parser = freezing_threshold_parser)] - freezing_threshold: Option, + /// Freezing threshold. Controls how long a canister can be inactive before being frozen. + /// Supports duration suffixes: s (seconds), m (minutes), h (hours), d (days), w (weeks). + /// A bare number is treated as seconds. + #[arg(long)] + freezing_threshold: Option, /// Upper limit on cycles reserved for future resource payments. /// Memory allocations that would push the reserved balance above this limit will fail. @@ -231,13 +234,13 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: } update = update.with_memory_allocation(memory_allocation.get()); } - if let Some(freezing_threshold) = args.freezing_threshold { + if let Some(freezing_threshold) = &args.freezing_threshold { if configured_settings.freezing_threshold.is_some() { ctx.term.write_line( "Warning: Freezing threshold is already set in icp.yaml; this new value will be overridden on next settings sync" )? } - update = update.with_freezing_threshold(freezing_threshold); + update = update.with_freezing_threshold(freezing_threshold.get()); } if let Some(reserved_cycles_limit) = &args.reserved_cycles_limit { if configured_settings.reserved_cycles_limit.is_some() { @@ -288,13 +291,6 @@ fn compute_allocation_parser(compute_allocation: &str) -> Result { Err("Must be a percent between 0 and 100".to_string()) } -fn freezing_threshold_parser(freezing_threshold: &str) -> Result { - if let Ok(num) = freezing_threshold.parse::() { - return Ok(num); - } - Err("Must be a value between 0..2^64-1 inclusive".to_string()) -} - fn log_visibility_parser(log_visibility: &str) -> Result { match log_visibility { "public" => Ok(LogVisibility::Public), diff --git a/crates/icp-cli/src/operations/settings.rs b/crates/icp-cli/src/operations/settings.rs index 6780fb5e..651e6b8c 100644 --- a/crates/icp-cli/src/operations/settings.rs +++ b/crates/icp-cli/src/operations/settings.rs @@ -81,7 +81,7 @@ pub(crate) async fn sync_settings( ref log_visibility, compute_allocation, ref memory_allocation, - freezing_threshold, + ref freezing_threshold, ref reserved_cycles_limit, ref wasm_memory_limit, ref wasm_memory_threshold, @@ -119,7 +119,10 @@ pub(crate) async fn sync_settings( .as_ref() .map(|m| m.get()) .is_none_or(|s| current_settings.memory_allocation.0.to_u64() == Some(s)) - && freezing_threshold.is_none_or(|s| s == current_settings.freezing_threshold) + && freezing_threshold + .as_ref() + .map(|d| d.get()) + .is_none_or(|s| s == current_settings.freezing_threshold) && reserved_cycles_limit .as_ref() .is_none_or(|s| s.get() == current_settings.reserved_cycles_limit) @@ -142,7 +145,7 @@ pub(crate) async fn sync_settings( .with_optional_log_visibility(log_visibility_setting) .with_optional_compute_allocation(compute_allocation) .with_optional_memory_allocation(memory_allocation.as_ref().map(|m| m.get())) - .with_optional_freezing_threshold(freezing_threshold) + .with_optional_freezing_threshold(freezing_threshold.as_ref().map(|d| d.get())) .with_optional_reserved_cycles_limit(reserved_cycles_limit.as_ref().map(|r| r.get())) .with_optional_wasm_memory_limit(wasm_memory_limit.as_ref().map(|m| m.get())) .with_optional_wasm_memory_threshold(wasm_memory_threshold.as_ref().map(|m| m.get())) diff --git a/crates/icp-cli/tests/canister_create_tests.rs b/crates/icp-cli/tests/canister_create_tests.rs index 304b2d35..77373a0a 100644 --- a/crates/icp-cli/tests/canister_create_tests.rs +++ b/crates/icp-cli/tests/canister_create_tests.rs @@ -92,7 +92,7 @@ async fn canister_create_with_settings() { log_visibility: public compute_allocation: 1 memory_allocation: 4294967296 - freezing_threshold: 2592000 + freezing_threshold: 30d reserved_cycles_limit: 1000000000000 {NETWORK_RANDOM_PORT} diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index 2aeb012f..1e4c8043 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -728,7 +728,7 @@ async fn canister_settings_update_miscellaneous() { "--memory-allocation", "6GiB", "--freezing-threshold", - "8640000", + "100d", "--reserved-cycles-limit", "6t", "--wasm-memory-limit", diff --git a/crates/icp/src/canister/mod.rs b/crates/icp/src/canister/mod.rs index a5311958..2c568a45 100644 --- a/crates/icp/src/canister/mod.rs +++ b/crates/icp/src/canister/mod.rs @@ -5,7 +5,7 @@ use icp_canister_interfaces::cycles_ledger::CanisterSettingsArg; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::parsers::{CyclesAmount, MemoryAmount}; +use crate::parsers::{CyclesAmount, DurationAmount, MemoryAmount}; pub mod build; pub mod recipe; @@ -214,7 +214,8 @@ pub struct Settings { pub memory_allocation: Option, /// Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen. - pub freezing_threshold: Option, + /// Supports duration suffixes in YAML: s, m, h, d, w (e.g. "30d" or "4w"). + pub freezing_threshold: Option, /// Upper limit on cycles reserved for future resource payments. /// Memory allocations that would push the reserved balance above this limit will fail. @@ -239,7 +240,7 @@ pub struct Settings { impl From for CanisterSettingsArg { fn from(settings: Settings) -> Self { CanisterSettingsArg { - freezing_threshold: settings.freezing_threshold.map(Nat::from), + freezing_threshold: settings.freezing_threshold.map(|d| Nat::from(d.get())), controllers: None, reserved_cycles_limit: settings.reserved_cycles_limit.map(|c| Nat::from(c.get())), log_visibility: settings.log_visibility.map(Into::into), diff --git a/crates/icp/src/parsers.rs b/crates/icp/src/parsers.rs index d089aba1..b0a74730 100644 --- a/crates/icp/src/parsers.rs +++ b/crates/icp/src/parsers.rs @@ -1,4 +1,4 @@ -//! Parsing of token and cycle amounts with support for suffixes (k, m, b, t) and underscores. +//! Parsing of token, cycle, memory, and duration amounts with support for suffixes and underscores. use bigdecimal::{BigDecimal, Signed}; use num_bigint::BigUint; @@ -312,6 +312,145 @@ impl From for MemoryAmount { } } +const SECONDS_PER_MINUTE: u64 = 60; +const SECONDS_PER_HOUR: u64 = 3600; +const SECONDS_PER_DAY: u64 = 86400; +const SECONDS_PER_WEEK: u64 = 604800; + +fn parse_duration_str(s: &str) -> Result { + let s = s.trim(); + if s.is_empty() { + return Err("Duration cannot be empty".to_string()); + } + let lower = s.to_lowercase(); + let (number_part, factor) = if lower.ends_with('w') { + (&s[..s.len() - 1], SECONDS_PER_WEEK) + } else if lower.ends_with('d') { + (&s[..s.len() - 1], SECONDS_PER_DAY) + } else if lower.ends_with('h') { + (&s[..s.len() - 1], SECONDS_PER_HOUR) + } else if lower.ends_with('m') { + (&s[..s.len() - 1], SECONDS_PER_MINUTE) + } else if lower.ends_with('s') { + (&s[..s.len() - 1], 1u64) + } else { + (s, 1u64) + }; + let cleaned = number_part.trim().replace('_', ""); + if cleaned.is_empty() { + return Err(format!("Invalid duration: '{s}'")); + } + let value: u64 = cleaned + .parse() + .map_err(|_| format!("Invalid duration: '{s}'"))?; + value + .checked_mul(factor) + .ok_or_else(|| format!("Duration too large: '{s}'")) +} + +/// A duration in seconds. +/// +/// Deserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w) +/// and optional underscore separators. +/// +/// Suffixes (case-insensitive): +/// - `s` — seconds +/// - `m` — minutes (×60) +/// - `h` — hours (×3600) +/// - `d` — days (×86400) +/// - `w` — weeks (×604800) +/// +/// A bare number without suffix is treated as seconds. +#[derive(Clone, Debug, PartialEq, Eq, JsonSchema)] +#[schemars(untagged)] +pub enum DurationAmount { + Number(u64), + Str(String), +} + +impl DurationAmount { + pub fn get(&self) -> u64 { + match self { + DurationAmount::Number(n) => *n, + DurationAmount::Str(s) => { + parse_duration_str(s).unwrap_or_else(|e| panic!("invalid duration '{}': {}", s, e)) + } + } + } +} + +impl<'de> Deserialize<'de> for DurationAmount { + fn deserialize(d: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum Raw { + Number(u64), + Str(String), + } + let v = Raw::deserialize(d).map_err(|_| { + serde::de::Error::custom( + "duration must be a number (seconds) or a string with optional suffix (s, m, h, d, w), e.g. 2592000 or \"30d\"", + ) + })?; + let c = match v { + Raw::Number(n) => DurationAmount::Number(n), + Raw::Str(ref s) => { + parse_duration_str(s).map_err(serde::de::Error::custom)?; + DurationAmount::Str(s.clone()) + } + }; + Ok(c) + } +} + +impl Serialize for DurationAmount { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + DurationAmount::Number(n) => serializer.serialize_u64(*n), + DurationAmount::Str(s) => serializer.serialize_str(s), + } + } +} + +impl FromStr for DurationAmount { + type Err = String; + + fn from_str(s: &str) -> Result { + parse_duration_str(s)?; + Ok(DurationAmount::Str(s.to_string())) + } +} + +impl fmt::Display for DurationAmount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.get().fmt(f) + } +} + +impl From for u64 { + fn from(d: DurationAmount) -> Self { + d.get() + } +} + +impl From for DurationAmount { + fn from(n: u64) -> Self { + DurationAmount::Number(n) + } +} + +impl PartialEq for DurationAmount { + fn eq(&self, other: &u64) -> bool { + self.get() == *other + } +} + #[cfg(test)] mod tests { use super::*; @@ -427,4 +566,78 @@ mod tests { let m: MemoryAmount = serde_yaml::from_str(yaml).unwrap(); assert_eq!(m.get(), 4294967296); } + + #[test] + fn duration_amount_from_str_plain() { + assert_eq!("60".parse::().unwrap().get(), 60); + assert_eq!("2592000".parse::().unwrap().get(), 2592000); + } + + #[test] + fn duration_amount_from_str_underscores() { + assert_eq!( + "2_592_000".parse::().unwrap().get(), + 2592000 + ); + } + + #[test] + fn duration_amount_from_str_suffixes() { + assert_eq!("60s".parse::().unwrap().get(), 60); + assert_eq!("90m".parse::().unwrap().get(), 5400); + assert_eq!("24h".parse::().unwrap().get(), 86400); + assert_eq!("30d".parse::().unwrap().get(), 2592000); + assert_eq!("4w".parse::().unwrap().get(), 2419200); + } + + #[test] + fn duration_amount_from_str_case_insensitive() { + assert_eq!("30D".parse::().unwrap().get(), 2592000); + assert_eq!("1W".parse::().unwrap().get(), 604800); + assert_eq!("24H".parse::().unwrap().get(), 86400); + assert_eq!("60S".parse::().unwrap().get(), 60); + assert_eq!("90M".parse::().unwrap().get(), 5400); + } + + #[test] + fn duration_amount_from_str_underscores_with_suffix() { + assert_eq!( + "2_592_000s".parse::().unwrap().get(), + 2592000 + ); + } + + #[test] + fn duration_amount_from_str_errors() { + assert!("abc".parse::().is_err()); + assert!("".parse::().is_err()); + assert!("1x".parse::().is_err()); + assert!("1.5d".parse::().is_err()); + assert!("-1d".parse::().is_err()); + } + + #[test] + fn duration_amount_deserialize() { + let yaml = "30d"; + let d: DurationAmount = serde_yaml::from_str(yaml).unwrap(); + assert_eq!(d.get(), 2592000); + + let yaml = "2592000"; + let d: DurationAmount = serde_yaml::from_str(yaml).unwrap(); + assert_eq!(d.get(), 2592000); + + let yaml = "2_592_000"; + let d: DurationAmount = serde_yaml::from_str(yaml).unwrap(); + assert_eq!(d.get(), 2592000); + } + + #[test] + fn duration_amount_partial_eq_u64() { + let d = DurationAmount::Number(2592000); + assert!(d == 2592000); + assert!(d != 0); + + let d = DurationAmount::Str("30d".to_string()); + assert!(d == 2592000); + } } diff --git a/docs/concepts/environments.md b/docs/concepts/environments.md index 805f46a1..515724a5 100644 --- a/docs/concepts/environments.md +++ b/docs/concepts/environments.md @@ -137,7 +137,7 @@ environments: settings: backend: compute_allocation: 20 - freezing_threshold: 7776000 # 90 days + freezing_threshold: 90d ``` ### Environment-Specific Settings diff --git a/docs/guides/managing-environments.md b/docs/guides/managing-environments.md index b01c7eab..d2bcb4ec 100644 --- a/docs/guides/managing-environments.md +++ b/docs/guides/managing-environments.md @@ -57,7 +57,7 @@ environments: settings: backend: compute_allocation: 20 - freezing_threshold: 7776000 # 90 days + freezing_threshold: 90d environment_variables: LOG_LEVEL: "error" ``` @@ -176,11 +176,11 @@ environments: settings: frontend: memory_allocation: 4gib - freezing_threshold: 7776000 # 90 days + freezing_threshold: 90d backend: compute_allocation: 20 reserved_cycles_limit: 50t - freezing_threshold: 7776000 # 90 days + freezing_threshold: 90d environment_variables: API_ENV: "production" ``` diff --git a/docs/reference/canister-settings.md b/docs/reference/canister-settings.md index 1d3e37b5..dc0ed6bf 100644 --- a/docs/reference/canister-settings.md +++ b/docs/reference/canister-settings.md @@ -47,19 +47,21 @@ If not set, the canister uses dynamic memory allocation. ### freezing_threshold -Time in seconds before the canister freezes due to low cycles. +Time before the canister freezes due to low cycles. | Property | Value | |----------|-------| -| Type | Integer | -| Unit | Seconds | -| Default | 2,592,000 (30 days) | +| Type | Integer or string with duration suffix | +| Unit | Seconds (accepts duration suffixes) | +| Default | 2,592,000 seconds (30 days) | ```yaml settings: - freezing_threshold: 7776000 # 90 days + freezing_threshold: 90d ``` +Duration values accept suffixes: `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks). Underscores are supported in the numeric part (e.g. `2_592_000`). A bare number is treated as seconds. Raw second counts are also accepted for backwards compatibility. + The canister freezes if its cycles balance would be exhausted within this threshold. ### reserved_cycles_limit @@ -169,7 +171,7 @@ canisters: settings: compute_allocation: 5 memory_allocation: 2gib - freezing_threshold: 2592000 # 30 days + freezing_threshold: 30d reserved_cycles_limit: 5t wasm_memory_limit: 1gib wasm_memory_threshold: 512mib @@ -196,7 +198,7 @@ environments: settings: backend: compute_allocation: 20 # Production override - freezing_threshold: 7776000 # 90 days + freezing_threshold: 90d environment_variables: ENV: "production" ``` diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 31f341a3..49c7bf78 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -230,7 +230,7 @@ Examples: * `--controller ` — One or more controllers for the canister. Repeat `--controller` to specify multiple * `--compute-allocation ` — Optional compute allocation (0 to 100). Represents guaranteed compute capacity * `--memory-allocation ` — Optional memory allocation in bytes. If unset, memory is allocated dynamically. Supports suffixes: kb, kib, mb, mib, gb, gib (e.g. "4gib" or "2.5kb") -* `--freezing-threshold ` — Optional freezing threshold in seconds. Controls how long a canister can be inactive before being frozen +* `--freezing-threshold ` — Optional freezing threshold. Controls how long a canister can be inactive before being frozen. Supports duration suffixes: s (seconds), m (minutes), h (hours), d (days), w (weeks). A bare number is treated as seconds * `--reserved-cycles-limit ` — Optional upper limit on cycles reserved for future resource payments. Memory allocations that would push the reserved balance above this limit will fail. Supports suffixes: k (thousand), m (million), b (billion), t (trillion) * `-q`, `--quiet` — Suppress human-readable output; print only canister IDs, one per line, to stdout * `--cycles ` — Cycles to fund canister creation. Supports suffixes: k (thousand), m (million), b (billion), t (trillion) @@ -443,7 +443,7 @@ Change a canister's settings to specified values Warning: This removes all existing controllers not in the new list. If you don't include yourself, you will lose control of the canister. * `--compute-allocation ` * `--memory-allocation ` — Memory allocation in bytes. Supports suffixes: kb, kib, mb, mib, gb, gib (e.g. "4gib" or "2.5kb") -* `--freezing-threshold ` +* `--freezing-threshold ` — Freezing threshold. Controls how long a canister can be inactive before being frozen. Supports duration suffixes: s (seconds), m (minutes), h (hours), d (days), w (weeks). A bare number is treated as seconds * `--reserved-cycles-limit ` — Upper limit on cycles reserved for future resource payments. Memory allocations that would push the reserved balance above this limit will fail. Supports suffixes: k (thousand), m (million), b (billion), t (trillion) * `--wasm-memory-limit ` — Wasm memory limit in bytes. Supports suffixes: kb, kib, mb, mib, gb, gib (e.g. "4gib" or "2.5kb") * `--wasm-memory-threshold ` — Wasm memory threshold in bytes. Supports suffixes: kb, kib, mb, mib, gb, gib (e.g. "4gib" or "2.5kb") diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 697e9812..1e656177 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -353,7 +353,7 @@ See [Canister Settings Reference](canister-settings.md) for all options. settings: compute_allocation: 5 memory_allocation: 4gib - freezing_threshold: 2592000 # 30 days + freezing_threshold: 30d reserved_cycles_limit: 1t wasm_memory_limit: 1gib wasm_memory_threshold: 512mib @@ -362,7 +362,7 @@ settings: KEY: "value" ``` -Memory values accept suffixes: `kb` (1000), `kib` (1024), `mb`, `mib`, `gb`, `gib`. Cycles values accept suffixes: `k` (thousand), `m` (million), `b` (billion), `t` (trillion). Decimals and underscores are supported (e.g. `2.5gib`, `1_000_000`). +Memory values accept suffixes: `kb` (1000), `kib` (1024), `mb`, `mib`, `gb`, `gib`. Cycles values accept suffixes: `k` (thousand), `m` (million), `b` (billion), `t` (trillion). Duration values accept suffixes: `s` (seconds), `m` (minutes), `h` (hours), `d` (days), `w` (weeks). Decimals and underscores are supported where applicable (e.g. `2.5gib`, `1_000_000`). ## Init Args @@ -465,7 +465,7 @@ environments: memory_allocation: 4gib backend: compute_allocation: 30 - freezing_threshold: 7776000 # 90 days + freezing_threshold: 90d environment_variables: ENV: "production" init_args: diff --git a/docs/schemas/canister-yaml-schema.json b/docs/schemas/canister-yaml-schema.json index 7a8b96f5..b0e9035d 100644 --- a/docs/schemas/canister-yaml-schema.json +++ b/docs/schemas/canister-yaml-schema.json @@ -150,6 +150,19 @@ ], "description": "An amount of cycles.\n\nDeserializes from a number or a string with suffixes (k, m, b, t) and optional underscore separators." }, + "DurationAmount": { + "anyOf": [ + { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "A duration in seconds.\n\nDeserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w)\nand optional underscore separators.\n\nSuffixes (case-insensitive):\n- `s` — seconds\n- `m` — minutes (×60)\n- `h` — hours (×3600)\n- `d` — days (×86400)\n- `w` — weeks (×604800)\n\nA bare number without suffix is treated as seconds." + }, "InitArgsFormat": { "description": "Format specifier for init args content.", "oneOf": [ @@ -329,13 +342,15 @@ ] }, "freezing_threshold": { - "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.", - "format": "uint64", - "minimum": 0, - "type": [ - "integer", - "null" - ] + "anyOf": [ + { + "$ref": "#/$defs/DurationAmount" + }, + { + "type": "null" + } + ], + "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.\nSupports duration suffixes in YAML: s, m, h, d, w (e.g. \"30d\" or \"4w\")." }, "log_visibility": { "anyOf": [ diff --git a/docs/schemas/environment-yaml-schema.json b/docs/schemas/environment-yaml-schema.json index b4842d91..840b4d86 100644 --- a/docs/schemas/environment-yaml-schema.json +++ b/docs/schemas/environment-yaml-schema.json @@ -13,6 +13,19 @@ ], "description": "An amount of cycles.\n\nDeserializes from a number or a string with suffixes (k, m, b, t) and optional underscore separators." }, + "DurationAmount": { + "anyOf": [ + { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "A duration in seconds.\n\nDeserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w)\nand optional underscore separators.\n\nSuffixes (case-insensitive):\n- `s` — seconds\n- `m` — minutes (×60)\n- `h` — hours (×3600)\n- `d` — days (×86400)\n- `w` — weeks (×604800)\n\nA bare number without suffix is treated as seconds." + }, "InitArgsFormat": { "description": "Format specifier for init args content.", "oneOf": [ @@ -141,13 +154,15 @@ ] }, "freezing_threshold": { - "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.", - "format": "uint64", - "minimum": 0, - "type": [ - "integer", - "null" - ] + "anyOf": [ + { + "$ref": "#/$defs/DurationAmount" + }, + { + "type": "null" + } + ], + "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.\nSupports duration suffixes in YAML: s, m, h, d, w (e.g. \"30d\" or \"4w\")." }, "log_visibility": { "anyOf": [ diff --git a/docs/schemas/icp-yaml-schema.json b/docs/schemas/icp-yaml-schema.json index a1e054a4..52b5ef2c 100644 --- a/docs/schemas/icp-yaml-schema.json +++ b/docs/schemas/icp-yaml-schema.json @@ -280,6 +280,19 @@ ], "description": "An amount of cycles.\n\nDeserializes from a number or a string with suffixes (k, m, b, t) and optional underscore separators." }, + "DurationAmount": { + "anyOf": [ + { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "A duration in seconds.\n\nDeserializes from a number (seconds) or a string with duration suffix (s, m, h, d, w)\nand optional underscore separators.\n\nSuffixes (case-insensitive):\n- `s` — seconds\n- `m` — minutes (×60)\n- `h` — hours (×3600)\n- `d` — days (×86400)\n- `w` — weeks (×604800)\n\nA bare number without suffix is treated as seconds." + }, "EnvironmentManifest": { "properties": { "canisters": { @@ -812,13 +825,15 @@ ] }, "freezing_threshold": { - "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.", - "format": "uint64", - "minimum": 0, - "type": [ - "integer", - "null" - ] + "anyOf": [ + { + "$ref": "#/$defs/DurationAmount" + }, + { + "type": "null" + } + ], + "description": "Freezing threshold in seconds. Controls how long a canister can be inactive before being frozen.\nSupports duration suffixes in YAML: s, m, h, d, w (e.g. \"30d\" or \"4w\")." }, "log_visibility": { "anyOf": [ diff --git a/examples/icp-canister-settings/icp.yaml b/examples/icp-canister-settings/icp.yaml index 2891ad9c..ac496640 100644 --- a/examples/icp-canister-settings/icp.yaml +++ b/examples/icp-canister-settings/icp.yaml @@ -12,7 +12,7 @@ canisters: settings: compute_allocation: 1 memory_allocation: 4gib - freezing_threshold: 2592000 # 30 days + freezing_threshold: 30d reserved_cycles_limit: 1t wasm_memory_limit: 1gib wasm_memory_threshold: 512mib