/*************************GO-LICENSE-START*********************************
* Copyright 2014 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.
*************************GO-LICENSE-END***********************************/
package com.thoughtworks.go.server.service;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.thoughtworks.go.config.CaseInsensitiveString;
import com.thoughtworks.go.config.PipelineConfig;
import com.thoughtworks.go.domain.NullPipeline;
import com.thoughtworks.go.domain.Pipeline;
import com.thoughtworks.go.domain.SchedulingContext;
import com.thoughtworks.go.domain.buildcause.BuildCause;
import com.thoughtworks.go.domain.buildcause.BuildCauseOutOfDateException;
import com.thoughtworks.go.server.transaction.TransactionTemplate;
import com.thoughtworks.go.util.Clock;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
@Component
public class PipelineScheduleQueue {
private static final Logger LOGGER = Logger.getLogger(PipelineScheduleQueue.class);
private PipelineService pipelineService;
private TransactionTemplate transactionTemplate;
private Map<String, BuildCause> toBeScheduled = new ConcurrentHashMap<>();
private Map<String, BuildCause> mostRecentScheduled = new ConcurrentHashMap<>();
private InstanceFactory instanceFactory;
@Autowired
public PipelineScheduleQueue(PipelineService pipelineService, TransactionTemplate transactionTemplate, InstanceFactory instanceFactory) {
this.pipelineService = pipelineService;
this.transactionTemplate = transactionTemplate;
this.instanceFactory = instanceFactory;
}
public BuildCause mostRecentScheduled(String pipelineName) {
synchronized (mutexForPipelineName(pipelineName)) {
BuildCause buildCause = mostRecentScheduled.get(pipelineName);
if (buildCause != null) {
return buildCause;
}
mostRecentScheduled.put(pipelineName, mostRecentScheduledBuildCause(pipelineName));
return mostRecentScheduled.get(pipelineName);
}
}
private BuildCause mostRecentScheduledBuildCause(String pipelineName) {
Pipeline pipeline = pipelineService.mostRecentFullPipelineByName(pipelineName);
return pipeline instanceof NullPipeline ? BuildCause.createNeverRun() : pipeline.getBuildCause();
}
public void schedule(String pipelineName, BuildCause buildCause) {
synchronized (mutexForPipelineName(pipelineName)) {
BuildCause current = toBeScheduled.get(pipelineName);
if (current == null || buildCause.trumps(current)) {
toBeScheduled.put(pipelineName, buildCause);
}
}
}
public void cancelSchedule(String pipelineName) {
synchronized (mutexForPipelineName(pipelineName)) {
toBeScheduled.remove(pipelineName);
}
}
public synchronized Map<String, BuildCause> toBeScheduled() {
return new HashMap<>(toBeScheduled);
}
public void finishSchedule(String pipelineName, BuildCause buildCause, BuildCause newCause) {
synchronized (mutexForPipelineName(pipelineName)) {
if (buildCause.equals(toBeScheduled.get(pipelineName))) {
toBeScheduled.remove(pipelineName);
}
mostRecentScheduled.put(pipelineName, newCause);
}
}
public void clearPipeline(String pipelineName) {
synchronized (mutexForPipelineName(pipelineName)) {
toBeScheduled.remove(pipelineName);
mostRecentScheduled.remove(pipelineName);
}
}
//TODO: #5163 - this is a concurrency issue - talk to Rajesh or JJ
public boolean hasBuildCause(String pipelineName) {
BuildCause buildCause = toBeScheduled.get(pipelineName);
return buildCause != null && buildCause.getMaterialRevisions().totalNumberOfModifications() > 0;
}
public boolean hasForcedBuildCause(String pipelineName) {
synchronized (mutexForPipelineName(pipelineName)) {
BuildCause buildCause = toBeScheduled.get(pipelineName);
return buildCause != null && buildCause.isForced();
}
}
/**
* @deprecated Only for test
*/
public void clear() {
mostRecentScheduled.clear();
toBeScheduled.clear();
}
public Pipeline createPipeline(final BuildCause buildCause, final PipelineConfig pipelineConfig, final SchedulingContext context, final String md5, final Clock clock) {
return (Pipeline) transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
String pipelineName = CaseInsensitiveString.str(pipelineConfig.name());
Pipeline pipeline = null;
if (shouldCancel(buildCause, pipelineName)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("[Pipeline Schedule] Cancelling scheduling as build cause %s is the same as the most recent schedule", buildCause));
}
cancelSchedule(pipelineName);
} else {
try {
Pipeline newPipeline = instanceFactory.createPipelineInstance(pipelineConfig, buildCause, context, md5, clock);
pipeline = pipelineService.save(newPipeline);
finishSchedule(pipelineName, buildCause, pipeline.getBuildCause());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("[Pipeline Schedule] Successfully scheduled pipeline %s, buildCause:%s, configOrigin: %s",
pipelineName, buildCause,pipelineConfig.getOrigin()));
}
} catch (BuildCauseOutOfDateException e) {
cancelSchedule(pipelineName);
LOGGER.info(String.format("[Pipeline Schedule] Build cause %s is out of date. Scheduling is cancelled. Go will reschedule this pipeline. configOrigin: %s",
buildCause, pipelineConfig.getOrigin()));
}
}
return pipeline;
}
});
}
private boolean shouldCancel(BuildCause buildCause, String pipelineName) {
return !buildCause.isForced() && buildCause.isSameAs(mostRecentScheduled(pipelineName));
}
private String mutexForPipelineName(String pipelineName) {
return String.format("%s-%s", PipelineScheduleQueue.class.getName(), pipelineName).intern();
}
}