Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/com/google/devtools/build/lib/remote/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ java_library(
"//src/main/java/com/google/devtools/build/lib:runtime",
"//src/main/java/com/google/devtools/build/lib:runtime/command_line_path_factory",
"//src/main/java/com/google/devtools/build/lib/actions",
"//src/main/java/com/google/devtools/build/lib/cmdline",
"//src/main/java/com/google/devtools/build/lib/actions:action_input_helper",
"//src/main/java/com/google/devtools/build/lib/actions:action_lookup_data",
"//src/main/java/com/google/devtools/build/lib/actions:action_output_directory_helper",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import com.google.common.util.concurrent.SettableFuture;
import com.google.devtools.build.lib.actions.ActionInput;
import com.google.devtools.build.lib.actions.Artifact;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.actions.ArtifactPathResolver;
import com.google.devtools.build.lib.actions.EnvironmentalExecException;
import com.google.devtools.build.lib.actions.ExecException;
Expand Down Expand Up @@ -512,7 +513,8 @@ public MerkleTree uncachedBuildMerkleTreeVisitor(
}

@Nullable
private static ByteString buildSalt(Spawn spawn, @Nullable SpawnScrubber spawnScrubber, ImmutableMap<String, String> mnemonicCacheSalts) {
private static ByteString buildSalt(
Spawn spawn, @Nullable SpawnScrubber spawnScrubber, @Nullable String contentKey, ImmutableMap<String, String> mnemonicCacheSalts) {
CacheSalt.Builder saltBuilder =
CacheSalt.newBuilder().setMayBeExecutedRemotely(Spawns.mayBeExecutedRemotely(spawn));

Expand All @@ -522,9 +524,14 @@ private static ByteString buildSalt(Spawn spawn, @Nullable SpawnScrubber spawnSc
saltBuilder.setWorkspace(workspace);
}

if (spawnScrubber != null) {
saltBuilder.setScrubSalt(
CacheSalt.ScrubSalt.newBuilder().setSalt(spawnScrubber.getSalt()).build());
String scrubSalt = spawnScrubber != null ? spawnScrubber.getSalt() : "";

if (contentKey != null) {
scrubSalt = scrubSalt + ":ck=" + contentKey;
}

if (!scrubSalt.isEmpty()) {
saltBuilder.setScrubSalt(CacheSalt.ScrubSalt.newBuilder().setSalt(scrubSalt).build());
}

// Add mnemonic-specific salt for targeted cache invalidation.
Expand All @@ -536,6 +543,45 @@ private static ByteString buildSalt(Spawn spawn, @Nullable SpawnScrubber spawnSc
return saltBuilder.build().toByteString();
}

/**
* Reads the content key file for a TestRunner spawn. Derives the path from the spawn's output
* paths: a test log at {@code bazel-out/<cfg>/testlogs/<pkg>/<name>/test.log} implies the
* content key at {@code bazel-out/<cfg>/bin/<pkg>/<name>.content_key}.
*
* <p>Returns null if the file cannot be found or read (e.g. aspect not enabled).
*/
@Nullable
private String readContentKey(Spawn spawn) {
Label label = spawn.getTargetLabel();
if (label == null) {
return null;
}
String relativeContentKeyPath =
label.getPackageName() + "/" + label.getName() + ".content_key";

for (ActionInput output : spawn.getOutputFiles()) {
String path = output.getExecPathString();
int idx = path.indexOf("/testlogs/");
if (idx >= 0) {
// e.g. bazel-out/k8-fastbuild/testlogs/foo/bar/baz/test.log
// → bazel-out/k8-fastbuild/bin/foo/bar/baz.content_key
String configPrefix = path.substring(0, idx); // "bazel-out/k8-fastbuild"
Path keyFile =
execRoot.getRelative(configPrefix + "/bin/" + relativeContentKeyPath);
if (keyFile.exists()) {
try {
return new String(keyFile.getInputStream().readAllBytes(),
java.nio.charset.StandardCharsets.UTF_8).trim();
} catch (java.io.IOException e) {
// Non-fatal: fall back to normal cache key.
}
}
break;
}
}
return null;
}

/**
* Semaphore for limiting the concurrent number of Merkle tree input roots we compute and keep in
* memory.
Expand Down Expand Up @@ -653,6 +699,20 @@ public RemoteAction buildRemoteAction(Spawn spawn, SpawnExecutionContext context
RemotePathResolver.createMapped(baseRemotePathResolver, execRoot, spawn.getPathMapper());
ToolSignature toolSignature = getToolSignature(spawn, context);
SpawnScrubber spawnScrubber = scrubber != null ? scrubber.forSpawn(spawn) : null;

// For TestRunner actions with --experimental_test_content_key: inject jar omission
// programmatically when the content key is present. This keeps jar-omission in sync with
// key injection without requiring an xplat.cfg entry — when the key is absent (aspect not
// run, file not downloaded, stale files from a disabled flag, etc.), jars remain in the
// Merkle tree and serve as the cache discriminator (safe fallback, no false hits).
String contentKeyForSalt = null;
if (remoteOptions.testContentKey && "TestRunner".equals(spawn.getMnemonic())) {
contentKeyForSalt = readContentKey(spawn);
if (contentKeyForSalt != null && spawnScrubber != null) {
spawnScrubber = spawnScrubber.withJarOmission();
}
}

final MerkleTree merkleTree =
buildInputMerkleTree(spawn, context, toolSignature, spawnScrubber, remotePathResolver);

Expand Down Expand Up @@ -684,7 +744,7 @@ public RemoteAction buildRemoteAction(Spawn spawn, SpawnExecutionContext context
platform,
context.getTimeout(),
Spawns.mayBeCachedRemotely(spawn),
buildSalt(spawn, spawnScrubber, mnemonicCacheSalts));
buildSalt(spawn, spawnScrubber, contentKeyForSalt, mnemonicCacheSalts));

ActionKey actionKey = digestUtil.computeActionKey(action);

Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/google/devtools/build/lib/remote/Scrubber.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,35 @@ private SpawnScrubber(Config.Rule ruleProto) {
this.salt = ruleProto.getTransform().getSalt();
}

private SpawnScrubber(SpawnScrubber base, ImmutableList<Pattern> extraOmittedInputPatterns) {
this.mnemonicPattern = base.mnemonicPattern;
this.labelPattern = base.labelPattern;
this.kindPattern = base.kindPattern;
this.matchTools = base.matchTools;
this.omittedInputPatterns =
ImmutableList.<Pattern>builder()
.addAll(base.omittedInputPatterns)
.addAll(extraOmittedInputPatterns)
.build();
this.omittedInputPatternsExternal = base.omittedInputPatternsExternal;
this.argReplacements = base.argReplacements;
this.omittedEnvVars = base.omittedEnvVars;
this.salt = base.salt;
}

/** Returns a new SpawnScrubber identical to this one but with first-party .jar inputs omitted.
*
* <p>External/maven jars (under .../bin/external/) are intentionally kept in the Merkle tree
* so that version bumps are detected naturally without needing the content key to cover them.
* First-party jars are omitted because the content key already captures exactly which
* first-party class bytecodes the test uses.
*/
public SpawnScrubber withJarOmission() {
return new SpawnScrubber(
this,
ImmutableList.of(Pattern.compile(".*/bin/(?!external/).*[.]jar$")));
}

private String emptyToAll(String s) {
return s.isEmpty() ? ".*" : s;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ public final class RemoteOptions extends CommonRemoteOptions {
+ "disable TLS.")
public String remoteExecutor;

@Option(
name = "experimental_test_content_key",
defaultValue = "false",
documentationCategory = OptionDocumentationCategory.REMOTE,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"If enabled, TestRunner actions include a per-test content key in the remote cache"
+ " salt. The content key (produced by //rules/test:compute_test_key_aspect) hashes"
+ " only the class bytecodes transitively used by the test. Combined with jar-omission"
+ " scrubbing rules, this makes remote cache hits insensitive to unused dependency"
+ " changes, avoiding unnecessary test re-executions.")
public boolean testContentKey;

@Option(
name = "experimental_remote_execution_keepalive",
defaultValue = "false",
Expand Down
Loading