package com.hubspot.blazar.data.service; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.transaction.Transactional; import javax.ws.rs.NotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Joiner; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.hubspot.blazar.base.DiscoveredModule; import com.hubspot.blazar.base.GitInfo; import com.hubspot.blazar.base.Module; import com.hubspot.blazar.data.dao.ModuleDao; public class ModuleService { private static final Logger LOG = LoggerFactory.getLogger(ModuleService.class); private final ModuleDao moduleDao; private final DependenciesService dependenciesService; @Inject public ModuleService(ModuleDao moduleDao, DependenciesService dependenciesService) { this.moduleDao = moduleDao; this.dependenciesService = dependenciesService; } public Optional<Module> get(int moduleId) { return moduleDao.get(moduleId); } public Set<Module> getByBranch(int branchId) { return moduleDao.getByBranch(branchId); } public int getBranchIdFromModuleId(int moduleId) { return moduleDao.getBranchIdFromModuleId(moduleId); } public void checkModuleExists(int moduleId) { Optional<Module> maybeModule = get(moduleId); if (!maybeModule.isPresent()) { throw new NotFoundException(String.format("Module %d does not exist", moduleId)); } } /** * The updated modules are all the active modules that have been updated during the module discovery phase. * The list contains all modules that were (re)discovered (they are in a DiscoveredModule * subclass of the Module class) + all the registered modules that were not deactivated or * deleted during the discovery check (they are in a Module class, for those we just updated their build configs). * Essentially the list of updated modules is the list of all modules that are * eligible for building. We use the list to identify which modules were deleted/deactivated in order to deactivate * them in the db and then we persist the changes in each updated module. The DiscoveredModule entries in the list * may have updated build configs and updated dependencies. The Module entries have only their build configs updated. * @param branch * @param updatedModules * @return */ @Transactional public void persistModulesAndDependencies(GitInfo branch, Set<Module> updatedModules) { Set<Module> registeredActiveModules = getByBranch(branch.getId().get()).stream().filter(Module::isActive) .collect(Collectors.toSet()); Map<String, Module> updatedModulesByName = Maps.uniqueIndex(updatedModules, Module::getName); Map<String, Module> registeredActiveModulesByName = Maps.uniqueIndex(registeredActiveModules, Module::getName); LOG.debug("Registered Active Modules: {}", registeredActiveModulesByName.toString()); LOG.debug("Updated Modules: {}", updatedModulesByName.toString()); deleteRemovedModules(updatedModulesByName, registeredActiveModulesByName); updateExistingNotRediscoveredModules(updatedModules, registeredActiveModulesByName); createNewlyDiscoveredModules(branch, updatedModules, registeredActiveModulesByName); updateRediscoveredModules(updatedModules, registeredActiveModulesByName); } // For re-discovered modules we will update their module entries in db if the // build config has been refreshed and also update the relevant entries in the dependencies // tables. private void updateRediscoveredModules(Set<Module> updatedModules, Map<String, Module> registeredActiveModulesByName) { Set<Module> rediscoveredModules = updatedModules.stream() .filter(module -> module.getClass() == DiscoveredModule.class && registeredActiveModulesByName.containsKey(module.getName())).collect(Collectors.toSet()); rediscoveredModules.forEach(rediscoveredModule -> { Module previousModuleInstance = registeredActiveModulesByName.get(rediscoveredModule.getName()); int moduleId = previousModuleInstance.getId().get(); DiscoveredModule rediscoveredModuleWithId = ((DiscoveredModule)rediscoveredModule).withId(moduleId); if (buildConfigChanged(previousModuleInstance, rediscoveredModule)) { LOG.debug("Rediscovered module {}(type:{}, id:{}) has a changed build config. We will persist it.", rediscoveredModule.getName(), rediscoveredModule.getType(), moduleId); checkAffectedRowCount(moduleDao.update(rediscoveredModuleWithId)); } else { LOG.debug("Rediscovered module {}(type:{}, id:{}) has no changes in its build config, will not persist it", rediscoveredModule.getName(), rediscoveredModule.getType(), moduleId); } LOG.debug("Persisting dependencies for rediscovered module {}(type:{}, id:{})", rediscoveredModule.getName(), rediscoveredModule.getType(), moduleId); dependenciesService.update(rediscoveredModuleWithId); }); } // For newly discovered modules we will create module entries and will also create entries in the dependencies // tables. private void createNewlyDiscoveredModules(GitInfo branch, Set<Module> updatedModules, Map<String, Module> registeredActiveModulesByName) { Set<Module> newlyDiscoveredModules = updatedModules.stream() .filter(module -> module.getClass() == DiscoveredModule.class && !registeredActiveModulesByName.containsKey(module.getName())).collect(Collectors.toSet()); newlyDiscoveredModules.forEach(newModule -> { LOG.debug("Persisting newly discovered module {}(type:{})", newModule.getName(), newModule.getType()); int moduleId = moduleDao.insert(branch.getId().get(), newModule); LOG.debug("Persisted newly discovered module {}:{} with id:{})", newModule.getName(), newModule.getType(), moduleId); LOG.debug("Persisting dependencies for newly discovered module {}(type:{}, id:{})", newModule.getName(), newModule.getType(), moduleId); DiscoveredModule persistedModule = ((DiscoveredModule) newModule).withId(moduleId); dependenciesService.insert(persistedModule); }); } private void updateExistingNotRediscoveredModules(Set<Module> updatedModules, Map<String, Module> registeredActiveModulesByName) { // For already registered modules that were not rediscovered we will just update the module in db if the // build config has been refreshed Set<Module> alreadyRegisteredAndNotRediscovedModules = updatedModules.stream() .filter(module -> module.getClass() == Module.class).collect(Collectors.toSet()); alreadyRegisteredAndNotRediscovedModules.forEach(existingModule -> { Module previousModuleInstance = registeredActiveModulesByName.get(existingModule.getName()); if (!previousModuleInstance.equals(existingModule)) { LOG.debug("Existing module {}(type:{}, id:{}) has a changed build config. We will persist it.", existingModule.getName(), existingModule.getType(), existingModule.getId().get()); checkAffectedRowCount(moduleDao.update(existingModule)); } else { LOG.debug("Existing module {}(type:{}, id:{}) has no changes in its build config, will not persist it", existingModule.getName(), existingModule.getType(), existingModule.getId().get()); } }); } // For the modules that were removed we will deactivate them in db and remove their dependencies private void deleteRemovedModules(Map<String, Module> updatedModulesByName, Map<String, Module> registeredActiveModulesByName) { Set<String> deletedModuleNames = Sets.difference(registeredActiveModulesByName.keySet(), updatedModulesByName.keySet()); LOG.debug("The following modules were removed from code and will be removed from database: [{}]", Joiner.on(" ,").join(deletedModuleNames)); for (String deletedModuleName : deletedModuleNames) { Module module = registeredActiveModulesByName.get(deletedModuleName); LOG.debug("Module '{}' has been removed from code. Will deactivate the module and delete its dependencies from database", deletedModuleName); checkAffectedRowCount(moduleDao.deactivate(module.getId().get())); LOG.debug("Module '{}' was set to inactive in database", deletedModuleName); dependenciesService.delete(module.getId().get()); LOG.debug("Dependencies of removed module {} were deleted from database", deletedModuleName); } } private static void checkAffectedRowCount(int affectedRows) { Preconditions.checkState(affectedRows == 1, "Expected to update 1 row but updated %s", affectedRows); } private boolean buildConfigChanged(Module previousModuleInstance, Module rediscoveredModule) { return !previousModuleInstance.getBuildConfig().equals(rediscoveredModule.getBuildConfig()) || !previousModuleInstance.getResolvedBuildConfig().equals(rediscoveredModule.getResolvedBuildConfig()); } }