-
Notifications
You must be signed in to change notification settings - Fork 271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(http/retry): model PeekTrailersBody<B>
with Frame<T>
#3559
feat(http/retry): model PeekTrailersBody<B>
with Frame<T>
#3559
Conversation
6a2e600
to
bc29711
Compare
PeekTrailersBody<B>
with Frame<T>
(work-in-progress)PeekTrailersBody<B>
with Frame<T>
this commit introduces a `compat` submodule to `linkerd-http-retry`. this helps us frontrun the task of replacing all of the finicky control flow in `PeekTrailersBody<B>` using the antiquated `data()` and `trailers()` future combinators. instead, we can perform our peeking in terms of an approximation of `http_body_util::BodyExt::frame()`. to accomplish this, this commit vendors a copy of the `Frame<T>` type. we can use this to preemptively model our peek body in terms of this type, and move to the "real" version of it when we're upgrading in pr #3504. additionally, this commit includes a type called `ForwardCompatibleBody<B>`, and a variant of the `Frame<'a, T>` combinator. these are a bit boilerplate-y, admittedly, but the pleasant part of this is that we have, in effect, migrated the trickiest body middleware in advance of #3504. once we upgrade to http-body 1.0, all of these types can be removed. https://docs.rs/http-body-util/latest/http_body_util/trait.BodyExt.html#method.frame https://docs.rs/http-body-util/0.1.2/src/http_body_util/combinators/frame.rs.html#10 Signed-off-by: katelyn martin <[email protected]>
bc29711
to
5d34dc0
Compare
This comment was marked as resolved.
This comment was marked as resolved.
5d34dc0
to
ada0f6d
Compare
this commit reworks `PeekTrailersBody<B>`. the most important goal here is replacing the control flow of `read_body()`, which polls a body using `BodyExt` future combinators `data()` and `frame()` for up to two frames, with varying levels of persistence depending on outcomes. to quote #3556: > the intent of this type is to only yield the asynchronous task > responsible for reading the body once. depending on what the inner > body yields, and when, this can result in combinations of: no data > frames and no trailers, no data frames with trailers, one data frame > and no trailers, one data frame with trailers, or two data frames. > moreover, depending on which of these are yielded, the body will call > .await some scenarios, and only poll functions once in others. > > migrating this to the Frame<T> and poll_frame() style of the 1.0 Body > interface, away from the 0.4 interface that provides distinct > poll_data() and poll_trailers() methods, is fundamentally tricky. this means that `PeekTrailersBody<B>` is notably difficult to port across the http-body 0.4/1.0 upgrade boundary. this body middleware must navigate a number of edge conditions, and once it _has_ obtained a `Frame<T>`, make use of conversion methods to ascertain whether it is a data or trailers frame, due to the fact that its internal enum representation is not exposed publicly. one it has done all of that, it must do the same thing once more to examine the second frame. this commit uses the compatibility facilities and backported `Frame<T>` introduced in the previous commit, and rewrites this control flow using a form of the `BodyExt::frame()` combinator. this means that this middleware is forward-compatible with http-body 1.0, which will dramatically simplify the remaining migration work to be done in #3504. see linkerd/linkerd2#8733 for more information and other links related to this ongoing migration work. Signed-off-by: katelyn martin <[email protected]>
this commit addresses a `TODO` note, and tightens the enforcement of a rule defined by the v0.4 signature of the `Body` trait. this commit changes the mock body type, used in tests, so that it will panic if the caller improperly polls for a trailers frame before the final data frame has been yielded. previously, a comment indicated that we were "fairly sure" this was okay. while that may have been acceptable in practice, the changes in the previous commit mean that we now properly respect these rules. thus, a panic can be placed here, to enforce that "[is] only be called once `poll_data()` returns `None`", per the documentation. Signed-off-by: katelyn martin <[email protected]>
ada0f6d
to
8bd1dc9
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💬 breadcrumbs for review...
.map_err(|frame| match frame.into_trailers() { | ||
Ok(trls) => trls, | ||
// Safety: this is not reachable, we called `into_data()` above. | ||
Err(_) => unreachable!("into_data() and `into_trailers()` both returned `Err(_)`"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm unsure how much to worry about the commented out //Unknown(Box<dyn Frameish>),
variant in the vendored frame code. in other words, might this someday be reachable?
Result<T, E>
gives us something we can pattern match on, which Frame<T>
does not for the sake of semantic versioning. if we fear hitting this in the future, we could define a local enum like so...
enum ParsedFrame<T> {
Data(T),
Trailers(HeaderMap),
Other(Frame<T>),
}
...and return that here. what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or this could be Option<Either<B::Data, HeaderMap>>
, and then the caller can be adapted so this is handled as an Empty body... Since the API specifically doesn't give us exhaustive matching it's probably best to handle it cleanly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agreed. this is addressed in 501b3cf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really cool! A few suggestions
.map_err(|frame| match frame.into_trailers() { | ||
Ok(trls) => trls, | ||
// Safety: this is not reachable, we called `into_data()` above. | ||
Err(_) => unreachable!("into_data() and `into_trailers()` both returned `Err(_)`"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or this could be Option<Either<B::Data, HeaderMap>>
, and then the caller can be adapted so this is handled as an Empty body... Since the API specifically doesn't give us exhaustive matching it's probably best to handle it cleanly.
<#3559 (comment)> this is a nicer name than `Unknown` for this case. not to mention, we'll want that name shortly to address the possibility of unknown frame variants. Signed-off-by: katelyn martin <[email protected]>
this commit makes the inner enum variants private. #3559 (comment) Signed-off-by: katelyn martin <[email protected]>
#3559 (comment) Signed-off-by: katelyn martin <[email protected]>
@olix0r, review comments have been addressed! i'd love if you could take a quick look before i hit the merge button 🙂 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks great!
see linkerd/linkerd2#8733. pr #3559 introduced some compatibility facilities to allow us to write code in terms of `http_body_util::BodyExt::frame()`, front-running the upgrade to be performed in #3504. some `ReplayBody` tests use the defunct `data()` and `trailers()` interfaces. this branch ports _two_ such unit tests. other tests are saved for a fast follow-on, as the `chunk(..)` and `read_to_string(..)` helpers will need some slightly more involved tweaks. dd4fbcd --- * refactor(http/retry): `replays_trailers()` uses `Frame<T>` see linkerd/linkerd2#8733. this commit upgrades a test that uses defunct `data()` and `trailers()` futures. Signed-off-by: katelyn martin <[email protected]> * refactor(http/retry): `trailers_only()` uses `Frame<T>` see linkerd/linkerd2#8733. this commit upgrades a test that uses defunct `data()` and `trailers()` futures. Signed-off-by: katelyn martin <[email protected]> --------- Signed-off-by: katelyn martin <[email protected]>
pr #3559 (dd4fbcd) refactored our trailer peeking body middleware to model its buffering in terms of the `Frame<T>` type used in `http-body`'s 1.0 release. this commit performs a similar change for the other piece of body middleware that super linkerd's retry facilities: `ReplayBody<B>`. the inner body `B` is now wrapped in the `ForwardCompatibleBody<B>` adapter, and we now poll it in terms of frames. NB: polling the underlying in terms of frames has a subtle knock-on effect regarding when we observe the trailers, in the liminal period between this refactor and the subsequent upgrade to hyper 1.0, whilst we must still implement the existing 0.4 interface for `Body` that includes `poll_trailers()`. see the comment above `replay_trailers` for more on this, describing why we now initialize this to `true`. relatedly, this is why we now longer delegate down to `B::poll_trailers` ourselves. it will have already been called by our adapter. `ReplayBody::is_end_stream()` now behaves identically when initially polling a body compared to subsequent replays. this is fine, as `is_end_stream()` is a hint that facilitates optimizations. we do still report the end properly, we just won't be quite as prescient on the initial playthrough. see: - linkerd/linkerd2#8733. - #3559 Signed-off-by: katelyn martin <[email protected]>
pr #3559 (dd4fbcd) refactored our trailer peeking body middleware to model its buffering in terms of the `Frame<T>` type used in `http-body`'s 1.0 release. this commit performs a similar change for the other piece of body middleware that super linkerd's retry facilities: `ReplayBody<B>`. the inner body `B` is now wrapped in the `ForwardCompatibleBody<B>` adapter, and we now poll it in terms of frames. NB: polling the underlying in terms of frames has a subtle knock-on effect regarding when we observe the trailers, in the liminal period between this refactor and the subsequent upgrade to hyper 1.0, whilst we must still implement the existing 0.4 interface for `Body` that includes `poll_trailers()`. see the comment above `replay_trailers` for more on this, describing why we now initialize this to `true`. relatedly, this is why we now longer delegate down to `B::poll_trailers` ourselves. it will have already been called by our adapter. `ReplayBody::is_end_stream()` now behaves identically when initially polling a body compared to subsequent replays. this is fine, as `is_end_stream()` is a hint that facilitates optimizations (hyperium/http-body#143). we do still report the end properly, we just won't be quite as prescient on the initial playthrough. see: - linkerd/linkerd2#8733. - #3559 Signed-off-by: katelyn martin <[email protected]>
pr #3559 (dd4fbcd) refactored our trailer peeking body middleware to model its buffering in terms of the `Frame<T>` type used in `http-body`'s 1.0 release. this commit performs a similar change for the other piece of body middleware that super linkerd's retry facilities: `ReplayBody<B>`. the inner body `B` is now wrapped in the `ForwardCompatibleBody<B>` adapter, and we now poll it in terms of frames. NB: polling the underlying in terms of frames has a subtle knock-on effect regarding when we observe the trailers, in the liminal period between this refactor and the subsequent upgrade to hyper 1.0, whilst we must still implement the existing 0.4 interface for `Body` that includes `poll_trailers()`. see the comment above `replay_trailers` for more on this, describing why we now initialize this to `true`. relatedly, this is why we now longer delegate down to `B::poll_trailers` ourselves. it will have already been called by our adapter. `ReplayBody::is_end_stream()` now behaves identically when initially polling a body compared to subsequent replays. this is fine, as `is_end_stream()` is a hint that facilitates optimizations (hyperium/http-body#143). we do still report the end properly, we just won't be quite as prescient on the initial playthrough. see: - linkerd/linkerd2#8733. - #3559 Signed-off-by: katelyn martin <[email protected]>
pr #3559 (dd4fbcd) refactored our trailer peeking body middleware to model its buffering in terms of the `Frame<T>` type used in `http-body`'s 1.0 release. this branch performs a similar change for the other piece of body middleware that super linkerd's retry facilities: `ReplayBody<B>`. the inner body `B` is now wrapped in the `ForwardCompatibleBody<B>` adapter, and we now poll it in terms of frames. NB: polling the underlying in terms of frames has a subtle knock-on effect regarding when we observe the trailers, in the liminal period between this refactor and the subsequent upgrade to hyper 1.0, whilst we must still implement the existing 0.4 interface for `Body` that includes `poll_trailers()`. see the comment above `replay_trailers` for more on this, describing why we now initialize this to `true`. relatedly, this is why we no longer delegate down to `B::poll_trailers` ourselves. it will have already been called by our adapter. `ReplayBody::is_end_stream()` now behaves identically when initially polling a body compared to subsequent replays. this is fine, as `is_end_stream()` is a hint that facilitates optimizations (hyperium/http-body#143). we do still report the end properly, we just won't be quite as prescient on the initial playthrough. in the same manner as the existing `frame()` method mimics `http_body_util::BodyExt::frame()`, this branch introduces a new `ForwardCompatibleBody::poll_frame()` method. this allows us to poll the compatibility layer for a `Frame<T>`. see: - linkerd/linkerd2#8733. - #3559 --- * nit(http/retry): install tracing subscriber in tests some tests do not set up a tracing subscriber, because they do not use the shared `Test::new()` helper function used elsewhere in this test suite. to provide a trace of the test's execution in the event of a failure, initialize a tracing subscriber in some additional unit tests. Signed-off-by: katelyn martin <[email protected]> * feat(http/retry): `ForwardCompatibleBody<B>` exposes hints this commit removes the `cfg(test)` gate on the method exposing `B::is_end_stream()`, and introduces another method also exposing the `size_hint()` method. we will want these in order to implement these methods for `ReplayBody<B>`. Signed-off-by: katelyn martin <[email protected]> * refactor(http/retry): `ForwardCompatibleBody::poll_frame()` in the same manner as the existing `frame()` method mimics `http_body_util::BodyExt::frame()`, this commit introduces a new `ForwardCompatibleBody::poll_frame()` method. this allows us to poll the compatibility layer for a `Frame<T>`. Signed-off-by: katelyn martin <[email protected]> * feat(http/retry): `ReplayBody<B>` polls for frames pr #3559 (dd4fbcd) refactored our trailer peeking body middleware to model its buffering in terms of the `Frame<T>` type used in `http-body`'s 1.0 release. this commit performs a similar change for the other piece of body middleware that super linkerd's retry facilities: `ReplayBody<B>`. the inner body `B` is now wrapped in the `ForwardCompatibleBody<B>` adapter, and we now poll it in terms of frames. NB: polling the underlying in terms of frames has a subtle knock-on effect regarding when we observe the trailers, in the liminal period between this refactor and the subsequent upgrade to hyper 1.0, whilst we must still implement the existing 0.4 interface for `Body` that includes `poll_trailers()`. see the comment above `replay_trailers` for more on this, describing why we now initialize this to `true`. relatedly, this is why we now longer delegate down to `B::poll_trailers` ourselves. it will have already been called by our adapter. `ReplayBody::is_end_stream()` now behaves identically when initially polling a body compared to subsequent replays. this is fine, as `is_end_stream()` is a hint that facilitates optimizations (hyperium/http-body#143). we do still report the end properly, we just won't be quite as prescient on the initial playthrough. see: - linkerd/linkerd2#8733. - #3559 Signed-off-by: katelyn martin <[email protected]> * feat(http/retry): `is_end_stream()` traces this commit introduces some trace-level diagnostics tracking how the replay body has determined whether or not it has reached the end of the stream. Signed-off-by: katelyn martin <[email protected]> * nit(http/retry): capitalize trace event messages Signed-off-by: katelyn martin <[email protected]> --------- Signed-off-by: katelyn martin <[email protected]>
in #3559 (4b53081), we introduced a backported `Frame<T>` type, and a `ForwardCompatibleBody<B>` type that allows us to interact with a `http_body::Body` circa 0.4.6 in terms of frame-based interfaces that match those of the 1.0 interface. see linkerd/linkerd2#8733 for more information on upgrading hyper. in #3559, we narrowly added this as an internal submodule of the `linkerd-http-retry` library. these facilities however, would have utility in other places such as `linkerd-app-core`. this commit pulls these compatibility shims out into a `linkerd-http-body-compat` library so that they can be imported and reused elsewhere. Signed-off-by: katelyn martin <[email protected]>
* refactor(http/retry): outline `ForwardCompatibleBody<B>` in #3559 (4b53081), we introduced a backported `Frame<T>` type, and a `ForwardCompatibleBody<B>` type that allows us to interact with a `http_body::Body` circa 0.4.6 in terms of frame-based interfaces that match those of the 1.0 interface. see linkerd/linkerd2#8733 for more information on upgrading hyper. in #3559, we narrowly added this as an internal submodule of the `linkerd-http-retry` library. these facilities however, would have utility in other places such as `linkerd-app-core`. this commit pulls these compatibility shims out into a `linkerd-http-body-compat` library so that they can be imported and reused elsewhere. Signed-off-by: katelyn martin <[email protected]> * nit(http/body-compat): tidy `combinators` imports Signed-off-by: katelyn martin <[email protected]> --------- Signed-off-by: katelyn martin <[email protected]>
`linkerd-app-core` includes an error recovery body middleware. this middleware will gracefully catch and report errors encountered when polling an inner body, and via an `R`-typed recovery strategy provided by the caller, will attempt to map the error to a gRPC status code denoting an error. before we upgrade to hyper 1.0 in service of linkerd/linkerd2#8733, we add some test coverage to ensure that we preserve the behavior of this middleware. see: * linkerd/linkerd2#8733 * #3614. for historical context on this tower layer, see: * #222 * #1246 * #1282 --- * refactor(http/retry): outline `ForwardCompatibleBody<B>` in #3559 (4b53081), we introduced a backported `Frame<T>` type, and a `ForwardCompatibleBody<B>` type that allows us to interact with a `http_body::Body` circa 0.4.6 in terms of frame-based interfaces that match those of the 1.0 interface. see linkerd/linkerd2#8733 for more information on upgrading hyper. in #3559, we narrowly added this as an internal submodule of the `linkerd-http-retry` library. these facilities however, would have utility in other places such as `linkerd-app-core`. this commit pulls these compatibility shims out into a `linkerd-http-body-compat` library so that they can be imported and reused elsewhere. Signed-off-by: katelyn martin <[email protected]> * nit(http/body-compat): tidy `combinators` imports Signed-off-by: katelyn martin <[email protected]> * refactor(app/core): hoist `errors::code_header` helper Signed-off-by: katelyn martin <[email protected]> * refactor(app/core): `l5d-*` constants are headers these are header values. `http::HeaderName` has a const fn constructor, so let's use that. Signed-off-by: katelyn martin <[email protected]> * refactor(app/core): grpc constants are headers Signed-off-by: katelyn martin <[email protected]> * refactor(app/core): hoist `l5d-` and `grpc-` constants Signed-off-by: katelyn martin <[email protected]> * refactor(app/core): outline `ResponseBody` middleware we'll add a few tests for this middleware shortly. this commit moves this middleware out into its own submodule. Signed-off-by: katelyn martin <[email protected]> * refactor(app/core): encapsulate `ResponseBody` enum for other body middleware, we hide inner enum variants and their constituent members by using the "inner" pattern. this commit tweaks `ResponseBody` to follow suit, such that it now holds an `Inner`, but does not expose its passthrough and rescue variants to callers. Signed-off-by: katelyn martin <[email protected]> * docs(app/core): document `ResponseBody<R, B>` this adds a small documentation comment describing what this type does. Signed-off-by: katelyn martin <[email protected]> * refactor(app/core): a unit test suite for rescue body this commit introduces a test suite for our error recovery middleware. this body middleware provides a mechanism to "rescue" errors, gracefully mapping an error encountered when polling a gRPC body into e.g. trailers with a gRPC status code. before we upgrade this middleware in service of linkerd/linkerd2#8733, we add some test coverage to ensure that we preserve this middleware. Signed-off-by: katelyn martin <[email protected]> --------- Signed-off-by: katelyn martin <[email protected]>
this branch contains a sequence of commits that refactor
PeekTrailersBody<B>
.this branch is specifically focused on making this body middleware
forward-compatible with the 1.0 interface(s) of
http_body::Body
andhttp_body_util::BodyExt
.it does this in two main steps: (1) temporarily vendoring
http_body::Frame<T>
and providing a compatibility shim that provides a
frame()
method for a body,and (2) modeling
PeekTrailersBody<B>
and its peeking logic in terms of thisFrame<'a, T>
future.feat(http/retry): add
Frame<T>
compatibility facilitiesthis commit introduces a
compat
submodule tolinkerd-http-retry
.this helps us frontrun the task of replacing all of the finicky control flow in
PeekTrailersBody<B>
using the antiquateddata()
andtrailers()
futurecombinators. instead, we can perform our peeking in terms of an approximation
of
http_body_util::BodyExt::frame()
.to accomplish this, this commit vendors a copy of the
Frame<T>
type. we canuse this to preemptively model our peek body in terms of this type, and move to
the "real" version of it when we're upgrading in pr #3504.
additionally, this commit includes a type called
ForwardCompatibleBody<B>
,and a variant of the
Frame<'a, T>
combinator. these are a bit boilerplate-y,admittedly, but the pleasant part of this is that we have, in effect, migrated
the trickiest body middleware in advance of #3504. once we upgrade to http-body
1.0, all of these types can be removed.
Signed-off-by: katelyn martin [email protected]
refactor(http/retry):
PeekTrailersBody<B>
usesBodyExt::frame()
this commit reworks
PeekTrailersBody<B>
.the most important goal here is replacing the control flow of
read_body()
,which polls a body using
BodyExt
future combinatorsdata()
andframe()
for up to two frames, with varying levels of persistence depending on outcomes.
to quote #3556:
this means that
PeekTrailersBody<B>
is notably difficult to port across thehttp-body 0.4/1.0 upgrade boundary.
this body middleware must navigate a number of edge conditions, and once it
has obtained a
Frame<T>
, make use of conversion methods to ascertainwhether it is a data or trailers frame, due to the fact that its internal enum
representation is not exposed publicly. one it has done all of that, it must do
the same thing once more to examine the second frame.
this commit uses the compatibility facilities and backported
Frame<T>
introduced in the previous commit, and rewrites this control flow using a form
of the
BodyExt::frame()
combinator.this means that this middleware is forward-compatible with http-body 1.0, which
will dramatically simplify the remaining migration work to be done in #3504.
see linkerd/linkerd2#8733 for more information and
other links related to this ongoing migration work.
refactor(http/retry): mock body enforces
poll_trailers()
contractthis commit addresses a
TODO
note, and tightens the enforcement of a ruledefined by the v0.4 signature of the
Body
trait.this commit changes the mock body type, used in tests, so that it will panic if
the caller improperly polls for a trailers frame before the final data frame
has been yielded.
previously, a comment indicated that we were "fairly sure" this was okay. while
that may have been acceptable in practice, the changes in the previous commit
mean that we now properly respect these rules.
thus, a panic can be placed here, to enforce that "[is] only be called once
poll_data()
returnsNone
", per the documentation.