Repository: bazelbuild/bazel
Issue: --remote_default_host_platform_properties should invalidate actions
Labels: type: bug
team-Remote-Exec

If the contents of this flag change then skyframe actions should also be invalidated.

cc @katre 

Issue comments:
This makes sense, and definitely needs to happen. Thanks for catching it.
@buchgr did you mean `--remote_default_platform_properties` flag?
Yes!

Pull request review comments:
maybe introduce a new reason?
sure, just wanted to make sure that it works this way first, and it is!

I also spend some time trying to avoid using actionKeyContext - no good alternative so far - requires tooo many changes (like a lot). Will try add it to the BuildOptions maybe in the next experiment's round.

Patch that resolved the issue:
diff --git a/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
index ca6195da22a3bc..899e05bb2cc847 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/ActionCacheChecker.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Predicate;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.devtools.build.lib.actions.ActionAnalysisMetadata.MiddlemanType;
 import com.google.devtools.build.lib.actions.cache.ActionCache;
 import com.google.devtools.build.lib.actions.cache.DigestUtils;
@@ -33,6 +34,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import javax.annotation.Nullable;
 
 /**
@@ -223,7 +225,8 @@ public Token getTokenIfNeedToExecute(
       Iterable<Artifact> resolvedCacheArtifacts,
       Map<String, String> clientEnv,
       EventHandler handler,
-      MetadataHandler metadataHandler) {
+      MetadataHandler metadataHandler,
+      Map<String, String> remoteDefaultPlatformProperties) {
     // TODO(bazel-team): (2010) For RunfilesAction/SymlinkAction and similar actions that
     // produce only symlinks we should not check whether inputs are valid at all - all that matters
     // that inputs and outputs are still exist (and new inputs have not appeared). All other checks
@@ -252,7 +255,7 @@ public Token getTokenIfNeedToExecute(
       actionInputs = resolvedCacheArtifacts;
     }
     ActionCache.Entry entry = getCacheEntry(action);
-    if (mustExecute(action, entry, handler, metadataHandler, actionInputs, clientEnv)) {
+    if (mustExecute(action, entry, handler, metadataHandler, actionInputs, clientEnv, remoteDefaultPlatformProperties)) {
       if (entry != null) {
         removeCacheEntry(action);
       }
@@ -271,7 +274,8 @@ protected boolean mustExecute(
       EventHandler handler,
       MetadataHandler metadataHandler,
       Iterable<Artifact> actionInputs,
-      Map<String, String> clientEnv) {
+      Map<String, String> clientEnv,
+      Map<String, String> remoteDefaultPlatformProperties) {
     // Unconditional execution can be applied only for actions that are allowed to be executed.
     if (unconditionalExecution(action)) {
       Preconditions.checkState(action.isVolatile());
@@ -304,6 +308,12 @@ protected boolean mustExecute(
       actionCache.accountMiss(MissReason.DIFFERENT_ENVIRONMENT);
       return true;
     }
+    if (!entry.getRemoteDefaultPlatformPropertiesDigest().equals(
+        DigestUtils.fromEnv(remoteDefaultPlatformProperties))) {
+      reportDefaultPlatformChanged(handler, action);
+      actionCache.accountMiss(MissReason.DIFFERENT_DEFAULT_PLATFORM);
+      return true;
+    }
 
     entry.getFileDigest();
     actionCache.accountHit();
@@ -333,7 +343,8 @@ private static FileArtifactValue getMetadataMaybe(
   }
 
   public void updateActionCache(
-      Action action, Token token, MetadataHandler metadataHandler, Map<String, String> clientEnv)
+      Action action, Token token, MetadataHandler metadataHandler, Map<String, String> clientEnv,
+      Map<String, String> remoteDefaultPlatformProperties)
       throws IOException {
     Preconditions.checkState(
         cacheConfig.enabled(), "cache unexpectedly disabled, action: %s", action);
@@ -346,7 +357,7 @@ public void updateActionCache(
     Map<String, String> usedClientEnv = computeUsedClientEnv(action, clientEnv);
     ActionCache.Entry entry =
         new ActionCache.Entry(
-            action.getKey(actionKeyContext), usedClientEnv, action.discoversInputs());
+            action.getKey(actionKeyContext), usedClientEnv, action.discoversInputs(), ImmutableSortedMap.copyOf(remoteDefaultPlatformProperties));
     for (Artifact output : action.getOutputs()) {
       // Remove old records from the cache if they used different key.
       String execPath = output.getExecPathString();
@@ -473,7 +484,7 @@ protected void checkMiddlemanAction(
       // Compute the aggregated middleman digest.
       // Since we never validate action key for middlemen, we should not store
       // it in the cache entry and just use empty string instead.
-      entry = new ActionCache.Entry("", ImmutableMap.<String, String>of(), false);
+      entry = new ActionCache.Entry("", ImmutableMap.<String, String>of(), false, ImmutableSortedMap.of());
       for (Artifact input : action.getInputs()) {
         entry.addFile(input.getExecPath(), getMetadataMaybe(metadataHandler, input));
       }
@@ -531,6 +542,10 @@ private static void reportCorruptedCacheEntry(@Nullable EventHandler handler, Ac
     reportRebuild(handler, action, "cache entry is corrupted");
   }
 
+  private static void reportDefaultPlatformChanged(@Nullable EventHandler handler, Action action) {
+    reportRebuild(handler, action, "remote default platform properties were changed");
+  }
+
   /** Wrapper for all context needed by the ActionCacheChecker to handle a single action. */
   public static final class Token {
     private final String cacheKey;
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
index c6ab5604a23644..ff38f6f3035db0 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/ActionCache.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Lists;
 import com.google.devtools.build.lib.actions.FileArtifactValue;
 import com.google.devtools.build.lib.actions.cache.Protos.ActionCacheStatistics;
@@ -73,7 +74,7 @@ public interface ActionCache {
   final class Entry {
     /** Unique instance to represent a corrupted cache entry. */
     public static final ActionCache.Entry CORRUPTED =
-        new ActionCache.Entry(null, ImmutableMap.<String, String>of(), false);
+        new ActionCache.Entry(null, ImmutableMap.of(), false, ImmutableSortedMap.of());
 
     private final String actionKey;
     @Nullable
@@ -83,23 +84,28 @@ final class Entry {
     private Map<String, FileArtifactValue> mdMap;
     private Md5Digest md5Digest;
     private final Md5Digest usedClientEnvDigest;
+    private final Md5Digest remoteDefaultPlatformPropertiesDigest;
 
-    public Entry(String key, Map<String, String> usedClientEnv, boolean discoversInputs) {
+    public Entry(String key, Map<String, String> usedClientEnv, boolean discoversInputs,
+                 ImmutableSortedMap<String, String> remoteDefaultPlatformProperties) {
       actionKey = key;
       this.usedClientEnvDigest = DigestUtils.fromEnv(usedClientEnv);
       files = discoversInputs ? new ArrayList<String>() : null;
       mdMap = new HashMap<>();
+      remoteDefaultPlatformPropertiesDigest = DigestUtils.fromEnv(remoteDefaultPlatformProperties);
     }
 
     public Entry(
         String key,
         Md5Digest usedClientEnvDigest,
         @Nullable List<String> files,
-        Md5Digest md5Digest) {
+        Md5Digest md5Digest,
+        Md5Digest remoteDefaultPlatformPropertiesDigest) {
       actionKey = key;
       this.usedClientEnvDigest = usedClientEnvDigest;
       this.files = files;
       this.md5Digest = md5Digest;
+      this.remoteDefaultPlatformPropertiesDigest = remoteDefaultPlatformPropertiesDigest;
       mdMap = null;
     }
 
@@ -171,6 +177,7 @@ public String toString() {
       StringBuilder builder = new StringBuilder();
       builder.append("      actionKey = ").append(actionKey).append("\n");
       builder.append("      usedClientEnvKey = ").append(usedClientEnvDigest).append("\n");
+      builder.append("      remoteDefaultPlatformDigest = ").append(remoteDefaultPlatformPropertiesDigest).append("\n");
       builder.append("      digestKey = ");
       if (md5Digest == null) {
         builder.append(DigestUtils.fromMetadata(mdMap)).append(" (from mdMap)\n");
@@ -188,6 +195,10 @@ public String toString() {
       }
       return builder.toString();
     }
+
+    public Md5Digest getRemoteDefaultPlatformPropertiesDigest() {
+          return remoteDefaultPlatformPropertiesDigest;
+      }
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java b/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
index f28478fb254b45..a46f9b244da70e 100644
--- a/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
+++ b/src/main/java/com/google/devtools/build/lib/actions/cache/CompactPersistentActionCache.java
@@ -374,12 +374,14 @@ private static byte[] encode(StringIndexer indexer, ActionCache.Entry entry) {
       // + 5 bytes max for the file list length
       // + 5 bytes max for each file id
       // + 16 bytes for the environment digest
+      // + 16 bytes for the remote default platform properties digest
       int maxSize =
           VarInt.MAX_VARINT_SIZE
               + actionKeyBytes.length
               + Md5Digest.MD5_SIZE
               + VarInt.MAX_VARINT_SIZE
               + files.size() * VarInt.MAX_VARINT_SIZE
+              + Md5Digest.MD5_SIZE
               + Md5Digest.MD5_SIZE;
       ByteArrayOutputStream sink = new ByteArrayOutputStream(maxSize);
 
@@ -394,6 +396,7 @@ private static byte[] encode(StringIndexer indexer, ActionCache.Entry entry) {
       }
 
       DigestUtils.write(entry.getUsedClientEnvDigest(), sink);
+      DigestUtils.write(entry.getRemoteDefaultPlatformPropertiesDigest(), sink);
 
       return sink.toByteArray();
     } catch (IOException e) {
@@ -433,11 +436,12 @@ private static ActionCache.Entry decode(StringIndexer indexer, byte[] data) thro
       }
 
       Md5Digest usedClientEnvDigest = DigestUtils.read(source);
+      Md5Digest remoteDefaultPlatformPropertiesDigest = DigestUtils.read(source);
 
       if (source.remaining() > 0) {
         throw new IOException("serialized entry data has not been fully decoded");
       }
-      return new ActionCache.Entry(actionKey, usedClientEnvDigest, files, md5Digest);
+      return new ActionCache.Entry(actionKey, usedClientEnvDigest, files, md5Digest, remoteDefaultPlatformPropertiesDigest);
     } catch (BufferUnderflowException e) {
       throw new IOException("encoded entry data is incomplete", e);
     }
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
index 304cd369e06722..3650584cfab732 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/ActionExecutionFunction.java
@@ -138,6 +138,7 @@ public SkyValue compute(SkyKey skyKey, Environment env)
       // the value of the flag changes. We are doing this conditionally only in Bazel if remote
       // execution is available in order to not introduce additional skyframe edges in Blaze.
       PrecomputedValue.REMOTE_OUTPUTS_MODE.get(env);
+      PrecomputedValue.REMOTE_DEFAULT_PLATFORM_PROPERTIES.get(env);
     }
 
     // Look up the parts of the environment that influence the action.
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
index 8ce07edf9a0dcb..622842ddfbde57 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/PrecomputedValue.java
@@ -110,6 +110,9 @@ public static <T> Injected injected(Precomputed<T> precomputed, T value) {
   public static final Precomputed<RemoteOutputsMode> REMOTE_OUTPUTS_MODE =
       new Precomputed<>(Key.create("remote_outputs_mode"));
 
+  public static final Precomputed<Map<String, String>> REMOTE_DEFAULT_PLATFORM_PROPERTIES =
+      new Precomputed<>(Key.create("remote_default_platform_properties"));
+
   private final Object value;
 
   @AutoCodec.Instantiator
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
index 1726c706ba1f06..397c523572323e 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeActionExecutor.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -73,6 +74,7 @@
 import com.google.devtools.build.lib.actions.SpawnResult.MetadataLog;
 import com.google.devtools.build.lib.actions.StoppedScanningActionEvent;
 import com.google.devtools.build.lib.actions.TargetOutOfDateException;
+import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.actions.cache.MetadataHandler;
 import com.google.devtools.build.lib.buildtool.BuildRequestOptions;
 import com.google.devtools.build.lib.cmdline.Label;
@@ -639,6 +641,8 @@ Token checkActionCache(
       Map<String, String> clientEnv) {
     Token token;
     try (SilentCloseable c = profiler.profile(ProfilerTask.ACTION_CHECK, action.describe())) {
+      RemoteOptions remoteOptions = this.options.getOptions(RemoteOptions.class);
+      SortedMap<String, String> remoteDefaultProperties = remoteOptions != null ? remoteOptions.getRemoteDefaultExecProperties() : ImmutableSortedMap.of();
       token =
           actionCacheChecker.getTokenIfNeedToExecute(
               action,
@@ -647,7 +651,10 @@ Token checkActionCache(
               options.getOptions(BuildRequestOptions.class).explanationPath != null
                   ? reporter
                   : null,
-              metadataHandler);
+              metadataHandler,
+              remoteDefaultProperties);
+    } catch (UserExecException e) {
+      throw new IllegalStateException("failed to check action cache for " + action.prettyPrint(), e);
     }
     if (token == null) {
       boolean eventPosted = false;
@@ -699,8 +706,10 @@ void updateActionCache(
       return;
     }
     try {
-      actionCacheChecker.updateActionCache(action, token, metadataHandler, clientEnv);
-    } catch (IOException e) {
+      RemoteOptions remoteOptions = this.options.getOptions(RemoteOptions.class);
+      SortedMap<String, String> remoteDefaultProperties = remoteOptions != null ? remoteOptions.getRemoteDefaultExecProperties() : ImmutableSortedMap.of();
+      actionCacheChecker.updateActionCache(action, token, metadataHandler, clientEnv, remoteDefaultProperties);
+    } catch (IOException | UserExecException e) {
       // Skyframe has already done all the filesystem access needed for outputs and swallows
       // IOExceptions for inputs. So an IOException is impossible here.
       throw new IllegalStateException(
diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
index ad330bfddc6532..b1f254cf7f3cfc 100644
--- a/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
+++ b/src/main/java/com/google/devtools/build/lib/skyframe/SkyframeExecutor.java
@@ -63,6 +63,7 @@
 import com.google.devtools.build.lib.actions.FilesetOutputSymlink;
 import com.google.devtools.build.lib.actions.MetadataProvider;
 import com.google.devtools.build.lib.actions.ResourceManager;
+import com.google.devtools.build.lib.actions.UserExecException;
 import com.google.devtools.build.lib.analysis.AnalysisProtos.ActionGraphContainer;
 import com.google.devtools.build.lib.analysis.AspectCollection;
 import com.google.devtools.build.lib.analysis.BlazeDirectories;
@@ -2673,6 +2674,11 @@ public void sync(
             ? remoteOptions.remoteOutputsMode
             // If no value is specified then set it to some value so that it's not null.
             : RemoteOutputsMode.ALL);
+    try {
+      setRemoteDefaultPlatformProperties(remoteOptions != null ? remoteOptions.getRemoteDefaultExecProperties() : ImmutableMap.of());
+    } catch (UserExecException e) {
+      throw new IllegalStateException("Failed to initialize the graph", e);
+    }
     syncPackageLoading(
         packageCacheOptions,
         pathPackageLocator,
@@ -2713,6 +2719,10 @@ protected void syncPackageLoading(
     invalidateTransientErrors();
   }
 
+  private void setRemoteDefaultPlatformProperties(Map<String, String> remoteDefaultPlatformProperties) {
+    PrecomputedValue.REMOTE_DEFAULT_PLATFORM_PROPERTIES.set(injectable(), remoteDefaultPlatformProperties);
+  }
+
   private void getActionEnvFromOptions(CoreOptions opt) {
     // ImmutableMap does not support null values, so use a LinkedHashMap instead.
     LinkedHashMap<String, String> actionEnvironment = new LinkedHashMap<>();
diff --git a/src/main/protobuf/action_cache.proto b/src/main/protobuf/action_cache.proto
index 5694ebe34aeb0a..50846f73c930f4 100644
--- a/src/main/protobuf/action_cache.proto
+++ b/src/main/protobuf/action_cache.proto
@@ -40,6 +40,7 @@ message ActionCacheStatistics {
     CORRUPTED_CACHE_ENTRY = 4;
     NOT_CACHED = 5;
     UNCONDITIONAL_EXECUTION = 6;
+    DIFFERENT_DEFAULT_PLATFORM = 7;
   }
 
   // Detailed information for a particular miss reason.


Test patch:
diff --git a/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java b/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
index 42ac908cc9f065..8c77f24bb41d52 100644
--- a/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
+++ b/src/test/java/com/google/devtools/build/lib/actions/ActionCacheCheckerTest.java
@@ -18,6 +18,7 @@
 
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
 import com.google.devtools.build.lib.actions.ActionCacheChecker.Token;
 import com.google.devtools.build.lib.actions.cache.ActionCache;
 import com.google.devtools.build.lib.actions.cache.CompactPersistentActionCache;
@@ -107,10 +108,36 @@ private void runAction(Action action, Map<String, String> clientEnv) throws Exce
     }
 
     Token token = cacheChecker.getTokenIfNeedToExecute(
-        action, null, clientEnv, null, metadataHandler);
+        action, null, clientEnv, null, metadataHandler, ImmutableSortedMap.of());
     if (token != null) {
       // Real action execution would happen here.
-      cacheChecker.updateActionCache(action, token, metadataHandler, clientEnv);
+      cacheChecker.updateActionCache(action, token, metadataHandler, clientEnv, ImmutableSortedMap.of());
+    }
+  }
+
+  private void runAction(Action action, Map<String, String> clientEnv, Map<String, String> platform)
+          throws Exception {
+    MetadataHandler metadataHandler = new FakeMetadataHandler();
+
+    for (Artifact artifact : action.getOutputs()) {
+      Path path = artifact.getPath();
+
+      // Record all action outputs as files to be deleted across tests to prevent cross-test
+      // pollution.  We need to do this on a path basis because we don't know upfront which file
+      // system they live in so we cannot just recreate the file system.  (E.g. all NullActions
+      // share an in-memory file system to hold dummy outputs.)
+      filesToDelete.add(path);
+
+      if (!path.exists()) {
+        FileSystemUtils.writeContentAsLatin1(path, "");
+      }
+    }
+
+    Token token = cacheChecker.getTokenIfNeedToExecute(
+            action, null, clientEnv, null, metadataHandler, platform);
+    if (token != null) {
+      // Real action execution would happen here.
+      cacheChecker.updateActionCache(action, token, metadataHandler, clientEnv, platform);
     }
   }
 
@@ -220,6 +247,36 @@ public Iterable<String> getClientEnvironmentVariables() {
             .build());
   }
 
+  @Test
+  public void testDifferentRemoteDefaultPlatform() throws Exception {
+    Action action = new NullAction();
+    Map<String, String> env = new HashMap<>();
+    env.put("unused-var", "1");
+
+    Map<String, String> platform = new HashMap<>();
+    platform.put("some-var", "1");
+    // Not cached.
+    runAction(action, env, platform);
+    // Cache hit because nothing changed.
+    runAction(action, env, platform);
+    // Cache miss because platform changed to an empty from a previous value.
+    runAction(action, env, ImmutableSortedMap.of());
+    // Cache hit with an empty platform.
+    runAction(action, env, ImmutableSortedMap.of());
+    // Cache miss because platform changed to a value from an empty one.
+    runAction(action, env, ImmutableSortedMap.copyOf(platform));
+    platform.put("another-var", "1234");
+    // Cache miss because platform value changed.
+    runAction(action, env, ImmutableSortedMap.copyOf(platform));
+
+    assertStatistics(
+            2,
+            new MissDetailsBuilder()
+                    .set(MissReason.DIFFERENT_DEFAULT_PLATFORM, 3)
+                    .set(MissReason.NOT_CACHED, 1)
+                    .build());
+  }
+
   @Test
   public void testDifferentFiles() throws Exception {
     Action action = new NullAction();
diff --git a/src/test/shell/bazel/remote/remote_execution_test.sh b/src/test/shell/bazel/remote/remote_execution_test.sh
index 6b05261919114b..b017af1bce2e6e 100755
--- a/src/test/shell/bazel/remote/remote_execution_test.sh
+++ b/src/test/shell/bazel/remote/remote_execution_test.sh
@@ -1373,6 +1373,67 @@ EOF
   [[ ! -f bazel-bin/test.runfiles/MANIFEST ]] || fail "expected output manifest to exist"
 }
 
+
+function test_platform_default_properties_invalidation() {
+  # Test that when changing values of --remote_default_platform_properties all actions are
+  # invalidated.
+mkdir -p test
+  cat << EOF >> test/BUILD
+load(":skylark.bzl", "test_rule")
+
+test_rule(
+    name = "test",
+    out = "output.txt",
+    tags = ["no-cache", "no-remote", "local"]
+)
+EOF
+
+  cat << 'EOF' >> test/skylark.bzl
+def _test_impl(ctx):
+  ctx.actions.run_shell(outputs = [ctx.outputs.out],
+                        command = ["touch", ctx.outputs.out.path])
+  files_to_build = depset([ctx.outputs.out])
+  return DefaultInfo(
+      files = files_to_build,
+  )
+
+test_rule = rule(
+    implementation=_test_impl,
+    attrs = {
+        "out": attr.output(mandatory = True),
+    },
+)
+EOF
+
+  bazel build \
+  --genrule_strategy=remote \
+     --remote_executor=grpc://localhost:${worker_port} \
+    --remote_default_platform_properties='properties:{name:"build" value:"1234"}' \
+    //test:test >& $TEST_log || fail "Failed to build //a:remote"
+
+  expect_log "1 process: 1 remote"
+
+  bazel build \
+  --genrule_strategy=remote \
+    --remote_executor=grpc://localhost:${worker_port} \
+    --remote_default_platform_properties='properties:{name:"build" value:"88888"}' \
+    //test:test >& $TEST_log || fail "Failed to build //a:remote"
+
+  # Changing --remote_default_platform_properties value should invalidate SkyFrames in-memory
+  # caching and make it re-run the action.
+  expect_log "1 process: 1 remote"
+
+  bazel  build \
+  --genrule_strategy=remote \
+    --remote_executor=grpc://localhost:${worker_port} \
+    --remote_default_platform_properties='properties:{name:"build" value:"88888"}' \
+    //test:test >& $TEST_log || fail "Failed to build //a:remote"
+
+  # The same value of --remote_default_platform_properties should NOT invalidate SkyFrames in-memory cache
+  #  and make the action should not be re-run.
+  expect_log "0 processes"
+}
+
 # TODO(alpha): Add a test that fails remote execution when remote worker
 # supports sandbox.