This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new d1e87338 fix(spec): parse commit duration options and align their 
defaults with Java (#671)
d1e87338 is described below

commit d1e87338ef71443cbea9b92d82d247e8ae2fbf25
Author: jackylee <[email protected]>
AuthorDate: Thu Aug 6 20:54:40 2026 +0800

    fix(spec): parse commit duration options and align their defaults with Java 
(#671)
---
 crates/paimon/src/spec/core_options.rs | 130 +++++++++++++++++++++++++++++++--
 1 file changed, 123 insertions(+), 7 deletions(-)

diff --git a/crates/paimon/src/spec/core_options.rs 
b/crates/paimon/src/spec/core_options.rs
index e4ce0ca0..6e3dc5bf 100644
--- a/crates/paimon/src/spec/core_options.rs
+++ b/crates/paimon/src/spec/core_options.rs
@@ -97,8 +97,12 @@ const IGNORE_DELETE_FALLBACK_KEYS: &[&str] = &[
 const DIFF_PARALLELISM_OPTION: &str = "diff.parallelism";
 const DEFAULT_DIFF_PARALLELISM: usize = 4;
 const DEFAULT_COMMIT_MAX_RETRIES: u32 = 10;
-const DEFAULT_COMMIT_TIMEOUT_MS: u64 = 120_000;
-const DEFAULT_COMMIT_MIN_RETRY_WAIT_MS: u64 = 1_000;
+/// Java types `commit.timeout` as a duration with no default and treats an
+/// absent value as `Long.MAX_VALUE`, i.e. the retry budget alone bounds the
+/// loop. `u64::MAX` is the equivalent sentinel here: the value is only ever
+/// compared against elapsed time, never used in arithmetic.
+const DEFAULT_COMMIT_TIMEOUT_MS: u64 = u64::MAX;
+const DEFAULT_COMMIT_MIN_RETRY_WAIT_MS: u64 = 10;
 const DEFAULT_COMMIT_MAX_RETRY_WAIT_MS: u64 = 10_000;
 pub const SCAN_TIMESTAMP_MILLIS_OPTION: &str = "scan.timestamp-millis";
 pub const SCAN_VERSION_OPTION: &str = "scan.version";
@@ -906,24 +910,32 @@ impl<'a> CoreOptions<'a> {
             .unwrap_or(DEFAULT_COMMIT_MAX_RETRIES)
     }
 
+    /// Commit timeout (`commit.timeout`), in milliseconds.
+    ///
+    /// Defaults to unbounded, matching Java: with no timeout configured, only
+    /// `commit.max-retries` bounds the commit retry loop.
     pub fn commit_timeout_ms(&self) -> u64 {
         self.options
             .get(COMMIT_TIMEOUT_OPTION)
-            .and_then(|v| v.parse().ok())
+            .and_then(|v| parse_duration_millis(v))
             .unwrap_or(DEFAULT_COMMIT_TIMEOUT_MS)
     }
 
+    /// Initial backoff before the first commit retry 
(`commit.min-retry-wait`),
+    /// in milliseconds. Doubles per retry up to 
[`Self::commit_max_retry_wait_ms`].
     pub fn commit_min_retry_wait_ms(&self) -> u64 {
         self.options
             .get(COMMIT_MIN_RETRY_WAIT_OPTION)
-            .and_then(|v| v.parse().ok())
+            .and_then(|v| parse_duration_millis(v))
             .unwrap_or(DEFAULT_COMMIT_MIN_RETRY_WAIT_MS)
     }
 
+    /// Backoff ceiling between commit retries (`commit.max-retry-wait`), in
+    /// milliseconds.
     pub fn commit_max_retry_wait_ms(&self) -> u64 {
         self.options
             .get(COMMIT_MAX_RETRY_WAIT_OPTION)
-            .and_then(|v| v.parse().ok())
+            .and_then(|v| parse_duration_millis(v))
             .unwrap_or(DEFAULT_COMMIT_MAX_RETRY_WAIT_MS)
     }
 
@@ -1359,6 +1371,40 @@ fn parse_memory_size(value: &str) -> Option<i64> {
     num.checked_mul(multiplier)
 }
 
+/// Parse a duration string to milliseconds, mirroring Java Paimon's
+/// `TimeUtils.parseDuration`.
+///
+/// Accepts every unit label Java accepts — `d`/`day(s)`, `h`/`hour(s)`,
+/// `min`/`m`/`minute(s)`, `s`/`sec(s)`/`second(s)`, `ms`/`milli(s)`/
+/// `millisecond(s)`, `µs`/`micro(s)`/`microsecond(s)`, `ns`/`nano(s)`/
+/// `nanosecond(s)` — plus a bare number, which Java reads as milliseconds.
+/// Sub-millisecond units are truncated towards zero, as `Duration.toMillis()`
+/// does. Returns `None` for an empty string, a missing or non-numeric number,
+/// an unrecognized unit, or a value that would overflow `u64`, matching the
+/// inputs on which Java throws.
+fn parse_duration_millis(value: &str) -> Option<u64> {
+    let value = value.trim();
+    if value.is_empty() {
+        return None;
+    }
+
+    let pos = value
+        .find(|c: char| !c.is_ascii_digit())
+        .unwrap_or(value.len());
+    let (num_str, unit_str) = value.split_at(pos);
+    let num: u64 = num_str.trim().parse().ok()?;
+    match unit_str.trim().to_ascii_lowercase().as_str() {
+        "d" | "day" | "days" => num.checked_mul(24 * 60 * 60 * 1000),
+        "h" | "hour" | "hours" => num.checked_mul(60 * 60 * 1000),
+        "min" | "m" | "minute" | "minutes" => num.checked_mul(60 * 1000),
+        "s" | "sec" | "secs" | "second" | "seconds" => num.checked_mul(1000),
+        "" | "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 
Some(num),
+        "µs" | "micro" | "micros" | "microsecond" | "microseconds" => Some(num 
/ 1_000),
+        "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => Some(num / 
1_000_000),
+        _ => None,
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -1803,6 +1849,76 @@ mod tests {
         assert_eq!(parse_memory_size("-1"), None);
     }
 
+    #[test]
+    fn test_parse_duration_millis_accepts_every_java_unit_spelling() {
+        // Every label in Java `TimeUtils.TimeUnit`, plus the bare number that
+        // Java reads as milliseconds.
+        for (input, expected) in [
+            ("60000", 60_000),
+            ("2 d", 2 * 24 * 60 * 60 * 1000),
+            ("2 day", 2 * 24 * 60 * 60 * 1000),
+            ("2 days", 2 * 24 * 60 * 60 * 1000),
+            ("3 h", 3 * 60 * 60 * 1000),
+            ("3 hour", 3 * 60 * 60 * 1000),
+            ("3 hours", 3 * 60 * 60 * 1000),
+            ("2 min", 120_000),
+            ("2 m", 120_000),
+            ("2 minute", 120_000),
+            ("2 minutes", 120_000),
+            ("30 s", 30_000),
+            ("30 sec", 30_000),
+            ("30 secs", 30_000),
+            ("30 second", 30_000),
+            ("30 seconds", 30_000),
+            ("500 ms", 500),
+            ("500 milli", 500),
+            ("500 millis", 500),
+            ("500 millisecond", 500),
+            ("500 milliseconds", 500),
+            ("1500 µs", 1),
+            ("1500 micros", 1),
+            ("1500000 ns", 1),
+            ("1500000 nanoseconds", 1),
+        ] {
+            assert_eq!(
+                parse_duration_millis(input),
+                Some(expected),
+                "unexpected result for {input}"
+            );
+        }
+    }
+
+    #[test]
+    fn test_parse_duration_millis_is_case_and_space_insensitive() {
+        // Java lowercases the unit label and trims around it.
+        assert_eq!(parse_duration_millis("2MIN"), Some(120_000));
+        assert_eq!(parse_duration_millis("  30   S  "), Some(30_000));
+    }
+
+    #[test]
+    fn test_parse_duration_millis_rejects_unknown_and_malformed_input() {
+        assert_eq!(parse_duration_millis("2 weeks"), None);
+        assert_eq!(parse_duration_millis("min"), None);
+        assert_eq!(parse_duration_millis(""), None);
+        assert_eq!(parse_duration_millis("-5"), None);
+        // Java raises "numeric overflow" here; `None` lets callers fall back 
to
+        // their default instead of wrapping.
+        assert_eq!(parse_duration_millis("9223372036854775807 d"), None);
+    }
+
+    #[test]
+    fn test_commit_wait_options_accept_java_duration_strings() {
+        let options = HashMap::from([
+            (COMMIT_TIMEOUT_OPTION.to_string(), "2 min".to_string()),
+            (COMMIT_MIN_RETRY_WAIT_OPTION.to_string(), "1 s".to_string()),
+            (COMMIT_MAX_RETRY_WAIT_OPTION.to_string(), "30 s".to_string()),
+        ]);
+        let core = CoreOptions::new(&options);
+        assert_eq!(core.commit_timeout_ms(), 120_000);
+        assert_eq!(core.commit_min_retry_wait_ms(), 1_000);
+        assert_eq!(core.commit_max_retry_wait_ms(), 30_000);
+    }
+
     #[test]
     fn test_partition_options_defaults() {
         let options = HashMap::new();
@@ -2056,8 +2172,8 @@ mod tests {
         let core = CoreOptions::new(&options);
         assert_eq!(core.bucket(), -1);
         assert_eq!(core.commit_max_retries(), 10);
-        assert_eq!(core.commit_timeout_ms(), 120_000);
-        assert_eq!(core.commit_min_retry_wait_ms(), 1_000);
+        assert_eq!(core.commit_timeout_ms(), u64::MAX);
+        assert_eq!(core.commit_min_retry_wait_ms(), 10);
         assert_eq!(core.commit_max_retry_wait_ms(), 10_000);
         assert!(!core.row_tracking_enabled());
         assert_eq!(core.manifest_compression(), "zstd");

Reply via email to