Skip to content

Commit 76b5f10

Browse files
cratelynolix0r
andauthored
refactor(http/upgrade): Http11Upgrade is Clone (#3540)
* refactor(http/upgrade): `Http11Upgrade::insert_half` matches on `self` this is a noöp change, to set the stage for subsequent changes to the internal model of `Http11Upgrade`. this `inner` field will shortly be an option, and this will make it easier to only follow these panicking branches when the inner lock is `Some(_)`. Signed-off-by: katelyn martin <[email protected]> * refactor(http/upgrade): `Http11Upgade` stores an `Option<T>` this commit hinges on this change to the upgrade middleware's `inner` field. we still retain a reference-counted copy of the `Inner` state, but now we may store `None` here. ``` pub struct Http11Upgrade { half: Half, - inner: Arc<Inner>, + inner: Option<Arc<Inner>>, } ``` a new branch is added to the `insert_half` method that consumes the "sender" and inserts an upgrade future; when this is `None` it will do nothing, rather than panicking. Signed-off-by: katelyn martin <[email protected]> * refactor(http/upgrade): `Half` marker is `Copy` this type is an empty flag to indicate whether an `Http11Upgrade` extension corresponds to the server or client half of the upgrade future channel. this type is made copy, to facilitate making the `Http11Upgrade` extension safely cloneable. Signed-off-by: katelyn martin <[email protected]> * refactor(http/upgrade): `Http11Upgrade` is `Clone` this commit makes `Http11Upgrade` a cloneable type. see <linkerd/linkerd2#8733>. in the 1.0 interface of the `http` crate, request and response extensions must now satisfy a `Clone` bound. `Http11Upgrade` was written before this was the case, and is very intentionally designed around the idea that it *not* be cloneable. `insert_half()` in particular could cause the proxy to panic if it were to clone a request or response's extensions. it might call `insert_half()` a second time, and discover that the `TryLock<T>` had already been set. moreover, holding on to a copy of the extensions would prevent the `Drop` method for `Inner` from being called. This would cause connections that negotiate an HTTP/1.1 upgrade to deadlock due to the `OnUpgrade` futures never being polled, and failing to create a `Duplex` that acts as the connection's I/O transport. this commit makes use of the alterations to `Http11Upgrade` made in previous commits, and adds a *safe* implementation of `Clone`. by only shallowly copying the extension, we tie the upgrade glue to a *specific* request/response. the extension can be cloned, but any generated copies will be inert. Signed-off-by: katelyn martin <[email protected]> * chore(http/upgrade): fix broken intradoc links Signed-off-by: katelyn martin <[email protected]> * chore(http/upgrade): add `thiserror` dependency Signed-off-by: katelyn martin <[email protected]> * refactor(proxy/http): use `.await` syntax `FutureExt::map_ok()` won't work if we try to return an error from this block. the `and_then()` adaptor is used to chain futures, and also won't work given a synchronous closure. this can be done with the equivalent `.await` syntax, and leaves a nicer hole for us to propagate other errors here, shortly. Signed-off-by: katelyn martin <[email protected]> * review(http/upgrade): propagate `insert_half()` failures #3540 (comment) Signed-off-by: katelyn martin <[email protected]> Co-Authored-By: Oliver Gould <[email protected]> * docs(http/upgrade): tweak comment Signed-off-by: katelyn martin <[email protected]> --------- Signed-off-by: katelyn martin <[email protected]> Co-authored-by: Oliver Gould <[email protected]>
1 parent b8d29a2 commit 76b5f10

File tree

6 files changed

+59
-20
lines changed

6 files changed

+59
-20
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1859,6 +1859,7 @@ dependencies = [
18591859
"linkerd-io",
18601860
"linkerd-stack",
18611861
"pin-project",
1862+
"thiserror 2.0.11",
18621863
"tokio",
18631864
"tower",
18641865
"tracing",

linkerd/http/upgrade/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ hyper = { workspace = true, default-features = false, features = [
2020
"client",
2121
] }
2222
pin-project = "1"
23+
thiserror = "2"
2324
tokio = { version = "1", default-features = false }
2425
tower = { version = "0.4", default-features = false }
2526
tracing = "0.1"

linkerd/http/upgrade/src/glue.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,12 @@ impl<B> PinnedDrop for UpgradeBody<B> {
126126
let this = self.project();
127127
// If an HTTP/1 upgrade was wanted, send the upgrade future.
128128
if let Some((upgrade, on_upgrade)) = this.upgrade.take() {
129-
upgrade.insert_half(on_upgrade);
129+
if let Err(error) = upgrade.insert_half(on_upgrade) {
130+
tracing::warn!(
131+
?error,
132+
"upgrade body could not send upgrade future upon completion"
133+
);
134+
}
130135
}
131136
}
132137
}

linkerd/http/upgrade/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub mod upgrade;
4444
/// > fields' semantics. This includes but is not limited to:
4545
/// >
4646
/// > - `Proxy-Connection` (Appendix C.2.2 of [HTTP/1.1])
47-
/// > - `Keep-Alive` (Section 19.7.1 of [RFC2068])
47+
/// > - `Keep-Alive` (Section 19.7.1 of \[RFC2068\])
4848
/// > - `TE` (Section 10.1.4)
4949
/// > - `Transfer-Encoding` (Section 6.1 of [HTTP/1.1])
5050
/// > - `Upgrade` (Section 7.8)

linkerd/http/upgrade/src/upgrade.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,9 @@ use try_lock::TryLock;
2222
/// inserted into the `Request::extensions()`. If the HTTP1 client service
2323
/// also detects an upgrade, the two `OnUpgrade` futures will be joined
2424
/// together with the glue in this type.
25-
// Note: this relies on there only having been 2 Inner clones, so don't
26-
// implement `Clone` for this type.
2725
pub struct Http11Upgrade {
2826
half: Half,
29-
inner: Arc<Inner>,
27+
inner: Option<Arc<Inner>>,
3028
}
3129

3230
/// A named "tuple" returned by [`Http11Upgade::halves()`] of the two halves of
@@ -50,7 +48,7 @@ struct Inner {
5048
upgrade_drain_signal: Option<drain::Watch>,
5149
}
5250

53-
#[derive(Debug)]
51+
#[derive(Clone, Copy, Debug)]
5452
enum Half {
5553
Server,
5654
Client,
@@ -63,6 +61,13 @@ pub struct Service<S> {
6361
upgrade_drain_signal: drain::Watch,
6462
}
6563

64+
#[derive(Debug, thiserror::Error)]
65+
#[error("OnUpgrade future has already been inserted: half={half:?}")]
66+
pub struct AlreadyInserted {
67+
half: Half,
68+
pub upgrade: OnUpgrade,
69+
}
70+
6671
// === impl Http11Upgrade ===
6772

6873
impl Http11Upgrade {
@@ -80,35 +85,42 @@ impl Http11Upgrade {
8085
Http11UpgradeHalves {
8186
server: Http11Upgrade {
8287
half: Half::Server,
83-
inner: inner.clone(),
88+
inner: Some(inner.clone()),
8489
},
8590
client: Http11Upgrade {
8691
half: Half::Client,
87-
inner,
92+
inner: Some(inner.clone()),
8893
},
8994
}
9095
}
9196

92-
pub fn insert_half(self, upgrade: OnUpgrade) {
93-
match self.half {
94-
Half::Server => {
95-
let mut lock = self
96-
.inner
97+
pub fn insert_half(self, upgrade: OnUpgrade) -> Result<(), AlreadyInserted> {
98+
match self {
99+
Self {
100+
inner: Some(inner),
101+
half: Half::Server,
102+
} => {
103+
let mut lock = inner
97104
.server
98105
.try_lock()
99106
.expect("only Half::Server touches server TryLock");
100107
debug_assert!(lock.is_none());
101108
*lock = Some(upgrade);
109+
Ok(())
102110
}
103-
Half::Client => {
104-
let mut lock = self
105-
.inner
111+
Self {
112+
inner: Some(inner),
113+
half: Half::Client,
114+
} => {
115+
let mut lock = inner
106116
.client
107117
.try_lock()
108118
.expect("only Half::Client touches client TryLock");
109119
debug_assert!(lock.is_none());
110120
*lock = Some(upgrade);
121+
Ok(())
111122
}
123+
Self { inner: None, half } => Err(AlreadyInserted { half, upgrade }),
112124
}
113125
}
114126
}
@@ -121,6 +133,25 @@ impl fmt::Debug for Http11Upgrade {
121133
}
122134
}
123135

136+
/// An [`Http11Upgrade`] can be cloned.
137+
///
138+
/// NB: Only the original copy of this extension may insert an [`OnUpgrade`] future into its half
139+
/// of the channel. Calling [`insert_half()`][Http11Upgrade::insert_half] on any clones of an
140+
/// upgrade extension will result in an error.
141+
// See the [`Drop`] implementation provided by `Inner` for more information.
142+
impl Clone for Http11Upgrade {
143+
fn clone(&self) -> Self {
144+
Self {
145+
half: self.half,
146+
// We do *NOT* deeply clone our reference to `Inner`.
147+
//
148+
// `Http11Upgrade::insert_half()` and the `Inner` type's `Drop` glue rely on there only
149+
// being one copy of the client and sender halves of the upgrade channel.
150+
inner: None,
151+
}
152+
}
153+
}
154+
124155
/// When both halves have dropped, check if both sides are inserted,
125156
/// and if so, spawn the upgrade task.
126157
impl Drop for Inner {

linkerd/proxy/http/src/h1.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ where
136136
client.as_ref().unwrap().request(req)
137137
};
138138

139-
Box::pin(rsp_fut.err_into().map_ok(move |mut rsp| {
139+
Box::pin(async move {
140+
let mut rsp = rsp_fut.await?;
140141
if is_http_connect {
141142
// Add an extension to indicate that this a response to a CONNECT request.
142143
debug_assert!(
@@ -161,14 +162,14 @@ where
161162
if is_upgrade(&rsp) {
162163
trace!("Client response is HTTP/1.1 upgrade");
163164
if let Some(upgrade) = upgrade {
164-
upgrade.insert_half(hyper::upgrade::on(&mut rsp));
165+
upgrade.insert_half(hyper::upgrade::on(&mut rsp))?;
165166
}
166167
} else {
167168
linkerd_http_upgrade::strip_connection_headers(rsp.headers_mut());
168169
}
169170

170-
rsp.map(BoxBody::new)
171-
}))
171+
Ok(rsp.map(BoxBody::new))
172+
})
172173
}
173174
}
174175

0 commit comments

Comments
 (0)