Skip to content

Process[IO] that spawns child processes might not respect cancellation #3693

@fredshonorio

Description

@fredshonorio

The jvm implementation of Process[IO] in linux might not respect cancellation if the main process does not itself kill the child processes.

A simple example, this should not respect the cancel and take +10 seconds to finish:

//> using scala "2.13.16"
//> using dep "co.fs2::fs2-io:3.12.2"
import cats.effect.{IO, IOApp}
import fs2.io.process.ProcessBuilder
import scala.concurrent.duration._
object Main extends IOApp.Simple {
  def run: IO[Unit] =
    ProcessBuilder("bash", List("-c", "sleep 10 & sleep 10")).spawn[IO].use { proc =>
      proc.stdout.merge(proc.stderr).compile.drain.start.flatTap(_ => IO.sleep(200.millis)).flatMap(_.cancel)
    }
}

This one will stop immediately:

//> using scala "2.13.16"
//> using dep "co.fs2::fs2-io:3.12.2"
import cats.effect.{IO, IOApp}
import fs2.io.process.ProcessBuilder
import scala.concurrent.duration._
object Main extends IOApp.Simple {
  def run: IO[Unit] =
    ProcessBuilder("sleep", List("10")).spawn[IO].use { proc =>
      proc.stdout.merge(proc.stderr).compile.drain.start.flatMap(_.cancel)
    }
}

⚠️ what follows includes LLM "facts", words are mine ⚠️

To fix the real case where this happened to me, I ended up using a jdk Process and calling destroyForcibly on all descendants. This apparently is subject to a race condition where processes could be spawned while the loop is happening.
It seems the correct approach (in linux) would be to use process groups, then fs2 would simply kill the whole group. This is apparently not supported by the jvm, so it would require jni.

At the very least I think this needs a mention in the docs, I will PR that soon unless someone proposes a solution.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions