@@ -36,6 +36,7 @@ import java.util.concurrent.TimeUnit
3636import scala .concurrent .duration ._
3737
3838final class CompletableFutureTerminationTest extends CatsEffectSuite {
39+
3940 import CompletableFutureTerminationTest ._
4041
4142 private val duration : FiniteDuration =
@@ -65,81 +66,105 @@ final class CompletableFutureTerminationTest extends CatsEffectSuite {
6566 //
6667 // See: https://docs.oracle.com/en/java/javase/14/docs/api/java.net.http/java/net/http/HttpResponse.BodySubscriber.html
6768 test(" Terminating an effect generated from a CompletableFuture" ) {
68- (Semaphore [IO ](1L ), Deferred [IO , Observation [HttpResponse [String ]]], Semaphore [IO ](1L )).tupled
69- .flatMap { case (stallServer, observation, gotRequest) =>
70- // Acquire the `stallServer` semaphore so that the server will not
71- // return _any_ bytes until we release a permit.
72- stallServer.acquire *>
73- // Acquire the `gotRequest` semaphore. The server will release this
74- // once it gets our Request. We wait until this happens to start our
75- // timeout logic.
76- gotRequest.acquire *>
77- // Start a Http4s Server, it will be terminated at the conclusion of
78- // this test.
79- stallingServerR[IO ](stallServer, gotRequest).use { (server : Server ) =>
80- // Call the server, using the JDK client. We call directly with
81- // the JDK client because we need to have low level control over
82- // the result to observe whether or not the
83- // java.util.concurrent.CompletableFuture is still executing (and
84- // holding on to resources).
85- callServer[IO ](server).flatMap((cf : CompletableFuture [HttpResponse [String ]]) =>
86- // Attach a handler onto the result. This will populate our
87- // `observation` Deferred value when the CompletableFuture
88- // finishes for any reason.
89- //
90- // We start executing this in the background, so that we
91- // asynchronously populate our Observation.
92- observeCompletableFuture(observation, cf).start.flatMap(fiber =>
93- // Wait until we are sure the Http4s Server has received the
94- // request.
95- gotRequest.acquire *>
96- // Lift the CompletableFuture to a IO value and attach a
97- // (short) timeout to the termination.
98- //
99- // Important! The IO result _must_ be terminated via the
100- // timeout _before any bytes_ have been received by the JDK
101- // HttpClient in order to validate resource safety. Once we
102- // start getting bytes back, the CompletableFuture _is
103- // complete_ and we are in a different context.
104- //
105- // Notice that we release stallServer _after_ the
106- // timeout. _This is the crux of this entire test_. Once
107- // we release `stallServer`, the Http4s Server will
108- // attempt to send back an Http Response to our JDK
109- // client. If the CompletableFuture and associated
110- // resources were properly cleaned up after the
111- // timeoutTo terminated the running effect, then the JDK
112- // client connection will either be closed, or the
113- // attempt to invoke `complete` on the
114- // `CompletableFuture` will fail, in both cases
115- // releasing any resources being held. If not, then it
116- // will still receive bytes, meaning there is a resource
117- // leak.
118- IO .fromCompletableFuture(IO (cf))
119- .void
120- .timeoutTo(duration, stallServer.release) *>
121- // After the timeout has triggered, wait for the observation to complete.
122- fiber.join *>
123- // Check our observation. Whether or not there is an exception
124- // is not actually relevant to the success case. What _is_
125- // important is that there is no result. If there is a result,
126- // then that means that _after_ `timeoutTo` released
127- // `stallServer` the CompletableFuture for the Http response
128- // body still processed data, which indicates a resource leak.
129- observation.get.flatMap {
130- case Observation (None , _) => IO .pure(true )
131- case otherwise =>
132- IO .raiseError(new AssertionError (s " Expected no result, got $otherwise" ))
69+ assume(
70+ JdkVersion .supportsCancellation,
71+ " This test checks cancellation behavior, which was only introduced in JDK 16."
72+ )
73+
74+ JdkHttpClient .defaultHttpClientResource[IO ].use { client =>
75+ (Semaphore [IO ](1L ), Deferred [IO , Observation [HttpResponse [String ]]], Semaphore [IO ](1L )).tupled
76+ .flatMap { case (stallServer, observation, gotRequest) =>
77+ // Acquire the `stallServer` semaphore so that the server will not
78+ // return _any_ bytes until we release a permit.
79+ stallServer.acquire *>
80+ // Acquire the `gotRequest` semaphore. The server will release this
81+ // once it gets our Request. We wait until this happens to start our
82+ // timeout logic.
83+ gotRequest.acquire *>
84+ // Start a Http4s Server, it will be terminated at the conclusion of
85+ // this test.
86+ Async [IO ].bracket(stallingServerR[IO ](stallServer, gotRequest).allocated) {
87+ case (server : Server , _) =>
88+ // Call the server, using the JDK client. We call directly with
89+ // the JDK client because we need to have low level control over
90+ // the result to observe whether or not the
91+ // java.util.concurrent.CompletableFuture is still executing (and
92+ // holding on to resources).
93+ callServer[IO ](client, server).flatMap(
94+ (cf : CompletableFuture [HttpResponse [String ]]) =>
95+ // Attach a handler onto the result. This will populate our
96+ // `observation` Deferred value when the CompletableFuture
97+ // finishes for any reason.
98+ //
99+ // We start executing this in the background, so that we
100+ // asynchronously populate our Observation.
101+ observeCompletableFuture(observation, cf).start.flatMap(fiber =>
102+ // Wait until we are sure the Http4s Server has received the
103+ // request.
104+ gotRequest.acquire *>
105+ // Lift the CompletableFuture to a IO value and attach a
106+ // (short) timeout to the termination.
107+ //
108+ // Important! The IO result _must_ be terminated via the
109+ // timeout _before any bytes_ have been received by the JDK
110+ // HttpClient in order to validate resource safety. Once we
111+ // start getting bytes back, the CompletableFuture _is
112+ // complete_ and we are in a different context.
113+ //
114+ // Notice that we release stallServer _after_ the
115+ // timeout. _This is the crux of this entire test_. Once
116+ // we release `stallServer`, the Http4s Server will
117+ // attempt to send back an Http Response to our JDK
118+ // client. If the CompletableFuture and associated
119+ // resources were properly cleaned up after the
120+ // timeoutTo terminated the running effect, then the JDK
121+ // client connection will either be closed, or the
122+ // attempt to invoke `complete` on the
123+ // `CompletableFuture` will fail, in both cases
124+ // releasing any resources being held. If not, then it
125+ // will still receive bytes, meaning there is a resource
126+ // leak.
127+ IO .fromCompletableFuture(IO (cf))
128+ .void
129+ .timeoutTo(duration, stallServer.release) *>
130+ // After the timeout has triggered, wait for the observation to complete.
131+ fiber.join *>
132+ // Check our observation. Whether or not there is an exception
133+ // is not actually relevant to the success case. What _is_
134+ // important is that there is no result. If there is a result,
135+ // then that means that _after_ `timeoutTo` released
136+ // `stallServer` the CompletableFuture for the Http response
137+ // body still processed data, which indicates a resource leak.
138+ observation.get.flatMap {
139+ case Observation (None , _) => IO .pure(true )
140+ case otherwise =>
141+ IO .raiseError(new AssertionError (s " Expected no result, got $otherwise" ))
142+ }
143+ )
144+ )
145+ } { case (_, release) =>
146+ release.timed
147+ .flatMap { case (duration, _) =>
148+ IO {
149+ assert(
150+ clue(duration) < serverTimeout,
151+ " Finalization didn't complete until server shutdown timeout was reached, a connection is likely leaked by the client"
152+ )
133153 }
134- )
135- )
136- }
137- }
154+ }
155+
156+ }
157+ }
158+ }
138159 }
139160}
140161
141162object CompletableFutureTerminationTest {
142163
164+ val a : Boolean = () < 5 .seconds
165+
166+ private val serverTimeout = 5 .seconds
167+
143168 /** ADT to contain the result of an invocation to
144169 * [[java.util.concurrent.CompletionStage#handleAsync ]]
145170 *
@@ -179,14 +204,12 @@ object CompletableFutureTerminationTest {
179204 EmberServerBuilder
180205 .default[F ]
181206 .withHttpApp(
182- Kleisli (
183- Function .const(
184- gotRequest.release *>
185- semaphore.permit.use(_ => F .pure(Response [F ]()))
186- )
207+ Kleisli .liftF(
208+ gotRequest.release *>
209+ semaphore.permit.use(_ => F .pure(Response [F ]()))
187210 )
188211 )
189- .withShutdownTimeout(1 .second )
212+ .withShutdownTimeout(serverTimeout )
190213 .withPort(port " 0 " )
191214 .build
192215
@@ -218,11 +241,11 @@ object CompletableFutureTerminationTest {
218241 * in a [[java.util.concurrent.CompletableFuture ]].
219242 */
220243 private def callServer [F [_]](
244+ client : HttpClient ,
221245 server : Server
222246 )(implicit F : Sync [F ]): F [CompletableFuture [HttpResponse [String ]]] =
223247 for {
224248 jURI <- F .catchNonFatal(new URI (server.baseUri.renderString))
225- client <- F .delay(HttpClient .newHttpClient)
226249 result <- F .delay(
227250 client.sendAsync(HttpRequest .newBuilder(jURI).build(), HttpResponse .BodyHandlers .ofString)
228251 )
0 commit comments