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

Add Resource-based simple constructors #1015

Open
wants to merge 7 commits into
base: series/0.9
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,16 @@ jobs:
matrix:
os: [ubuntu-latest]
scala: [2.12, 2.13, 3]
java: [temurin@11, temurin@17]
java: [temurin@11, temurin@17, temurin@21]
exclude:
- scala: 2.12
java: temurin@17
- scala: 2.12
java: temurin@21
- scala: 3
java: temurin@17
- scala: 3
java: temurin@21
runs-on: ${{ matrix.os }}
timeout-minutes: 60
steps:
Expand Down Expand Up @@ -70,6 +74,19 @@ jobs:
if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false'
run: sbt +update

- name: Setup Java (temurin@21)
id: setup-java-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: sbt

- name: sbt update
if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false'
run: sbt +update

- name: Check that workflows are up to date
run: sbt githubWorkflowCheck

Expand Down Expand Up @@ -152,6 +169,19 @@ jobs:
if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false'
run: sbt +update

- name: Setup Java (temurin@21)
id: setup-java-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: sbt

- name: sbt update
if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false'
run: sbt +update

- name: Download target directories (2.12)
uses: actions/download-artifact@v4
with:
Expand Down Expand Up @@ -246,6 +276,19 @@ jobs:
if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false'
run: sbt +update

- name: Setup Java (temurin@21)
id: setup-java-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: sbt

- name: sbt update
if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false'
run: sbt +update

- name: Submit Dependencies
uses: scalacenter/sbt-dependency-submission@v2
with:
Expand Down Expand Up @@ -291,6 +334,19 @@ jobs:
if: matrix.java == 'temurin@17' && steps.setup-java-temurin-17.outputs.cache-hit == 'false'
run: sbt +update

- name: Setup Java (temurin@21)
id: setup-java-temurin-21
if: matrix.java == 'temurin@21'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
cache: sbt

- name: sbt update
if: matrix.java == 'temurin@21' && steps.setup-java-temurin-21.outputs.cache-hit == 'false'
run: sbt +update

- name: Generate site
run: sbt docs/tlSite

Expand Down
1 change: 1 addition & 0 deletions .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pull_request_rules:
- status-success=Build and Test (ubuntu-latest, 2.12, temurin@11)
- status-success=Build and Test (ubuntu-latest, 2.13, temurin@11)
- status-success=Build and Test (ubuntu-latest, 2.13, temurin@17)
- status-success=Build and Test (ubuntu-latest, 2.13, temurin@21)
- status-success=Build and Test (ubuntu-latest, 3, temurin@11)
- status-success=Generate Site (ubuntu-latest, temurin@11)
actions:
Expand Down
6 changes: 3 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ ThisBuild / developers := List(
)

ThisBuild / tlJdkRelease := Some(11)
ThisBuild / githubWorkflowJavaVersions := Seq("11", "17").map(JavaSpec.temurin(_))
ThisBuild / githubWorkflowJavaVersions := Seq("11", "17", "21").map(JavaSpec.temurin(_))
ThisBuild / tlCiReleaseBranches := Seq("series/0.9")
ThisBuild / tlSitePublishBranch := Some("series/0.9")

Expand Down Expand Up @@ -109,8 +109,8 @@ lazy val docsSettings =
"HTTP4S_VERSION_SHORT" -> http4sV.split("\\.").take(2).mkString("."),
"SCALA_VERSION" -> CrossVersion.binaryScalaVersion(scalaVersion.value),
"SCALA_VERSIONS" -> formatCrossScalaVersions((core / crossScalaVersions).value.toList)
),
unusedCompileDependenciesFilter -= moduleFilter()
)
// unusedCompileDependenciesFilter -= moduleFilter()
)

