[7.0.0] Make lockfile extension updates more obviously correct (#20400)

* Removes code paths in `BazelLockFileModule` that are never taken now
that lockfile update events are replayed by Skyframe.
* `BazelModuleResolutionEvent` no longer contains a `BazelLockFileValue`
that is internally inconsistent: Previously, this values combined the
(potentially updated) extension usages resulting from the resolution
with the old locked extension results obtained from the previous state
of the lockfile. The logic in `BazelLockFileModule` handled this
correctly, but it should now be more obvious that it does so.
* No longer parse the lockfile twice, once in `BazelLockFileFunction`
and once in `BazelLockFileModule`.

Closes #20311.

Commit
https://github.com/bazelbuild/bazel/commit/1691f60c2ec222a392c8708ddc4cc35a969c4f7c

PiperOrigin-RevId: 586744057
Change-Id: Id85dbde627878c18baf3c375a0091a4f989b146b

Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
index d07e293..d97a030 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BUILD
@@ -107,7 +107,6 @@
         "//src/main/java/com/google/devtools/build/lib/util:abrupt_exit_exception",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//third_party:flogger",
-        "//third_party:gson",
         "//third_party:guava",
         "//third_party:jsr305",
     ],
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
index cc8c2f5..29f1a41 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelDepGraphFunction.java
@@ -128,18 +128,17 @@
         calculateUniqueNameForUsedExtensionId(extensionUsagesById);
 
     if (lockfileMode.equals(LockfileMode.UPDATE)) {
-      // This will keep all module extension evaluation results, some of which may be stale due to
-      // changed usages. They will be removed in BazelLockFileModule.
-      BazelLockFileValue updateLockfile =
-          lockfile.toBuilder()
-              .setLockFileVersion(BazelLockFileValue.LOCK_FILE_VERSION)
+      BazelLockFileValue resolutionOnlyLockfile =
+          BazelLockFileValue.builder()
               .setModuleFileHash(root.getModuleFileHash())
               .setFlags(flags)
               .setLocalOverrideHashes(localOverrideHashes)
               .setModuleDepGraph(depGraph)
               .build();
       env.getListener()
-          .post(BazelModuleResolutionEvent.create(updateLockfile, extensionUsagesById));
+          .post(
+              BazelModuleResolutionEvent.create(
+                  lockfile, resolutionOnlyLockfile, extensionUsagesById));
     }
 
     return BazelDepGraphValue.create(
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java
index 4036eb3..96fe6ac 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileModule.java
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.ImmutableTable;
@@ -31,7 +32,6 @@
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.lib.vfs.Root;
 import com.google.devtools.build.lib.vfs.RootedPath;
-import com.google.gson.JsonSyntaxException;
 import java.io.IOException;
 import java.util.HashMap;
 import java.util.Map;
@@ -61,30 +61,22 @@
 
   @Override
   public void afterCommand() throws AbruptExitException {
-    if (moduleResolutionEvent == null && extensionResolutionEventsMap.isEmpty()) {
-      return; // nothing changed, do nothing!
-    }
-
-    RootedPath lockfilePath =
-        RootedPath.toRootedPath(Root.fromPath(workspaceRoot), LabelConstants.MODULE_LOCKFILE_NAME);
-
-    // Read the existing lockfile (if none exists, will get an empty lockfile value) and get its
-    // module extension usages. This information is needed to determine which extension results are
-    // now stale and need to be removed.
-    BazelLockFileValue oldLockfile;
-    try {
-      oldLockfile = BazelLockFileFunction.getLockfileValue(lockfilePath);
-    } catch (IOException | JsonSyntaxException | NullPointerException e) {
-      logger.atSevere().withCause(e).log(
-          "Failed to read and parse the MODULE.bazel.lock file with error: %s."
-              + " Try deleting it and rerun the build.",
-          e.getMessage());
+    if (moduleResolutionEvent == null) {
+      // Command does not use Bazel modules or the lockfile mode is not update.
+      // Since Skyframe caches events, they are replayed even when nothing has changed.
+      Preconditions.checkState(extensionResolutionEventsMap.isEmpty());
       return;
     }
-    ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> oldExtensionUsages;
+
+    BazelLockFileValue oldLockfile = moduleResolutionEvent.getOnDiskLockfileValue();
+    BazelLockFileValue newLockfile;
     try {
-      oldExtensionUsages =
-          BazelDepGraphFunction.getExtensionUsagesById(oldLockfile.getModuleDepGraph());
+      // Create an updated version of the lockfile, keeping only the extension results from the old
+      // lockfile that are still up-to-date and adding the newly resolved extension results.
+      newLockfile =
+          moduleResolutionEvent.getResolutionOnlyLockfileValue().toBuilder()
+              .setModuleExtensions(combineModuleExtensions(oldLockfile))
+              .build();
     } catch (ExternalDepsException e) {
       logger.atSevere().withCause(e).log(
           "Failed to read and parse the MODULE.bazel.lock file with error: %s."
@@ -93,25 +85,12 @@
       return;
     }
 
-    // Create an updated version of the lockfile with the events updates
-    BazelLockFileValue lockfile;
-    if (moduleResolutionEvent != null) {
-      lockfile = moduleResolutionEvent.getLockfileValue();
-    } else {
-      lockfile = oldLockfile;
-    }
-    lockfile =
-        lockfile.toBuilder()
-            .setModuleExtensions(
-                combineModuleExtensions(lockfile.getModuleExtensions(), oldExtensionUsages))
-            .build();
-
     // Write the new value to the file, but only if needed. This is not just a performance
     // optimization: whenever the lockfile is updated, most Skyframe nodes will be marked as dirty
     // on the next build, which breaks commands such as `bazel config` that rely on
     // com.google.devtools.build.skyframe.MemoizingEvaluator#getDoneValues.
-    if (!lockfile.equals(oldLockfile)) {
-      updateLockfile(lockfilePath, lockfile);
+    if (!newLockfile.equals(oldLockfile)) {
+      updateLockfile(workspaceRoot, newLockfile);
     }
     this.moduleResolutionEvent = null;
     this.extensionResolutionEventsMap.clear();
@@ -120,30 +99,25 @@
   /**
    * Combines the old extensions stored in the lockfile -if they are still valid- with the new
    * extensions from the events (if any)
-   *
-   * @param oldModuleExtensions Module extensions stored in the current lockfile
-   * @param oldExtensionUsages Module extension usages stored in the current lockfile
    */
   private ImmutableMap<
           ModuleExtensionId, ImmutableMap<ModuleExtensionEvalFactors, LockFileModuleExtension>>
-      combineModuleExtensions(
-          ImmutableMap<
-                  ModuleExtensionId,
-                  ImmutableMap<ModuleExtensionEvalFactors, LockFileModuleExtension>>
-              oldModuleExtensions,
-          ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> oldExtensionUsages) {
-
+      combineModuleExtensions(BazelLockFileValue oldLockfile) throws ExternalDepsException {
     Map<ModuleExtensionId, ImmutableMap<ModuleExtensionEvalFactors, LockFileModuleExtension>>
         updatedExtensionMap = new HashMap<>();
 
-    // Add old extensions if they are still valid
-    oldModuleExtensions.forEach(
-        (moduleExtensionId, innerMap) -> {
-          ModuleExtensionEvalFactors firstEntryKey = innerMap.keySet().iterator().next();
-          if (shouldKeepExtension(moduleExtensionId, firstEntryKey, oldExtensionUsages)) {
-            updatedExtensionMap.put(moduleExtensionId, innerMap);
-          }
-        });
+    // Keep old extensions if they are still valid.
+    ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> oldExtensionUsages =
+        BazelDepGraphFunction.getExtensionUsagesById(oldLockfile.getModuleDepGraph());
+    for (var entry : oldLockfile.getModuleExtensions().entrySet()) {
+      var moduleExtensionId = entry.getKey();
+      var factorToLockedExtension = entry.getValue();
+      ModuleExtensionEvalFactors firstEntryFactors =
+          factorToLockedExtension.keySet().iterator().next();
+      if (shouldKeepExtension(moduleExtensionId, firstEntryFactors, oldExtensionUsages)) {
+        updatedExtensionMap.put(moduleExtensionId, factorToLockedExtension);
+      }
+    }
 
     // Add the new resolved extensions
     for (var event : extensionResolutionEventsMap.values()) {
@@ -200,11 +174,6 @@
       }
     }
 
-    // If moduleResolutionEvent is null, then no usage has changed and all locked extension
-    // resolutions are still up-to-date.
-    if (moduleResolutionEvent == null) {
-      return true;
-    }
     // Otherwise, compare the current usages of this extension with the ones in the lockfile. We
     // trim the usages to only the information that influences the evaluation of the extension so
     // that irrelevant changes (e.g. locations or imports) don't cause the extension to be removed.
@@ -218,10 +187,12 @@
   /**
    * Updates the data stored in the lockfile (MODULE.bazel.lock)
    *
-   * @param lockfilePath Rooted path to lockfile
+   * @param workspaceRoot Root of the workspace where the lockfile is located
    * @param updatedLockfile The updated lockfile data to save
    */
-  public static void updateLockfile(RootedPath lockfilePath, BazelLockFileValue updatedLockfile) {
+  public static void updateLockfile(Path workspaceRoot, BazelLockFileValue updatedLockfile) {
+    RootedPath lockfilePath =
+        RootedPath.toRootedPath(Root.fromPath(workspaceRoot), LabelConstants.MODULE_LOCKFILE_NAME);
     try {
       FileSystemUtils.writeContent(
           lockfilePath.asPath(),
@@ -249,5 +220,4 @@
     this.extensionResolutionEventsMap.put(
         extensionResolutionEvent.getExtensionId(), extensionResolutionEvent);
   }
-
 }
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionEvent.java b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionEvent.java
index cd9c1e6..867eec90 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionEvent.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/bzlmod/BazelModuleResolutionEvent.java
@@ -29,25 +29,39 @@
  */
 public final class BazelModuleResolutionEvent implements Postable {
 
-  private final BazelLockFileValue lockfileValue;
+  private final BazelLockFileValue onDiskLockfileValue;
+  private final BazelLockFileValue resolutionOnlyLockfileValue;
   private final ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage>
       extensionUsagesById;
 
   private BazelModuleResolutionEvent(
-      BazelLockFileValue lockfileValue,
+      BazelLockFileValue onDiskLockfileValue,
+      BazelLockFileValue resolutionOnlyLockfileValue,
       ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesById) {
-    this.lockfileValue = lockfileValue;
+    this.onDiskLockfileValue = onDiskLockfileValue;
+    this.resolutionOnlyLockfileValue = resolutionOnlyLockfileValue;
     this.extensionUsagesById = extensionUsagesById;
   }
 
   public static BazelModuleResolutionEvent create(
-      BazelLockFileValue lockFileValue,
+      BazelLockFileValue onDiskLockfileValue,
+      BazelLockFileValue resolutionOnlyLockfileValue,
       ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage> extensionUsagesById) {
-    return new BazelModuleResolutionEvent(lockFileValue, extensionUsagesById);
+    return new BazelModuleResolutionEvent(
+        onDiskLockfileValue, resolutionOnlyLockfileValue, extensionUsagesById);
   }
 
-  public BazelLockFileValue getLockfileValue() {
-    return lockfileValue;
+  /** Returns the contents of the lockfile as it existed on disk before the current build. */
+  public BazelLockFileValue getOnDiskLockfileValue() {
+    return onDiskLockfileValue;
+  }
+
+  /**
+   * Returns the result of Bazel module resolution in the form of a lockfile without any information
+   * about module extension results.
+   */
+  public BazelLockFileValue getResolutionOnlyLockfileValue() {
+    return resolutionOnlyLockfileValue;
   }
 
   public ImmutableTable<ModuleExtensionId, ModuleKey, ModuleExtensionUsage>
diff --git a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java
index 54c00ba..f11407e 100644
--- a/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java
+++ b/src/test/java/com/google/devtools/build/lib/bazel/bzlmod/BazelLockFileFunctionTest.java
@@ -34,7 +34,6 @@
 import com.google.devtools.build.lib.bazel.repository.RepositoryOptions.LockfileMode;
 import com.google.devtools.build.lib.bazel.repository.starlark.StarlarkRepositoryModule;
 import com.google.devtools.build.lib.clock.BlazeClock;
-import com.google.devtools.build.lib.cmdline.LabelConstants;
 import com.google.devtools.build.lib.packages.semantics.BuildLanguageOptions;
 import com.google.devtools.build.lib.pkgcache.PathPackageLocator;
 import com.google.devtools.build.lib.rules.repository.LocalRepositoryFunction;
@@ -57,7 +56,6 @@
 import com.google.devtools.build.lib.util.io.TimestampGranularityMonitor;
 import com.google.devtools.build.lib.vfs.FileStateKey;
 import com.google.devtools.build.lib.vfs.Root;
-import com.google.devtools.build.lib.vfs.RootedPath;
 import com.google.devtools.build.lib.vfs.SyscallCache;
 import com.google.devtools.build.skyframe.EvaluationContext;
 import com.google.devtools.build.skyframe.EvaluationResult;
@@ -183,11 +181,8 @@
                         if (localOverrideHashes == null) {
                           return null;
                         }
-                        RootedPath lockfilePath =
-                            RootedPath.toRootedPath(
-                                Root.fromPath(rootDirectory), LabelConstants.MODULE_LOCKFILE_NAME);
                         BazelLockFileModule.updateLockfile(
-                            lockfilePath,
+                            rootDirectory,
                             BazelLockFileValue.builder()
                                 .setModuleFileHash(key.moduleHash())
                                 .setFlags(flags)
@@ -546,6 +541,7 @@
   abstract static class UpdateLockFileKey implements SkyKey {
 
     abstract String moduleHash();
+
     abstract ImmutableMap<ModuleKey, Module> depGraph();
 
     abstract ImmutableMap<String, ModuleOverride> overrides();