Skip to content

Finalizers not executed when fiber canceled quickly after start #4488

@TomasMikula

Description

@TomasMikula

If a fiber is successfully started (via start or supervise), I'd naturally expect it's finalizers (guarantee, guaranteeCase) to be guaranteed to execute. That's not what happens.

By successfully started, I mean that the respective effect, start or supervise, completed successfully.

Reproduction

Cats Effect version: 3.6.3

Below are reproductions using both

  • start (see object StartAndCancel), and
  • Supervisor#supervise (see object SupervisorRelease)

to start fibers.

import cats.effect.std.Supervisor
import cats.effect.{ExitCode, IO, IOApp, Ref}

object StartAndCancel extends FiberFinalizationTest {

  override def startAndCancelTask(task: IO[Unit]): IO[Unit] =
    task
      .start
      .flatMap(_.cancel) // since `start` completed, `task` finalizers should run, but sometimes don't

}

object SupervisorRelease extends FiberFinalizationTest {

  override  def startAndCancelTask(task: IO[Unit]): IO[Unit] =
    Supervisor[IO]
      .use { supervisor =>
        supervisor.supervise(task)
      } // since `supervise` completed, `task` finalizers should run, but sometimes don't
      .void

}

abstract class FiberFinalizationTest extends IOApp {

  def startAndCancelTask(task: IO[Unit]): IO[Unit]

  override def run(args: List[String]): IO[ExitCode] =
    runTestN(100)
      .as(ExitCode.Success)

  private def runTestN(n: Int): IO[Unit] =
    runTest
      .parReplicateA(n)
      .flatMap { results =>
        val succeededCount = results.count(_ == true)
        val failedCount = results.count(_ == false)
        IO.println(s"Succeeded $succeededCount, failed $failedCount.")
      }

  private def runTest: IO[Boolean] =
    for {
      ref <- Ref[IO].of(0)
      _ <- startAndCancelTask(mkTask(ref))
      result <- ref.get
    } yield result == 3

  private def mkTask(ref: Ref[IO, Int]): IO[Unit] =
    IO
      .unit
      .guarantee(ref.update(_ + 1))
      .guaranteeCase(_ => ref.update(_ + 2))

}

Output

When running the above two programs, I get

StartAndCancel

Succeeded 0, failed 100.

I.e. all 100 runs failed to execute finalizers.

SupervisorRelease

Succeeded 61, failed 39.

I.e. 39 out of 100 runs failed to execute finalizers.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions