Adding stable sorting for execlog
RELNOTES: Introducing --execution_log_binary_file and --execution_log_json_file that output a stable sorted execution log. They will offer a stable replacement to --experimental_execution_log_file.
PiperOrigin-RevId: 243808737
diff --git a/src/main/java/com/google/devtools/build/lib/BUILD b/src/main/java/com/google/devtools/build/lib/BUILD
index 34a6a8d..1c1bc6d 100644
--- a/src/main/java/com/google/devtools/build/lib/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/BUILD
@@ -140,6 +140,7 @@
"//src/main/java/com/google/devtools/build/lib/vfs",
"//third_party:guava",
"//third_party/protobuf:protobuf_java",
+ "//third_party/protobuf:protobuf_java_util",
],
)
@@ -867,6 +868,7 @@
":runtime",
":util",
"//src/main/java/com/google/devtools/build/lib/actions",
+ "//src/main/java/com/google/devtools/build/lib/bazel/execlog:stable_sort",
"//src/main/java/com/google/devtools/build/lib/shell",
"//src/main/java/com/google/devtools/build/lib/skyframe/serialization/autocodec",
"//src/main/java/com/google/devtools/build/lib/vfs",
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java b/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java
index ab03ba8..55779ab 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/SpawnLogModule.java
@@ -13,6 +13,7 @@
// limitations under the License.
package com.google.devtools.build.lib.bazel;
+import com.google.devtools.build.lib.bazel.execlog.StableSort;
import com.google.devtools.build.lib.buildtool.BuildRequest;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.exec.ExecutionOptions;
@@ -23,46 +24,136 @@
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.AsynchronousFileOutputStream;
+import com.google.devtools.build.lib.util.io.MessageOutputStreamWrapper.BinaryOutputStreamWrapper;
+import com.google.devtools.build.lib.util.io.MessageOutputStreamWrapper.JsonOutputStreamWrapper;
+import com.google.devtools.build.lib.util.io.MessageOutputStreamWrapper.MessageOutputStreamCollection;
+import com.google.devtools.build.lib.vfs.FileSystem;
+import com.google.devtools.build.lib.vfs.Path;
+import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
/**
* Module providing on-demand spawn logging.
*/
public final class SpawnLogModule extends BlazeModule {
+ /**
+ * SpawnLogContext will log to a temporary file as the execution is being performed. rawOutput is
+ * the path to that temporary file.
+ */
private SpawnLogContext spawnLogContext;
+ private Path rawOutput;
+
+ /**
+ * After the execution is done, the temporary file contents will be sorted and logged as the user
+ * requested, to binary and/or json files. We will open the streams at the beginning of the
+ * command so that any errors (e.g., unwritable location) will be surfaced before the execution
+ * begins.
+ */
+ private MessageOutputStreamCollection outputStreams;
+
+ private CommandEnvironment env;
+
+ private void clear() {
+ spawnLogContext = null;
+ outputStreams = new MessageOutputStreamCollection();
+ rawOutput = null;
+ env = null;
+ }
+
+ private void initOutputs(CommandEnvironment env) throws IOException {
+ clear();
+ this.env = env;
+
+ ExecutionOptions executionOptions = env.getOptions().getOptions(ExecutionOptions.class);
+ if (executionOptions == null) {
+ return;
+ }
+ FileSystem fileSystem = env.getRuntime().getFileSystem();
+ Path workingDirectory = env.getWorkingDirectory();
+
+ if (executionOptions.executionLogBinaryFile != null) {
+ outputStreams.addStream(
+ new BinaryOutputStreamWrapper(
+ workingDirectory
+ .getRelative(executionOptions.executionLogBinaryFile)
+ .getOutputStream()));
+ }
+
+ if (executionOptions.executionLogJsonFile != null) {
+ outputStreams.addStream(
+ new JsonOutputStreamWrapper(
+ workingDirectory
+ .getRelative(executionOptions.executionLogJsonFile)
+ .getOutputStream()));
+ }
+
+ AsynchronousFileOutputStream outStream = null;
+ if (executionOptions.executionLogFile != null) {
+ rawOutput = env.getRuntime().getFileSystem().getPath(executionOptions.executionLogFile);
+ outStream =
+ new AsynchronousFileOutputStream(
+ workingDirectory.getRelative(executionOptions.executionLogFile));
+ } else if (!outputStreams.isEmpty()) {
+ // Execution log requested but raw log file not specified
+ File file = File.createTempFile("exec", ".log");
+ rawOutput = fileSystem.getPath(file.getAbsolutePath());
+ outStream = new AsynchronousFileOutputStream(rawOutput);
+ }
+
+ if (outStream == null) {
+ // No logging needed
+ clear();
+ return;
+ }
+
+ spawnLogContext = new SpawnLogContext(env.getExecRoot(), outStream);
+ }
+
@Override
public void executorInit(CommandEnvironment env, BuildRequest request, ExecutorBuilder builder) {
env.getEventBus().register(this);
- ExecutionOptions executionOptions = env.getOptions().getOptions(ExecutionOptions.class);
- if (executionOptions != null
- && executionOptions.executionLogFile != null
- && !executionOptions.executionLogFile.isEmpty()) {
- try {
- spawnLogContext = new SpawnLogContext(
- env.getExecRoot(),
- new AsynchronousFileOutputStream(executionOptions.executionLogFile));
- } catch (IOException e) {
- env.getReporter().handle(Event.error(e.getMessage()));
- env.getBlazeModuleEnvironment().exit(new AbruptExitException(
- "Error found creating SpawnLogContext", ExitCode.COMMAND_LINE_ERROR));
- }
+
+ try {
+ initOutputs(env);
+ } catch (IOException e) {
+ env.getReporter().handle(Event.error(e.getMessage()));
+ env.getBlazeModuleEnvironment()
+ .exit(
+ new AbruptExitException(
+ "Error initializing execution log", ExitCode.COMMAND_LINE_ERROR, e));
+ }
+
+ if (spawnLogContext != null) {
builder.addActionContext(spawnLogContext);
builder.addStrategyByContext(SpawnLogContext.class, "");
- } else {
- spawnLogContext = null;
}
}
@Override
- public void afterCommand() {
+ public void afterCommand() throws AbruptExitException {
+ boolean done = false;
if (spawnLogContext != null) {
try {
spawnLogContext.close();
+ if (!outputStreams.isEmpty()) {
+ InputStream in = rawOutput.getInputStream();
+ StableSort.stableSort(in, outputStreams);
+ outputStreams.close();
+ }
+ done = true;
} catch (IOException e) {
- throw new RuntimeException(e);
+ throw new AbruptExitException(ExitCode.LOCAL_ENVIRONMENTAL_ERROR, e);
} finally {
- spawnLogContext = null;
+ if (!done && !outputStreams.isEmpty()) {
+ env.getReporter()
+ .handle(
+ Event.warn(
+ "Execution log might not have been populated. Raw execution log is at "
+ + rawOutput));
+ }
+ clear();
}
}
}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD b/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD
index 59f1202..8cb3158 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/BUILD
@@ -57,6 +57,7 @@
"//src/main/java/com/google/devtools/build/lib:io",
"//src/main/java/com/google/devtools/build/lib:runtime",
"//src/main/java/com/google/devtools/build/lib:util",
+ "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//src/main/java/com/google/devtools/common/options",
"//third_party:guava",
],
@@ -66,6 +67,8 @@
name = "debugging-options",
srcs = ["DebuggingOptions.java"],
deps = [
+ "//src/main/java/com/google/devtools/build/lib:util",
+ "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
"//src/main/java/com/google/devtools/common/options",
],
)
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java b/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java
index 4bed8b0..db2fef8 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/DebuggingOptions.java
@@ -14,6 +14,8 @@
package com.google.devtools.build.lib.bazel.debug;
+import com.google.devtools.build.lib.util.OptionsUtils;
+import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.Option;
import com.google.devtools.common.options.OptionDocumentationCategory;
import com.google.devtools.common.options.OptionEffectTag;
@@ -27,7 +29,8 @@
category = "verbosity",
documentationCategory = OptionDocumentationCategory.LOGGING,
effectTags = {OptionEffectTag.UNKNOWN},
+ converter = OptionsUtils.PathFragmentConverter.class,
help =
"Log certain Workspace Rules events into this file as delimited WorkspaceEvent protos.")
- public String workspaceRulesLogFile;
+ public PathFragment workspaceRulesLogFile;
}
diff --git a/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java b/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java
index 9efbcf6..90f6da2 100644
--- a/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java
+++ b/src/main/java/com/google/devtools/build/lib/bazel/debug/WorkspaceRuleModule.java
@@ -23,6 +23,7 @@
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.io.AsynchronousFileOutputStream;
+import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.common.options.OptionsBase;
import java.io.IOException;
@@ -42,10 +43,12 @@
return;
}
- String logFile = env.getOptions().getOptions(DebuggingOptions.class).workspaceRulesLogFile;
- if (logFile != null && !logFile.isEmpty()) {
+ PathFragment logFile =
+ env.getOptions().getOptions(DebuggingOptions.class).workspaceRulesLogFile;
+ if (logFile != null) {
try {
- outFileStream = new AsynchronousFileOutputStream(logFile);
+ outFileStream =
+ new AsynchronousFileOutputStream(env.getWorkingDirectory().getRelative(logFile));
} catch (IOException e) {
env.getReporter().handle(Event.error(e.getMessage()));
env.getBlazeModuleEnvironment()
diff --git a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
index 3247b924..843178d 100644
--- a/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/exec/ExecutionOptions.java
@@ -436,14 +436,36 @@
public boolean statsSummary;
@Option(
- name = "experimental_execution_log_file",
- defaultValue = "",
- category = "verbosity",
- documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
- effectTags = {OptionEffectTag.UNKNOWN},
- help = "Log the executed spawns into this file as delimited Spawn protos."
- )
- public String executionLogFile;
+ name = "experimental_execution_log_file",
+ defaultValue = "null",
+ category = "verbosity",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "Log the executed spawns into this file as delimited Spawn protos.")
+ public PathFragment executionLogFile;
+
+ @Option(
+ name = "execution_log_binary_file",
+ defaultValue = "null",
+ category = "verbosity",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help = "Log the executed spawns into this file as delimited Spawn protos.")
+ public PathFragment executionLogBinaryFile;
+
+ @Option(
+ name = "execution_log_json_file",
+ defaultValue = "null",
+ category = "verbosity",
+ documentationCategory = OptionDocumentationCategory.UNCATEGORIZED,
+ effectTags = {OptionEffectTag.UNKNOWN},
+ converter = OptionsUtils.PathFragmentConverter.class,
+ help =
+ "Log the executed spawns into this file as json representation of the delimited Spawn"
+ + " protos.")
+ public PathFragment executionLogJsonFile;
@Option(
name = "experimental_split_xml_generation",
diff --git a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
index 9420d8c..894b767 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/RemoteModule.java
@@ -174,8 +174,10 @@
try {
List<ClientInterceptor> interceptors = new ArrayList<>();
- if (!remoteOptions.experimentalRemoteGrpcLog.isEmpty()) {
- rpcLogFile = new AsynchronousFileOutputStream(remoteOptions.experimentalRemoteGrpcLog);
+ if (remoteOptions.experimentalRemoteGrpcLog != null) {
+ rpcLogFile =
+ new AsynchronousFileOutputStream(
+ env.getWorkingDirectory().getRelative(remoteOptions.experimentalRemoteGrpcLog));
interceptors.add(new LoggingInterceptor(rpcLogFile, env.getRuntime().getClock()));
}
diff --git a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
index 961b108..755b6e5 100644
--- a/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
+++ b/src/main/java/com/google/devtools/build/lib/remote/options/RemoteOptions.java
@@ -205,9 +205,10 @@
@Option(
name = "experimental_remote_grpc_log",
- defaultValue = "",
+ defaultValue = "null",
category = "remote",
documentationCategory = OptionDocumentationCategory.REMOTE,
+ converter = OptionsUtils.PathFragmentConverter.class,
effectTags = {OptionEffectTag.UNKNOWN},
help =
"If specified, a path to a file to log gRPC call related details. This log consists of a"
@@ -216,7 +217,7 @@
+ "protobufs with each message prefixed by a varint denoting the size of the"
+ " following serialized protobuf message, as performed by the method "
+ "LogEntry.writeDelimitedTo(OutputStream).")
- public String experimentalRemoteGrpcLog;
+ public PathFragment experimentalRemoteGrpcLog;
@Option(
name = "incompatible_remote_symlinks",
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/AsynchronousFileOutputStream.java b/src/main/java/com/google/devtools/build/lib/util/io/AsynchronousFileOutputStream.java
index 172750b..1482e54 100644
--- a/src/main/java/com/google/devtools/build/lib/util/io/AsynchronousFileOutputStream.java
+++ b/src/main/java/com/google/devtools/build/lib/util/io/AsynchronousFileOutputStream.java
@@ -22,6 +22,7 @@
import com.google.common.util.concurrent.SettableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.devtools.build.lib.concurrent.ThreadSafety;
+import com.google.devtools.build.lib.vfs.Path;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.Message;
import com.google.protobuf.MessageLite;
@@ -29,8 +30,6 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
-import java.nio.file.Files;
-import java.nio.file.Paths;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingDeque;
@@ -50,11 +49,11 @@
// To store any exception raised from the writes.
private final AtomicReference<Throwable> exception = new AtomicReference<>();
- public AsynchronousFileOutputStream(String filename) throws IOException {
+ public AsynchronousFileOutputStream(Path path) throws IOException {
this(
- filename,
+ path.toString(),
new BufferedOutputStream( // Use a buffer of 100 kByte, scientifically chosen at random.
- Files.newOutputStream(Paths.get(filename)), 100000));
+ path.getOutputStream(), 100000));
}
@VisibleForTesting
diff --git a/src/main/java/com/google/devtools/build/lib/util/io/MessageOutputStreamWrapper.java b/src/main/java/com/google/devtools/build/lib/util/io/MessageOutputStreamWrapper.java
new file mode 100644
index 0000000..58aea9c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/util/io/MessageOutputStreamWrapper.java
@@ -0,0 +1,107 @@
+// Copyright 2019 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.util.io;
+
+import com.google.common.base.Preconditions;
+import com.google.protobuf.Message;
+import com.google.protobuf.util.JsonFormat;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+
+/** Creating a MessageOutputStream from an OutputStream */
+public class MessageOutputStreamWrapper {
+ /** Outputs the messages in binary format */
+ public static class BinaryOutputStreamWrapper implements MessageOutputStream {
+ private final OutputStream stream;
+
+ public BinaryOutputStreamWrapper(OutputStream stream) {
+ this.stream = Preconditions.checkNotNull(stream);
+ }
+
+ @Override
+ public void write(Message m) throws IOException {
+ Preconditions.checkNotNull(m);
+ m.writeDelimitedTo(stream);
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+ }
+
+ /** Outputs the messages in JSON text format */
+ public static class JsonOutputStreamWrapper implements MessageOutputStream {
+ private final OutputStream stream;
+ private final JsonFormat.Printer printer = JsonFormat.printer().includingDefaultValueFields();
+
+ public JsonOutputStreamWrapper(OutputStream stream) {
+ Preconditions.checkNotNull(stream);
+ this.stream = stream;
+ }
+
+ @Override
+ public void write(Message m) throws IOException {
+ Preconditions.checkNotNull(m);
+ stream.write(printer.print(m).getBytes(StandardCharsets.UTF_8));
+ }
+
+ @Override
+ public void close() throws IOException {
+ stream.close();
+ }
+ }
+
+ /** Outputs the messages in JSON text format */
+ public static class MessageOutputStreamCollection implements MessageOutputStream {
+ private final ArrayList<MessageOutputStream> streams = new ArrayList<>();
+
+ public boolean isEmpty() {
+ return streams.isEmpty();
+ }
+
+ public void addStream(MessageOutputStream m) {
+ streams.add(m);
+ }
+
+ @Override
+ public void write(Message m) throws IOException {
+ for (MessageOutputStream stream : streams) {
+ stream.write(m);
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ IOException firstException = null;
+ for (MessageOutputStream stream : streams) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ if (firstException == null) {
+ firstException = e;
+ } else {
+ firstException.addSuppressed(e);
+ }
+ }
+ }
+ if (firstException != null) {
+ throw firstException;
+ }
+ }
+ }
+}