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 1a477a480a79ef..7b338ed489feb0 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 @@ -541,7 +541,8 @@ public void setRemoteAnalysisCachingDependenciesProvider( * #setRemoteAnalysisCachingDependenciesProvider(RemoteAnalysisCachingDependenciesProvider)} for * the exact point. */ - private RemoteAnalysisCachingDependenciesProvider getRemoteAnalysisCachingDependenciesProvider() { + @VisibleForTesting // productionVisibility = Visibility.PRIVATE + public RemoteAnalysisCachingDependenciesProvider getRemoteAnalysisCachingDependenciesProvider() { return remoteAnalysisCachingDependenciesProvider; } diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD index bbe478229af673..a33978c9ec49da 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/BUILD @@ -88,6 +88,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/unsafe:unsafe-provider", "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects", "//src/main/java/com/google/devtools/common/options", + "//src/main/protobuf:file_invalidation_data_java_proto", "//third_party:auto_value", "//third_party:caffeine", "//third_party:error_prone_annotations", diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecs.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecs.java index ad7f4a74dee1aa..5b8349ae70c38c 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecs.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/ObjectCodecs.java @@ -252,8 +252,15 @@ public Object deserializeMemoizedAndBlocking( public Object deserializeWithSkyframe( FingerprintValueService fingerprintValueService, ByteString data) throws SerializationException { + return deserializeWithSkyframe(fingerprintValueService, data.newCodedInput()); + } + + @Nullable + public Object deserializeWithSkyframe( + FingerprintValueService fingerprintValueService, CodedInputStream codedIn) + throws SerializationException { return SharedValueDeserializationContext.deserializeWithSkyframe( - getCodecRegistry(), getDependencies(), fingerprintValueService, data); + getCodecRegistry(), getDependencies(), fingerprintValueService, codedIn); } static Object deserializeStreamFully(CodedInputStream codedIn, DeserializationContext context) diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SharedValueDeserializationContext.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SharedValueDeserializationContext.java index c9c50118fd3458..040ef8c37e425c 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SharedValueDeserializationContext.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SharedValueDeserializationContext.java @@ -141,9 +141,8 @@ static Object deserializeWithSkyframe( ObjectCodecRegistry codecRegistry, ImmutableClassToInstanceMap dependencies, FingerprintValueService fingerprintValueService, - ByteString bytes) + CodedInputStream codedIn) throws SerializationException { - CodedInputStream codedIn = bytes.newCodedInput(); // Enabling aliasing of `codedIn` here might be better for performance but causes deserialized // values to differ subtly from the input values, complicating testing. // diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetriever.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetriever.java index 495cabf9e561d1..a4396b8623e813 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetriever.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetriever.java @@ -28,6 +28,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.devtools.build.lib.skyframe.serialization.FingerprintValueStore.MissingFingerprintValueException; import com.google.devtools.build.lib.skyframe.serialization.analysis.ClientId; +import com.google.devtools.build.lib.skyframe.serialization.proto.DataType; import com.google.devtools.build.skyframe.IntVersion; import com.google.devtools.build.skyframe.SkyFunction; import com.google.devtools.build.skyframe.SkyFunction.Environment; @@ -36,8 +37,10 @@ import com.google.devtools.build.skyframe.SkyKey; import com.google.devtools.build.skyframe.SkyValue; import com.google.protobuf.ByteString; +import com.google.protobuf.CodedInputStream; import java.io.IOException; import java.util.Arrays; +import java.util.HexFormat; import java.util.Optional; import java.util.concurrent.ExecutionException; @@ -207,17 +210,17 @@ public static RetrievalResult tryRetrieve( Futures.addCallback( keyWriteStatus, FutureHelpers.FAILURE_REPORTING_CALLBACK, directExecutor()); } - ListenableFuture valueBytes; + ListenableFuture futureValueBytes; try { - valueBytes = + futureValueBytes = fingerprintValueService.get( fingerprintValueService.fingerprint( frontierNodeVersion.concat(keyBytes.getObject().toByteArray()))); } catch (IOException e) { throw new SerializationException("key lookup failed for " + key, e); } - nextState = new WaitingForFutureValueBytes(valueBytes); - switch (futuresShim.dependOnFuture(valueBytes)) { + nextState = new WaitingForFutureValueBytes(futureValueBytes); + switch (futuresShim.dependOnFuture(futureValueBytes)) { case DONE: break; // continues to the next state case NOT_DONE: @@ -243,9 +246,40 @@ case WaitingForFutureValueBytes(ListenableFuture futureValueBytes): nextState = NoCachedData.NO_CACHED_DATA; break; } - Object value = - codecs.deserializeWithSkyframe( - fingerprintValueService, ByteString.copyFrom(valueBytes)); + var codedIn = CodedInputStream.newInstance(valueBytes); + // Skips over the invalidation data key. + // + // TODO: b/364831651 - this code is a temporary and will eventually be removed when + // this read is performed in the AnalysisCacheService. + try { + int dataTypeOrdinal = codedIn.readInt32(); + switch (DataType.forNumber(dataTypeOrdinal)) { + case DATA_TYPE_EMPTY: + break; + case DATA_TYPE_FILE: + // fall through + case DATA_TYPE_LISTING: + { + var unusedKey = codedIn.readString(); + break; + } + case DATA_TYPE_NODE: + { + var unusedKey = PackedFingerprint.readFrom(codedIn); + break; + } + default: + throw new SerializationException( + String.format( + "for key=%s, got unexpected data type with ordinal %d from value" + + " bytes=%s", + key, dataTypeOrdinal, HexFormat.of().formatHex(valueBytes))); + } + } catch (IOException e) { + throw new SerializationException("Error parsing invalidation data key", e); + } + + Object value = codecs.deserializeWithSkyframe(fingerprintValueService, codedIn); if (!(value instanceof ListenableFuture)) { nextState = new RetrievedValue((SkyValue) value); break; diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD index 4fd945b296a658..f35a555acaf49c 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD @@ -59,24 +59,36 @@ java_library( java_library( name = "frontier_serializer", - srcs = ["FrontierSerializer.java"], + srcs = [ + "FrontierSerializer.java", + "SelectedEntrySerializer.java", + ], deps = [ ":dependencies_provider", ":event_listener", + ":file_dependency_serializer", + ":file_op_node_map", + ":invalidation_data_reference_or_future", + ":long_version_getter_test_injection", ":options", "//src/main/java/com/google/devtools/build/lib/actions:action_lookup_data", "//src/main/java/com/google/devtools/build/lib/actions:action_lookup_key", "//src/main/java/com/google/devtools/build/lib/actions:artifacts", "//src/main/java/com/google/devtools/build/lib/cmdline", + "//src/main/java/com/google/devtools/build/lib/concurrent", "//src/main/java/com/google/devtools/build/lib/events", "//src/main/java/com/google/devtools/build/lib/skyframe:aspect_key_creator", + "//src/main/java/com/google/devtools/build/lib/skyframe:filesystem_keys", "//src/main/java/com/google/devtools/build/lib/skyframe:skyframe_cluster", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization", + "//src/main/java/com/google/devtools/build/lib/util:TestType", "//src/main/java/com/google/devtools/build/lib/versioning:long_version_getter", "//src/main/java/com/google/devtools/build/skyframe", "//src/main/java/com/google/devtools/build/skyframe:skyframe-objects", "//src/main/protobuf:failure_details_java_proto", + "//src/main/protobuf:file_invalidation_data_java_proto", "//third_party:guava", + "//third_party:jsr305", "@com_google_protobuf//:protobuf_java", ], ) @@ -109,6 +121,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/analysis:analysis_cluster", "//src/main/java/com/google/devtools/build/lib/skyframe:filesystem_keys", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization", + "//src/main/java/com/google/devtools/build/lib/util:TestType", "//src/main/java/com/google/devtools/build/lib/versioning:long_version_getter", "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment", @@ -194,3 +207,13 @@ java_library( name = "client_id", srcs = ["ClientId.java"], ) + +java_library( + name = "long_version_getter_test_injection", + srcs = ["LongVersionGetterTestInjection.java"], + deps = [ + "//src/main/java/com/google/devtools/build/lib/util:TestType", + "//src/main/java/com/google/devtools/build/lib/versioning:long_version_getter", + "//third_party:guava", + ], +) diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java index 1b431871714863..33ab9b1a9df1a8 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FileDependencySerializer.java @@ -26,6 +26,7 @@ import static com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.ConstantFileData.CONSTANT_FILE; import static com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.ConstantListingData.CONSTANT_LISTING; import static com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.ConstantNodeData.CONSTANT_NODE; +import static com.google.devtools.build.lib.util.TestType.isInTest; import static com.google.devtools.build.lib.vfs.RootedPath.toRootedPath; import static java.lang.Math.max; import static java.nio.charset.StandardCharsets.UTF_8; @@ -178,10 +179,11 @@ NodeDataInfoOrFuture registerDependency(AbstractNestedFileOpNodes node) { FileDataInfoOrFuture populateFutureFileDataInfo(FutureFileDataInfo future) { FileKey key = future.key(); RootedPath rootedPath = key.argument(); + RootedPath parentRootedPath; // Builtin files don't change. - if (rootedPath.getRoot().getFileSystem() instanceof BundledFileSystem + if ((rootedPath.getRoot().getFileSystem() instanceof BundledFileSystem) // Assumes that the root folder doesn't change. - || rootedPath.getRootRelativePath().isEmpty()) { + || (parentRootedPath = rootedPath.getParentDirectory()) == null) { return future.completeWith(CONSTANT_FILE); } @@ -199,7 +201,12 @@ FileDataInfoOrFuture populateFutureFileDataInfo(FutureFileDataInfo future) { return future.failWith(e); } } - var uploader = new FileInvalidationDataUploader(rootedPath, realRootedPath, initialMtsv); + var uploader = + new FileInvalidationDataUploader( + /* rootedPath= */ rootedPath, + /* parentRootedPath= */ parentRootedPath, + /* realRootedPath= */ realRootedPath, + initialMtsv); return future.completeWith( Futures.transform( fullyResolvePath(value.isSymlink() ? value.getUnresolvedLinkTarget() : null, uploader), @@ -222,9 +229,12 @@ private final class FileInvalidationDataUploader private long mtsv; private FileInvalidationDataUploader( - RootedPath rootedPath, RootedPath realRootedPath, long initialMtsv) { + RootedPath rootedPath, + RootedPath parentRootedPath, + RootedPath realRootedPath, + long initialMtsv) { this.rootedPath = rootedPath; - this.parentRootedPath = rootedPath.getParentDirectory(); + this.parentRootedPath = parentRootedPath; this.realRootedPath = realRootedPath; this.mtsv = initialMtsv; } @@ -357,6 +367,15 @@ private ListenableFuture processSymlinks( Path linkPath, PathFragment link, FileInvalidationDataUploader uploader) { + if (link.isAbsolute()) { + if (isInTest()) { + // Test environments may use absolute symlinks, which aren't allowed in production + // environments with analysis caching. Skips further dependency resolution for those. + return immediateVoidFuture(); + } + throw new IllegalStateException( + String.format("Absolute symlink not permitted: %s contained %s", linkPath, link)); + } Symlink.Builder symlinkData = uploader.addSymlinksBuilder().setContents(link.getPathString()); PathFragment linkParent = parentRootedPath.getRootRelativePath(); PathFragment unresolvedTarget = linkParent.getRelative(link); diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializer.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializer.java index e24f6d4e7f82af..d4ffc98a48f659 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializer.java +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializer.java @@ -15,10 +15,10 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.google.common.util.concurrent.Futures.immediateFailedFuture; -import static com.google.common.util.concurrent.MoreExecutors.directExecutor; import static com.google.devtools.build.lib.skyframe.serialization.analysis.FrontierSerializer.SelectionMarking.ACTIVE; import static com.google.devtools.build.lib.skyframe.serialization.analysis.FrontierSerializer.SelectionMarking.FRONTIER_CANDIDATE; +import static com.google.devtools.build.lib.skyframe.serialization.analysis.LongVersionGetterTestInjection.getVersionGetterForTesting; +import static com.google.devtools.build.lib.util.TestType.isInTest; import static java.util.concurrent.ForkJoinPool.commonPool; import com.google.common.annotations.VisibleForTesting; @@ -26,7 +26,6 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.eventbus.EventBus; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.devtools.build.lib.actions.ActionLookupData; import com.google.devtools.build.lib.actions.ActionLookupKey; @@ -45,21 +44,16 @@ import com.google.devtools.build.lib.skyframe.serialization.ObjectCodecs; import com.google.devtools.build.lib.skyframe.serialization.ProfileCollector; import com.google.devtools.build.lib.skyframe.serialization.SerializationException; -import com.google.devtools.build.lib.skyframe.serialization.SerializationResult; -import com.google.devtools.build.lib.skyframe.serialization.analysis.RemoteAnalysisCachingEventListener.SerializedNodeEvent; +import com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.FrontierNodeVersion; import com.google.devtools.build.lib.skyframe.serialization.analysis.RemoteAnalysisCachingOptions.RemoteAnalysisCacheMode; import com.google.devtools.build.lib.versioning.LongVersionGetter; import com.google.devtools.build.skyframe.InMemoryGraph; import com.google.devtools.build.skyframe.InMemoryNodeEntry; import com.google.devtools.build.skyframe.SkyKey; -import com.google.protobuf.ByteString; import java.io.BufferedOutputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -73,7 +67,6 @@ * --experimental_remote_analysis_cache_mode=upload}. */ public final class FrontierSerializer { - private FrontierSerializer() {} /** @@ -85,7 +78,7 @@ private FrontierSerializer() {} public static Optional serializeAndUploadFrontier( RemoteAnalysisCachingDependenciesProvider dependenciesProvider, SkyframeExecutor skyframeExecutor, - LongVersionGetter unusedVersionGetter, + LongVersionGetter versionGetter, Reporter reporter, EventBus eventBus) throws InterruptedException { @@ -113,7 +106,6 @@ public static Optional serializeAndUploadFrontier( return Optional.empty(); } - var profileCollector = new ProfileCollector(); ObjectCodecs codecs; try { codecs = futureCodecs.get(); @@ -129,34 +121,44 @@ public static Optional serializeAndUploadFrontier( reporter.handle(Event.info(String.format("Initializing codecs took %s\n", stopwatch))); - var writeStatuses = Collections.synchronizedList(new ArrayList>()); - AtomicInteger frontierValueCount = new AtomicInteger(); - selection.entrySet().parallelStream() - .forEach( - entry -> { - if (entry.getValue() != FRONTIER_CANDIDATE) { - return; - } + FrontierNodeVersion frontierVersion; + try { + frontierVersion = dependenciesProvider.getSkyValueVersion(); + } catch (SerializationException e) { + String message = "error computing frontier version " + e.getMessage(); + reporter.error(null, message); + return Optional.of(createFailureDetail(message, Code.SERIALIZED_FRONTIER_PROFILE_FAILED)); + } - SkyKey key = entry.getKey(); + var profileCollector = new ProfileCollector(); + var frontierValueCount = new AtomicInteger(); - try { - serializeAndUploadEntry( - dependenciesProvider, codecs, key, profileCollector, writeStatuses, graph); - frontierValueCount.getAndIncrement(); - eventBus.post(new SerializedNodeEvent(key)); - } catch (SerializationException e) { - writeStatuses.add(immediateFailedFuture(e)); - } - }); + if (versionGetter == null) { + if (isInTest()) { + versionGetter = getVersionGetterForTesting(); + } else { + throw new NullPointerException("missing versionGetter"); + } + } + + ListenableFuture writeStatus = + SelectedEntrySerializer.uploadSelection( + graph, + versionGetter, + codecs, + frontierVersion, + selection, + dependenciesProvider.getFingerprintValueService(), + eventBus, + profileCollector, + frontierValueCount); reporter.handle( Event.info( String.format("Serialized %s frontier entries in %s", frontierValueCount, stopwatch))); try { - var unusedNull = - Futures.whenAllSucceed(writeStatuses).call(() -> null, directExecutor()).get(); + var unusedNull = writeStatus.get(); } catch (ExecutionException e) { Throwable cause = e.getCause(); String message = cause.getMessage(); @@ -185,43 +187,6 @@ public static Optional serializeAndUploadFrontier( return Optional.empty(); } - private static void serializeAndUploadEntry( - RemoteAnalysisCachingDependenciesProvider dependenciesProvider, - ObjectCodecs codecs, - SkyKey key, - ProfileCollector profileCollector, - List> writeStatuses, - InMemoryGraph graph) - throws SerializationException { - var fingerprintValueService = dependenciesProvider.getFingerprintValueService(); - SerializationResult keyBytes = - codecs.serializeMemoizedAndBlocking(fingerprintValueService, key, profileCollector); - var keyWriteStatus = keyBytes.getFutureToBlockWritesOn(); - if (keyWriteStatus != null) { - writeStatuses.add(keyWriteStatus); - } - - InMemoryNodeEntry node = checkNotNull(graph.getIfPresent(key), key); - SerializationResult valueBytes = - codecs.serializeMemoizedAndBlocking( - fingerprintValueService, node.getValue(), profileCollector); - var writeStatusFuture = valueBytes.getFutureToBlockWritesOn(); - if (writeStatusFuture != null) { - writeStatuses.add(writeStatusFuture); - } - - // Associates the SkyKey to the SkyValue. - // - // TODO: b/364831651 - determine the version metadata that should also be part of this key. - writeStatuses.add( - fingerprintValueService.put( - fingerprintValueService.fingerprint( - dependenciesProvider - .getSkyValueVersion() - .concat(keyBytes.getObject().toByteArray())), - valueBytes.getObject().toByteArray())); - } - private static void dumpUploadManifest(PrintStream out, Map selection) { var frontierCandidates = ImmutableList.builder(); var activeSet = ImmutableList.builder(); diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/LongVersionGetterTestInjection.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/LongVersionGetterTestInjection.java new file mode 100644 index 00000000000000..6b8c7af6b7d40c --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/LongVersionGetterTestInjection.java @@ -0,0 +1,47 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.skyframe.serialization.analysis; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.devtools.build.lib.util.TestType.isInTest; + +import com.google.devtools.build.lib.versioning.LongVersionGetter; + +/** + * Allows injecting a {@link LongVersionGetter} implementation to {@link FrontierSerializer} in + * tests. + */ +@SuppressWarnings("NonFinalStaticField") +public final class LongVersionGetterTestInjection { + private static LongVersionGetter versionGetter = null; + private static boolean wasAccessed = false; + + static LongVersionGetter getVersionGetterForTesting() { + checkState(isInTest()); + wasAccessed = true; + return checkNotNull(versionGetter, "injectVersionGetterForTesting must be called first"); + } + + public static void injectVersionGetterForTesting(LongVersionGetter versionGetter) { + checkState(isInTest()); + LongVersionGetterTestInjection.versionGetter = versionGetter; + } + + public static boolean wasGetterAccessed() { + return wasAccessed; + } + + private LongVersionGetterTestInjection() {} +} diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/SelectedEntrySerializer.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/SelectedEntrySerializer.java new file mode 100644 index 00000000000000..dbc225ad648ef0 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis/SelectedEntrySerializer.java @@ -0,0 +1,407 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.skyframe.serialization.analysis; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.util.concurrent.Futures.immediateFailedFuture; +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; +import static com.google.devtools.build.lib.skyframe.FileOpNodeOrFuture.EmptyFileOpNode.EMPTY_FILE_OP_NODE; +import static com.google.devtools.build.lib.skyframe.serialization.analysis.FrontierSerializer.SelectionMarking.FRONTIER_CANDIDATE; +import static com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.ConstantFileData.CONSTANT_FILE; +import static com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.ConstantListingData.CONSTANT_LISTING; +import static com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.ConstantNodeData.CONSTANT_NODE; +import static com.google.devtools.build.lib.skyframe.serialization.proto.DataType.DATA_TYPE_EMPTY; +import static com.google.devtools.build.lib.skyframe.serialization.proto.DataType.DATA_TYPE_FILE; +import static com.google.devtools.build.lib.skyframe.serialization.proto.DataType.DATA_TYPE_LISTING; +import static com.google.devtools.build.lib.skyframe.serialization.proto.DataType.DATA_TYPE_NODE; +import static java.util.concurrent.ForkJoinPool.commonPool; + +import com.google.common.collect.ImmutableMap; +import com.google.common.eventbus.EventBus; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.devtools.build.lib.actions.ActionLookupData; +import com.google.devtools.build.lib.actions.ActionLookupKey; +import com.google.devtools.build.lib.actions.Artifact.DerivedArtifact; +import com.google.devtools.build.lib.concurrent.QuiescingFuture; +import com.google.devtools.build.lib.skyframe.FileOpNodeOrFuture.FileOpNode; +import com.google.devtools.build.lib.skyframe.FileOpNodeOrFuture.FileOpNodeOrEmpty; +import com.google.devtools.build.lib.skyframe.FileOpNodeOrFuture.FutureFileOpNode; +import com.google.devtools.build.lib.skyframe.serialization.FingerprintValueService; +import com.google.devtools.build.lib.skyframe.serialization.ObjectCodecs; +import com.google.devtools.build.lib.skyframe.serialization.PackedFingerprint; +import com.google.devtools.build.lib.skyframe.serialization.ProfileCollector; +import com.google.devtools.build.lib.skyframe.serialization.SerializationException; +import com.google.devtools.build.lib.skyframe.serialization.SerializationResult; +import com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.FrontierNodeVersion; +import com.google.devtools.build.lib.skyframe.serialization.analysis.FrontierSerializer.SelectionMarking; +import com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.FileInvalidationDataInfo; +import com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.FutureFileDataInfo; +import com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.FutureListingDataInfo; +import com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.FutureNodeDataInfo; +import com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.InvalidationDataInfo; +import com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.ListingInvalidationDataInfo; +import com.google.devtools.build.lib.skyframe.serialization.analysis.InvalidationDataInfoOrFuture.NodeInvalidationDataInfo; +import com.google.devtools.build.lib.skyframe.serialization.analysis.RemoteAnalysisCachingEventListener.SerializedNodeEvent; +import com.google.devtools.build.lib.versioning.LongVersionGetter; +import com.google.devtools.build.skyframe.InMemoryGraph; +import com.google.devtools.build.skyframe.InMemoryNodeEntry; +import com.google.devtools.build.skyframe.SkyKey; +import com.google.protobuf.ByteString; +import com.google.protobuf.CodedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import javax.annotation.Nullable; + +/** + * Supports uploading a selected set of {@link SkyKey}s to a {@link FingerprintValueService}. + * + *

Execution is encapsulated via the {@link #uploadSelection} method. + * + *

Each persisted entry consists of the following: + * + *

    + *
  • Key: formatted as {@code fingerprint(, )}. + *
  • Invalidation Data and Value: formatted as {@code , }. + *
+ * + *

Note that both {@code } and {@code } are intended to + * have relatively small immediate representations. When there is a large amount of data, it will be + * expressed via references (e.g., keys to a other {@link FingerprintValueService} entries). + */ +final class SelectedEntrySerializer implements Consumer> { + private final InMemoryGraph graph; + private final ObjectCodecs codecs; + private final FrontierNodeVersion frontierVersion; + + private final FingerprintValueService fingerprintValueService; + + private final FileOpNodeMemoizingLookup fileOpNodes; + private final FileDependencySerializer fileDependencySerializer; + + private final WriteStatusesFuture writeStatuses; + + private final EventBus eventBus; + private final ProfileCollector profileCollector; + private final AtomicInteger frontierValueCount; + + /** + * Uploads the {@link FRONTIER_CANDIDATE} marked entries of {@code selection} to {@code + * fingerprintValueService}. + */ + static ListenableFuture uploadSelection( + InMemoryGraph graph, + LongVersionGetter versionGetter, + ObjectCodecs codecs, + FrontierNodeVersion frontierVersion, + ImmutableMap selection, + FingerprintValueService fingerprintValueService, + EventBus eventBus, + ProfileCollector profileCollector, + AtomicInteger frontierValueCount) { + var fileOpNodes = new FileOpNodeMemoizingLookup(graph); + var fileDependencySerializer = + new FileDependencySerializer(versionGetter, graph, fingerprintValueService); + var writeStatuses = new WriteStatusesFuture(); + var serializer = + new SelectedEntrySerializer( + graph, + codecs, + frontierVersion, + fingerprintValueService, + fileOpNodes, + fileDependencySerializer, + writeStatuses, + eventBus, + profileCollector, + frontierValueCount); + selection.entrySet().parallelStream().forEach(serializer); + writeStatuses.notifyAllStarted(); + return writeStatuses; + } + + private SelectedEntrySerializer( + InMemoryGraph graph, + ObjectCodecs codecs, + FrontierNodeVersion frontierVersion, + FingerprintValueService fingerprintValueService, + FileOpNodeMemoizingLookup fileOpNodes, + FileDependencySerializer fileDependencySerializer, + WriteStatusesFuture writeStatuses, + EventBus eventBus, + ProfileCollector profileCollector, + AtomicInteger frontierValueCount) { + this.graph = graph; + this.codecs = codecs; + this.frontierVersion = frontierVersion; + this.fingerprintValueService = fingerprintValueService; + this.fileOpNodes = fileOpNodes; + this.fileDependencySerializer = fileDependencySerializer; + this.writeStatuses = writeStatuses; + this.eventBus = eventBus; + this.profileCollector = profileCollector; + this.frontierValueCount = frontierValueCount; + } + + @Override + public void accept(Map.Entry entry) { + if (!entry.getValue().equals(FRONTIER_CANDIDATE)) { + return; + } + SkyKey key = entry.getKey(); + + // TODO: b/371508153 - only upload nodes that were freshly computed by this invocation and + // unaffected by local, un-submitted changes. + + try { + switch (key) { + case ActionLookupKey actionLookupKey: + uploadEntry(actionLookupKey, actionLookupKey); + break; + case ActionLookupData lookupData: + uploadEntry(lookupData, checkNotNull(lookupData.getActionLookupKey(), lookupData)); + break; + case DerivedArtifact artifact: + // This case handles the subclasses of DerivedArtifact. DerivedArtifact itself will show + // up here as ActionLookupData. + uploadEntry(artifact, checkNotNull(artifact.getArtifactOwner(), artifact)); + break; + default: + throw new AssertionError("Unexpected selected type: " + key + " " + key.getClass()); + } + frontierValueCount.getAndIncrement(); + eventBus.post(new SerializedNodeEvent(key)); + } catch (SerializationException e) { + writeStatuses.addWriteStatus(immediateFailedFuture(e)); + } + } + + /** + * Uploads the entry associated with {@code key} persisting alongside it, the file dependencies + * associated with {@code dependencyKey}. + */ + private void uploadEntry(SkyKey key, ActionLookupKey dependencyKey) + throws SerializationException { + writeStatuses.selectedEntryStarting(); + commonPool().execute(new FileOpNodeProcessor(serializeEntry(key), dependencyKey)); + } + + /** Key and value bytes representing a serialized Skyframe entry. */ + private static class SerializedEntry { + private final PackedFingerprint versionedKey; + private final byte[] valueBytes; + + private SerializedEntry(PackedFingerprint versionedKey, byte[] valueBytes) { + this.versionedKey = versionedKey; + this.valueBytes = valueBytes; + } + } + + private SerializedEntry serializeEntry(SkyKey key) throws SerializationException { + SerializationResult keyBytes = + codecs.serializeMemoizedAndBlocking(fingerprintValueService, key, profileCollector); + writeStatuses.addWriteStatus(keyBytes.getFutureToBlockWritesOn()); + + InMemoryNodeEntry node = checkNotNull(graph.getIfPresent(key), key); + SerializationResult valueBytes = + codecs.serializeMemoizedAndBlocking( + fingerprintValueService, node.getValue(), profileCollector); + writeStatuses.addWriteStatus(valueBytes.getFutureToBlockWritesOn()); + + PackedFingerprint versionedKey = + fingerprintValueService.fingerprint( + frontierVersion.concat(keyBytes.getObject().toByteArray())); + + return new SerializedEntry(versionedKey, valueBytes.getObject().toByteArray()); + } + + private final class FileOpNodeProcessor implements FutureCallback, Runnable { + private final SerializedEntry entry; + private final ActionLookupKey dependencyKey; + + private FileOpNodeProcessor(SerializedEntry entry, ActionLookupKey dependencyKey) { + this.entry = entry; + this.dependencyKey = dependencyKey; + } + + @Override + public void run() { + switch (fileOpNodes.computeNode(dependencyKey)) { + case FileOpNodeOrEmpty nodeOrEmpty: + onSuccess(nodeOrEmpty); + break; + case FutureFileOpNode future: + Futures.addCallback(future, this, directExecutor()); + break; + } + } + + @Override + public final void onSuccess(FileOpNodeOrEmpty nodeOrEmpty) { + var handler = new InvalidationDataInfoHandler(); + switch (nodeOrEmpty) { + case FileOpNode node: + switch (fileDependencySerializer.registerDependency(node)) { + case InvalidationDataInfo dataInfo: + handler.onSuccess(dataInfo); + break; + case FutureFileDataInfo futureFile: + Futures.addCallback(futureFile, handler, directExecutor()); + break; + case FutureListingDataInfo futureListing: + Futures.addCallback(futureListing, handler, directExecutor()); + break; + case FutureNodeDataInfo futureNode: + Futures.addCallback(futureNode, handler, directExecutor()); + break; + } + break; + case EMPTY_FILE_OP_NODE: + handler.onSuccess(/* dataInfo= */ null); + break; + } + } + + @Override + public final void onFailure(Throwable t) { + writeStatuses.notifyWriteFailure(t); + } + + private final class InvalidationDataInfoHandler + implements FutureCallback { + /** + * Saves the entry for to the {@link FingerprintValueService}. + * + *

The entry includes both value and the associated invalidation data. More precisely, it + * consists of the following components. + * + *

    + *
  1. If {@code dataInfo} is null, the invalidation data is the {@link DATA_TYPE_EMPTY} + * value only. + *
  2. Otherwise when {@code dataInfo} is non-null, the invalidation data starts with a type + * value, {@link DATA_TYPE_FILE}, {@link DATA_TYPE_LISTING}, or {@link DATA_TYPE_NODE} + * depending on {@code dataInfo}'s type. The invalidation data cache key follows the + * type value. + *
  3. The {@link #entry}'s value bytes. + *
+ */ + @Override + public void onSuccess(@Nullable InvalidationDataInfo dataInfo) { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + CodedOutputStream codedOut = CodedOutputStream.newInstance(bytesOut); + + try { + switch (dataInfo) { + case CONSTANT_FILE: + case CONSTANT_LISTING: + case CONSTANT_NODE: + // fall through + case null: + codedOut.writeEnumNoTag(DATA_TYPE_EMPTY.getNumber()); + break; + case FileInvalidationDataInfo file: + codedOut.writeEnumNoTag(DATA_TYPE_FILE.getNumber()); + codedOut.writeStringNoTag(file.cacheKey()); + writeStatuses.addWriteStatus(file.writeStatus()); + break; + case ListingInvalidationDataInfo listing: + codedOut.writeEnumNoTag(DATA_TYPE_LISTING.getNumber()); + codedOut.writeStringNoTag(listing.cacheKey()); + writeStatuses.addWriteStatus(listing.writeStatus()); + break; + case NodeInvalidationDataInfo node: + codedOut.writeEnumNoTag(DATA_TYPE_NODE.getNumber()); + node.cacheKey().writeTo(codedOut); + writeStatuses.addWriteStatus(node.writeStatus()); + break; + } + codedOut.writeRawBytes(entry.valueBytes); + codedOut.flush(); + } catch (IOException e) { + // A ByteArrayOutputStream backed CodedOutputStream doesn't throw IOExceptions. + throw new AssertionError(e); + } + byte[] entryBytes = bytesOut.toByteArray(); + writeStatuses.addWriteStatus(fingerprintValueService.put(entry.versionedKey, entryBytes)); + + // IMPORTANT: when this completes, no more write statuses can be added. + writeStatuses.selectedEntryDone(); + } + + @Override + public final void onFailure(Throwable t) { + writeStatuses.notifyWriteFailure(t); + } + } + } + + private static class WriteStatusesFuture extends QuiescingFuture + implements FutureCallback { + private void selectedEntryStarting() { + increment(); + } + + private void selectedEntryDone() { + decrement(); + } + + private void notifyAllStarted() { + decrement(); + } + + private void notifyWriteFailure(Throwable t) { + notifyException(t); + } + + @Override + protected Void getValue() { + return null; + } + + private void addWriteStatus(@Nullable ListenableFuture writeStatus) { + if (writeStatus == null) { + return; + } + increment(); + Futures.addCallback(writeStatus, (FutureCallback) this, directExecutor()); + } + + /** + * Implementation of {@link FutureCallback}. + * + * @deprecated only for use via {@link #addWriteStatus} + */ + @Override + @Deprecated // only called via addWriteStatus + public void onSuccess(Void unused) { + decrement(); + } + + /** + * Implementation of {@link FutureCallback}. + * + * @deprecated only for use via {@link #addWriteStatus} + */ + @Override + @Deprecated // only called via addWriteStatus + public void onFailure(Throwable t) { + notifyWriteFailure(t); + } + } +} diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils/BUILD b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils/BUILD index 31c2f85f4c2b46..89d350d410a229 100644 --- a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils/BUILD +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils/BUILD @@ -27,6 +27,7 @@ java_library( ["*.java"], exclude = DUMPER_SOURCES + [ "FakeDirectories.java", + "FakeInvalidationDataHelper.java", "RoundTripping.java", "SerializationDepsUtils.java", ], @@ -93,3 +94,12 @@ java_library( "//third_party:jsr305", ], ) + +java_library( + name = "fake_invalidation_data_helper", + srcs = ["FakeInvalidationDataHelper.java"], + deps = [ + "//src/main/protobuf:file_invalidation_data_java_proto", + "@com_google_protobuf//:protobuf_java", + ], +) diff --git a/src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils/FakeInvalidationDataHelper.java b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils/FakeInvalidationDataHelper.java new file mode 100644 index 00000000000000..7bcad8406c5572 --- /dev/null +++ b/src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils/FakeInvalidationDataHelper.java @@ -0,0 +1,48 @@ +// Copyright 2025 The Bazel Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.devtools.build.lib.skyframe.serialization.testutils; + +import static com.google.devtools.build.lib.skyframe.serialization.proto.DataType.DATA_TYPE_EMPTY; + +import com.google.protobuf.ByteString; +import com.google.protobuf.CodedOutputStream; +import java.io.IOException; + +/** + * Helper for injecting fake file invalidation data. + * + *

{@link com.google.devtools.build.lib.skyframe.serialization.analysis.FrontierSerializer} + * inserts invalidation data the serialized bytes of top-level serialized values. Tests that don't + * use the frontier serializer can use {@link #prependFakeInvalidationData} to inject a stubbed + * version of this data. + */ +public final class FakeInvalidationDataHelper { + + /** Prepends the {@code value} bytes with fake invalidation data. */ + public static ByteString prependFakeInvalidationData(ByteString value) { + // We expect the DATA_TYPE_EMPTY ordinal to occupy only 1 byte. + ByteString.Output output = ByteString.newOutput(value.size() + 1); + CodedOutputStream codedOutput = CodedOutputStream.newInstance(output); + try { + codedOutput.writeEnumNoTag(DATA_TYPE_EMPTY.getNumber()); + codedOutput.flush(); // Important to flush before writing the original ByteString + value.writeTo(output); + } catch (IOException e) { + throw new AssertionError(e); // No failure expected here. + } + return output.toByteString(); + } + + private FakeInvalidationDataHelper() {} +} diff --git a/src/main/protobuf/file_invalidation_data.proto b/src/main/protobuf/file_invalidation_data.proto index 41e848da61fd0f..48abad1e1b6470 100644 --- a/src/main/protobuf/file_invalidation_data.proto +++ b/src/main/protobuf/file_invalidation_data.proto @@ -147,3 +147,15 @@ message Symlink { // Optional. int64 parent_mtsv = 2; } + +// Markers to indicate different types of invalidation data. +enum DataType { + // To conform with: + // https://protobuf.dev/programming-guides/style/#enums + DATA_TYPE_UNSPECIFIED = 0; + DATA_TYPE_EMPTY = 1; // No invalidation data. + DATA_TYPE_FILE = 2; // FileInvalidationData. + DATA_TYPE_LISTING = 3; // DirectoryListingInvalidationData. + // A node type, representing a recursive aggregate of files and listings. + DATA_TYPE_NODE = 4; +} diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD index 24c050ac2b7b83..db439da576eb52 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD +++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/BUILD @@ -437,6 +437,7 @@ java_library( "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec:serialization-constant", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils:dumper", + "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils:fake_invalidation_data_helper", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/testutils:round-tripping", "//src/main/java/com/google/devtools/build/lib/vfs", "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment", diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java index ac756ccba8c326..088b691982d8f0 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/SkyValueRetrieverTest.java @@ -19,6 +19,7 @@ import static com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.ObservedFutureStatus.DONE; import static com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.ObservedFutureStatus.NOT_DONE; import static com.google.devtools.build.lib.skyframe.serialization.SkyValueRetriever.Restart.RESTART; +import static com.google.devtools.build.lib.skyframe.serialization.testutils.FakeInvalidationDataHelper.prependFakeInvalidationData; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; @@ -681,7 +682,7 @@ private void uploadKeyValuePair( .put( fingerprintValueService.fingerprint( version.concat(keyBytes.getObject().toByteArray())), - valueBytes.getObject().toByteArray()) + prependFakeInvalidationData(valueBytes.getObject()).toByteArray()) .get(); } diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD index e66ca50dd2bae7..3f5f3ef29ebd24 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD +++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/BUILD @@ -33,6 +33,7 @@ java_library( "//third_party:guava", "//third_party:jsr305", "//third_party:junit4", + "//third_party:mockito", "//third_party:truth", "//third_party/pprof:profile_java_proto", "@com_google_protobuf//:protobuf_java", @@ -49,7 +50,10 @@ java_test( "//src/main/java/com/google/devtools/build/lib/skyframe:sky_functions", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization", "//src/main/java/com/google/devtools/build/lib/skyframe/serialization:serialization_module", + "//src/main/java/com/google/devtools/build/lib/skyframe/serialization/analysis:long_version_getter_test_injection", + "//src/main/java/com/google/devtools/build/lib/versioning:long_version_getter", "//third_party:junit4", + "//third_party:mockito", "//third_party:truth", ], ) diff --git a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializerTest.java b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializerTest.java index ef464c02b8fa6a..7a2776b8d09158 100644 --- a/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializerTest.java +++ b/src/test/java/com/google/devtools/build/lib/skyframe/serialization/analysis/FrontierSerializerTest.java @@ -14,17 +14,28 @@ package com.google.devtools.build.lib.skyframe.serialization.analysis; import static com.google.common.truth.Truth.assertThat; +import static com.google.devtools.build.lib.skyframe.serialization.analysis.LongVersionGetterTestInjection.injectVersionGetterForTesting; +import static org.mockito.Mockito.mock; import com.google.devtools.build.lib.runtime.BlazeRuntime; import com.google.devtools.build.lib.skyframe.SkyFunctions; import com.google.devtools.build.lib.skyframe.serialization.FingerprintValueService; import com.google.devtools.build.lib.skyframe.serialization.SerializationModule; +import com.google.devtools.build.lib.versioning.LongVersionGetter; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @RunWith(JUnit4.class) public final class FrontierSerializerTest extends FrontierSerializerTestBase { + private final LongVersionGetter versionGetter = mock(LongVersionGetter.class); + + @Before + public void injectVersionGetter() { + injectVersionGetterForTesting(versionGetter); + } + private class ModuleWithOverrides extends SerializationModule { @Override