package com.hubspot.blazar.visitor.modulebuild;
import static com.hubspot.blazar.base.InterProjectBuild.State.CANCELLED;
import static com.hubspot.blazar.base.InterProjectBuild.State.FAILED;
import static com.hubspot.blazar.base.InterProjectBuild.State.SUCCEEDED;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
import com.hubspot.blazar.base.BuildOptions;
import com.hubspot.blazar.base.BuildTrigger;
import com.hubspot.blazar.base.DependencyGraph;
import com.hubspot.blazar.base.GitInfo;
import com.hubspot.blazar.base.InterProjectBuild;
import com.hubspot.blazar.base.InterProjectBuildMapping;
import com.hubspot.blazar.base.ModuleBuild;
import com.hubspot.blazar.base.visitor.AbstractModuleBuildVisitor;
import com.hubspot.blazar.data.service.BranchService;
import com.hubspot.blazar.data.service.InterProjectBuildMappingService;
import com.hubspot.blazar.data.service.InterProjectBuildService;
import com.hubspot.blazar.data.service.ModuleService;
import com.hubspot.blazar.data.service.RepositoryBuildService;
import com.hubspot.blazar.exception.NonRetryableBuildException;
public class InterProjectModuleBuildVisitor extends AbstractModuleBuildVisitor {
private static final Logger LOG = LoggerFactory.getLogger(InterProjectModuleBuildVisitor.class);
private final ModuleService moduleService;
private BranchService branchService;
private RepositoryBuildService repositoryBuildService;
private final InterProjectBuildService interProjectBuildService;
private final InterProjectBuildMappingService interProjectBuildMappingService;
@Inject
public InterProjectModuleBuildVisitor(ModuleService moduleService,
BranchService branchService,
RepositoryBuildService repositoryBuildService,
InterProjectBuildService interProjectBuildService,
InterProjectBuildMappingService interProjectBuildMappingService) {
this.moduleService = moduleService;
this.branchService = branchService;
this.repositoryBuildService = repositoryBuildService;
this.interProjectBuildService = interProjectBuildService;
this.interProjectBuildMappingService = interProjectBuildMappingService;
}
@Override
protected void visitSucceeded(ModuleBuild build) throws Exception {
Optional<InterProjectBuildMapping> mapping = interProjectBuildMappingService.getByModuleBuildId(build.getId().get());
if (!mapping.isPresent()) {
return;
}
InterProjectBuildMapping updatedMapping = mapping.get().withModuleBuildId(InterProjectBuild.State.SUCCEEDED);
interProjectBuildMappingService.updateBuilds(updatedMapping);
LOG.info("ModuleBuild {} with IPB mapping {} was successful looking for child builds to start", build.getId(), mapping);
buildChildren(build, updatedMapping);
LOG.info("Checking if module build {} was las build in IPB with mapping {}", build.getId().get(), mapping);
checkAndCompleteInterProjectBuild(build, mapping.get().getInterProjectBuildId());
}
@Override
protected void visitCancelled(ModuleBuild build) throws Exception {
Optional<InterProjectBuildMapping> mapping = interProjectBuildMappingService.getByModuleBuildId(build.getId().get());
if (!mapping.isPresent()) {
return;
}
visitCancelledOrFailed(CANCELLED, build, mapping.get());
}
@Override
protected void visitFailed(ModuleBuild build) throws Exception {
Optional<InterProjectBuildMapping> mapping = interProjectBuildMappingService.getByModuleBuildId(build.getId().get());
if (!mapping.isPresent()) {
return;
}
visitCancelledOrFailed(FAILED, build, mapping.get());
}
private void visitCancelledOrFailed(InterProjectBuild.State state, ModuleBuild build, InterProjectBuildMapping mapping) throws Exception {
InterProjectBuildMapping updatedMapping = mapping.withModuleBuildId(state);
interProjectBuildMappingService.updateBuilds(updatedMapping);
LOG.info("ModuleBuild {} with IPB mapping {} {} looking for child nodes to cancel", build.getId(), updatedMapping.getState(), mapping);
cancelSubTree(build, updatedMapping);
LOG.info("Checking if module build {} was last build in IPB with mapping {}", build.getId().get(), mapping);
checkAndCompleteInterProjectBuild(build, mapping.getInterProjectBuildId());
}
/**
* Here we find the child nodes in the graph that can be built and build them.
* In order to not start extra repository builds we also find "chains" of dependencies that
* are in the same repository & branch and build those together in the same repository build.
*/
private void buildChildren(ModuleBuild build, InterProjectBuildMapping mapping) {
InterProjectBuild interProjectBuild = interProjectBuildService.getWithId(mapping.getInterProjectBuildId()).get();
DependencyGraph graph = interProjectBuild.getDependencyGraph().get();
Map<Integer, InterProjectBuildMapping> mappingsForInterProjectBuildByModuleId =
interProjectBuildMappingService.getMappingsForInterProjectBuildByModuleId(mapping.getInterProjectBuildId());
// We group modules to be launched by branch so we can start one repository build per branch with all the modules.
SetMultimap<Integer, Integer> launchableBranchToModuleMap = HashMultimap.create();
for (int moduleId : graph.reachableVertices(build.getModuleId())) {
int branchId = moduleService.getBranchIdFromModuleId(moduleId);
if (shouldBuildModule(moduleId, branchId, mapping.getInterProjectBuildId(), graph, mappingsForInterProjectBuildByModuleId)) {
launchableBranchToModuleMap.put(branchId, moduleId);
}
}
LOG.info("Found {} modules downstream of {} that can be started", launchableBranchToModuleMap.size(), mapping);
for (Map.Entry<Integer, Set<Integer>> entry : Multimaps.asMap(launchableBranchToModuleMap).entrySet()) {
Set<Integer> launchableModules = entry.getValue();
GitInfo gitInfo = branchService.get(entry.getKey()).get();
BuildTrigger buildTrigger = BuildTrigger.forInterProjectBuild(interProjectBuild.getId().get());
BuildOptions buildOptions = new BuildOptions(launchableModules, BuildOptions.BuildDownstreams.NONE, false);
long buildId = repositoryBuildService.enqueue(gitInfo, buildTrigger, buildOptions);
for (Integer moduleId : launchableModules) {
interProjectBuildMappingService.insert(InterProjectBuildMapping.makeNewMapping(interProjectBuild.getId().get(), gitInfo.getId().get(), Optional.of(buildId), moduleId));
}
LOG.debug("Queued repo build {} as part of InterProjectBuild {}", buildId, interProjectBuild.getId().get());
}
}
private boolean shouldBuildModule(int moduleId, int branchId, long interProjectBuildId, DependencyGraph graph, Map<Integer, InterProjectBuildMapping> mappingsForInterProjectBuildByModuleId) {
// don't start builds of modules that have mappings
if (mappingsForInterProjectBuildByModuleId.containsKey(moduleId)) {
LOG.debug("Not starting inter project build for module {} because mapping for it exists {}", moduleId, mappingsForInterProjectBuildByModuleId.get(moduleId));
return false;
}
// Check all upstreams complete & successful
for (Integer upstream: graph.getAllUpstreamNodes(moduleId)) {
if (!checkThatAllUpstreamsSucceedOrShareBranchId(upstream, branchId, mappingsForInterProjectBuildByModuleId)) {
LOG.info("Not starting inter project build for module {} because upstreams not complete", moduleId);
return false;
}
}
LOG.debug("Found that module {} on branch {} part of IPB {} should be built", moduleId, branchId, interProjectBuildId);
return true;
}
private boolean checkThatAllUpstreamsSucceedOrShareBranchId(int moduleId, int branchId, Map<Integer, InterProjectBuildMapping> mappingsForInterProjectBuildByModuleId) {
// See if we've started a build for this
InterProjectBuildMapping mappingFound = mappingsForInterProjectBuildByModuleId.get(moduleId);
if (mappingFound == null && branchId == moduleService.getBranchIdFromModuleId(moduleId)) {
return true;
}
return mappingFound != null && mappingFound.getState().equals(InterProjectBuild.State.SUCCEEDED);
}
/**
* Create interProjectBuild mappings for all modules that are downstream of this one
* with state `CANCELLED`. This allows us to see what builds were supposed to run
* according to the inter project graph but did not get executed.
*/
private void cancelSubTree(ModuleBuild build, InterProjectBuildMapping mapping) throws NonRetryableBuildException {
long interProjectBuildId = mapping.getInterProjectBuildId();
InterProjectBuild interProjectBuild = interProjectBuildService.getWithId(interProjectBuildId).get();
LOG.info("Canceling builds dependent on {} matching IPB mapping {}", build.getId(), mapping);
Map<Integer, InterProjectBuildMapping> mappingsForInterProjectBuildByModuleId =
interProjectBuildMappingService.getMappingsForInterProjectBuildByModuleId(interProjectBuildId);
DependencyGraph graph = interProjectBuild.getDependencyGraph().get();
Deque<Integer> deque = new ArrayDeque<>();
Set<Integer> visited = new HashSet<>();
deque.addAll(graph.outgoingVertices(build.getModuleId()));
while (!deque.isEmpty()) {
int moduleId = deque.pop();
boolean mappingExists = mappingsForInterProjectBuildByModuleId.containsKey(moduleId);
if (visited.add(moduleId) && !mappingExists) {
LOG.info("Module {} was downstream of {} module {} in IPB {} creating cancelled mapping");
interProjectBuildMappingService.insert(new InterProjectBuildMapping(Optional.<Long>absent(), interProjectBuild.getId().get(), moduleService.getBranchIdFromModuleId(moduleId), Optional.<Long>absent(), moduleId, Optional.<Long>absent(), CANCELLED));
deque.addAll(Sets.difference(graph.outgoingVertices(moduleId), visited));
}
}
}
/**
* Checks if this was the last build in the IPB and marks it as complete if so.
*/
private void checkAndCompleteInterProjectBuild(ModuleBuild build, long interProjectBuildId) {
InterProjectBuild ipb = interProjectBuildService.getWithId(interProjectBuildId).get();
if (!isLastModuleBuildInGraph(ipb)) {
LOG.debug("Module build {} of module {} finished but the associated inter-project-build {} is not done yet", build.getId().get(), build.getModuleId(), ipb.getId().get());
return;
}
interProjectBuildService.finish(InterProjectBuild.getFinishedBuild(ipb, getFinalStateForInterProjectBuild(interProjectBuildId)));
}
private boolean isLastModuleBuildInGraph(InterProjectBuild build) {
Map<Integer, InterProjectBuildMapping> mappingsForInterProjectBuildByModuleId =
interProjectBuildMappingService.getMappingsForInterProjectBuildByModuleId(build.getId().get());
// ensure that all _other_ modules are done, so remove this one
for (int moduleId : build.getDependencyGraph().get().getTopologicalSort()) {
// No mapping means no build started -- this is not the last build
if (!mappingsForInterProjectBuildByModuleId.containsKey(moduleId)) {
return false;
}
// This mapping is not in a finished state -- this is not the last build
InterProjectBuildMapping mapping = mappingsForInterProjectBuildByModuleId.get(moduleId);
if (!mapping.getState().isFinished()) {
return false;
}
}
// We made it -- we're the last build
return true;
}
// Identifies final state for IPB
private InterProjectBuild.State getFinalStateForInterProjectBuild(long interProjectBuildId) {
Set<InterProjectBuildMapping> mappings = interProjectBuildMappingService.getMappingsForInterProjectBuild(interProjectBuildId);
Set<InterProjectBuild.State> states = mappings.stream().map(InterProjectBuildMapping::getState).collect(Collectors.toSet());
if (states.contains(FAILED)) {
return FAILED;
}
if (states.contains(CANCELLED)) {
return CANCELLED;
}
if (states.equals(ImmutableSet.of(SUCCEEDED))) {
return SUCCEEDED;
}
throw new IllegalStateException(String.format("Found un expected states %s in mappings for IPB %s expected SUCCEEDED", states, interProjectBuildId));
}
}