package com.hubspot.blazar.data.service;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.transaction.Transactional;
import javax.ws.rs.NotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.eventbus.EventBus;
import com.hubspot.blazar.base.BuildConfig;
import com.hubspot.blazar.base.Module;
import com.hubspot.blazar.base.ModuleActivityPage;
import com.hubspot.blazar.base.ModuleBuild;
import com.hubspot.blazar.base.ModuleBuild.State;
import com.hubspot.blazar.base.ModuleBuildInfo;
import com.hubspot.blazar.base.RepositoryBuild;
import com.hubspot.blazar.data.dao.ModuleBuildDao;
import com.hubspot.blazar.data.dao.ModuleDao;
@Singleton
public class ModuleBuildService {
public static final int MAX_MODULE_ACTIVITY_PAGE_SIZE = 100;
private static final Logger LOG = LoggerFactory.getLogger(ModuleBuildService.class);
private final ModuleBuildDao moduleBuildDao;
private final ModuleDao moduleDao;
private final EventBus eventBus;
@Inject
public ModuleBuildService(ModuleBuildDao moduleBuildDao, ModuleDao moduleDao, EventBus eventBus) {
this.moduleBuildDao = moduleBuildDao;
this.moduleDao = moduleDao;
this.eventBus = eventBus;
}
public Optional<ModuleBuild> get(long id) {
return moduleBuildDao.get(id);
}
public Set<ModuleBuild> getByRepositoryBuild(long repoBuildId) {
return moduleBuildDao.getByRepositoryBuild(repoBuildId);
}
public Set<ModuleBuild> getByState(State state) {
return moduleBuildDao.getByState(state);
}
public ModuleActivityPage getModuleActivityPage(int moduleId, Optional<Integer> maybeFromBuildNumber, Optional<Integer> maybePageSize) {
int pageSize = MAX_MODULE_ACTIVITY_PAGE_SIZE;
if (maybePageSize.isPresent() && 0 < maybePageSize.get() && maybePageSize.get() < MAX_MODULE_ACTIVITY_PAGE_SIZE) {
pageSize = maybePageSize.get();
}
List<ModuleBuildInfo> builds;
long start = System.currentTimeMillis();
if (maybeFromBuildNumber.isPresent() && 0 < maybeFromBuildNumber.get()) {
int fromBuildNumber = maybeFromBuildNumber.get();
builds = moduleBuildDao.getLimitedModuleBuildHistory(moduleId, fromBuildNumber, pageSize);
} else {
builds = moduleBuildDao.getLatestLimitedModuleBuildHistory(moduleId, pageSize);
}
LOG.debug("Got {} builds of activity for module {} in {}", builds.size(), moduleId, System.currentTimeMillis() - start);
// calculate remaining
if (builds.isEmpty()) {
return new ModuleActivityPage(builds, 0);
}
int lastBuildNumber = builds.get(builds.size() - 1).getBranchBuild().getBuildNumber();
/**
* We have to actually count this, because if a module gets added to a branch its build history
* starts at the build# for the build it was added on, and doesn't go back all the way to "1"
*/
Optional<Integer> remainingCount = moduleBuildDao.getRemainingBuildCountForPagedHistory(moduleId, lastBuildNumber);
// the count() aggregation shouldn't return nothing, but in case it does:
if (!remainingCount.isPresent()) {
return new ModuleActivityPage(builds, 0);
}
return new ModuleActivityPage(builds, remainingCount.get());
}
public Optional<ModuleBuild> getPreviousBuild(ModuleBuild build) {
return moduleBuildDao.getPreviousBuild(build);
}
public Optional<ModuleBuild> getPreviousBuild(Module module) {
return moduleBuildDao.getPreviousBuild(module);
}
public Optional<ModuleBuild> getByModuleAndNumber(int moduleId, int buildNumber) {
return moduleBuildDao.getByModuleAndNumber(moduleId, buildNumber);
}
public void skip(RepositoryBuild repositoryBuild, Module module) {
int nextBuildNumber = repositoryBuild.getBuildNumber();
LOG.info("Skipping build for module {} with build number {}", module.getId().get(), nextBuildNumber);
ModuleBuild build = ModuleBuild.skippedBuild(repositoryBuild, module, nextBuildNumber);
build = skip(build);
LOG.info("Skipped build for module {} with id {}", module.getId().get(), build.getId().get());
}
public ModuleBuild enqueue(RepositoryBuild repositoryBuild, Module module, BuildConfig buildConfig, BuildConfig resolvedBuildConfig) {
int nextBuildNumber = repositoryBuild.getBuildNumber();
LOG.info("Enqueuing build for module {} with build number {}", module.getId().get(), nextBuildNumber);
ModuleBuild build = enqueue(ModuleBuild.queuedBuild(repositoryBuild, module, nextBuildNumber, buildConfig, resolvedBuildConfig));
LOG.info("Enqueued build for module {} with id {}", module.getId().get(), build.getId().get());
return build;
}
@Transactional
public ModuleBuild setStateToLaunching(long moduleBuildId, String buildContainerId) {
ModuleBuild moduleBuild = getBuildWithExpectedState(moduleBuildId, State.LAUNCHING);
ModuleBuild inProgress = moduleBuild.toBuilder().setState(State.IN_PROGRESS).setTaskId(Optional.of(buildContainerId)).build();
update(inProgress);
return inProgress;
}
@Transactional
public ModuleBuild setStateToSucceded(long moduleBuildId) {
ModuleBuild moduleBuild = getBuildWithExpectedState(moduleBuildId, State.IN_PROGRESS);
ModuleBuild succeeded = moduleBuild.toBuilder().setState(State.SUCCEEDED).setEndTimestamp(Optional.of(System.currentTimeMillis())).build();
update(succeeded);
return succeeded;
}
@Transactional
public ModuleBuild setStateToFailed(long moduleBuildId) {
ModuleBuild moduleBuild = getBuildWithExpectedState(moduleBuildId, State.IN_PROGRESS);
ModuleBuild failed = moduleBuild.toBuilder().setState(State.FAILED).setEndTimestamp(Optional.of(System.currentTimeMillis())).build();
update(failed);
return failed;
}
@Transactional
protected ModuleBuild skip(ModuleBuild build) {
long id = moduleBuildDao.skip(build);
build = build.toBuilder().setId(Optional.of(id)).build();
eventBus.post(build);
return build;
}
@Transactional
protected ModuleBuild enqueue(ModuleBuild build) {
long id = moduleBuildDao.enqueue(build);
build = build.toBuilder().setId(Optional.of(id)).build();
checkAffectedRowCount(moduleDao.updatePendingBuild(build));
eventBus.post(build);
return build;
}
@Transactional
public void setToLaunching(ModuleBuild build) {
updateModuleBuildAndModuleInfo(build);
eventBus.post(build);
}
@Transactional
void updateModuleBuildAndModuleInfo(ModuleBuild build) {
Preconditions.checkArgument(build.getStartTimestamp().isPresent());
checkAffectedRowCount(moduleBuildDao.begin(build));
checkAffectedRowCount(moduleDao.updateInProgressBuild(build));
}
@Transactional
public void update(ModuleBuild build) {
if (build.getState().isComplete()) {
Preconditions.checkArgument(build.getEndTimestamp().isPresent());
checkAffectedRowCount(moduleBuildDao.complete(build));
checkAffectedRowCount(moduleDao.updateLastBuild(build));
} else {
checkAffectedRowCount(moduleBuildDao.update(build));
}
eventBus.post(build);
}
@Transactional
public void cancel(ModuleBuild build) {
if (build.getState().isComplete()) {
throw new IllegalStateException(String.format("Build %d has already completed", build.getId().get()));
}
if (build.getState().isWaiting()) {
updateModuleBuildAndModuleInfo(build.toBuilder().setState(State.LAUNCHING).setStartTimestamp(Optional.of(System.currentTimeMillis())).build());
}
update(build.toBuilder().setState(State.CANCELLED).setEndTimestamp(Optional.of(System.currentTimeMillis())).build());
}
@Transactional
public ModuleBuild fail(ModuleBuild build) {
if (build.getState().isComplete()) {
throw new IllegalStateException(String.format("Build %d has already completed", build.getId().get()));
}
if (build.getState().isWaiting()) {
updateModuleBuildAndModuleInfo(build.toBuilder()
.setState(State.LAUNCHING)
.setStartTimestamp(Optional.of(System.currentTimeMillis()))
.build());
}
ModuleBuild failed = build.toBuilder()
.setState(State.FAILED)
.setEndTimestamp(Optional.of(System.currentTimeMillis()))
.build();
update(failed);
return failed;
}
/**
* This creates a failed module build without posting any build events. This allows us to fail module builds
* without having visitors do things for build states that this module is essentially skipping:
* For example we can avoid launching containers into singularity {@Link LaunchSingularityTaskBuildVisitor}.
* This means that we skip all the visitors for ModuleBuilds except the ones that trigger on `State.FAILED`
*/
@Transactional
public ModuleBuild createFailedBuild(RepositoryBuild repositoryBuild, Module module) {
int nextBuildNumber = repositoryBuild.getBuildNumber();
LOG.info("Repository build {} (build # {}) has failed. Will first create an ENQUEUED module build for module {} and then will fail it (will fake module build passing through all intermediate states from ENQUEUED to FAILED).",
repositoryBuild.getId().get(), nextBuildNumber, module.getId().get());
ModuleBuild queued =
ModuleBuild.queuedBuild(repositoryBuild, module, nextBuildNumber, BuildConfig.makeDefaultBuildConfig(), BuildConfig.makeDefaultBuildConfig());
long id = moduleBuildDao.enqueue(queued);
ModuleBuild queuedWithId = queued.toBuilder().setId(Optional.of(id)).build();
checkAffectedRowCount(moduleDao.updatePendingBuild(queuedWithId));
LOG.info("Repository build {} (build # {}) has failed: Persisted module build {} in state ENQUEUED for module {}",
repositoryBuild.getId().get(), nextBuildNumber, id, module.getId().get());
ModuleBuild failed = fail(queuedWithId);
LOG.info("Repository build {} (build # {}) has failed: Module build {} was set to state LAUNCHING and then to state FAILED for module {}",
repositoryBuild.getId().get(), nextBuildNumber, id, module.getId().get());
eventBus.post(failed);
return failed;
}
public int updateBuildClusterName(long moduleBuildId, String buildClusterName) {
return moduleBuildDao.updateBuildClusterName(moduleBuildId, buildClusterName);
}
private static void checkAffectedRowCount(int affectedRows) {
Preconditions.checkState(affectedRows == 1, "Expected to update 1 row but updated %s", affectedRows);
}
private ModuleBuild getBuildWithError(long moduleBuildId) {
Optional<ModuleBuild> maybeBuild = get(moduleBuildId);
if (maybeBuild.isPresent()) {
return maybeBuild.get();
} else {
throw new NotFoundException("No module build found with id: " + moduleBuildId);
}
}
private ModuleBuild getBuildWithExpectedState(long moduleBuildId, State expected) {
ModuleBuild build = getBuildWithError(moduleBuildId);
if (build.getState() == expected) {
return build;
} else {
throw new IllegalStateException(String.format("Module Build is in state %s, expected %s", build.getState(), expected));
}
}
}