Skip to content

Commit 2867b3f

Browse files
committed
Add name resolution API
1 parent 78880bd commit 2867b3f

File tree

11 files changed

+1882
-70
lines changed

11 files changed

+1882
-70
lines changed

grpc/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,13 @@ url = "2.5.0"
1010
tokio = { version = "1.37.0", features = ["sync"] }
1111
tonic = { version = "0.13.0", path = "../tonic", default-features = false, features = ["codegen"] }
1212
futures-core = "0.3.31"
13+
once_cell = "1.19.0"
14+
hickory-resolver = { version = "0.25.1", optional = true }
15+
rand = "0.8.5"
16+
17+
[dev-dependencies]
18+
hickory-server = "0.25.2"
19+
20+
[features]
21+
default = ["hickory_dns"]
22+
hickory_dns = ["dep:hickory-resolver"]
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
*
3+
* Copyright 2025 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
use rand::Rng;
20+
use std::{sync::Mutex, time::Duration};
21+
22+
/// TODO(arjan-bal): Move this
23+
#[derive(Clone)]
24+
pub struct BackoffConfig {
25+
/// The amount of time to backoff after the first failure.
26+
pub base_delay: Duration,
27+
28+
/// The factor with which to multiply backoffs after a
29+
/// failed retry. Should ideally be greater than 1.
30+
pub multiplier: f64,
31+
32+
/// The factor with which backoffs are randomized.
33+
pub jitter: f64,
34+
35+
/// The upper bound of backoff delay.
36+
pub max_delay: Duration,
37+
}
38+
39+
pub struct ExponentialBackoff {
40+
config: BackoffConfig,
41+
42+
/// The delay for the next retry, without the random jitter. Store as f64
43+
/// to avoid rounding errors.
44+
next_delay_secs: Mutex<f64>,
45+
}
46+
47+
/// This is a backoff configuration with the default values specified
48+
/// at https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md.
49+
///
50+
/// This should be useful for callers who want to configure backoff with
51+
/// non-default values only for a subset of the options.
52+
pub const DEFAULT_EXPONENTIAL_CONFIG: BackoffConfig = BackoffConfig {
53+
base_delay: Duration::from_secs(1),
54+
multiplier: 1.6,
55+
jitter: 0.2,
56+
max_delay: Duration::from_secs(120),
57+
};
58+
59+
impl ExponentialBackoff {
60+
pub fn new(mut config: BackoffConfig) -> Self {
61+
// Adjust params to get them in valid ranges.
62+
// 0 <= base_dealy <= max_delay
63+
config.base_delay = config.base_delay.min(config.max_delay);
64+
// 1 <= multiplier
65+
config.multiplier = config.multiplier.max(1.0);
66+
// 0 <= jitter <= 1
67+
config.jitter = config.jitter.max(0.0);
68+
config.jitter = config.jitter.min(1.0);
69+
let next_delay_secs = config.base_delay.as_secs_f64();
70+
ExponentialBackoff {
71+
config,
72+
next_delay_secs: Mutex::new(next_delay_secs),
73+
}
74+
}
75+
76+
pub fn reset(&self) {
77+
let mut next_delay = self.next_delay_secs.lock().unwrap();
78+
*next_delay = self.config.base_delay.as_secs_f64();
79+
}
80+
81+
pub fn backoff_duration(&self) -> Duration {
82+
let mut next_delay = self.next_delay_secs.lock().unwrap();
83+
let cur_delay =
84+
*next_delay * (1.0 + self.config.jitter * rand::thread_rng().gen_range(-1.0..1.0));
85+
*next_delay = self
86+
.config
87+
.max_delay
88+
.as_secs_f64()
89+
.min(*next_delay * self.config.multiplier);
90+
Duration::from_secs_f64(cur_delay)
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use std::time::Duration;
97+
98+
use crate::client::name_resolution::backoff::{BackoffConfig, ExponentialBackoff};
99+
100+
// Epsilon for floating point comparisons if needed, though Duration
101+
// comparisons are often better.
102+
const EPSILON: f64 = 1e-9;
103+
104+
#[test]
105+
fn base_less_than_max() {
106+
let config = BackoffConfig {
107+
base_delay: Duration::from_secs(10),
108+
multiplier: 123.0,
109+
jitter: 0.0,
110+
max_delay: Duration::from_secs(100),
111+
};
112+
let backoff = ExponentialBackoff::new(config.clone());
113+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
114+
}
115+
116+
#[test]
117+
fn base_more_than_max() {
118+
let config = BackoffConfig {
119+
multiplier: 123.0,
120+
jitter: 0.0,
121+
base_delay: Duration::from_secs(100),
122+
max_delay: Duration::from_secs(10),
123+
};
124+
let backoff = ExponentialBackoff::new(config.clone());
125+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
126+
}
127+
128+
#[test]
129+
fn negative_multiplier() {
130+
let config = BackoffConfig {
131+
multiplier: -123.0,
132+
jitter: 0.0,
133+
base_delay: Duration::from_secs(10),
134+
max_delay: Duration::from_secs(100),
135+
};
136+
let backoff = ExponentialBackoff::new(config.clone());
137+
// multiplier gets clipped to 1.
138+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
139+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
140+
}
141+
142+
#[test]
143+
fn negative_jitter() {
144+
let config = BackoffConfig {
145+
multiplier: 1.0,
146+
jitter: -10.0,
147+
base_delay: Duration::from_secs(10),
148+
max_delay: Duration::from_secs(100),
149+
};
150+
let backoff = ExponentialBackoff::new(config.clone());
151+
// jitter gets clipped to 0.
152+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
153+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
154+
}
155+
156+
#[test]
157+
fn jitter_greater_than_one() {
158+
let config = BackoffConfig {
159+
multiplier: 1.0,
160+
jitter: 2.0,
161+
base_delay: Duration::from_secs(10),
162+
max_delay: Duration::from_secs(100),
163+
};
164+
let backoff = ExponentialBackoff::new(config.clone());
165+
// jitter gets clipped to 1.
166+
// 0 <= duration <= 20.
167+
let duration = backoff.backoff_duration();
168+
assert_eq!(duration.lt(&Duration::from_secs(20)), true);
169+
assert_eq!(duration.gt(&Duration::from_secs(0)), true);
170+
171+
let duration = backoff.backoff_duration();
172+
assert_eq!(duration.lt(&Duration::from_secs(20)), true);
173+
assert_eq!(duration.gt(&Duration::from_secs(0)), true);
174+
}
175+
176+
#[test]
177+
fn backoff_reset_no_jitter() {
178+
let config = BackoffConfig {
179+
multiplier: 2.0,
180+
jitter: 0.0,
181+
base_delay: Duration::from_secs(1),
182+
max_delay: Duration::from_secs(15),
183+
};
184+
let backoff = ExponentialBackoff::new(config.clone());
185+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(1));
186+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(2));
187+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(4));
188+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(8));
189+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
190+
// Duration is capped to max_delay.
191+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
192+
193+
// reset and repeat.
194+
backoff.reset();
195+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(1));
196+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(2));
197+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(4));
198+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(8));
199+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
200+
// Duration is capped to max_delay.
201+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
202+
}
203+
204+
#[test]
205+
fn backoff_with_jitter() {
206+
let config = BackoffConfig {
207+
multiplier: 2.0,
208+
jitter: 0.2,
209+
base_delay: Duration::from_secs(1),
210+
max_delay: Duration::from_secs(15),
211+
};
212+
let backoff = ExponentialBackoff::new(config.clone());
213+
// 0.8 <= duration <= 1.2.
214+
let duration = backoff.backoff_duration();
215+
assert_eq!(duration.gt(&Duration::from_secs_f64(0.8 - EPSILON)), true);
216+
assert_eq!(duration.lt(&Duration::from_secs_f64(1.2 + EPSILON)), true);
217+
// 1.6 <= duration <= 2.4.
218+
let duration = backoff.backoff_duration();
219+
assert_eq!(duration.gt(&Duration::from_secs_f64(1.6 - EPSILON)), true);
220+
assert_eq!(duration.lt(&Duration::from_secs_f64(2.4 + EPSILON)), true);
221+
// 3.2 <= duration <= 4.8.
222+
let duration = backoff.backoff_duration();
223+
assert_eq!(duration.gt(&Duration::from_secs_f64(3.2 - EPSILON)), true);
224+
assert_eq!(duration.lt(&Duration::from_secs_f64(4.8 + EPSILON)), true);
225+
}
226+
}

0 commit comments

Comments
 (0)