[9.4.0] Don't release the repo contents cache lock that wasn't acquired (#31518) (#31674)

### Description

`BazelRepositoryModule.afterCommand` only releases the shared lock of
the local repo contents cache if the process holds it
(`LocalRepoContentsCache#holdsSharedLock`), rather than whenever the
cache is enabled.

### Motivation

If `beforeCommand` rejected the cache path, e.g. because the output base
lies inside it, it failed before acquiring the lock, and the release in
`afterCommand` then crashed the server with an internal error (exit code
37) instead of reporting the rejection. Reported in
https://github.com/bazelbuild/bazel/pull/31452#issuecomment-5971506616.

Work towards #31456

### Build API Changes

No

### Release Notes

RELNOTES: None

Closes #31518


COPYBARA_INTEGRATE_REVIEW=https://github.com/bazelbuild/bazel/pull/31518
from fmeum:repo-contents-cache-lock-release
930b47a41d1cd32e7dee6c99ebd3db48c2bf335a
PiperOrigin-RevId: 995832431
Change-Id: I9807199432d7d243b6e6bb0affda025ccec47f1c

Commit
https://github.com/bazelbuild/bazel/commit/b3dbbc117475f91cc4dbb255a72aa8f648cc02d8

Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
Co-authored-by: Ian (Hee) Cha <heec@google.com>
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
index 6cc5240..8835aa2 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/BazelRepositoryModule.java
@@ -791,7 +791,8 @@
 
   @Override
   public void afterCommand() throws AbruptExitException {
-    if (repositoryCache.getRepoContentsCache().isEnabled()) {
+    // The lock isn't held if beforeCommand rejected the repo contents cache path.
+    if (repositoryCache.getRepoContentsCache().holdsSharedLock()) {
       try {
         repositoryCache.getRepoContentsCache().releaseSharedLock();
       } catch (IOException e) {
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/LocalRepoContentsCache.java b/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/LocalRepoContentsCache.java
index 1df4c5d..a446285 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/LocalRepoContentsCache.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/repository/cache/LocalRepoContentsCache.java
@@ -190,6 +190,10 @@
     sharedLock = FileSystemLock.get(path.getRelative(LOCK_PATH), LockMode.SHARED);
   }
 
+  public boolean holdsSharedLock() {
+    return sharedLock != null;
+  }
+
   public void releaseSharedLock() throws IOException {
     Preconditions.checkState(sharedLock != null);
     sharedLock.close();
diff --git a/src/test/py/bazel/bzlmod/repo_contents_cache_test.py b/src/test/py/bazel/bzlmod/repo_contents_cache_test.py
index 204f676..f282117 100644
--- a/src/test/py/bazel/bzlmod/repo_contents_cache_test.py
+++ b/src/test/py/bazel/bzlmod/repo_contents_cache_test.py
@@ -101,6 +101,20 @@
     except OSError:
       pass  # Not a symlink means not cached, which is expected
 
+  def testOutputBaseInsideRepoContentsCache_rejected(self):
+    _, stdout, _ = self.RunBazel(['info', 'output_base'])
+    output_base = stdout[0].strip()
+
+    # The command fails with an error rather than a crash, and the server stays
+    # usable.
+    exit_code, _, stderr = self.RunBazel(
+        ['info', '--repo_contents_cache=' + output_base], allow_failure=True
+    )
+    self.assertNotEqual(exit_code, 0)
+    self.assertNotEqual(exit_code, 37)
+    self.assertIn('is inside the repo contents cache', '\n'.join(stderr))
+    self.RunBazel(['info'])
+
   def testCachedAfterCleanExpunge(self):
     self.ScratchFile(
         'MODULE.bazel',