package com.hubspot.blazar.visitor.modulebuild;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.collect.Sets;
import com.hubspot.blazar.base.DependencyGraph;
import com.hubspot.blazar.base.ModuleBuild;
import com.hubspot.blazar.base.ModuleBuild.State;
import com.hubspot.blazar.base.RepositoryBuild;
import com.hubspot.blazar.base.visitor.AbstractModuleBuildVisitor;
import com.hubspot.blazar.data.service.ModuleBuildService;
import com.hubspot.blazar.data.service.RepositoryBuildService;
import com.hubspot.blazar.exception.NonRetryableBuildException;
import com.hubspot.blazar.externalservice.BuildClusterService;
@Singleton
public class QueuedModuleBuildVisitor extends AbstractModuleBuildVisitor {
private static final Logger LOG = LoggerFactory.getLogger(QueuedModuleBuildVisitor.class);
private final RepositoryBuildService repositoryBuildService;
private final ModuleBuildService moduleBuildService;
private final BuildClusterService buildClusterService;
@Inject
public QueuedModuleBuildVisitor(RepositoryBuildService repositoryBuildService,
ModuleBuildService moduleBuildService,
BuildClusterService buildClusterService) {
this.repositoryBuildService = repositoryBuildService;
this.moduleBuildService = moduleBuildService;
this.buildClusterService = buildClusterService;
}
@Override
protected void visitQueued(ModuleBuild moduleBuild) throws Exception {
RepositoryBuild repositoryBuild = repositoryBuildService.get(moduleBuild.getRepoBuildId()).get();
// we eagerly launch the build containers without waiting for upstreams to finish
launchBuildContainer(moduleBuild);
if (upstreamsComplete(repositoryBuild, moduleBuild)) {
setModuleBuildStateToLaunching(moduleBuild);
} else {
moduleBuildService.update(moduleBuild.toBuilder().setState(State.WAITING_FOR_UPSTREAM_BUILD).build());
}
}
@Override
protected void visitWaitingForUpstreamBuild(ModuleBuild moduleBuild) throws Exception {
RepositoryBuild repositoryBuild = repositoryBuildService.get(moduleBuild.getRepoBuildId()).get();
if (upstreamsComplete(repositoryBuild, moduleBuild)) {
setModuleBuildStateToLaunching(moduleBuild);
}
}
private boolean upstreamsComplete(RepositoryBuild repositoryBuild, ModuleBuild build) {
DependencyGraph dependencyGraph = repositoryBuild.getDependencyGraph().get();
String buildingStatusLogPrefix = String.format("ModuleBuild %s for Module %s ", build.getId().get(), build.getModuleId());
if (dependencyGraph.incomingVertices(build.getModuleId()).isEmpty()) {
LOG.info("{} has no upstreams it is ready to build.", buildingStatusLogPrefix);
return true;
} else {
Set<ModuleBuild> moduleBuilds = moduleBuildService.getByRepositoryBuild(build.getRepoBuildId());
Set<Integer> buildingModules = extractModuleIds(filterSucceeded(moduleBuilds));
Set<Integer> upstreamModules = dependencyGraph.getAllUpstreamNodes(build.getModuleId());
Set<Integer> buildingUpstreams = Sets.intersection(buildingModules, upstreamModules);
if (buildingUpstreams.isEmpty()) {
LOG.info("{} is no longer waiting for upstreams and is ready to build", buildingStatusLogPrefix);
return true;
}
Set<Long> runningUpstreamModuleBuildIds = new HashSet<>();
for (ModuleBuild moduleBuild : moduleBuilds) {
if (buildingUpstreams.contains(moduleBuild.getModuleId())) {
runningUpstreamModuleBuildIds.add(build.getId().get());
}
}
LOG.info("{} is waiting for ModuleBuilds: {}", buildingStatusLogPrefix, runningUpstreamModuleBuildIds);
return false;
}
}
// Here we will eagerly launch the build container in the cluster before we check whether there are upstreams
// that will need to finish before this build can commence.
// We start the build containers eagerly since it takes quite long to spin up the docker images and we want
// to take advantage of the time upstreams may be building to launch the container.
// So despite launching the containers here we are NOT YET setting the state of the module build
// to LAUNCHING because this would signal the build container to start building.
private void launchBuildContainer(ModuleBuild moduleBuild) throws Exception {
final String buildCluster;
try {
buildCluster = buildClusterService.launchBuildContainer(moduleBuild);
LOG.info("Docker container was successfully launched in cluster {} for module build {}",
buildCluster, moduleBuild.getId().get());
} catch (Exception e) {
throw new NonRetryableBuildException(String.format("An error occurred while launching docker container for module build %s. Will fail the build", moduleBuild), e);
}
}
// when a module is ready to start building we signal the build container to start the build by setting the
// state of the module build to LAUNCHING
private void setModuleBuildStateToLaunching(ModuleBuild moduleBuild) throws Exception {
ModuleBuild launching = moduleBuild.toBuilder()
.setStartTimestamp(Optional.of(System.currentTimeMillis()))
.setState(State.LAUNCHING)
.build();
moduleBuildService.setToLaunching(launching);
LOG.info("Updated status of Module Build {} to {}", launching.getId().get(), launching.getState());
}
private static Set<ModuleBuild> filterSucceeded(Set<ModuleBuild> builds) {
Set<State> allowedStates = EnumSet.complementOf(EnumSet.of(State.SUCCEEDED, State.SKIPPED));
Set<ModuleBuild> filtered = new HashSet<>();
for (ModuleBuild build : builds) {
if (allowedStates.contains(build.getState())) {
filtered.add(build);
}
}
return filtered;
}
private static Set<Integer> extractModuleIds(Set<ModuleBuild> builds) {
Set<Integer> moduleIds = new HashSet<>();
for (ModuleBuild build : builds) {
moduleIds.add(build.getModuleId());
}
return moduleIds;
}
}