build_manager: terminate processes if cancelling does not work;
authorFabian Huch <huch@in.tum.de>
Tue, 06 Aug 2024 15:00:37 +0200
changeset 80645 a1dce0cc6c26
parent 80644 6a996ad11af2
child 80646 b4e116523cb6
build_manager: terminate processes if cancelling does not work;
etc/options
src/Pure/Build/build_manager.scala
--- a/etc/options	Tue Aug 06 13:54:10 2024 +0200
+++ b/etc/options	Tue Aug 06 15:00:37 2024 +0200
@@ -252,6 +252,9 @@
 option build_manager_timeout : real = 28800
   -- "timeout for user-submitted tasks (seconds > 0)"
 
+option build_manager_cancel_timeout : real = 180.0
+  -- "timeout for graceful cancelling (seconds > 0)"
+
 option build_manager_delay : real = 1.0
   -- "delay build manager loop"
 
--- a/src/Pure/Build/build_manager.scala	Tue Aug 06 13:54:10 2024 +0200
+++ b/src/Pure/Build/build_manager.scala	Tue Aug 06 15:00:37 2024 +0200
@@ -753,13 +753,22 @@
 
   object Runner {
     object State {
-      def empty: State = new State(Map.empty, Map.empty)
+      def init(options: Options): State =
+        new State(Map.empty, Map.empty, Map.empty, options.seconds("build_manager_cancel_timeout"))
     }
 
     class State private(
       process_futures: Map[String, Future[Build_Process]],
-      result_futures: Map[String, Future[Process_Result]]
+      result_futures: Map[String, Future[Process_Result]],
+      cancelling_until: Map[String, Time],
+      cancel_timeout: Time
     ) {
+      private def copy(
+        process_futures: Map[String, Future[Build_Process]] = process_futures,
+        result_futures: Map[String, Future[Process_Result]] = result_futures,
+        cancelling_until: Map[String, Time] = cancelling_until,
+      ): State = new State(process_futures, result_futures, cancelling_until, cancel_timeout)
+
       def is_empty = process_futures.isEmpty && result_futures.isEmpty
 
       def init(context: Context): State = {
@@ -770,13 +779,21 @@
               case Exn.Res(process) => process.run()
               case Exn.Exn(exn) => Process_Result(Process_Result.RC.interrupt).error(exn.getMessage)
             })
-        new State(
+
+        copy(
           process_futures + (context.name -> process_future),
           result_futures + (context.name -> result_future))
       }
 
       def running: List[String] = process_futures.keys.toList
 
+      private def do_terminate(name: String): Boolean =
+        if (cancelling_until(name) <= Time.now()) true
+        else {
+          process_futures(name).join.terminate()
+          false
+        }
+
       def update: (State, Map[String, Process_Result]) = {
         val finished =
           for ((name, future) <- result_futures if future.is_finished)
@@ -786,21 +803,32 @@
               case Exn.Exn(exn) => Process_Result(Process_Result.RC.interrupt).error(exn.getMessage)
             })
 
-        val process_futures1 = process_futures.filterNot((name, _) => finished.contains(name))
-        val result_futures1 = result_futures.filterNot((name, _) => finished.contains(name))
-
-        (new State(process_futures1, result_futures1), finished)
+        val state1 =
+          copy(
+            process_futures.filterNot((name, _) => finished.contains(name)),
+            result_futures.filterNot((name, _) => finished.contains(name)),
+            cancelling_until.filterNot((name, _) => finished.contains(name) && !do_terminate(name)))
+        (state1, finished)
       }
 
-      def cancel(cancelled: List[String]): State = {
-        for (name <- cancelled) {
-          val process_future = process_futures(name)
-          if (process_future.is_finished) process_future.join.cancel()
-          else process_future.cancel()
+      private def do_cancel(name: String): Boolean = 
+        process_futures.get(name) match {
+          case Some(process_future) =>
+            if (process_future.is_finished) {
+              process_future.join.cancel()
+              true
+            } else {
+              process_future.cancel()
+              false
+            }
+          case None => false
         }
 
-        new State(process_futures.filterNot((name, _) => cancelled.contains(name)), result_futures)
-      }
+      def cancel(cancelled: List[String]): State =
+        copy(
+          process_futures.filterNot((name, _) => cancelled.contains(name)),
+          cancelling_until = cancelling_until ++
+            cancelled.filter(do_cancel).map(_ -> (Time.now() + cancel_timeout)))
     }
   }
 
@@ -966,7 +994,7 @@
 
     override def stopped(state: Runner.State): Boolean = progress.stopped && state.is_empty
 
-    def init: Runner.State = Runner.State.empty
+    def init: Runner.State = Runner.State.init(store.options)
     def loop_body(state: Runner.State): Runner.State = {
       val state1 =
         if (progress.stopped) state
@@ -1472,6 +1500,7 @@
       catch { case exn: Throwable => close(); throw exn }
 
     def cancel(): Unit = Option(_process).foreach(_.interrupt())
+    def terminate(): Unit = Option(_process).foreach(_.terminate())
 
     def close(): Unit = {
       Option(_dir).foreach(ssh.rm_tree)