engine: better handling for image builds in multiple manifests. Fixes https://github.com/windmilleng/tilt/issues/2992#3362
Conversation
|
I think this isn't too complex a solution, and sets us up pretty well to make things better incrementally. The most important bit of the PR is this comment: |
|
had a good convo with Maia offlist about this; I'm going to write some graph accessors to make it more clear what all the nested loops are doing |
1ba3c54 to
473896d
Compare
|
ok, i moved all the for loops in handleBuildResults into graph traversal accessors. ptal? |
|
hoo boy this is a big PR! I'm still going through it, but while I do, some tests for these cases could be good:
|
maiamcc
left a comment
There was a problem hiding this comment.
Broad feedback:
-
the second loop in
handleBuildResults, where we're RE-dirtying anything that depends on something that we just recycled, seems superfluous -- what if when we recycled a result we DIDN'T scrub its file changes, and instead in the build controller said "oh, this result is recycled and no files have changed since it was marked recycled, so it doesn't need to be built" and it were a no-op in the builder that way? (would have to store "recycledAt" as a timestamp instead of a boolean.) -
@landism and I are both having a lot of trouble wrapping our heads around this. If you don't take the above suggestion, (i.e. if recycling at build time and recycling on build complete remain distinct), consider breaking it into two PRs for each of those parts?
-
what I was trying to articulate in slack is that from the naming, I expect
targetQueue.ShouldRecycleto check theRecycledflag we set on the result when handling build results. I.e. I understand "recycled" to mean "we took this result from a different manifest's build", which is distinct from "nothing that directly affects this target has changed" (both of which fall under the umbrella of "we don't need to build this target right now"). I can see how this distinction will be less useful in the future world. It's only questionably relevant now but at minimum the transition from the old world is confusing. -
GH won't let me comment on this cuz it's not a line you changed but in
targetQueue.RunBuilds:func (q *TargetQueue) RunBuilds(handler BuildHandler) error { for _, target := range q.sortedTargets { id := target.ID() if q.depsNeedBuild[id] { // If the dependencies are dirty, we can't use any state from the previous build. result, err := handler(target, store.BuildState{}, q.dependencyResults(target)) if err != nil { return err } q.results[id] = result } else if q.needsOwnBuild[id] { // If only files are dirty, we can try to do an incremental build. result, err := handler(target, q.state[id], q.dependencyResults(target)) if err != nil { return err } ...I think these two cases are not functionally different anymore: i can't find anything in the build handler for either ibad or dcbad that actually does anything with
state. could you merge these two cases / remove (or leave a note to remove) thestatearg from the handler if we're not using it? would make this code easier to read and reason about.
| ms.MutableBuildStatus(id).LastResult = result | ||
| } | ||
|
|
||
| // Remove pending file changes that were consumed by this build. |
There was a problem hiding this comment.
this comment is no longer accurate
| // across multiple manifests. | ||
| // | ||
| // If an image target succeeds, we want to record that success | ||
| // in all manifests that depend on that image. |
There was a problem hiding this comment.
this isn't all this function does -- this comment should probably go somewhere below, and possibly add a higher-level docstring here that explains the purpose of the function as a whole
| brManifestName model.ManifestName, br model.BuildRecord, results store.BuildResultSet) { | ||
| isBuildSuccess := br.Error == nil | ||
|
|
||
| for _, mt := range engineState.Targets() { |
There was a problem hiding this comment.
fwiw, this code is gnarly enough that i'd appreciate comments for each paragraph
| } else { | ||
| } else if q.shouldRecycle(id) { | ||
| // Otherwise, we can re-use results from the previous build. | ||
| // If needsOwnBuild is false, then LastSuccessfulResult must exist if it's empty. |
| q.results[id] = lastResult | ||
| q.results[id] = lastResult.Recycle() | ||
| } else { | ||
| return fmt.Errorf("Internal error: build marked both clean and not recyclable") |
There was a problem hiding this comment.
| return fmt.Errorf("Internal error: build marked both clean and not recyclable") | |
| return fmt.Errorf("Internal error: target marked both clean and not recyclable") |
|
cool, let me send out two independent prs:
|
|
FYI here are notes i took inline while going through this PR with Matt -- some suggestions for how to clarify comments, and some questions/comments about implementation. If that's a helpful format, feel free to use, otherwise I can submit them all as PR comments when this is ready for re-review. |
…ilt, and not return results for targets they didn't build. This should make #3362 much simpler and easy to reason about. Also adds test harnesses for different build graph configurations.
|
ok, i sent out
after those PRs go in, the whole concept of recycling will magically go away, since it's really just working around ImageBuildAndDeployers return results for things they didn't build |
…ilt, and not return results for targets they didn't build. This should make #3362 much simpler and easy to reason about. Also adds test harnesses for different build graph configurations.
…ilt, and not return results for targets they didn't build. This should make #3362 much simpler and easy to reason about. Also adds test harnesses for different build graph configurations.
…ilt, and not return results for targets they didn't build. This should make #3362 much simpler and easy to reason about. Also adds test harnesses for different build graph configurations.
…ilt, and not return results for targets they didn't build. This should make #3362 much simpler and easy to reason about. Also adds test harnesses for different build graph configurations.
…ilt, and not return results for targets they didn't build. This should make #3362 much simpler and easy to reason about. Also adds test harnesses for different build graph configurations.
…ilt, and not return results for targets they didn't build. This should make #3362 much simpler and easy to reason about. Also adds test harnesses for different build graph configurations.
eed6c70 to
0ef22a0
Compare
nicks
left a comment
There was a problem hiding this comment.
OK, I added a more tests, and removed the notion of recycling.
All the other comments should be addressed or moot
|
Is this waiting for review or changes? |
|
waiting on review! haven't been pushing on it too hard since there's a lot of other things in the air |
maiamcc
left a comment
There was a problem hiding this comment.
cool, looks good behavior-wise. Most of my comments are cosmetic things about position/wording of comments, variable naming etc.--I think they'll help comprehensibility but ofc feel free to take or leave them.
I'd also suggest doing a search thru the code for "recycle"/"recycling" since I think it still appears in a few places but we've moved away from that terminology
| return | ||
| } | ||
|
|
||
| _, isImageResult := result.(store.ImageBuildResult) |
There was a problem hiding this comment.
hmm, would we ever hit this point in the code with something that WASN'T an image result? like, can there exist two k8s targets of the same name across two different manifests?
There was a problem hiding this comment.
live update results aren't reusable, added a comment about this
| // build but its dependency was built. | ||
| engineState.ForEachMutableBuildStatusDependingOn(results, func(id, reverseDepID model.TargetID, mn model.ManifestName, reverseDepStatus *store.BuildStatus) { | ||
| if mn == mt.Manifest.Name { | ||
| // We're only worried about manifests that we didn't build. |
There was a problem hiding this comment.
| // We're only worried about manifests that we didn't build. | |
| // We're only worried about manifests that we didn't just build. |
| // We also have manifestB, which contains imageB depending on imageCommon. | ||
| // | ||
| // We need to mark imageB as dirty, because it was not built in the manifestA | ||
| // build but its dependency was built. |
There was a problem hiding this comment.
(idk if this comment is quite right but i DO think it's useful to mention the deploy-targets-depending-on-image-targets case--when this comment only talks about inter-image dependencies, it seems kinda special-case-y, but really this bit of code is much more generic than that!)
| // build but its dependency was built. | |
| // build but its dependency was built. | |
| // Note that this logic also applies to deploy targets depending on image targets. If we built manifestA, which contains imageX and deploy target k8sA, and manifestB contains imageX and deploy target k8sB, we need to mark target k8sB as dirty so that Tilt actually deploys the changes to imageX. |
|
|
||
| _, ok := results[reverseDepID] | ||
| if ok { | ||
| // The reverseDep is fresh! Skip it. |
There was a problem hiding this comment.
| // The reverseDep is fresh! Skip it. | |
| // The reverseDep is fresh (i.e. was just built)! Skip it. |
| return | ||
| } | ||
|
|
||
| _, depIsNew := results[id] |
There was a problem hiding this comment.
doesn't the iterator guarantee that we'll only ever get id if it's in results, making this check redundant?
There was a problem hiding this comment.
yep! this is vestigial
| } | ||
|
|
||
| func updateBuildStatus(status *store.BuildStatus, br model.BuildRecord, result store.BuildResult, isBuildSuccess bool) { | ||
| // Record the last result |
There was a problem hiding this comment.
| // Record the last result | |
| // Record the result |
(otherwise it seems like you're talking about the result BEFORE the one you're currently processing)
| // | ||
| // This has a minor bug: if one of the images in a set failed, but another one | ||
| // succeeded, this doesn't mark any of them as successful. We should fix this, | ||
| // though personally, I think LastSuccessfulResult has problematic semantics |
There was a problem hiding this comment.
| // though personally, I think LastSuccessfulResult has problematic semantics | |
| // though personally, I (Nick) think LastSuccessfulResult has problematic semantics |
| } | ||
|
|
||
| // Update build statuses for duplicated image targets in other manifests. | ||
| // This ensures that those images targets aren't rebuilt. |
There was a problem hiding this comment.
| // This ensures that those images targets aren't rebuilt. | |
| // This ensures that those images targets aren't redundantly rebuilt. |
| BuildReasonFlagTriggerCLI: "CLI Trigger", | ||
| BuildReasonFlagTriggerUnknown: "Unknown Trigger", | ||
| BuildReasonFlagTiltfileArgs: "Tilt Args", | ||
| BuildReasonFlagChangedDep: "New Image", |
There was a problem hiding this comment.
I'm not sure I as a user would understand what this meant... why not just "dependency updated" or something?
There was a problem hiding this comment.
i changed it to "image updated", since we don't really use the "dependency" language in other user-facing parts of tilt
| call = f.nextCall("m1 build2") | ||
| assert.Equal(t, m1.K8sTarget(), call.k8s()) | ||
|
|
||
| imageStateM1I0 := call.state[m1.ImageTargets[0].ID()] |
There was a problem hiding this comment.
this test seems of a piece with the tests above in buildcontroller_test.go, any reason it's here instead of there? nbd, just seems odd.
There was a problem hiding this comment.
ya, probably. i think a lot of these need to be reorganized anyway
this is the source of a lot of the infinite rebuild problems in #3362 There are a lot of weird cases where the original image existence check would fail, and so we would deliberately rebuild the image. This makes the check more robust, and adds logging when it does fail so that we have some hope of figuring out the problem.
this is the source of a lot of the infinite rebuild problems in #3362 There are a lot of weird cases where the original image existence check would fail, and so we would deliberately rebuild the image. This makes the check more robust, and adds logging when it does fail so that we have some hope of figuring out the problem.
this is the source of a lot of the infinite rebuild problems in #3362 There are a lot of weird cases where the original image existence check would fail, and so we would deliberately rebuild the image. This makes the check more robust, and adds logging when it does fail so that we have some hope of figuring out the problem.
this is the source of a lot of the infinite rebuild problems in #3362 There are a lot of weird cases where the original image existence check would fail, and so we would deliberately rebuild the image. This makes the check more robust, and adds logging when it does fail so that we have some hope of figuring out the problem.
this is the source of a lot of the infinite rebuild problems in #3362 There are a lot of weird cases where the original image existence check would fail, and so we would deliberately rebuild the image. This makes the check more robust, and adds logging when it does fail so that we have some hope of figuring out the problem.
Hello @landism, @maiamcc,
Please review the following commits I made in branch nicks/ch6738:
c995054 (2020-05-21 11:07:44 -0400)
engine: better handling for image builds in multiple manifests. Fixes #2992
Code review reminders, by giving a LGTM you attest that: