Skip to content

Commit a3a0b9f

Browse files
authored
feat(outbound): Discover timeouts and retries (#3095)
This change updates linkerd2-proxy-api to v0.14.0, which introduces per-route timeout and retry policies. These API responses are bound to the recently introduced outbound retry policies. Support for the legacy request_timeout configuration (which in rare situations may be set by older control planes) is maintained, but is now deprecated in favor of the newer timeout policies.
1 parent 2c03996 commit a3a0b9f

File tree

15 files changed

+243
-54
lines changed

15 files changed

+243
-54
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,9 +2294,9 @@ dependencies = [
22942294

22952295
[[package]]
22962296
name = "linkerd2-proxy-api"
2297-
version = "0.13.1"
2297+
version = "0.14.0"
22982298
source = "registry+https://github.com/rust-lang/crates.io-index"
2299-
checksum = "65678e4c506a7e5fdf1a664c629a9b658afa70e254dffcd24df72e937b2c0159"
2299+
checksum = "26c72fb98d969e1e94e95d52a6fcdf4693764702c369e577934256e72fb5bc61"
23002300
dependencies = [
23012301
"h2",
23022302
"http",

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ members = [
8484
[profile.release]
8585
debug = 1
8686
lto = true
87+
88+
[workspace.dependencies]
89+
linkerd2-proxy-api = "0.14.0"

linkerd/app/inbound/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ linkerd-meshtls-rustls = { path = "../../meshtls/rustls", optional = true }
3030
linkerd-proxy-client-policy = { path = "../../proxy/client-policy" }
3131
linkerd-tonic-stream = { path = "../../tonic-stream" }
3232
linkerd-tonic-watch = { path = "../../tonic-watch" }
33-
linkerd2-proxy-api = { version = "0.13", features = ["inbound"] }
33+
linkerd2-proxy-api = { workspace = true, features = ["inbound"] }
3434
once_cell = "1"
3535
parking_lot = "0.12"
3636
rangemap = "1"

linkerd/app/integration/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ ipnet = "2"
3333
linkerd-app = { path = "..", features = ["allow-loopback"] }
3434
linkerd-app-core = { path = "../core" }
3535
linkerd-metrics = { path = "../../metrics", features = ["test_util"] }
36-
linkerd2-proxy-api = { version = "0.13", features = [
36+
linkerd2-proxy-api = { workspace = true, features = [
3737
"destination",
3838
"arbitrary",
3939
] }

linkerd/app/outbound/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ ahash = "0.8"
2020
bytes = "1"
2121
http = "0.2"
2222
futures = { version = "0.3", default-features = false }
23-
linkerd2-proxy-api = { version = "0.13", features = ["outbound"] }
23+
linkerd2-proxy-api = { workspace = true, features = ["outbound"] }
2424
once_cell = "1"
2525
parking_lot = "0.12"
2626
pin-project = "1"

linkerd/http/route/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ tracing = "0.1"
1717
url = "2"
1818

1919
[dependencies.linkerd2-proxy-api]
20-
version = "0.13"
20+
workspace = true
2121
features = ["http-route", "grpc-route"]
2222
optional = true
2323

linkerd/proxy/api-resolve/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Implements the Resolve trait using the proxy's gRPC API
1111

1212
[dependencies]
1313
futures = { version = "0.3", default-features = false }
14-
linkerd2-proxy-api = { version = "0.13", features = ["destination"] }
14+
linkerd2-proxy-api = { workspace = true, features = ["destination"] }
1515
linkerd-addr = { path = "../../addr" }
1616
linkerd-error = { path = "../../error" }
1717
linkerd-proxy-core = { path = "../core" }

linkerd/proxy/client-policy/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ linkerd-proxy-api-resolve = { path = "../api-resolve" }
3030
linkerd-proxy-core = { path = "../core" }
3131

3232
[dependencies.linkerd2-proxy-api]
33-
version = "0.13"
33+
workspace = true
3434
optional = true
3535
features = ["outbound"]
3636

linkerd/proxy/client-policy/src/grpc.rs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,14 +136,35 @@ pub mod proto {
136136
#[error("invalid filter: {0}")]
137137
Filter(#[from] InvalidFilter),
138138

139+
#[error("missing {0}")]
140+
Missing(&'static str),
141+
139142
#[error("invalid failure accrual policy: {0}")]
140143
Breaker(#[from] InvalidFailureAccrual),
141144

145+
#[error("{0}")]
146+
Retry(#[from] InvalidRetry),
147+
142148
#[error("invalid request timeout: {0}")]
143149
RequestTimeout(#[from] prost_types::DurationError),
144150

145-
#[error("missing {0}")]
146-
Missing(&'static str),
151+
#[error("{0}")]
152+
Timeout(#[from] crate::http::proto::InvalidTimeouts),
153+
}
154+
155+
#[derive(Debug, thiserror::Error)]
156+
pub enum InvalidRetry {
157+
#[error("invalid max-retries: {0}")]
158+
MaxRetries(u32),
159+
160+
#[error("invalid condition")]
161+
Condition,
162+
163+
#[error("invalid timeout: {0}")]
164+
Timeout(#[from] prost_types::DurationError),
165+
166+
#[error("invalid backoff: {0}")]
167+
Backoff(#[from] crate::proto::InvalidBackoff),
147168
}
148169

149170
#[derive(Debug, thiserror::Error)]
@@ -219,6 +240,9 @@ pub mod proto {
219240
matches,
220241
backends,
221242
filters,
243+
timeouts,
244+
retry,
245+
allow_l5d_request_headers,
222246
request_timeout,
223247
} = proto;
224248

@@ -236,8 +260,9 @@ pub mod proto {
236260
.ok_or(InvalidGrpcRoute::Missing("distribution"))?
237261
.try_into()?;
238262

239-
let mut params = RouteParams::default();
240-
params.timeouts.request = request_timeout.map(TryInto::try_into).transpose()?;
263+
let mut params = RouteParams::try_from_proto(timeouts, retry, allow_l5d_request_headers)?;
264+
let legacy = request_timeout.map(TryInto::try_into).transpose()?;
265+
params.timeouts.request = params.timeouts.request.or(legacy);
241266

242267
Ok(Rule {
243268
matches,
@@ -250,6 +275,52 @@ pub mod proto {
250275
})
251276
}
252277

278+
impl RouteParams {
279+
fn try_from_proto(
280+
timeouts: Option<linkerd2_proxy_api::http_route::Timeouts>,
281+
retry: Option<grpc_route::Retry>,
282+
_allow_l5d_request_headers: bool,
283+
) -> Result<Self, InvalidGrpcRoute> {
284+
Ok(Self {
285+
retry: retry.map(Retry::try_from).transpose()?,
286+
timeouts: timeouts
287+
.map(crate::http::Timeouts::try_from)
288+
.transpose()?
289+
.unwrap_or_default(),
290+
})
291+
}
292+
}
293+
294+
impl TryFrom<outbound::grpc_route::Retry> for Retry {
295+
type Error = InvalidRetry;
296+
297+
fn try_from(retry: outbound::grpc_route::Retry) -> Result<Self, Self::Error> {
298+
let cond = retry.conditions.ok_or(InvalidRetry::Condition)?;
299+
let codes = Codes(Arc::new(
300+
[
301+
cond.cancelled.then_some(tonic::Code::Cancelled as u16),
302+
cond.deadine_exceeded
303+
.then_some(tonic::Code::DeadlineExceeded as u16),
304+
cond.resource_exhausted
305+
.then_some(tonic::Code::ResourceExhausted as u16),
306+
cond.internal.then_some(tonic::Code::Internal as u16),
307+
cond.unavailable.then_some(tonic::Code::Unavailable as u16),
308+
]
309+
.into_iter()
310+
.flatten()
311+
.collect(),
312+
));
313+
314+
Ok(Self {
315+
codes,
316+
max_retries: retry.max_retries as usize,
317+
max_request_bytes: retry.max_request_bytes as _,
318+
backoff: retry.backoff.map(crate::proto::try_backoff).transpose()?,
319+
timeout: retry.timeout.map(time::Duration::try_from).transpose()?,
320+
})
321+
}
322+
}
323+
253324
impl TryFrom<grpc_route::Distribution> for RouteDistribution<Filter> {
254325
type Error = InvalidDistribution;
255326
fn try_from(distribution: grpc_route::Distribution) -> Result<Self, Self::Error> {

linkerd/proxy/client-policy/src/http.rs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,37 @@ pub mod proto {
159159

160160
#[error("missing {0}")]
161161
Missing(&'static str),
162+
163+
#[error(transparent)]
164+
Timeout(#[from] InvalidTimeouts),
165+
166+
#[error(transparent)]
167+
Retry(#[from] InvalidRetry),
168+
}
169+
170+
#[derive(Debug, thiserror::Error)]
171+
pub enum InvalidRetry {
172+
#[error("invalid max-retries: {0}")]
173+
MaxRetries(u32),
174+
175+
#[error("invalid condition")]
176+
Condition,
177+
178+
#[error("invalid timeout: {0}")]
179+
Timeout(#[from] prost_types::DurationError),
180+
181+
#[error("invalid backoff: {0}")]
182+
Backoff(#[from] crate::proto::InvalidBackoff),
183+
}
184+
185+
#[derive(Debug, thiserror::Error)]
186+
pub enum InvalidTimeouts {
187+
#[error("invalid response timeout: {0}")]
188+
Response(prost_types::DurationError),
189+
#[error("invalid idle timeout: {0}")]
190+
Idle(prost_types::DurationError),
191+
#[error("invalid request timeout: {0}")]
192+
Request(prost_types::DurationError),
162193
}
163194

164195
#[derive(Debug, thiserror::Error)]
@@ -247,6 +278,9 @@ pub mod proto {
247278
matches,
248279
backends,
249280
filters,
281+
timeouts,
282+
retry,
283+
allow_l5d_request_headers,
250284
request_timeout,
251285
} = proto;
252286

@@ -264,8 +298,9 @@ pub mod proto {
264298
.ok_or(InvalidHttpRoute::Missing("distribution"))?
265299
.try_into()?;
266300

267-
let mut params = RouteParams::default();
268-
params.timeouts.request = request_timeout.map(TryInto::try_into).transpose()?;
301+
let mut params = RouteParams::try_from_proto(timeouts, retry, allow_l5d_request_headers)?;
302+
let legacy = request_timeout.map(TryInto::try_into).transpose()?;
303+
params.timeouts.request = params.timeouts.request.or(legacy);
269304

270305
Ok(Rule {
271306
matches,
@@ -278,6 +313,85 @@ pub mod proto {
278313
})
279314
}
280315

316+
impl RouteParams {
317+
fn try_from_proto(
318+
timeouts: Option<linkerd2_proxy_api::http_route::Timeouts>,
319+
retry: Option<http_route::Retry>,
320+
_allow_l5d_request_headers: bool,
321+
) -> Result<Self, InvalidHttpRoute> {
322+
Ok(Self {
323+
retry: retry.map(Retry::try_from).transpose()?,
324+
timeouts: timeouts
325+
.map(Timeouts::try_from)
326+
.transpose()?
327+
.unwrap_or_default(),
328+
})
329+
}
330+
}
331+
332+
impl TryFrom<linkerd2_proxy_api::http_route::Timeouts> for Timeouts {
333+
type Error = InvalidTimeouts;
334+
fn try_from(
335+
timeouts: linkerd2_proxy_api::http_route::Timeouts,
336+
) -> Result<Self, Self::Error> {
337+
Ok(Self {
338+
response: timeouts
339+
.response
340+
.map(time::Duration::try_from)
341+
.transpose()
342+
.map_err(InvalidTimeouts::Response)?,
343+
idle: timeouts
344+
.idle
345+
.map(time::Duration::try_from)
346+
.transpose()
347+
.map_err(InvalidTimeouts::Response)?,
348+
request: timeouts
349+
.request
350+
.map(time::Duration::try_from)
351+
.transpose()
352+
.map_err(InvalidTimeouts::Request)?,
353+
})
354+
}
355+
}
356+
357+
impl TryFrom<outbound::http_route::Retry> for Retry {
358+
type Error = InvalidRetry;
359+
fn try_from(retry: outbound::http_route::Retry) -> Result<Self, Self::Error> {
360+
fn range(
361+
r: outbound::http_route::retry::conditions::StatusRange,
362+
) -> Result<RangeInclusive<u16>, InvalidRetry> {
363+
let Ok(start) = u16::try_from(r.start) else {
364+
return Err(InvalidRetry::Condition);
365+
};
366+
let Ok(end) = u16::try_from(r.end) else {
367+
return Err(InvalidRetry::Condition);
368+
};
369+
if start == 0 || end == 0 || end > 599 || start > end {
370+
return Err(InvalidRetry::Condition);
371+
}
372+
Ok(start..=end)
373+
}
374+
375+
let status_ranges = StatusRanges(
376+
retry
377+
.conditions
378+
.ok_or(InvalidRetry::Condition)?
379+
.status_ranges
380+
.into_iter()
381+
.map(range)
382+
.collect::<Result<_, _>>()?,
383+
);
384+
Ok(Self {
385+
status_ranges,
386+
max_retries: u16::try_from(retry.max_retries)
387+
.map_err(|_| InvalidRetry::MaxRetries(retry.max_retries))?,
388+
max_request_bytes: retry.max_request_bytes as _,
389+
backoff: retry.backoff.map(crate::proto::try_backoff).transpose()?,
390+
timeout: retry.timeout.map(time::Duration::try_from).transpose()?,
391+
})
392+
}
393+
}
394+
281395
impl TryFrom<http_route::Distribution> for RouteDistribution<Filter> {
282396
type Error = InvalidDistribution;
283397
fn try_from(distribution: http_route::Distribution) -> Result<Self, Self::Error> {

0 commit comments

Comments
 (0)