During cycle detection, tolerate nodes already having the current graph version.

PiperOrigin-RevId: 245875100
diff --git a/src/main/java/com/google/devtools/build/skyframe/BUILD b/src/main/java/com/google/devtools/build/skyframe/BUILD
index b1ff51e..885f964 100644
--- a/src/main/java/com/google/devtools/build/skyframe/BUILD
+++ b/src/main/java/com/google/devtools/build/skyframe/BUILD
@@ -31,6 +31,7 @@
     ),
     deps = [
         ":skyframe-objects",
+        "//src/main/java/com/google/devtools/build/lib:bug-report",
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib:util",
         "//src/main/java/com/google/devtools/build/lib/clock",  # keep
diff --git a/src/main/java/com/google/devtools/build/skyframe/SimpleCycleDetector.java b/src/main/java/com/google/devtools/build/skyframe/SimpleCycleDetector.java
index 5c3cf92..fadd3da 100644
--- a/src/main/java/com/google/devtools/build/skyframe/SimpleCycleDetector.java
+++ b/src/main/java/com/google/devtools/build/skyframe/SimpleCycleDetector.java
@@ -22,6 +22,7 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.devtools.build.lib.bugreport.BugReport;
 import com.google.devtools.build.lib.profiler.AutoProfiler;
 import com.google.devtools.build.lib.util.GroupedList;
 import com.google.devtools.build.skyframe.ParallelEvaluatorContext.EnqueueParentBehavior;
@@ -133,6 +134,9 @@
               removeIncompleteChildrenForCycle(
                   key, entry, Iterables.concat(entry.getTemporaryDirectDeps()), evaluatorContext);
         }
+        if (maybeHandleVerifiedCleanNode(key, entry, evaluatorContext, graphPath)) {
+          continue;
+        }
         maybeMarkRebuilding(entry);
         GroupedList<SkyKey> directDeps = entry.getTemporaryDirectDeps();
         // Find out which children have errors. Similar logic to that in Evaluate#run().
@@ -186,13 +190,34 @@
         Iterable<SkyKey> cycle = graphPath.subList(cycleStart, graphPath.size());
         logger.info("Found cycle : " + cycle + " from " + graphPath);
         // Put this node into a consistent state for building if it is dirty.
-        if (entry.isDirty() && entry.getDirtyState() == NodeEntry.DirtyState.CHECK_DEPENDENCIES) {
-          // In the check deps state, entry has exactly one child not done yet. Note that this node
-          // must be part of the path to the cycle we have found (since done nodes cannot be in
-          // cycles, and this is the only missing one). Thus, it will not be removed below in
-          // removeDescendantsOfCycleValue, so it is safe here to signal that it is done.
-          entry.signalDep(evaluatorContext.getGraphVersion(), null);
-          maybeMarkRebuilding(entry);
+        if (entry.isDirty()) {
+          // If this loop runs more than once, we are in the peculiar position of entry not needing
+          // rebuilding even though it was signaled with the graph version. This can happen when the
+          // entry was previously evaluated at this version, but then invalidated anyway, even
+          // though nothing changed.
+          int loopCount = 0;
+          Version graphVersion = evaluatorContext.getGraphVersion();
+          while (entry.getDirtyState() == NodeEntry.DirtyState.CHECK_DEPENDENCIES) {
+            entry.signalDep(graphVersion, null);
+            loopCount++;
+          }
+          if (loopCount > 1 && !entry.getVersion().equals(graphVersion)) {
+            BugReport.sendBugReport(
+                new IllegalStateException(
+                    "Entry needed multiple signaling but didn't have the graph version: "
+                        + key
+                        + ", "
+                        + entry
+                        + ", "
+                        + graphVersion
+                        + ", "
+                        + graphPath));
+          }
+          if (entry.getDirtyState() == NodeEntry.DirtyState.NEEDS_REBUILDING) {
+            entry.markRebuilding();
+          } else if (maybeHandleVerifiedCleanNode(key, entry, evaluatorContext, graphPath)) {
+            continue;
+          }
         }
         if (evaluatorContext.keepGoing()) {
           // Any children of this node that we haven't already visited are not worth visiting,
@@ -295,6 +320,39 @@
   }
 
   /**
+   * Fully process {@code entry} if it is dirty but verified to be clean. This can only happen in
+   * rare circumstances where a node with a cycle is invalidated at the same version. Returns true
+   * if the entry was successfully processed, meaning that its value has been set and all reverse
+   * deps signaled.
+   */
+  private static boolean maybeHandleVerifiedCleanNode(
+      SkyKey key,
+      NodeEntry entry,
+      ParallelEvaluatorContext evaluatorContext,
+      List<SkyKey> graphPathForDebugging)
+      throws InterruptedException {
+    if (entry.getDirtyState() != NodeEntry.DirtyState.VERIFIED_CLEAN) {
+      return false;
+    }
+    Set<SkyKey> rdeps = entry.markClean();
+    evaluatorContext.signalValuesAndEnqueueIfReady(
+        key, rdeps, entry.getVersion(), EnqueueParentBehavior.SIGNAL);
+    ErrorInfo error = entry.getErrorInfo();
+    if (error.getCycleInfo().isEmpty()) {
+      BugReport.sendBugReport(
+          new IllegalStateException(
+              "Entry was unchanged from last build, but cycle was found this time and not"
+                  + " last time: "
+                  + key
+                  + ", "
+                  + entry
+                  + ", "
+                  + graphPathForDebugging));
+    }
+    return true;
+  }
+
+  /**
    * Marker value that we push onto a stack before we push a node's children on. When the marker
    * value is popped, we know that all the children are finished. We would use null instead, but
    * ArrayDeque does not permit null elements.