Skip to content

Commit 3c30ab8

Browse files
committed
Add name resolution API
1 parent 78880bd commit 3c30ab8

File tree

11 files changed

+1881
-70
lines changed

11 files changed

+1881
-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: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
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+
#[derive(Clone)]
23+
pub struct BackoffConfig {
24+
/// The amount of time to backoff after the first failure.
25+
pub base_delay: Duration,
26+
27+
/// The factor with which to multiply backoffs after a
28+
/// failed retry. Should ideally be greater than 1.
29+
pub multiplier: f64,
30+
31+
/// The factor with which backoffs are randomized.
32+
pub jitter: f64,
33+
34+
/// The upper bound of backoff delay.
35+
pub max_delay: Duration,
36+
}
37+
38+
pub struct ExponentialBackoff {
39+
config: BackoffConfig,
40+
41+
/// The delay for the next retry, without the random jitter. Store as f64
42+
/// to avoid rounding errors.
43+
next_delay_secs: Mutex<f64>,
44+
}
45+
46+
/// This is a backoff configuration with the default values specified
47+
/// at https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md.
48+
///
49+
/// This should be useful for callers who want to configure backoff with
50+
/// non-default values only for a subset of the options.
51+
pub const DEFAULT_EXPONENTIAL_CONFIG: BackoffConfig = BackoffConfig {
52+
base_delay: Duration::from_secs(1),
53+
multiplier: 1.6,
54+
jitter: 0.2,
55+
max_delay: Duration::from_secs(120),
56+
};
57+
58+
impl ExponentialBackoff {
59+
pub fn new(mut config: BackoffConfig) -> Self {
60+
// Adjust params to get them in valid ranges.
61+
// 0 <= base_dealy <= max_delay
62+
config.base_delay = config.base_delay.min(config.max_delay);
63+
// 1 <= multiplier
64+
config.multiplier = config.multiplier.max(1.0);
65+
// 0 <= jitter <= 1
66+
config.jitter = config.jitter.max(0.0);
67+
config.jitter = config.jitter.min(1.0);
68+
let next_delay_secs = config.base_delay.as_secs_f64();
69+
ExponentialBackoff {
70+
config,
71+
next_delay_secs: Mutex::new(next_delay_secs),
72+
}
73+
}
74+
75+
pub fn reset(&self) {
76+
let mut next_delay = self.next_delay_secs.lock().unwrap();
77+
*next_delay = self.config.base_delay.as_secs_f64();
78+
}
79+
80+
pub fn backoff_duration(&self) -> Duration {
81+
let mut next_delay = self.next_delay_secs.lock().unwrap();
82+
let cur_delay =
83+
*next_delay * (1.0 + self.config.jitter * rand::thread_rng().gen_range(-1.0..1.0));
84+
*next_delay = self
85+
.config
86+
.max_delay
87+
.as_secs_f64()
88+
.min(*next_delay * self.config.multiplier);
89+
Duration::from_secs_f64(cur_delay)
90+
}
91+
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use std::time::Duration;
96+
97+
use crate::client::name_resolution::backoff::{BackoffConfig, ExponentialBackoff};
98+
99+
// Epsilon for floating point comparisons if needed, though Duration
100+
// comparisons are often better.
101+
const EPSILON: f64 = 1e-9;
102+
103+
#[test]
104+
fn base_less_than_max() {
105+
let config = BackoffConfig {
106+
base_delay: Duration::from_secs(10),
107+
multiplier: 123.0,
108+
jitter: 0.0,
109+
max_delay: Duration::from_secs(100),
110+
};
111+
let backoff = ExponentialBackoff::new(config.clone());
112+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
113+
}
114+
115+
#[test]
116+
fn base_more_than_max() {
117+
let config = BackoffConfig {
118+
multiplier: 123.0,
119+
jitter: 0.0,
120+
base_delay: Duration::from_secs(100),
121+
max_delay: Duration::from_secs(10),
122+
};
123+
let backoff = ExponentialBackoff::new(config.clone());
124+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
125+
}
126+
127+
#[test]
128+
fn negative_multiplier() {
129+
let config = BackoffConfig {
130+
multiplier: -123.0,
131+
jitter: 0.0,
132+
base_delay: Duration::from_secs(10),
133+
max_delay: Duration::from_secs(100),
134+
};
135+
let backoff = ExponentialBackoff::new(config.clone());
136+
// multiplier gets clipped to 1.
137+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
138+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
139+
}
140+
141+
#[test]
142+
fn negative_jitter() {
143+
let config = BackoffConfig {
144+
multiplier: 1.0,
145+
jitter: -10.0,
146+
base_delay: Duration::from_secs(10),
147+
max_delay: Duration::from_secs(100),
148+
};
149+
let backoff = ExponentialBackoff::new(config.clone());
150+
// jitter gets clipped to 0.
151+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
152+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(10));
153+
}
154+
155+
#[test]
156+
fn jitter_greater_than_one() {
157+
let config = BackoffConfig {
158+
multiplier: 1.0,
159+
jitter: 2.0,
160+
base_delay: Duration::from_secs(10),
161+
max_delay: Duration::from_secs(100),
162+
};
163+
let backoff = ExponentialBackoff::new(config.clone());
164+
// jitter gets clipped to 1.
165+
// 0 <= duration <= 20.
166+
let duration = backoff.backoff_duration();
167+
assert_eq!(duration.lt(&Duration::from_secs(20)), true);
168+
assert_eq!(duration.gt(&Duration::from_secs(0)), true);
169+
170+
let duration = backoff.backoff_duration();
171+
assert_eq!(duration.lt(&Duration::from_secs(20)), true);
172+
assert_eq!(duration.gt(&Duration::from_secs(0)), true);
173+
}
174+
175+
#[test]
176+
fn backoff_reset_no_jitter() {
177+
let config = BackoffConfig {
178+
multiplier: 2.0,
179+
jitter: 0.0,
180+
base_delay: Duration::from_secs(1),
181+
max_delay: Duration::from_secs(15),
182+
};
183+
let backoff = ExponentialBackoff::new(config.clone());
184+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(1));
185+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(2));
186+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(4));
187+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(8));
188+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
189+
// Duration is capped to max_delay.
190+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
191+
192+
// reset and repeat.
193+
backoff.reset();
194+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(1));
195+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(2));
196+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(4));
197+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(8));
198+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
199+
// Duration is capped to max_delay.
200+
assert_eq!(backoff.backoff_duration(), Duration::from_secs(15));
201+
}
202+
203+
#[test]
204+
fn backoff_with_jitter() {
205+
let config = BackoffConfig {
206+
multiplier: 2.0,
207+
jitter: 0.2,
208+
base_delay: Duration::from_secs(1),
209+
max_delay: Duration::from_secs(15),
210+
};
211+
let backoff = ExponentialBackoff::new(config.clone());
212+
// 0.8 <= duration <= 1.2.
213+
let duration = backoff.backoff_duration();
214+
assert_eq!(duration.gt(&Duration::from_secs_f64(0.8 - EPSILON)), true);
215+
assert_eq!(duration.lt(&Duration::from_secs_f64(1.2 + EPSILON)), true);
216+
// 1.6 <= duration <= 2.4.
217+
let duration = backoff.backoff_duration();
218+
assert_eq!(duration.gt(&Duration::from_secs_f64(1.6 - EPSILON)), true);
219+
assert_eq!(duration.lt(&Duration::from_secs_f64(2.4 + EPSILON)), true);
220+
// 3.2 <= duration <= 4.8.
221+
let duration = backoff.backoff_duration();
222+
assert_eq!(duration.gt(&Duration::from_secs_f64(3.2 - EPSILON)), true);
223+
assert_eq!(duration.lt(&Duration::from_secs_f64(4.8 + EPSILON)), true);
224+
}
225+
}

0 commit comments

Comments
 (0)