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
Add BracketRequestResponse Middleware #3977
Conversation
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.
Wow.
It's too late in my day for me to give this the thought it deserves, but let me double check that I understand the strategy before diving in tomorrow: this won't save us from backends not draining the response body like #2663 would, but it centralizes all the workarounds we've implemented in various middlewares to simulate it?
To clarify, that's a "Wow, impressive work", not a "Wow, WTF". |
That is correct. Additionally, one could trivially bypass the release on body stream termination by discarding the response body, if they do so outside the scope of the middleware. This was the case before this commit as well. I've some half ideas on how we might be able to ensure finalizers run, even if the response body or even the whole response gets discarded, but that is beyond the scope of this PR (plus they may not even work, like I said "half ideas"). |
I should probably make a note about that edge case in the comments. |
@rossabaker I added a comment about that edge case. |
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 like it. Leaning into ContextRequest
instead of attributes avoids some options, but I wonder whether it impairs composability with other things that might already be a ContextRequest
. We don't have a good way of nesting contexts yet. Maybe that's not a problem in practice, or anything we can solve in series/0.21, but something to think about.
object BracketRequestResponse { | ||
|
||
/** A [[Response]] value with some arbitrary context added. */ | ||
final case class ContextResponse[F[_], A](context: A, response: Response[F]) |
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 think there are still bigger thoughts to be had about this and ContextRequest
and the old idea of turning message into a typeclass like @ChristopherDavenport has been proposing forever, but that's a 1.0 discussion.
* @note This is the same as [[#app]], but allows for the inner and outer | ||
* effect types to differ. | ||
*/ | ||
def app_[F[_]: Sync, G[_]: Sync]( |
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 think we called this 2
instead of _
when we did similar in Http4sDsl
.
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.
@rossabaker I'll update. _
is my default '
Haskell idioms. I never know what to name things.
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.
updated
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 think of the _
as being unit, as in traverse_
, but that's possibly just Cats looking for an '
as well.
* Response body, or in the event of any error, and is guaranteed to | ||
* only occur once. | ||
*/ | ||
object ConcurrentRequests { |
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 think maybe I was expecting to see an fs2.Signal
on this instead of those callbacks. I don't have a feel for whether that's easier or harder to work with.
|
||
object MaxActiveRequests { | ||
|
||
@deprecated(message = "Please use forHttpApp instead.", since = "0.21.14") |
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.
We use this name in a lot of places, so if we're moving this direction, we should follow up and do it in more places.
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.
@rossabaker I actually like the other name just fine, but I needed a fresh name due to the binary compatibility change. I'm certainly open to suggestions on how to go about naming this.
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.
Oh, the Concurrent => Sync
. You mentioned that, and then I didn't spot it. No, I think you did the right thing.
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 would like to rethink context request and response, and how they compose, and how they complement and contrast vault. I think there's a long design discussion to be had around this.
But within the constraints of 0.21, this seems reasonable and likely to fix some issues.
@rossabaker I'm a bit confused here. I removed a comment, and now CI is complaining about files not having headers...but they definitely have headers... A local |
This middleware allows for expressing bracketing operations, beginning at the reception of a Request, and ending when the Response _body_ is fully consumed, handling all error branches. It is semantically the same as `Bracket` from cats.effect. Because http4s defines an http services as `Kleisli[OptionT[F, ?], Request[F], Response[F]]`, you can't actually use any existing bracketing semantics from cats-effect or fs2 to bracket over the acquisition of the `Request` and the consumption of the `Response` _body_. Code of this structure, or very similar to it, already exists in http4s, but not generically. By implementing it generically, we are able to remove a great deal of code as well as make the semantics more clear. In addition to `BracketRequestResponse`, this commit contains the following other changes. * A new middleware is added named `ConcurrentRequests`. This middleware takes functions to run at the start and end of a request/response interaction which report the current value of the concurrent requests for the service. The following middlewares were re-written in terms of `BracketRequestResponse`. * `MaxActiveRequests` * Technically, this was re-written in terms of `ConcurrentRequests`. * As a result of the re-write, the constraint can be dropped from `Concurrent` to `Sync`. This may seem alarming, but it is actually safe. No asynchronous or cancellation is required to merely track the number of concurrent requests. The former implementation derived the `Concurrent` constraint from the use of a `Semaphore`, but since `ConcurrentRequests` only requires a `Ref`, only `Sync` is needed. * `Metrics` * This yielded a significant reduction in code and (subjectively) clarified the semantics. * The new implementation keeps adheres to the old semantics, with the following exceptions. * `decreaseActiveRequests` is now run _before_ the other metrics operations, formerly it was run _after_. This may be why there have been some reported instances of the active requests metric getting stuck at non-zero value. * `recordHeadersTime` is no longer called when the body is consumed, only when the `Response` (and thus the headers) are generated. This appears to have been a bug in the previous implementation. * When an error occurs in generation the `Response`, we only poll the `Clock` _one_ time and use that same value to report the elapsed time for both the `recordHeadersTime` and `recordAbnormalTermination`. The previous implementation polled twice in this case, even though they should effectively be occurring at the same time. See line `100` and `165` in the previous commit. * Note: This is why the tests were updated. The tests assumed, via `FakeClock`, that there would be two polling events in the error branch. * In the former implementation, metrics are only recorded on error if `errorResponseHandler` is yields a non-empty result. This includes _all_ metrics, e.g. `recordHeadersTime`, `recordTotalTime`, and `recordAbnormalTermination`. This seems to be a result of the fact that `recordTotalTime` _requires_ a `Status` value, which will not be available if `errorResponseHandler` yields `None`. Since `MetricsOps` is unchanged, this is still the case for invoking `recordTotalTime`, but we can invoke `recordAbnormalTermination` and `recordHeadersTime` even if `errorResponseHandler` yields `None`, so the implementation does this.
Run scalafmt and fix binary compatibility issues. The weakening of the constraints from `Concurrent` -> `Sync` in `MaxActiveRequests` is not binary compatible. The old method signatures were restored, and deprecated, and new methods were added which only require `Sync`.
Similar to ContextRequest, but for Response[F].
Also fixes some compilation errors due to the `_` -> `2` change that were originally missed.
Updated the note on `bracketRequestResponseCaseRoutes_` to more clearly indicate how one could determine which of the three exit branches `release` is being invoked in. Usually the caller won't need to care about this.
This comment was in the original implementation for this which was prototyped outside of the http4s main repo. It makes no sense in http4s proper.
Rebase on the latest series/0.21 also did nothing. |
*/ | ||
def bracketRequestResponseCaseRoutes[F[_], A]( | ||
acquire: F[A] | ||
)(release: (A, ExitCase[Throwable]) => F[Unit])(implicit |
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.
There should probably be a version of this that works in terms of Resource
. All the resources I would want to use with this middleware come as Resource
s.
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.
@nigredo-tori Are you suggesting a variant where the F
is Resource
?
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.
There's no reason other than More Names this can't be added, right?
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 suggesting something like
def bracketRequestResponseResourceRoutes[F[_], A](
resource: Resource[F, A]
)(implicit F: Bracket[F, Throwable]): ContextMiddleware[F, A]
Resource
wraps a pair of acquire
and release
. Libraries generally expose Resource
s other than those, so it makes sense to provide support for this use case.
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.
Unless I'm misunderstanding you (which is entirely possible) the only possible implementation of that signature which makes sense is something like this,
// I've not compile this, but I think it is correct
def bracketRequestResponseResourceRoutes[F[_], A](
resource: Resource[F, A]
)(implicit F: Bracket[F, Throwable]): ContextMiddleware[F, A] = {
(bracketRoutes: Kleisli[OptionT[F, *], ContextRequest[F], Response[F]]) =>
Kleisli((request: Request[F]) =>
resource.use(a =>
bracketRoutes.run(ContextRequest(a, resquest))
)
)
}
I'd be very happy to add that, but I want to make sure those semantics are what you want. The release
function of the Resource
will be run when the effect to generate the Response[F]
, but before Response.body: Stream[F, Byte]
is run. Is that what you are asking for?
My original indent for the BracketRequestResponse
middleware was to be able to attach a finalizer to termination of the Response.body
xor the F[Option[Response[F]]]
if the latter was empty or failed. Unfortunately there is no way to write that logic using Resource
with the current encodings. This is because the F[Option[Response[F]]]
doesn't currently compose with the Response.body: Stream[F, Byte]
scope.
Let me know if I'm just misunderstanding you.
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 was thinking more along these lines:
(contextRoutes: ContextRoutes[F, A]) => {
val contextRoutes0: ContextRoutes[F, (A, F[Unit])] =
contextRoutes.contramap(_.map(_._1))
bracketRequestResponseRoutes[F, (A, F[Unit])](resource.allocated)(_._2)(contextRoutes0)
}
I haven't compiled it, but the types should align. Resource.allocated
is the key here - it transforms a Resource
into something compatible with the acquire
/release
shape.
This would be sloppy with ExitCase
handling, though, since allocated
always releases with ExitCase.Completed
. We might want to first add some kind of Resource.allocatedCase
to cats-effect.
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.
Ah, that makes more sense. I didn't realize that Resource
exposed a function like allocated
.
I rolled back our customization of the copyright headers in favor of the default when we adopted sbt-spiewak, and after I merged that, CI is testing your files on the merge commit. I fixed it with |
@rossabaker thanks for taking a look! |
This middleware allows for expressing bracketing operations, beginning at the reception of a Request, and ending when the Response body is fully consumed, handling all error branches. It is semantically the same as
Bracket
from cats.effect. Because http4s defines an http services asKleisli[OptionT[F, ?], Request[F], Response[F]]
, you can't actually use any existing bracketing semantics from cats-effect or fs2 out of the box to bracket over the acquisition of theRequest
and the consumption of theResponse
body.Code of this structure, or very similar to it, already exists in http4s, but not generically. By implementing it generically, we are able to remove a great deal of code as well as make the semantics more clear.
In addition to
BracketRequestResponse
, this commit contains the following other changes.ConcurrentRequests
. This middleware takes functions to run at the start and end of a request/response interaction which report the current value of the concurrent requests for the service.The following middlewares were re-written in terms of
BracketRequestResponse
.MaxActiveRequests
ConcurrentRequests
.Concurrent
toSync
. This may seem alarming, but it is actually safe. No asynchronous or cancellation is required to merely track the number of concurrent requests. The former implementation derived theConcurrent
constraint from the use of aSemaphore
, but sinceConcurrentRequests
only requires aRef
, onlySync
is needed.Metrics
decreaseActiveRequests
is now run before the other metrics operations, formerly it was run after. This may be why there have been some reported instances of the active requests metric getting stuck at non-zero value.recordHeadersTime
is no longer called when the body is consumed, only when theResponse
(and thus the headers) are generated. This appears to have been a bug in the previous implementation.Response
, we only poll theClock
one time and use that same value to report the elapsed time for both therecordHeadersTime
andrecordAbnormalTermination
. The previous implementation polled twice in this case, even though they should effectively be occurring at the same time. See line100
and165
in the previous commit.FakeClock
, that there would be two polling events in the error branch.errorResponseHandler
is yields a non-empty result. This includes all metrics, e.g.recordHeadersTime
,recordTotalTime
, andrecordAbnormalTermination
. This seems to be a result of the fact thatrecordTotalTime
requires aStatus
value, which will not be available iferrorResponseHandler
yieldsNone
. SinceMetricsOps
is unchanged, this is still the case for invokingrecordTotalTime
, but we can invokerecordAbnormalTermination
andrecordHeadersTime
even iferrorResponseHandler
yieldsNone
, so the implementation does this.