From f6867b3ddeda967878480a6c2cb50559051302df Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Mon, 16 Mar 2026 17:42:53 -0700 Subject: [PATCH] Match Go TTL parsing: all-digit default to minutes, two-pass fit algorithm --- seaweed-volume/src/storage/needle/ttl.rs | 167 +++++++++-------------- 1 file changed, 65 insertions(+), 102 deletions(-) diff --git a/seaweed-volume/src/storage/needle/ttl.rs b/seaweed-volume/src/storage/needle/ttl.rs index d779465b0..38defa69d 100644 --- a/seaweed-volume/src/storage/needle/ttl.rs +++ b/seaweed-volume/src/storage/needle/ttl.rs @@ -72,23 +72,30 @@ impl TTL { } /// Parse from string like "3m", "4h", "5d", "6w", "7M", "8y". + /// If the string is all digits (no unit suffix), defaults to minutes. pub fn read(s: &str) -> Result { let s = s.trim(); if s.is_empty() { return Ok(TTL::EMPTY); } - let (num_str, unit_char) = s.split_at(s.len() - 1); + let last_byte = s.as_bytes()[s.len() - 1]; + let (num_str, unit_byte) = if last_byte >= b'0' && last_byte <= b'9' { + // All digits — default to minutes (matching Go) + (s, b'm') + } else { + (&s[..s.len() - 1], last_byte) + }; let count: u32 = num_str .parse() .map_err(|e| format!("invalid TTL count: {}", e))?; - let unit = match unit_char { - "m" => TTL_UNIT_MINUTE, - "h" => TTL_UNIT_HOUR, - "d" => TTL_UNIT_DAY, - "w" => TTL_UNIT_WEEK, - "M" => TTL_UNIT_MONTH, - "y" => TTL_UNIT_YEAR, - _ => return Err(format!("unknown TTL unit: {}", unit_char)), + let unit = match unit_byte { + b'm' => TTL_UNIT_MINUTE, + b'h' => TTL_UNIT_HOUR, + b'd' => TTL_UNIT_DAY, + b'w' => TTL_UNIT_WEEK, + b'M' => TTL_UNIT_MONTH, + b'y' => TTL_UNIT_YEAR, + _ => return Err(format!("unknown TTL unit: {}", unit_byte as char)), }; Ok(fit_ttl_count(count, unit)) } @@ -112,102 +119,58 @@ fn unit_to_seconds(count: u64, unit: u8) -> u64 { } } -/// Fit a count into a single byte, converting to larger unit if needed. +/// Fit a count+unit into a TTL that fits in a single byte count. +/// Matches Go's fitTtlCount: convert to total seconds, then find the best-fit unit. fn fit_ttl_count(count: u32, unit: u8) -> TTL { - if count <= 255 { - return TTL { - count: count as u8, - unit, - }; + let seconds = unit_to_seconds(count as u64, unit); + if seconds == 0 { + return TTL::EMPTY; } - // Try next larger unit - match unit { - TTL_UNIT_MINUTE => { - if count / 60 <= 255 { - return TTL { - count: (count / 60) as u8, - unit: TTL_UNIT_HOUR, - }; - } - if count / (60 * 24) <= 255 { - return TTL { - count: (count / (60 * 24)) as u8, - unit: TTL_UNIT_DAY, - }; - } - TTL { - count: 255, - unit: TTL_UNIT_DAY, - } - } - TTL_UNIT_HOUR => { - if count / 24 <= 255 { - return TTL { - count: (count / 24) as u8, - unit: TTL_UNIT_DAY, - }; - } - TTL { - count: 255, - unit: TTL_UNIT_DAY, - } - } - TTL_UNIT_DAY => { - if count / 7 <= 255 { - return TTL { - count: (count / 7) as u8, - unit: TTL_UNIT_WEEK, - }; - } - if count / 30 <= 255 { - return TTL { - count: (count / 30) as u8, - unit: TTL_UNIT_MONTH, - }; - } - if count / 365 <= 255 { - return TTL { - count: (count / 365) as u8, - unit: TTL_UNIT_YEAR, - }; - } - TTL { - count: 255, - unit: TTL_UNIT_YEAR, - } - } - TTL_UNIT_WEEK => { - if count * 7 / 30 <= 255 { - return TTL { - count: (count * 7 / 30) as u8, - unit: TTL_UNIT_MONTH, - }; - } - if count * 7 / 365 <= 255 { - return TTL { - count: (count * 7 / 365) as u8, - unit: TTL_UNIT_YEAR, - }; - } - TTL { - count: 255, - unit: TTL_UNIT_YEAR, - } - } - TTL_UNIT_MONTH => { - if count / 12 <= 255 { - return TTL { - count: (count / 12) as u8, - unit: TTL_UNIT_YEAR, - }; - } - TTL { - count: 255, - unit: TTL_UNIT_YEAR, - } - } - _ => TTL { count: 255, unit }, + + const YEAR_SECS: u64 = 3600 * 24 * 365; + const MONTH_SECS: u64 = 3600 * 24 * 30; + const WEEK_SECS: u64 = 3600 * 24 * 7; + const DAY_SECS: u64 = 3600 * 24; + const HOUR_SECS: u64 = 3600; + const MINUTE_SECS: u64 = 60; + + // First pass: try exact fits from largest to smallest + if seconds % YEAR_SECS == 0 && seconds / YEAR_SECS < 256 { + return TTL { count: (seconds / YEAR_SECS) as u8, unit: TTL_UNIT_YEAR }; + } + if seconds % MONTH_SECS == 0 && seconds / MONTH_SECS < 256 { + return TTL { count: (seconds / MONTH_SECS) as u8, unit: TTL_UNIT_MONTH }; + } + if seconds % WEEK_SECS == 0 && seconds / WEEK_SECS < 256 { + return TTL { count: (seconds / WEEK_SECS) as u8, unit: TTL_UNIT_WEEK }; + } + if seconds % DAY_SECS == 0 && seconds / DAY_SECS < 256 { + return TTL { count: (seconds / DAY_SECS) as u8, unit: TTL_UNIT_DAY }; + } + if seconds % HOUR_SECS == 0 && seconds / HOUR_SECS < 256 { + return TTL { count: (seconds / HOUR_SECS) as u8, unit: TTL_UNIT_HOUR }; + } + // Minutes: truncating division + if seconds / MINUTE_SECS < 256 { + return TTL { count: (seconds / MINUTE_SECS) as u8, unit: TTL_UNIT_MINUTE }; + } + // Second pass: truncating division from smallest to largest + if seconds / HOUR_SECS < 256 { + return TTL { count: (seconds / HOUR_SECS) as u8, unit: TTL_UNIT_HOUR }; + } + if seconds / DAY_SECS < 256 { + return TTL { count: (seconds / DAY_SECS) as u8, unit: TTL_UNIT_DAY }; + } + if seconds / WEEK_SECS < 256 { + return TTL { count: (seconds / WEEK_SECS) as u8, unit: TTL_UNIT_WEEK }; + } + if seconds / MONTH_SECS < 256 { + return TTL { count: (seconds / MONTH_SECS) as u8, unit: TTL_UNIT_MONTH }; + } + if seconds / YEAR_SECS < 256 { + return TTL { count: (seconds / YEAR_SECS) as u8, unit: TTL_UNIT_YEAR }; } + TTL::EMPTY } fn unit_to_char(unit: u8) -> char {