question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Response stream is not interrupted after mapping the effect type

See original GitHub issue

I am having troubles interrupting a stream which is sent in a response using http4s. It happens only if I try to convert the effect type of the routes:

All imports are omitted to save the space. I am sorry for very fat code samples, but it’s the best way I could narrow it down.

Here is my routes definition:

class TestController[F[_] : Concurrent : Timer](deferred: Deferred[F, Unit]) extends Http4sDsl[F] {

  private implicit val uuidEncoder: EntityEncoder[F, UUID] =
    EntityEncoder
        .stringEncoder[F]
        .contramap(_.toString + "\n")

  val routes: HttpRoutes[F] = HttpRoutes.of[F] {
    case GET -> Root / "stream" =>
      val stream = Stream
        .repeatEval(Concurrent[F].delay(UUID.randomUUID()).flatTap(_ => Timer[F].sleep(2.seconds)))
        .interruptWhen(deferred.get.attempt)
      Ok(stream)
    case POST -> Root / "complete" =>
      deferred.complete(()).flatMap(_ => Ok("stream is cancelled"))
  }

}

Here is how I instantiate those routes twice with multiple effects (IO[_] and EitherT[IO, String, _]) and then merge them into one route with final effect (IO[_]) and two prefixes:

object Routes {

  def createRoutes(
      deferred1: Deferred[IO, Unit],
      deferred2: Deferred[IO, Unit]
  )(implicit c: Concurrent[IO], timer: Timer[IO]): HttpRoutes[IO] = {

    type SpecialEff[T] = EitherT[IO, String, T]

    val ioToSpecial: IO ~> SpecialEff = EitherT.liftK
    val specialToIO: SpecialEff ~> IO =
      new ~>[SpecialEff, IO] {
        override def apply[A](fa: SpecialEff[A]): IO[A] = fa.value.flatMap {
          case Left(_)    => IO.raiseError(new RuntimeException("should never happen"))
          case Right(res) => IO.pure(res)
        }
      }

    val dsl = Http4sDsl[IO]
    import dsl._

    val ioTestRoutes: HttpRoutes[IO] = new TestController[IO](deferred1).routes
    val specialTestRoutes: HttpRoutes[SpecialEff] =
      new TestController[SpecialEff](deferred2.mapK(ioToSpecial)).routes

    val convertedSpecialRoutes: HttpRoutes[IO] = Kleisli { requestIO =>
      val requestSpecial =
        Request[SpecialEff]( // no mapK cause it uses Stream#translate for the body (which makes stream uninterruptible)
          method = requestIO.method,
          uri = requestIO.uri,
          httpVersion = requestIO.httpVersion,
          headers = requestIO.headers,
          body = requestIO.body.translateInterruptible(ioToSpecial),
          attributes = requestIO.attributes
        )

      val specialResult: OptionT[SpecialEff, Response[SpecialEff]] =
        specialTestRoutes(requestSpecial)

      val ioResult: IO[Option[Response[IO]]] = specialResult.value
          .map(_.map { specialResponse =>
            Response[IO]( // no mapK cause it uses Stream#translate for the body (which makes stream uninterruptible)
              status = specialResponse.status,
              httpVersion = specialResponse.httpVersion,
              headers = specialResponse.headers,
              body = specialResponse.body.translateInterruptible(specialToIO)
            )
          })
          .value
          .flatMap {
            case Right(maybeResp) =>
              IO.pure(maybeResp)
            case Left(error) =>
              BadRequest(error).map(Some(_))
          }

      OptionT(ioResult)
    }

    Router[IO]("/io" -> ioTestRoutes, "/special" -> convertedSpecialRoutes)
  }
}

I don’t know why, but /io/complete requests interrupt the stream opened with /io/stream just fine, but /special/complete ones do not interrupt the stream opened with /special/stream. Even though in both cases the corresponding Deferred instance is set successfully. I suspect that somehow I made the response body stream uninterruptible while mapping SpecialEff to IO or vice versa.

In case if that is an expected behaviour, could you please point me in the right direction to fix this? I am not expert in fs2 streams, but I don’t see any errors in the code.

Also, here is a sample project with this. It’s just three scala files, two of which are above: https://github.com/maximskripnik/stream-cancel-issue

Thanks in advance!

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:3
  • Comments:9 (6 by maintainers)

github_iconTop GitHub Comments

2reactions
mpilquistcommented, Nov 24, 2020

@maximskripnik Cool, I released fs2 2.4.6 with that fix.

2reactions
rossabakercommented, Nov 19, 2020

I asked our resident fs2 experts on Gitter to see if they can spot it, and once we find the answer, it might be an interesting problem to wind it back into http4s. This one has my interest, too. 😄

Read more comments on GitHub >

github_iconTop Results From Across the Web

Compose stream of effects for http response - Stack Overflow
I'm not familiar with fs2 but I would expect a mapAsync method that you can use like throttling.mapAsync(times.get(city.toUpperCase)) . It's ...
Read more >
RFC 2326: Real Time Streaming Protocol (RTSP) - IETF
Any response message which MUST NOT include a message body (such as the 1xx, 204, and 304 responses) is always terminated by the...
Read more >
RFC 8216: HTTP Live Streaming
The Media Initialization Section can be specified by an EXT-X-MAP tag (Section 4.3.2.5). The Media Initialization Section MUST NOT contain sample data.
Read more >
MediaPlayer - Android Developers
Calling start() has no effect on a MediaPlayer object that is already in the ... After the app has received the key request...
Read more >
Operating Systems: I/O Systems
A daisy-chain bus, ( not shown) is when a string of devices is connected ... The CPU has an interrupt-request line that is...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found