blob: 5d347c797db0276194f95b81f740fa60c0c22369 [file] [log] [blame]
// Copyright 2017 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.buildeventservice;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.lib.authandtls.AuthAndTLSOptions;
import com.google.devtools.build.lib.buildeventservice.BuildEventServiceTransport.ExitFunction;
import com.google.devtools.build.lib.buildeventservice.client.BuildEventServiceClient;
import com.google.devtools.build.lib.buildeventstream.ArtifactGroupNamer;
import com.google.devtools.build.lib.buildeventstream.BuildEventArtifactUploader;
import com.google.devtools.build.lib.buildeventstream.BuildEventProtocolOptions;
import com.google.devtools.build.lib.buildeventstream.BuildEventStreamProtos.Aborted.AbortReason;
import com.google.devtools.build.lib.buildeventstream.BuildEventTransport;
import com.google.devtools.build.lib.buildeventstream.transports.BuildEventStreamOptions;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.EventHandler;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BuildEventStreamer;
import com.google.devtools.build.lib.runtime.BuildEventTransportFactory;
import com.google.devtools.build.lib.runtime.CommandEnvironment;
import com.google.devtools.build.lib.runtime.CountingArtifactGroupNamer;
import com.google.devtools.build.lib.runtime.SynchronizedOutputStream;
import com.google.devtools.build.lib.util.AbruptExitException;
import com.google.devtools.build.lib.util.ExitCode;
import com.google.devtools.build.lib.util.LoggingUtil;
import com.google.devtools.build.lib.util.io.OutErr;
import com.google.devtools.common.options.OptionsBase;
import com.google.devtools.common.options.OptionsParsingException;
import com.google.devtools.common.options.OptionsParsingResult;
import java.io.IOException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Module responsible for the Build Event Transport (BEP) and Build Event Service (BES)
* functionality.
*/
public abstract class BuildEventServiceModule<BESOptionsT extends BuildEventServiceOptions>
extends BlazeModule {
private static final Logger logger = Logger.getLogger(BuildEventServiceModule.class.getName());
private OutErr outErr;
private BuildEventStreamer streamer;
private BESOptionsT besOptions;
private BuildEventProtocolOptions bepOptions;
private AuthAndTLSOptions authTlsOptions;
private BuildEventStreamOptions besStreamOptions;
private ImmutableSet<BuildEventTransport> bepTransports;
/** Whether an error in the Build Event Service upload causes the build to fail. */
protected boolean errorsShouldFailTheBuild() {
return true;
}
/** Report errors in the command line and possibly fail the build. */
protected void reportError(
EventHandler commandLineReporter,
ModuleEnvironment moduleEnvironment,
String msg,
Exception exception,
ExitCode exitCode) {
// Don't hide unchecked exceptions as part of the error reporting.
Throwables.throwIfUnchecked(exception);
logger.log(Level.SEVERE, msg, exception);
AbruptExitException abruptException = new AbruptExitException(msg, exitCode, exception);
commandLineReporter.handle(Event.error(exception.getMessage()));
moduleEnvironment.exit(abruptException);
}
@Override
public Iterable<Class<? extends OptionsBase>> getCommonCommandOptions() {
return ImmutableList.of(
optionsClass(),
AuthAndTLSOptions.class,
BuildEventStreamOptions.class,
BuildEventProtocolOptions.class);
}
@Override
public void beforeCommand(CommandEnvironment commandEnvironment) {
OptionsParsingResult parsingResult = commandEnvironment.getOptions();
besOptions = Preconditions.checkNotNull(parsingResult.getOptions(optionsClass()));
bepOptions =
Preconditions.checkNotNull(parsingResult.getOptions(BuildEventProtocolOptions.class));
authTlsOptions = Preconditions.checkNotNull(parsingResult.getOptions(AuthAndTLSOptions.class));
besStreamOptions =
Preconditions.checkNotNull(parsingResult.getOptions(BuildEventStreamOptions.class));
// Reset to null in case afterCommand was not called.
this.outErr = null;
if (!whitelistedCommands(besOptions).contains(commandEnvironment.getCommandName())) {
return;
}
streamer = tryCreateStreamer(commandEnvironment);
if (streamer != null) {
commandEnvironment.getReporter().addHandler(streamer);
commandEnvironment.getEventBus().register(streamer);
int bufferSize = besOptions.besOuterrBufferSize;
int chunkSize = besOptions.besOuterrChunkSize;
final SynchronizedOutputStream out = new SynchronizedOutputStream(bufferSize, chunkSize);
final SynchronizedOutputStream err = new SynchronizedOutputStream(bufferSize, chunkSize);
this.outErr = OutErr.create(out, err);
streamer.registerOutErrProvider(
new BuildEventStreamer.OutErrProvider() {
@Override
public Iterable<String> getOut() {
return out.readAndReset();
}
@Override
public Iterable<String> getErr() {
return err.readAndReset();
}
});
err.registerStreamer(streamer);
out.registerStreamer(streamer);
}
}
@Override
public OutErr getOutputListener() {
return outErr;
}
@Override
public void blazeShutdownOnCrash() {
if (streamer != null) {
logger.warning("Attempting to close BES streamer on crash");
streamer.close(AbortReason.INTERNAL);
}
}
@Override
public void afterCommand() {
if (streamer != null) {
if (!streamer.isClosed()) {
// This should not occur, but close with an internal error if a {@link BuildEventStreamer}
// bug manifests as an unclosed streamer.
logger.warning("Attempting to close BES streamer after command");
String msg = "BES was not properly closed";
LoggingUtil.logToRemote(Level.WARNING, msg, new IllegalStateException(msg));
streamer.close(AbortReason.INTERNAL);
}
this.streamer = null;
}
if (!besStreamOptions.keepBackendConnections) {
clearBesClient();
}
this.outErr = null;
this.bepTransports = null;
}
/** Returns {@code null} if no stream could be created. */
@Nullable
@VisibleForTesting
BuildEventStreamer tryCreateStreamer(CommandEnvironment env) {
Supplier<BuildEventArtifactUploader> uploaderSupplier =
Suppliers.memoize(
() ->
env.getRuntime()
.getBuildEventArtifactUploaderFactoryMap()
.select(bepOptions.buildEventUploadStrategy)
.create(env));
CountingArtifactGroupNamer namer = new CountingArtifactGroupNamer();
try {
BuildEventTransport besTransport;
try {
besTransport = tryCreateBesTransport(env, uploaderSupplier, namer);
} catch (IOException | OptionsParsingException e) {
reportError(
env.getReporter(),
env.getBlazeModuleEnvironment(),
"Failed to create BuildEventTransport: " + e,
e,
(e instanceof OptionsParsingException)
? ExitCode.COMMAND_LINE_ERROR
: ExitCode.TRANSIENT_BUILD_EVENT_SERVICE_UPLOAD_ERROR);
clearBesClient();
return null;
}
ImmutableSet<BuildEventTransport> bepFileTransports =
BuildEventTransportFactory.createFromOptions(
env, env.getBlazeModuleEnvironment()::exit, bepOptions, uploaderSupplier, namer);
ImmutableSet.Builder<BuildEventTransport> transportsBuilder =
ImmutableSet.<BuildEventTransport>builder().addAll(bepFileTransports);
if (besTransport != null) {
transportsBuilder.add(besTransport);
}
bepTransports = transportsBuilder.build();
if (!bepTransports.isEmpty()) {
return new BuildEventStreamer(bepTransports, env.getReporter(), besStreamOptions, namer);
}
} catch (Exception e) {
// TODO(lpino): This generic catch with Exception shouldn't exist, remove it once the code
// above is re-structured.
reportError(
env.getReporter(),
env.getBlazeModuleEnvironment(),
e.getMessage(),
e,
ExitCode.LOCAL_ENVIRONMENTAL_ERROR);
}
return null;
}
@Nullable
private BuildEventTransport tryCreateBesTransport(
CommandEnvironment env,
Supplier<BuildEventArtifactUploader> uploaderSupplier,
ArtifactGroupNamer namer)
throws IOException, OptionsParsingException {
OptionsParsingResult optionsProvider = env.getOptions();
if (Strings.isNullOrEmpty(besOptions.besBackend)) {
clearBesClient();
return null;
} else {
String invocationId = env.getCommandId().toString();
final String besResultsUrl;
if (!Strings.isNullOrEmpty(besOptions.besResultsUrl)) {
besResultsUrl =
besOptions.besResultsUrl.endsWith("/")
? besOptions.besResultsUrl + invocationId
: besOptions.besResultsUrl + "/" + invocationId;
env.getReporter().handle(Event.info("Streaming Build Event Protocol to " + besResultsUrl));
} else {
besResultsUrl = null;
env.getReporter()
.handle(
Event.info(
String.format(
"Streaming Build Event Protocol to %s build_request_id: %s "
+ "invocation_id: %s",
besOptions.besBackend, env.getBuildRequestId(), invocationId)));
}
BuildEventServiceClient client = getBesClient(besOptions, authTlsOptions);
BuildEventArtifactUploader artifactUploader = uploaderSupplier.get();
BuildEventServiceProtoUtil besProtoUtil =
new BuildEventServiceProtoUtil(
env.getBuildRequestId(),
invocationId,
besOptions.projectId,
env.getCommandName(),
keywords(besOptions, env.getRuntime().getStartupOptionsProvider()));
BuildEventTransport besTransport =
new BuildEventServiceTransport.Builder()
.closeTimeout(besOptions.besTimeout)
.publishLifecycleEvents(besOptions.besLifecycleEvents)
.setEventBus(env.getEventBus())
.build(
client,
artifactUploader,
bepOptions,
besProtoUtil,
env.getRuntime().getClock(),
bazelExitFunction(
env.getReporter(), env.getBlazeModuleEnvironment(), besResultsUrl),
namer);
return besTransport;
}
}
protected abstract Class<BESOptionsT> optionsClass();
protected abstract BuildEventServiceClient getBesClient(
BESOptionsT besOptions, AuthAndTLSOptions authAndTLSOptions)
throws IOException, OptionsParsingException;
protected abstract void clearBesClient();
protected abstract Set<String> whitelistedCommands(BESOptionsT besOptions);
protected Set<String> keywords(
BESOptionsT besOptions, @Nullable OptionsParsingResult startupOptionsProvider) {
return besOptions.besKeywords.stream()
.map(keyword -> "user_keyword=" + keyword)
.collect(ImmutableSet.toImmutableSet());
}
private ExitFunction bazelExitFunction(
EventHandler commandLineReporter, ModuleEnvironment moduleEnvironment, String besResultsUrl) {
return (String message, Throwable cause, ExitCode exitCode) -> {
if (exitCode == ExitCode.SUCCESS) {
Preconditions.checkState(cause == null, cause);
commandLineReporter.handle(Event.info("Build Event Protocol upload finished successfully"));
if (besResultsUrl != null) {
commandLineReporter.handle(
Event.info("Build Event Protocol results available at " + besResultsUrl));
}
} else {
Preconditions.checkState(cause != null, cause);
if (errorsShouldFailTheBuild()) {
commandLineReporter.handle(Event.error(message));
moduleEnvironment.exit(new AbruptExitException(exitCode, cause));
} else {
commandLineReporter.handle(Event.warn(message));
}
if (besResultsUrl != null) {
if (!Strings.isNullOrEmpty(besResultsUrl)) {
commandLineReporter.handle(
Event.info(
"Partial Build Event Protocol results may be available at " + besResultsUrl));
}
}
}
};
}
// TODO(lpino): This method shouldn exist. It only does because some tests are relying on the
// transport creation logic of this module directly.
@VisibleForTesting
ImmutableSet<BuildEventTransport> getBepTransports() {
return bepTransports;
}
}