def formatCrossScalaVersions(crossScalaVersions: List[String]): String = {
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/scala/org/http4s/jdkhttpclient/JdkHttpClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,28 @@ object JdkHttpClient {
* [[cats.effect.kernel.Async.executor executor]], sets the
* [[org.http4s.client.defaults.ConnectTimeout default http4s connect timeout]], and disables
* [[https://github.com/http4s/http4s-jdk-http-client/issues/200 TLS 1.3 on JDK 11]].
*
* On Java 21 and higher, prefer [[simpleResource]] as it actively closes the underlying client,
* releasing its resources early.
*/
def simple[F[_]](implicit F: Async[F]): F[Client[F]] =
defaultHttpClient[F].map(apply(_))

/** Like [[simple]], but wraps the client in a [[cats.effect.Resource Resource]] to ensure it's
* properly shut down on JVM 21 and higher. On lower Java versions, closing the resource does
* nothing (garbage collection will eventually clean up the client).
*/
def simpleResource[F[_]](implicit F: Async[F]): Resource[F, Client[F]] =
Copy link
Author

Choose a reason for hiding this comment

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

Name is up for discussion, I don't particular like simpleResource, but at least it's rather clear.

Copy link
Member

Choose a reason for hiding this comment

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

As already mentioned in #1011 (comment), we could just change the signature of simple and cut a 0.10 release; it should be rather simple for users to migrate.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should rename simple and simpleResource to something like unsafeDefault and default. We should encourage users to do the right thing (using the resource variant)

We can do that in a 0.10 release, or deprecate simple in this and create new new constructors.

Copy link
Author

Choose a reason for hiding this comment

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

@hamnis I can adjust the names, no problem, just a bit unsure about the unsafe prefix. On everything below Java 21, it's actually not unsafe in the sense CE usually uses this, but the only thing you can have (the Resource variant won't close anything there either).
So maybe a 0.10 with simple (returning a Resource, by re-using the name users get nudged towards this) and a def forEffect[F[_]: Async]: F[Client[F]] (or any better name) with docs explaining the caveats on Java 21+?

Copy link
Contributor

Choose a reason for hiding this comment

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

do we really need the forEffect constructor? Do we lose anything with just having the simple | default | insertyournamehere resource constructor ?

Copy link
Author

Choose a reason for hiding this comment

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

@hamnis It may cause some (unnecessary) trouble for users who now need to bubble up Resource usage in case they hadn't so far. But as allocated is safe on Java below 21, I changed the PR to only offer def simple[F[_]: Async]: Resource[F, …] and mentioned allocated in the docs, so people have a hint on a workaround. Hope that's a fair compromise!

Copy link
Author

Choose a reason for hiding this comment

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

Obviously, that means MiMa is unhappy and we need to release it as 0.10.

defaultHttpClientResource[F].map(apply(_))

private[jdkhttpclient] def defaultHttpClientResource[F[_]](implicit
F: Async[F]
): Resource[F, HttpClient] =
Resource.make[F, HttpClient](defaultHttpClient[F]) {
case c: AutoCloseable => Sync[F].blocking(c.close())
Copy link
Author

Choose a reason for hiding this comment

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

This is the most relevant bit, actively closing if HttpClient <: AutoCloseable which is the case on Java 21+.

case _ => Applicative[F].unit
}

private[jdkhttpclient] def defaultHttpClient[F[_]](implicit F: Async[F]): F[HttpClient] =
F.executor.flatMap { exec =>
F.delay {
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/scala/org/http4s/jdkhttpclient/JdkWSClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ object JdkWSClient {
})
} yield ()
}
// If the input side is still open (no close received from server), the JDK will not clean up the connection.
// This also implies the client can't be shutdown on Java 21+ as it waits for all open connections
// to be be closed. As we don't expect/handle anything coming on the input anymore
// at this point, we can safely abort.
_ <- F.delay(webSocket.abort())
Comment on lines +134 to +138
Copy link
Author

Choose a reason for hiding this comment

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

Without this, the test suite hangs when using simpleResource as the connections aren't closed. I'm no expert in web sockets, so this may have negative side effects. #853 tackles the same problem with a similar, but more elaborated approach, so it might be better to merge that one first.

Copy link
Author

Choose a reason for hiding this comment

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

Given that #853 isn't going anywhere soon, I think the fix in this PR is a good step for now. It avoids the test hang and improves the current situation without blocking even more elaborate solutions like #853.

} yield ()
}
.map { case (webSocket, queue, closedDef, sendSem) =>
Expand Down Expand Up @@ -164,7 +169,17 @@ object JdkWSClient {
* [[cats.effect.kernel.Async.executor executor]], sets the
* [[org.http4s.client.defaults.ConnectTimeout default http4s connect timeout]], and disables
* [[https://github.com/http4s/http4s-jdk-http-client/issues/200 TLS 1.3 on JDK 11]].
*
* * On Java 21 and higher, prefer [[simpleResource]] as it actively closes the underlying
* client, releasing its resources early.
*/
def simple[F[_]](implicit F: Async[F]): F[WSClient[F]] =
JdkHttpClient.defaultHttpClient[F].map(apply(_))

/** Like [[simple]], but wraps the client in a [[cats.effect.Resource Resource]] to ensure it's
* properly shut down on JVM 21 and higher. On lower Java versions, closing the resource does
* nothing (garbage collection will eventually clean up the client).
*/
def simpleResource[F[_]](implicit F: Async[F]): Resource[F, WSClient[F]] =
JdkHttpClient.defaultHttpClientResource[F].map(apply(_))
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import org.typelevel.ci._
import scala.concurrent.duration._

class JdkHttpClientSpec extends ClientRouteTestBattery("JdkHttpClient") {
def clientResource: Resource[IO, Client[IO]] = Resource.eval(JdkHttpClient.simple[IO])
def clientResource: Resource[IO, Client[IO]] = JdkHttpClient.simpleResource[IO]

// regression test for https://github.com/http4s/http4s-jdk-http-client/issues/395
test("Don't error with empty body and explicit Content-Length: 0") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import scala.concurrent.duration._
class JdkWSClientSpec extends CatsEffectSuite {

val webSocket: IOFixture[WSClient[IO]] =
ResourceSuiteLocalFixture("webSocket", Resource.eval(JdkWSClient.simple[IO]))
ResourceSuiteLocalFixture("webSocket", JdkWSClient.simpleResource[IO])
val echoServerUri: IOFixture[Uri] =
ResourceSuiteLocalFixture(
"echoServerUri",
Expand Down
Loading