Skip to content

Commit defd3ff

Browse files
Fix selective execution when multiple changes are made to one module under --watch (main branch) (#5033)
Forward port to `main` of #5032 If you have two modules and run `--watch {moduleA.fooCommand,moduleB.fooCommand}`, mill will run `moduleB.fooCommand` every 2nd change to `moduleA`, even if there is no changes to `moduleB`. The problem stems from the fact that when we save the changed inputs in selective execution, we did not store the inputs from `moduleB` and when selective execution runs again (the 2nd time), it thinks that *everything* in `moduleB` changed. This fix correctly saves the state of all modules when storing the selective execution state. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent c982215 commit defd3ff

File tree

5 files changed

+96
-50
lines changed

5 files changed

+96
-50
lines changed

core/define/src/mill/define/SelectiveExecution.scala

+19-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package mill.define
22

3-
import mill.api.{Result, Val, ExecResult}
3+
import mill.api.{ExecResult, Result, Val}
44
private[mill] trait SelectiveExecution {
5-
import SelectiveExecution._
5+
import SelectiveExecution.*
66
def computeHashCodeSignatures(
77
transitiveNamed: Seq[NamedTask[?]],
88
codeSignatures: Map[String, Int]
@@ -20,7 +20,10 @@ private[mill] trait SelectiveExecution {
2020
tasks: Seq[String]
2121
): Result[ChangedTasks]
2222

23-
def computeChangedTasks0(tasks: Seq[NamedTask[?]]): ChangedTasks
23+
def computeChangedTasks0(
24+
tasks: Seq[NamedTask[?]],
25+
computedMetadata: SelectiveExecution.Metadata.Computed
26+
): Option[ChangedTasks]
2427

2528
def resolve0(tasks: Seq[String]): Result[Array[String]]
2629

@@ -30,17 +33,27 @@ private[mill] trait SelectiveExecution {
3033

3134
def computeMetadata(
3235
tasks: Seq[NamedTask[?]]
33-
): (SelectiveExecution.Metadata, Map[Task[?], ExecResult[Val]])
36+
): SelectiveExecution.Metadata.Computed
3437
}
3538
object SelectiveExecution {
3639
case class Metadata(inputHashes: Map[String, Int], codeSignatures: Map[String, Int])
40+
object Metadata {
41+
case class Computed(
42+
metadata: Metadata,
43+
results: Map[Task[?], ExecResult[Val]]
44+
)
45+
}
3746

3847
implicit val rw: upickle.default.ReadWriter[Metadata] = upickle.default.macroRW
3948

4049
case class ChangedTasks(
4150
resolved: Seq[NamedTask[?]],
4251
changedRootTasks: Set[NamedTask[?]],
43-
downstreamTasks: Seq[NamedTask[?]],
44-
results: Map[Task[?], ExecResult[Val]]
52+
downstreamTasks: Seq[NamedTask[?]]
4553
)
54+
object ChangedTasks {
55+
56+
/** Indicates that all of the passed in tasks were changed. */
57+
def all(tasks: Seq[NamedTask[?]]): ChangedTasks = ChangedTasks(tasks, tasks.toSet, tasks)
58+
}
4659
}

core/eval/src/mill/eval/EvaluatorImpl.scala

+31-24
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package mill.eval
22

33
import mill.api.*
4-
import mill.api.internal.{ExecutionResultsApi, TestReporter, CompileProblemReporter}
5-
import mill.define.PathRef
4+
import mill.api.internal.{CompileProblemReporter, ExecutionResultsApi, TestReporter}
65
import mill.constants.OutFiles
7-
import mill.define.*
6+
import mill.constants.OutFiles.*
7+
import mill.define.{PathRef, *}
8+
import mill.define.internal.{ResolveChecker, TopoSorted, Watchable}
89
import mill.exec.{Execution, PlanImpl}
9-
import mill.define.internal.{ResolveChecker, Watchable}
10-
import OutFiles.*
11-
import mill.define.internal.TopoSorted
1210
import mill.resolve.Resolve
1311

1412
/**
@@ -144,21 +142,37 @@ final class EvaluatorImpl private[mill] (
144142
val selectiveExecutionEnabled = selectiveExecution && !targets.exists(_.isExclusiveCommand)
145143

146144
val selectedTasksOrErr =
147-
if (selectiveExecutionEnabled && os.exists(outPath / OutFiles.millSelectiveExecution)) {
145+
if (!selectiveExecutionEnabled) (targets, Map.empty, None)
146+
else {
148147
val (named, unnamed) =
149148
targets.partitionMap { case n: NamedTask[?] => Left(n); case t => Right(t) }
150-
val changedTasks = this.selective.computeChangedTasks0(named)
149+
val newComputedMetadata = SelectiveExecutionImpl.Metadata.compute(this, named)
151150

152-
val selectedSet = changedTasks.downstreamTasks.map(_.ctx.segments.render).toSet
151+
val selectiveExecutionStoredData = for {
152+
_ <- Option.when(os.exists(outPath / OutFiles.millSelectiveExecution))(())
153+
changedTasks <- this.selective.computeChangedTasks0(named, newComputedMetadata)
154+
} yield changedTasks
153155

154-
(
155-
unnamed ++ named.filter(t => t.isExclusiveCommand || selectedSet(t.ctx.segments.render)),
156-
changedTasks.results
157-
)
158-
} else (targets, Map.empty)
156+
selectiveExecutionStoredData match {
157+
case None =>
158+
// Ran when previous selective execution metadata is not available, which happens the first time you run
159+
// selective execution.
160+
(targets, Map.empty, Some(newComputedMetadata.metadata))
161+
case Some(changedTasks) =>
162+
val selectedSet = changedTasks.downstreamTasks.map(_.ctx.segments.render).toSet
163+
164+
(
165+
unnamed ++ named.filter(t =>
166+
t.isExclusiveCommand || selectedSet(t.ctx.segments.render)
167+
),
168+
newComputedMetadata.results,
169+
Some(newComputedMetadata.metadata)
170+
)
171+
}
172+
}
159173

160174
selectedTasksOrErr match {
161-
case (selectedTasks, selectiveResults) =>
175+
case (selectedTasks, selectiveResults, maybeNewMetadata) =>
162176
val evaluated: ExecutionResults =
163177
execution.executeTasks(
164178
selectedTasks,
@@ -199,15 +213,8 @@ final class EvaluatorImpl private[mill] (
199213
.flatten
200214
.toSeq
201215

202-
val allInputHashes = evaluated.transitiveResults
203-
.iterator
204-
.collect {
205-
case (t: InputImpl[_], ExecResult.Success(Val(value))) =>
206-
(t.ctx.segments.render, value.##)
207-
}
208-
.toMap
209-
210-
if (selectiveExecutionEnabled) {
216+
maybeNewMetadata.foreach { newMetadata =>
217+
val allInputHashes = newMetadata.inputHashes
211218
this.selective.saveMetadata(
212219
SelectiveExecution.Metadata(allInputHashes, codeSignatures)
213220
)

core/eval/src/mill/eval/SelectiveExecutionImpl.scala

+37-18
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package mill.eval
22

3-
import mill.api.{ExecResult, Result, Val}
43
import mill.api.internal.TestReporter
4+
import mill.api.{ExecResult, Result, Val}
55
import mill.constants.OutFiles
6-
import mill.define.{Evaluator, InputImpl, NamedTask, SelectMode, Task, SelectiveExecution}
76
import mill.define.SelectiveExecution.ChangedTasks
7+
import mill.define.*
88
import mill.exec.{CodeSigUtils, Execution, PlanImpl}
99
import mill.internal.SpanningForest
1010
import mill.internal.SpanningForest.breadthFirst
@@ -87,26 +87,42 @@ private[mill] class SelectiveExecutionImpl(evaluator: Evaluator)
8787
tasks,
8888
SelectMode.Separated,
8989
evaluator.allowPositionalCommandArgs
90-
).map(computeChangedTasks0(_))
90+
).map { tasks =>
91+
computeChangedTasks0(tasks, SelectiveExecutionImpl.Metadata.compute(evaluator, tasks))
92+
// If we did not have the metadata, presume everything was changed.
93+
.getOrElse(ChangedTasks.all(tasks))
94+
}
9195
}
9296

93-
def computeChangedTasks0(tasks: Seq[NamedTask[?]]): ChangedTasks = {
97+
/**
98+
* @return [[None]] when the metadata file is empty.
99+
* @note throws if the metadata file does not exist.
100+
*/
101+
def computeChangedTasks0(
102+
tasks: Seq[NamedTask[?]],
103+
computedMetadata: SelectiveExecution.Metadata.Computed
104+
): Option[ChangedTasks] = {
94105
val oldMetadataTxt = os.read(evaluator.outPath / OutFiles.millSelectiveExecution)
95106

96-
if (oldMetadataTxt == "") ChangedTasks(tasks, tasks.toSet, tasks, Map.empty)
97-
else {
107+
// We allow to clear the selective execution metadata to rerun all tasks.
108+
//
109+
// You would think that removing the file achieves the same result, however, blanking the file indicates that
110+
// this was intentional and you did not simply forgot to run `selective.prepare` beforehand.
111+
if (oldMetadataTxt == "") None
112+
else Some {
98113
val transitiveNamed = PlanImpl.transitiveNamed(tasks)
99114
val oldMetadata = upickle.default.read[SelectiveExecution.Metadata](oldMetadataTxt)
100-
val (newMetadata, results) =
101-
SelectiveExecutionImpl.Metadata.compute0(evaluator, transitiveNamed)
102115
val (changedRootTasks, downstreamTasks) =
103-
evaluator.selective.computeDownstream(transitiveNamed, oldMetadata, newMetadata)
116+
evaluator.selective.computeDownstream(
117+
transitiveNamed,
118+
oldMetadata,
119+
computedMetadata.metadata
120+
)
104121

105122
ChangedTasks(
106123
tasks,
107124
changedRootTasks.collect { case n: NamedTask[_] => n },
108-
downstreamTasks.collect { case n: NamedTask[_] => n },
109-
results
125+
downstreamTasks.collect { case n: NamedTask[_] => n }
110126
)
111127
}
112128
}
@@ -174,22 +190,22 @@ private[mill] class SelectiveExecutionImpl(evaluator: Evaluator)
174190

175191
def computeMetadata(
176192
tasks: Seq[NamedTask[?]]
177-
): (SelectiveExecution.Metadata, Map[Task[?], ExecResult[Val]]) =
193+
): SelectiveExecution.Metadata.Computed =
178194
SelectiveExecutionImpl.Metadata.compute(evaluator, tasks)
179195
}
180196
object SelectiveExecutionImpl {
181197
object Metadata {
182198
def compute(
183199
evaluator: Evaluator,
184200
tasks: Seq[NamedTask[?]]
185-
): (SelectiveExecution.Metadata, Map[Task[?], ExecResult[Val]]) = {
201+
): SelectiveExecution.Metadata.Computed = {
186202
compute0(evaluator, PlanImpl.transitiveNamed(tasks))
187203
}
188204

189205
def compute0(
190206
evaluator: Evaluator,
191207
transitiveNamed: Seq[NamedTask[?]]
192-
): (SelectiveExecution.Metadata, Map[Task[?], ExecResult[Val]]) = {
208+
): SelectiveExecution.Metadata.Computed = {
193209
val results: Map[NamedTask[?], mill.api.Result[Val]] = transitiveNamed
194210
.collect { case task: InputImpl[_] =>
195211
val ctx = new mill.define.TaskCtx.Impl(
@@ -212,10 +228,13 @@ object SelectiveExecutionImpl {
212228
val inputHashes = results.map {
213229
case (task, execResultVal) => (task.ctx.segments.render, execResultVal.get.value.hashCode)
214230
}
215-
new SelectiveExecution.Metadata(
216-
inputHashes,
217-
evaluator.codeSignatures
218-
) -> results.map { case (k, v) => (k, ExecResult.Success(v.get)) }
231+
SelectiveExecution.Metadata.Computed(
232+
new SelectiveExecution.Metadata(
233+
inputHashes,
234+
evaluator.codeSignatures
235+
),
236+
results.map { case (k, v) => (k, ExecResult.Success(v.get)) }
237+
)
219238
}
220239
}
221240

integration/invalidation/selective-execution/src/SelectiveExecutionTests.scala

+7
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,13 @@ object SelectiveExecutionWatchTests extends UtestIntegrationTestSuite {
223223
!output.contains("Computing fooCommand") && output.contains("Computing barCommand")
224224
}
225225

226+
// Test for a bug where modifying the sources 2nd time would run tasks from both modules.
227+
output0 = Nil
228+
modifyFile(workspacePath / "bar/bar.txt", _ + "!")
229+
eventually {
230+
!output.contains("Computing fooCommand") && output.contains("Computing barCommand")
231+
}
232+
226233
output0 = Nil
227234
modifyFile(workspacePath / "foo/foo.txt", _ + "!")
228235
eventually {

libs/main/src/mill/main/SelectiveExecutionModule.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ trait SelectiveExecutionModule extends mill.define.Module {
3939
if (tasks.isEmpty) Seq("__") else tasks,
4040
SelectMode.Multi
4141
).map { resolvedTasks =>
42-
val (metadata, _) = evaluator.selective.computeMetadata(resolvedTasks)
42+
val computed = evaluator.selective.computeMetadata(resolvedTasks)
4343

44-
evaluator.selective.saveMetadata(metadata)
44+
evaluator.selective.saveMetadata(computed.metadata)
4545
}
4646
}
4747

0 commit comments

Comments
 (0)