@@ -295,6 +295,11 @@ case class Execution(
295295 ec.fold(ExecutionContexts .RunNow )(new ExecutionContexts .ThreadPool (_))
296296 implicit val taskExecutionContext =
297297 if (exclusive) ExecutionContexts .RunNow else forkExecutionContext
298+ // We walk the task graph in topological order and schedule the futures
299+ // to run asynchronously. During this walk, we store the scheduled futures
300+ // in a dictionary. When scheduling each future, we are guaranteed that the
301+ // necessary upstream futures will have already been scheduled and stored,
302+ // due to the topological order of traversal.
298303 for (terminal <- terminals) {
299304 val deps = interGroupDeps(terminal)
300305
@@ -376,9 +381,11 @@ case class Execution(
376381
377382 val newFailures = res.newResults.values.count(r => r.asFailing.isDefined)
378383
384+ // Count new failures: tasks with upstream failures should be skipped, not failed.
379385 rootFailedCount.addAndGet(newFailures)
380386 completedCount.incrementAndGet()
381387
388+ // Always show the completed count in the header after a task finishes.
382389 logger.prompt.setPromptHeaderPrefix(formatHeaderPrefix())
383390
384391 if (failFast && res.newResults.values.exists(_.asSuccess.isEmpty))
@@ -404,9 +411,14 @@ case class Execution(
404411 }
405412 }
406413 } catch {
407- case e : mill.api.daemon.StopWithResponse [? ] => throw e
414+ case e : mill.api.daemon.StopWithResponse [? ] =>
415+ // Let StopWithResponse propagate: it is a controlled shutdown signal.
416+ throw e
408417 case e : Throwable if ! mill.api.daemon.internal.NonFatal (e) =>
418+ // Wrap fatal errors so Scala's Future machinery reports them instead of
419+ // silently terminating the future and leaving downstream Awaits hanging.
409420 val nonFatal = new Exception (s " fatal exception occurred: $e" , e)
421+ // Preserve the original stack trace, since that points at the real failure.
410422 nonFatal.setStackTrace(e.getStackTrace)
411423 throw nonFatal
412424 } finally {
@@ -416,9 +428,13 @@ case class Execution(
416428 }
417429 }
418430
431+ // Make sure we wait for all tasks from this batch to finish before starting the next
432+ // one, so we don't mix up exclusive and non-exclusive tasks running at the same time.
419433 terminals.map(t => (t, Await .result(futures(t), duration.Duration .Inf )))
420434 }
421435
436+ // Run all non-command tasks according to the configured thread count,
437+ // but run exclusive commands in linear order.
422438 val (nonExclusiveTasks, leafExclusiveCommands) = indexToTerminal.partition {
423439 case t : Task .Named [_] => ! downstreamOfExclusive.contains(t)
424440 case _ => ! serialCommandExec
@@ -431,13 +447,17 @@ case class Execution(
431447 val isOutermostExecution = executionNestingDepth.get() == 1
432448 val hasFailures = rootFailedCount.get() > 0
433449 val showFinalStatus = isOutermostExecution && (hasFailures || isFinalDepth)
450+ // Set final header showing SUCCESS/FAILED status:
451+ // - FAILED: show for any outermost execution with failures
452+ // - SUCCESS: only show for the final requested depth
434453 logger.prompt.setPromptHeaderPrefix(formatHeaderPrefix(completed = showFinalStatus))
435454
436455 logger.prompt.clearPromptStatuses()
437456
438457 val finishedOptsMap = (nonExclusiveResults ++ exclusiveResults).toMap
439458
440459 val taskInvalidationReasons = {
460+ // Convert versionMismatchReasons to Map[String, String] for invalidation-tree logging.
441461 import scala .jdk .CollectionConverters .ConcurrentMapHasAsScala
442462 versionMismatchReasons.asScala.collect {
443463 case (t : Task .Named [? ], reason) => t.ctx.segments.render -> reason
0 commit comments