/*
* Copyright 2016-present Facebook, Inc.
*
* 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.facebook.buck.cli;
import com.facebook.buck.distributed.BuildJobStateSerializer;
import com.facebook.buck.distributed.DistBuildMode;
import com.facebook.buck.distributed.DistBuildService;
import com.facebook.buck.distributed.DistBuildSlaveExecutor;
import com.facebook.buck.distributed.thrift.BuildJobState;
import com.facebook.buck.distributed.thrift.RunId;
import com.facebook.buck.distributed.thrift.StampedeId;
import com.facebook.buck.event.BuckEventListener;
import com.facebook.buck.event.ConsoleEvent;
import com.facebook.buck.event.listener.DistBuildSlaveEventBusListener;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.Pair;
import com.facebook.buck.timing.DefaultClock;
import com.facebook.buck.util.Console;
import com.facebook.buck.util.HumanReadableException;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.kohsuke.args4j.Option;
public class DistBuildRunCommand extends AbstractDistBuildCommand {
public static final String BUILD_STATE_FILE_ARG_NAME = "--build-state-file";
public static final String BUILD_STATE_FILE_ARG_USAGE = "File containing the BuildStateJob data.";
@Nullable
@Option(name = BUILD_STATE_FILE_ARG_NAME, usage = BUILD_STATE_FILE_ARG_USAGE)
private String buildStateFile;
@Nullable
@Option(
name = "--coordinator-port",
usage = "The local port that the build coordinator thrift server will listen on."
)
private int coordinatorPort = -1;
@Nullable
@Option(name = "--build-mode", usage = "The mode in which the distributed build is going to run.")
private DistBuildMode distBuildMode = DistBuildMode.REMOTE_BUILD;
@Nullable
@Option(
name = "--global-cache-dir",
usage = "Full path to an existing directory that will contain a global cache across builds."
)
private Path globalCacheDir;
private static final String RUN_ID_ARG_NAME = "--buildslave-run-id";
@Nullable
@Option(name = RUN_ID_ARG_NAME, usage = "Stampede RunId for this instance of BuildSlave.")
private String runId;
@Nullable private DistBuildSlaveEventBusListener slaveEventListener;
@Override
public boolean isReadOnly() {
return false;
}
@Override
public String getShortDescription() {
return "runs a distributed build in the current machine (experimental)";
}
@Override
public int runWithoutHelp(CommandRunnerParams params) throws IOException, InterruptedException {
Stopwatch stopwatch = Stopwatch.createStarted();
Console console = params.getConsole();
try (DistBuildService service = DistBuildFactory.newDistBuildService(params)) {
if (slaveEventListener != null) {
slaveEventListener.setDistBuildService(service);
slaveEventListener.setEventBus(params.getBuckEventBus());
}
try {
Pair<BuildJobState, String> jobStateAndBuildName =
getBuildJobStateAndBuildName(params.getCell().getFilesystem(), console, service);
BuildJobState jobState = jobStateAndBuildName.getFirst();
String buildName = jobStateAndBuildName.getSecond();
console
.getStdOut()
.println(
String.format(
"BuildJob depends on a total of [%d] input deps.",
jobState.getFileHashesSize()));
try (CommandThreadManager pool =
new CommandThreadManager(
getClass().getName(), getConcurrencyLimit(params.getBuckConfig()))) {
DistBuildSlaveExecutor distBuildExecutor =
DistBuildFactory.createDistBuildExecutor(
jobState,
params,
pool.getExecutor(),
service,
Preconditions.checkNotNull(distBuildMode),
coordinatorPort,
getStampedeIdOptional(),
getGlobalCacheDirOptional());
int returnCode = distBuildExecutor.buildAndReturnExitCode();
if (returnCode == 0) {
console.printSuccess(
String.format(
"Successfully ran distributed build [%s] in [%d millis].",
buildName, stopwatch.elapsed(TimeUnit.MILLISECONDS)));
} else {
console.printErrorText(
"Failed distributed build [%s] in [%d millis].",
buildName, stopwatch.elapsed(TimeUnit.MILLISECONDS));
}
return returnCode;
}
} catch (HumanReadableException e) {
logBuildFailureEvent(e.getHumanReadableErrorMessage(), slaveEventListener);
throw e;
} catch (Exception e) {
logBuildFailureEvent(e.getMessage(), slaveEventListener);
throw e;
}
}
}
/** Logs a severe error message prefixed with {@code BUILD SLAVE FAILED} to the frontend. */
private static void logBuildFailureEvent(
String failureMessage, @Nullable DistBuildSlaveEventBusListener slaveEventListener) {
if (slaveEventListener != null) {
slaveEventListener.logEvent(
ConsoleEvent.severe(String.format("BUILD SLAVE FAILED: %s", failureMessage)));
}
}
public Pair<BuildJobState, String> getBuildJobStateAndBuildName(
ProjectFilesystem filesystem, Console console, DistBuildService service) throws IOException {
if (buildStateFile != null) {
Path buildStateFilePath = Paths.get(buildStateFile);
console
.getStdOut()
.println(
String.format("Retrieving BuildJobState for from file [%s].", buildStateFilePath));
return new Pair<>(
BuildJobStateSerializer.deserialize(filesystem.newFileInputStream(buildStateFilePath)),
String.format("LocalFile=[%s]", buildStateFile));
} else {
StampedeId stampedeId = getStampedeId();
console
.getStdOut()
.println(String.format("Retrieving BuildJobState for build [%s].", stampedeId));
return new Pair<>(
service.fetchBuildJobState(stampedeId),
String.format("DistBuild=[%s]", stampedeId.toString()));
}
}
private Optional<Path> getGlobalCacheDirOptional() {
if (globalCacheDir == null) {
return Optional.empty();
} else {
if (!Files.isDirectory(globalCacheDir)) {
try {
Files.createDirectories(globalCacheDir);
} catch (IOException exception) {
throw new HumanReadableException(
exception,
"Directory passed for --global-cache-dir cannot be created. value=[%s]",
globalCacheDir.toString());
}
}
return Optional.of(globalCacheDir);
}
}
private void checkArgs() {
if (buildStateFile == null && (!getStampedeIdOptional().isPresent() || runId == null)) {
throw new HumanReadableException(
String.format(
"Options '%s' and '%s' are both required when '%s' is not provided.",
STAMPEDE_ID_ARG_NAME, RUN_ID_ARG_NAME, BUILD_STATE_FILE_ARG_NAME));
}
}
private void initEventListener() {
if (slaveEventListener == null) {
checkArgs();
RunId runId = new RunId();
runId.setId(this.runId);
ScheduledExecutorService networkScheduler = Executors.newScheduledThreadPool(1);
slaveEventListener =
new DistBuildSlaveEventBusListener(
getStampedeId(), runId, new DefaultClock(), networkScheduler);
}
}
@Override
public Iterable<BuckEventListener> getEventListeners() {
if (buildStateFile == null) {
initEventListener();
return ImmutableList.of(slaveEventListener);
} else {
return ImmutableList.of();
}
}
}