/*
* Copyright 2017 ThoughtWorks, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.thoughtworks.go.server.service;
import com.thoughtworks.go.config.*;
import com.thoughtworks.go.domain.*;
import com.thoughtworks.go.domain.activity.JobStatusCache;
import com.thoughtworks.go.listener.ConfigChangedListener;
import com.thoughtworks.go.listener.EntityConfigChangedListener;
import com.thoughtworks.go.plugin.infra.PluginManager;
import com.thoughtworks.go.server.dao.JobInstanceDao;
import com.thoughtworks.go.server.domain.JobStatusListener;
import com.thoughtworks.go.server.domain.Username;
import com.thoughtworks.go.server.messaging.JobResultMessage;
import com.thoughtworks.go.server.messaging.JobResultTopic;
import com.thoughtworks.go.server.service.result.OperationResult;
import com.thoughtworks.go.server.transaction.TransactionSynchronizationManager;
import com.thoughtworks.go.server.transaction.TransactionTemplate;
import com.thoughtworks.go.server.ui.JobInstancesModel;
import com.thoughtworks.go.server.ui.SortOrder;
import com.thoughtworks.go.server.util.Pagination;
import com.thoughtworks.go.serverhealth.HealthStateScope;
import com.thoughtworks.go.serverhealth.HealthStateType;
import com.thoughtworks.go.serverhealth.ServerHealthService;
import com.thoughtworks.go.serverhealth.ServerHealthState;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@Service
public class JobInstanceService implements JobPlanLoader, ConfigChangedListener {
private final JobInstanceDao jobInstanceDao;
private final PropertiesService buildPropertiesService;
private final JobResultTopic jobResultTopic;
private final JobStatusCache jobStatusCache;
private final TransactionTemplate transactionTemplate;
private final TransactionSynchronizationManager transactionSynchronizationManager;
private final JobResolverService jobResolverService;
private final EnvironmentConfigService environmentConfigService;
private final GoConfigService goConfigService;
private SecurityService securityService;
private PluginManager pluginManager;
private final ServerHealthService serverHealthService;
private final List<JobStatusListener> listeners;
private static final String NOT_AUTHORIZED_TO_VIEW_PIPELINE = "Not authorized to view pipeline";
private static Logger LOGGER = Logger.getLogger(JobInstanceService.class);
private static final Object LISTENERS_MODIFICATION_MUTEX = new Object();
@Autowired
JobInstanceService(JobInstanceDao jobInstanceDao, PropertiesService buildPropertiesService, JobResultTopic jobResultTopic, JobStatusCache jobStatusCache,
TransactionTemplate transactionTemplate, TransactionSynchronizationManager transactionSynchronizationManager, JobResolverService jobResolverService,
EnvironmentConfigService environmentConfigService, GoConfigService goConfigService,
SecurityService securityService, PluginManager pluginManager, ServerHealthService serverHealthService, JobStatusListener... listener) {
this.jobInstanceDao = jobInstanceDao;
this.buildPropertiesService = buildPropertiesService;
this.jobResultTopic = jobResultTopic;
this.jobStatusCache = jobStatusCache;
this.transactionTemplate = transactionTemplate;
this.transactionSynchronizationManager = transactionSynchronizationManager;
this.jobResolverService = jobResolverService;
this.environmentConfigService = environmentConfigService;
this.goConfigService = goConfigService;
this.securityService = securityService;
this.pluginManager = pluginManager;
this.serverHealthService = serverHealthService;
this.listeners = new ArrayList<>(Arrays.asList(listener));
this.goConfigService.register(this);
this.goConfigService.register(new PipelineConfigChangedListener());
}
public JobInstances latestCompletedJobs(String pipelineName, String stageName, String jobConfigName) {
return jobInstanceDao.latestCompletedJobs(pipelineName, stageName, jobConfigName, 25);
}
public int getJobHistoryCount(String pipelineName, String stageName, String jobConfigName) {
return jobInstanceDao.getJobHistoryCount(pipelineName, stageName, jobConfigName);
}
public JobInstances findJobHistoryPage(String pipelineName, String stageName, String jobConfigName, Pagination pagination, String username, OperationResult result) {
if (!goConfigService.currentCruiseConfig().hasPipelineNamed(new CaseInsensitiveString(pipelineName))) {
result.notFound("Not Found", "Pipeline not found", HealthStateType.general(HealthStateScope.GLOBAL));
return null;
}
if (!securityService.hasViewPermissionForPipeline(Username.valueOf(username), pipelineName)) {
result.unauthorized("Unauthorized", NOT_AUTHORIZED_TO_VIEW_PIPELINE, HealthStateType.general(HealthStateScope.forPipeline(pipelineName)));
return null;
}
return jobInstanceDao.findJobHistoryPage(pipelineName, stageName, jobConfigName, pagination.getPageSize(), pagination.getOffset());
}
public JobInstance buildByIdWithTransitions(long buildId) {
return jobInstanceDao.buildByIdWithTransitions(buildId);
}
public JobInstance buildById(long buildId) {
return jobInstanceDao.buildById(buildId);
}
public void updateAssignedInfo(JobInstance jobInstance) {
jobInstanceDao.updateAssignedInfo(jobInstance);
notifyJobStatusChangeListeners(jobInstance);
}
public void updateStateAndResult(final JobInstance job) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
internalUpdateJobStateAndResult(job);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("job status updated [%s]", job));
}
notifyJobStatusChangeListeners(job);
}
});
}
private void notifyJobStatusChangeListeners(final JobInstance job) {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
List<JobStatusListener> listeners1;
synchronized (LISTENERS_MODIFICATION_MUTEX) {
listeners1 = new ArrayList<>(listeners);
}
for (JobStatusListener jobStatusListener : listeners1) {
try {
jobStatusListener.jobStatusChanged(job);
} catch (Exception e) {
LOGGER.error("error notifying listener for job " + job, e);
}
}
}
});
}
/**
* This method exists only so that we can scope the transaction properly
*/
private void internalUpdateJobStateAndResult(final JobInstance job) {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
jobInstanceDao.updateStateAndResult(job);
if (job.isCompleted()) {
buildPropertiesService.saveCruiseProperties(job);
}
}
});
}
public JobPlan loadOriginalJobPlan(JobIdentifier jobId) {
JobIdentifier actualId = jobResolverService.actualJobIdentifier(jobId);
return jobInstanceDao.loadPlan(actualId.getBuildId());
}
public List<JobPlan> orderedScheduledBuilds() {
return jobInstanceDao.orderedScheduledBuilds();
}
public List<WaitingJobPlan> waitingJobPlans() {
List<JobPlan> jobPlans = orderedScheduledBuilds();
List<WaitingJobPlan> waitingJobPlans = new ArrayList<>();
for (JobPlan jobPlan : jobPlans) {
String envForJob = environmentConfigService.envForPipeline(jobPlan.getPipelineName());
waitingJobPlans.add(new WaitingJobPlan(jobPlan, envForJob));
}
return waitingJobPlans;
}
//TODO: Performance fix - we should be using CurrentActivity here
public JobInstances findHungJobs(List<String> liveAgentIdList) {
return jobInstanceDao.findHungJobs(liveAgentIdList);
}
public void cancelJob(final JobInstance job) {
LOGGER.info(String.format("cancelling job [%s]", job));
boolean cancelled = job.cancel();
if (cancelled) {
updateStateAndResult(job);
notifyJobCancelled(job);
}
}
private void notifyJobCancelled(final JobInstance instance) {
if (instance.isAssignedToAgent()) {
transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
jobResultTopic.post(new JobResultMessage(instance.getIdentifier(), instance.getResult(), instance.getAgentUuid()));
}
});
}
}
public void save(StageIdentifier stageIdentifier, long stageId, final JobInstance job) {
jobInstanceDao.save(stageId, job);
job.setIdentifier(new JobIdentifier(stageIdentifier, job));
notifyJobStatusChangeListeners(job);
}
public JobInstances currentJobsOfStage(String pipelineName, StageConfig stageConfig) {
JobInstances jobs = new JobInstances();
for (JobConfig jobConfig : stageConfig.allBuildPlans()) {
JobConfigIdentifier jobConfigIdentifier = new JobConfigIdentifier(pipelineName, CaseInsensitiveString.str(stageConfig.name()), CaseInsensitiveString.str(jobConfig.name()));
List<JobInstance> found = jobStatusCache.currentJobs(jobConfigIdentifier);
if (found.isEmpty()) {
jobs.add(new NullJobInstance(CaseInsensitiveString.str(jobConfig.name())));
} else {
jobs.addAll(found);
}
}
jobs.sortByName();
return jobs;
}
public List<JobIdentifier> allBuildingJobs() {
return jobInstanceDao.getBuildingJobs();
}
public void registerJobStateChangeListener(JobStatusListener jobStatusListener) {
synchronized (LISTENERS_MODIFICATION_MUTEX) {
listeners.add(jobStatusListener);
}
}
public void failJob(JobInstance jobInstance) {
jobInstance.fail();
if (jobInstance.isFailed()) {
updateStateAndResult(jobInstance);
notifyJobCancelled(jobInstance);
}
}
public JobInstancesModel completedJobsOnAgent(String uuid, JobHistoryColumns columnName, SortOrder order, int pageNumber, int pageSize) {
int total = totalCompletedJobsCountOn(uuid);
Pagination pagination = Pagination.pageByNumber(pageNumber, total, pageSize);
return completedJobsOnAgent(uuid, columnName, order, pagination);
}
public int totalCompletedJobsCountOn(String uuid) {
return jobInstanceDao.totalCompletedJobsOnAgent(uuid);
}
public JobInstancesModel completedJobsOnAgent(String uuid, JobHistoryColumns columnName, SortOrder order, Pagination pagination) {
List<JobInstance> jobInstances = jobInstanceDao.completedJobsOnAgent(uuid, columnName, order, pagination.getOffset(), pagination.getPageSize());
CruiseConfig cruiseConfig = goConfigService.getCurrentConfig();
for (JobInstance jobInstance : jobInstances) {
jobInstance.setPipelineStillConfigured(cruiseConfig.hasPipelineNamed(new CaseInsensitiveString(jobInstance.getPipelineName())));
}
return new JobInstancesModel(new JobInstances(jobInstances), pagination);
}
@Override
public void onConfigChange(CruiseConfig newCruiseConfig) {
for (ServerHealthState state : serverHealthService.getAllLogs()) {
HealthStateScope currentScope = state.getType().getScope();
if (currentScope.isForJob()) {
serverHealthService.removeByScope(currentScope);
}
}
}
class PipelineConfigChangedListener extends EntityConfigChangedListener<PipelineConfig> {
@Override
public void onEntityConfigChange(PipelineConfig pipelineConfig) {
for (ServerHealthState state : serverHealthService.getAllLogs()) {
HealthStateScope currentScope = state.getType().getScope();
if (currentScope.isForJob()) {
String[] split = currentScope.getScope().split("/");
if (split.length > 0 && new CaseInsensitiveString(split[0]).equals(pipelineConfig.name())) {
serverHealthService.removeByScope(currentScope);
}
}
}
}
}
public enum JobHistoryColumns {
pipeline("pipelineName"), stage("stageName"), job("name"), result("result"), completed("lastTransitionTime");
private final String columnName;
JobHistoryColumns(String columnName) {
this.columnName = columnName;
}
public String getColumnName() {
return columnName;
}
}
}