From d769498cdc6bb6044d0f575b7cfbddb506bb763f Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 12 May 2026 16:38:44 -0700 Subject: [PATCH 1/3] fix(retry): avoid truncating fractional retry_percent in TpsBudget --- tower/src/retry/budget/tps_budget.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tower/src/retry/budget/tps_budget.rs b/tower/src/retry/budget/tps_budget.rs index dc766636b..d00c453c9 100644 --- a/tower/src/retry/budget/tps_budget.rs +++ b/tower/src/retry/budget/tps_budget.rs @@ -78,13 +78,12 @@ impl TpsBudget { // If there is no percent, then you gain nothing from deposits. // Withdrawals can only be made against the reserve, over time. (0, 1) - } else if retry_percent <= 1.0 { - (1, (1.0 / retry_percent) as isize) - } else { - // Support for when retry_percent is between 1.0 and 1000.0, - // meaning for every deposit D, D * retry_percent withdrawals - // can be made. + } else if retry_percent <= 0.5 { + // Small fractional percents lose precision when 1/retry_percent is + // truncated to an isize, so scale deposits by 1000 here. (1000, (1000.0 / retry_percent) as isize) + } else { + (1, (1.0 / retry_percent) as isize) }; let reserve = (min_per_sec as isize) .saturating_mul(ttl.as_secs() as isize) // ttl is between 1 and 60 seconds @@ -257,4 +256,17 @@ mod tests { assert!(!bgt.withdraw()); } + + #[test] + fn tps_fractional_retry_percent_below_one() { + let bgt = TpsBudget::new(Duration::from_secs(1), 0, 0.6); + for _ in 0..10 { + bgt.deposit(); + } + let allowed = (0..10).filter(|_| bgt.withdraw()).count(); + assert!( + allowed <= 6, + "10 deposits at retry_percent=0.6 should allow at most 6 retries, got {allowed}" + ); + } } From 8165a229ba67c21291fb7ae07b15096cdb5e9446 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 26 May 2026 18:44:43 -0700 Subject: [PATCH 2/3] fix(retry): use 1000x scaling for all positive retry_percent values Collapses the fractional-percent and > 1 branches into one: scale deposits by 1000 and use (1000 / retry_percent) as the withdraw cost for every non-zero percent. This gives exact precision across the full [0, 1000] range without a special-case threshold. Signed-off-by: Sai Asish Y --- tower/src/retry/budget/tps_budget.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tower/src/retry/budget/tps_budget.rs b/tower/src/retry/budget/tps_budget.rs index d00c453c9..bac6c1fd2 100644 --- a/tower/src/retry/budget/tps_budget.rs +++ b/tower/src/retry/budget/tps_budget.rs @@ -78,12 +78,12 @@ impl TpsBudget { // If there is no percent, then you gain nothing from deposits. // Withdrawals can only be made against the reserve, over time. (0, 1) - } else if retry_percent <= 0.5 { - // Small fractional percents lose precision when 1/retry_percent is - // truncated to an isize, so scale deposits by 1000 here. - (1000, (1000.0 / retry_percent) as isize) } else { - (1, (1.0 / retry_percent) as isize) + // Scale deposits by 1000 so fractional percentages (where + // 1/retry_percent truncates badly) and values > 1 both stay + // precise. For example 0.6 -> (1000, 1666) gives exactly + // 6 retries per 10 deposits; 2.0 -> (1000, 500) gives 20. + (1000, (1000.0 / retry_percent) as isize) }; let reserve = (min_per_sec as isize) .saturating_mul(ttl.as_secs() as isize) // ttl is between 1 and 60 seconds From 6018fde44e0f7a613783f204d9a2ff9aada18bc6 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Sat, 22 Aug 2026 23:52:50 -0700 Subject: [PATCH 3/3] fix(retry): keep the direct ratio for retry_percent up to 0.5 Signed-off-by: Sai Asish Y --- tower/src/retry/budget/tps_budget.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tower/src/retry/budget/tps_budget.rs b/tower/src/retry/budget/tps_budget.rs index bac6c1fd2..b811be8cf 100644 --- a/tower/src/retry/budget/tps_budget.rs +++ b/tower/src/retry/budget/tps_budget.rs @@ -78,11 +78,13 @@ impl TpsBudget { // If there is no percent, then you gain nothing from deposits. // Withdrawals can only be made against the reserve, over time. (0, 1) + } else if retry_percent <= 0.5 { + (1, (1.0 / retry_percent) as isize) } else { - // Scale deposits by 1000 so fractional percentages (where - // 1/retry_percent truncates badly) and values > 1 both stay - // precise. For example 0.6 -> (1000, 1666) gives exactly - // 6 retries per 10 deposits; 2.0 -> (1000, 500) gives 20. + // Above 0.5, `1.0 / retry_percent` truncates to 1 (or 0 past 1.0), + // so scale deposits by 1000 to keep the ratio. For example + // 0.6 -> (1000, 1666) gives 6 retries per 10 deposits, and + // 2.0 -> (1000, 500) gives 20. (1000, (1000.0 / retry_percent) as isize) }; let reserve = (min_per_sec as isize) @@ -266,7 +268,8 @@ mod tests { let allowed = (0..10).filter(|_| bgt.withdraw()).count(); assert!( allowed <= 6, - "10 deposits at retry_percent=0.6 should allow at most 6 retries, got {allowed}" + "10 deposits at retry_percent=0.6 should allow at most 6 retries, got {}", + allowed ); } }