Skip to content
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

Close H2Stream readBuffer on data.endStream #7147

Merged
merged 4 commits into from
Jun 12, 2023
Merged

Conversation

armanbilge
Copy link
Member

Fixes #7146. The Channel introduced in #7096 was not being closed in the right place.

@armanbilge armanbilge linked an issue Jun 10, 2023 that may be closed by this pull request
Comment on lines 106 to 111
def extractAuthority(headers: List[(String, String)]): Option[Uri.Authority] =
headers.collectFirstSome {
case (PseudoHeaders.AUTHORITY, value) =>
val index = value.indexOf(":")
if (index > 0 && index < value.length) {
Option(
Uri.Authority(
userInfo = None,
host = Uri.RegName(value.take(index)),
port = value.drop(index + 1).toInt.some,
)
)
} else Option.empty
Uri.fromString(value).toOption.flatMap(_.authority)
case (_, _) => None
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test revealed another bug 😅

Copy link
Member

@danicheg danicheg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not an Ember expert, but changes look good to me

@@ -339,7 +339,9 @@ private[h2] class H2Stream[F[_]: Concurrent](
if (needsWindowUpdate && !isClosed && sizeReadOk) {
enqueue.offer(Chunk.singleton(H2Frame.WindowUpdate(id, windowSize - newSize)))
} else Applicative[F].unit
_ <- if (data.endStream) s.trailWith(List.empty).void else Applicative[F].unit
_ <-
if (data.endStream) s.readBuffer.close *> s.trailWith(List.empty).void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it's kind of bikeshedding, but is it intentional that H2Stream#receiveData doesn't close readBuffer on cancelation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good question, cancelation is handled in various places by sending an exception to the readBuffer.

private[H2Stream] def cancelWith(msg: String)(implicit F: Monad[F]): F[Unit] = {
// Unsure of this, but also unsure about exposing custom throwable
val t = new CancellationException(msg)
writeBlock.complete(Left(t)) >>
request.complete(Left(t)) >>
response.complete(Left(t)) >>
readBuffer.send(Left(t)) >>
trailers.complete(Left(t)).void
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but does it work for receiveData as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

receiveData is called only from the readLoop. The readLoop runs forever when you create a server or a client: the only way to cancel it is to close the server/client resource. But to close the server/client resource you must have first closed all on-going requests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the explanation. I can't beat my feeling suspicious about this. But this kind of leak perhaps would be noticeable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The H2 state machine is rather complex and difficult to reason about, it's not impossible there are some bugs here. This PR and the previous one it fixes (which itself was a fix) proves that 😅 probably we are missing some test coverage in this area, that would help address your suspicions, and part of that question is how to cancel receiveData in the first place.

@armanbilge armanbilge merged commit 153edf4 into series/0.23 Jun 12, 2023
17 checks passed
@armanbilge armanbilge deleted the pr/i7146 branch June 12, 2023 15:28
@Masynchin
Copy link

Hello all! I just discovered this PR and took a look at its changes. I found that whenA may be used instead of if then ... else Applicative[F].unit:

- _ <-
-    if (data.endStream) s.readBuffer.close *> s.trailWith(List.empty).void
-    else Applicative[F].unit

+ _ <- data.endStream.whenA(s.readBuffer.close *> s.trailWith(List.empty).void)

I am not that experienced at scala, so can I ask you, is whenA version more readable than if then else or it is not?

@armanbilge
Copy link
Member Author

armanbilge commented Jun 12, 2023

Yes, you are right, this would be a good place to use whenA: it is correct and probably more readable :)

It would look like this:

_ <- (s.readBuffer.close *> s.trailWith(List.empty)).void.whenA(data.endStream)

There is a (very small) performance penalty to use it instead of an if, which is one reason why I didn't consider changing it here.

@Masynchin
Copy link

There is a (very small) performance penalty to use it instead of an if

This performance penalty caused because of method calling rather then directly using if then else? Because the whenA's body is just if (cond) void(f) else unit. Correct me if I am wrong, I am just trying to learn more about Scala :)

@armanbilge
Copy link
Member Author

The signature for whenA looks like this:

def whenA[A](cond: Boolean)(f: => F[A]): F[Unit]

The => F[A] is a "by-name" parameter. This is used to preserve laziness in the case when the condition is false.

https://docs.scala-lang.org/tour/by-name-parameters.html

What this means is that instead of directly passing F[A] to whenA, it allocates a by-name wrapper for F[A], and passes that instead. Creating this wrapper incurs a very small penalty.

@Masynchin
Copy link

Thanks for the explanation, @armanbilge!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ember server hangs on HTTP/2 request with body
4 participants