package com.hubspot.blazar.data.service;
import static com.hubspot.blazar.base.ModuleDependency.Source.BUILD_CONFIG;
import static com.hubspot.blazar.base.ModuleDependency.Source.PLUGIN;
import static com.hubspot.blazar.base.ModuleDependency.Source.UNKNOWN;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.hubspot.blazar.base.Dependency;
import com.hubspot.blazar.base.DependencyGraph;
import com.hubspot.blazar.base.DiscoveredModule;
import com.hubspot.blazar.base.GitInfo;
import com.hubspot.blazar.base.Module;
import com.hubspot.blazar.base.graph.Edge;
import com.hubspot.blazar.data.dao.DependenciesDao;
import com.hubspot.blazar.data.util.GraphUtils;
@Singleton
public class DependenciesService {
private final DependenciesDao dependenciesDao;
private static final Logger LOG = LoggerFactory.getLogger(DependenciesService.class);
@Inject
public DependenciesService(DependenciesDao dependenciesDao) {
this.dependenciesDao = dependenciesDao;
}
public DependencyGraph buildInterProjectDependencyGraph(Set<Module> modulesTriggered) {
long start = System.currentTimeMillis();
SetMultimap<Integer, Integer> graph = computeGraphFromRootModules(-1, modulesTriggered);
long startGraph = System.currentTimeMillis();
Set<Integer> s = new HashSet<>();
for (Map.Entry<Integer, Integer> i : graph.entries()) {
s.add(i.getKey());
s.add(i.getValue());
}
LOG.info("graph is of size {}", s.size());
int maxWidth = 0;
for (Map.Entry<Integer, Collection<Integer>> entry : graph.asMap().entrySet()) {
if ( maxWidth < entry.getValue().size()) {
maxWidth = entry.getValue().size();
}
}
LOG.info("Graph is {} at its widest", maxWidth);
SetMultimap<Integer, Integer> transitiveReduction = GraphUtils.INSTANCE.transitiveReduction(graph);
LOG.info("Transitive reduction calculation took {}", System.currentTimeMillis() - startGraph);
List<Integer> topologicalSort = GraphUtils.INSTANCE.topologicalSort(graph);
Map<Integer, Set<Integer>> transitiveReductionMap = new HashMap<>(Multimaps.asMap(transitiveReduction));
DependencyGraph dependencyGraph = new DependencyGraph(transitiveReductionMap, topologicalSort);
LOG.info("Building graph took {}", System.currentTimeMillis() - start);
return dependencyGraph;
}
public DependencyGraph buildDependencyGraph(GitInfo gitInfo, Set<Module> allModules) {
// one module can't have any deps
if (allModules.size() == 1) {
int moduleId = allModules.iterator().next().getId().get();
List<Integer> topologicalSort = Collections.singletonList(moduleId);
Map<Integer, Set<Integer>> transitiveReduction = Collections.singletonMap(moduleId, Collections.<Integer>emptySet());
return new DependencyGraph(transitiveReduction, topologicalSort);
} else {
SetMultimap<Integer, Integer> fullGraph = computeGraphFromRootModules(gitInfo.getId().get(), allModules);
Set<Integer> moduleIds = new HashSet<>();
for (Module module : allModules) {
moduleIds.add(module.getId().get());
}
SetMultimap<Integer, Integer> repoGraph = GraphUtils.INSTANCE.retain(fullGraph, moduleIds);
SetMultimap<Integer, Integer> transitiveReduction = GraphUtils.INSTANCE.transitiveReduction(repoGraph);
List<Integer> topologicalSort = GraphUtils.INSTANCE.topologicalSort(transitiveReduction);
List<Integer> missingModules = findMissingModules(topologicalSort, allModules);
Map<Integer, Set<Integer>> transitiveReductionMap = new HashMap<>(Multimaps.asMap(transitiveReduction));
for (int missingModule : missingModules) {
transitiveReductionMap.put(missingModule, Collections.<Integer>emptySet());
}
return new DependencyGraph(transitiveReductionMap, concat(missingModules, topologicalSort));
}
}
private SetMultimap<Integer, Integer> computeGraphFromRootModules(int branchId, Set<Module> rootModules) {
SetMultimap<Integer, Integer> graph = HashMultimap.create();
Set<Integer> seenModules = new HashSet<>();
Queue<Integer> moduleQueue = new LinkedList<>();
List<Long> queryTimes = new ArrayList<>();
for (Module module : rootModules) {
moduleQueue.add(module.getId().get());
}
while (true) {
Set<Integer> modulesToProcess = new HashSet<>(moduleQueue);
modulesToProcess.removeAll(seenModules);
moduleQueue.clear();
if (modulesToProcess.isEmpty()) {
break;
}
long queryStart = System.currentTimeMillis();
Set<Edge> edges = dependenciesDao.getEdges(branchId, modulesToProcess);
long queryEnd = System.currentTimeMillis();
LOG.info("Query for {} took {}", modulesToProcess, queryEnd - queryStart);
queryTimes.add(queryEnd - queryStart);
for (Edge edge : edges) {
graph.put(edge.getSource(), edge.getTarget());
moduleQueue.add(edge.getTarget());
}
seenModules.addAll(modulesToProcess);
}
long sum = 0;
long max = 0;
long min = Long.MAX_VALUE;
for (long i : queryTimes) {
sum += i;
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
long average = queryTimes.size() == 0 ? 0 : sum / queryTimes.size();
LOG.info("MysqlQueries max: {} min: {} ct: {} each: {} total: {}", max, min, queryTimes.size(), average, sum);
return graph;
}
@Transactional
public void insert(DiscoveredModule module) {
dependenciesDao.insertProvidedDependencies(module.getBuildConfigProvidedDependencies());
dependenciesDao.insertProvidedDependencies(module.getPluginDiscoveredProvidedDependencies());
dependenciesDao.insertDependencies(module.getBuildConfigDependencies());
dependenciesDao.insertDependencies(module.getPluginDiscoveredDependencies());
}
@Transactional
public void update(DiscoveredModule module) {
updateProvidedDependencies(module);
updateDependencies(module);
}
@Transactional
public void delete(int moduleId) {
dependenciesDao.deleteProvidedDependencies(moduleId);
dependenciesDao.deleteDependencies(moduleId);
}
public Set<Dependency> getProvided(int moduleId) {
return dependenciesDao.getProvidedDependencies(moduleId);
}
public Set<Dependency> getDependencies(int moduleId) {
return dependenciesDao.getDependencies(moduleId);
}
public int getCountOfDependenciesWithoutSourceByBranchId(int branchId) {
return dependenciesDao.getCountOfDependenciesWithoutSourceByBranchId(branchId);
}
public int getCountOfProvidedDependenciesWithoutSourceByBranchId(int branchId) {
return dependenciesDao.getCountOfProvidedDependenciesWithoutSourceByBranchId(branchId);
}
public Set<GitInfo> getBranchesWithNonVersionedDependencies() {
return dependenciesDao.getBranchesWithNonVersionedDependencies();
}
private void updateProvidedDependencies(DiscoveredModule module) {
//TODO: remove that when no entries have UNKNOWN source
dependenciesDao.deleteProvidedDependenciesBySource(module.getId().get(), UNKNOWN);
if (!module.getBuildConfigProvidedDependencies().isEmpty()) {
dependenciesDao.deleteProvidedDependenciesBySource(module.getId().get(), BUILD_CONFIG);
dependenciesDao.insertProvidedDependencies(module.getBuildConfigProvidedDependencies());
}
if (!module.getPluginDiscoveredProvidedDependencies().isEmpty()) {
dependenciesDao.deleteProvidedDependenciesBySource(module.getId().get(), PLUGIN);
dependenciesDao.insertProvidedDependencies(module.getPluginDiscoveredProvidedDependencies());
}
}
private void updateDependencies(DiscoveredModule module) {
//TODO: remove that when no entries have UNKNOWN source
dependenciesDao.deleteDependenciesBySource(module.getId().get(), UNKNOWN);
if (!module.getBuildConfigDependencies().isEmpty()) {
dependenciesDao.deleteDependenciesBySource(module.getId().get(), BUILD_CONFIG);
dependenciesDao.insertDependencies(module.getBuildConfigDependencies());
}
if (!module.getPluginDiscoveredProvidedDependencies().isEmpty()) {
dependenciesDao.deleteDependenciesBySource(module.getId().get(), PLUGIN);
dependenciesDao.insertDependencies(module.getPluginDiscoveredDependencies());
}
}
private static List<Integer> findMissingModules(List<Integer> topologicalSort, Set<Module> allModules) {
List<Integer> missing = new ArrayList<>();
for (Module module : allModules) {
if (!topologicalSort.contains(module.getId().get())) {
missing.add(module.getId().get());
}
}
Collections.sort(missing);
return missing;
}
private static List<Integer> concat(List<Integer> list1, List<Integer> list2) {
return ImmutableList.copyOf(Iterables.concat(list1, list2));
}
}