/* * 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.rits.cloning.Cloner; import com.thoughtworks.go.config.*; import com.thoughtworks.go.domain.*; import com.thoughtworks.go.domain.activity.StageStatusCache; import com.thoughtworks.go.domain.feed.Author; import com.thoughtworks.go.domain.feed.FeedEntries; import com.thoughtworks.go.domain.feed.stage.StageFeedEntry; import com.thoughtworks.go.dto.DurationBean; import com.thoughtworks.go.dto.DurationBeans; import com.thoughtworks.go.i18n.LocalizedMessage; import com.thoughtworks.go.presentation.pipelinehistory.StageHistoryPage; import com.thoughtworks.go.presentation.pipelinehistory.StageInstanceModels; import com.thoughtworks.go.server.cache.GoCache; import com.thoughtworks.go.server.dao.FeedModifier; import com.thoughtworks.go.server.dao.PipelineDao; import com.thoughtworks.go.server.dao.StageDao; import com.thoughtworks.go.server.dao.sparql.StageRunFinder; import com.thoughtworks.go.server.domain.StageIdentity; import com.thoughtworks.go.server.domain.StageStatusListener; import com.thoughtworks.go.server.domain.Username; import com.thoughtworks.go.server.messaging.StageStatusMessage; import com.thoughtworks.go.server.messaging.StageStatusTopic; import com.thoughtworks.go.server.service.result.LocalizedOperationResult; 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.MingleCard; import com.thoughtworks.go.server.ui.ModificationForPipeline; import com.thoughtworks.go.server.ui.StageSummaryModel; import com.thoughtworks.go.server.ui.StageSummaryModels; import com.thoughtworks.go.server.util.Pagination; import com.thoughtworks.go.server.util.UserHelper; import com.thoughtworks.go.serverhealth.HealthStateScope; import com.thoughtworks.go.serverhealth.HealthStateType; 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.TransactionCallback; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import java.util.*; @Service public class StageService implements StageRunFinder, StageFinder { private static final Logger LOGGER = Logger.getLogger(StageService.class); private static final int FEED_PAGE_SIZE = 25; private StageDao stageDao; private JobInstanceService jobInstanceService; private SecurityService securityService; private PipelineDao pipelineDao; private final ChangesetService changesetService; private final GoConfigService goConfigService; private TransactionTemplate transactionTemplate; private TransactionSynchronizationManager transactionSynchronizationManager; private List<StageStatusListener> stageStatusListeners; private StageStatusTopic stageStatusTopic; private StageStatusCache stageStatusCache; private Cloner cloner = new Cloner(); private GoCache goCache; private static final String NOT_AUTHORIZED_TO_VIEW_PIPELINE = "Not authorized to view pipeline"; @Autowired public StageService(StageDao stageDao, JobInstanceService jobInstanceService, StageStatusTopic stageStatusTopic, StageStatusCache stageStatusCache, SecurityService securityService, PipelineDao pipelineDao, ChangesetService changesetService, GoConfigService goConfigService, TransactionTemplate transactionTemplate, TransactionSynchronizationManager transactionSynchronizationManager, GoCache goCache, StageStatusListener... stageStatusListeners) { this.stageDao = stageDao; this.jobInstanceService = jobInstanceService; this.stageStatusTopic = stageStatusTopic; this.stageStatusCache = stageStatusCache; this.securityService = securityService; this.pipelineDao = pipelineDao; this.changesetService = changesetService; this.goConfigService = goConfigService; this.transactionTemplate = transactionTemplate; this.transactionSynchronizationManager = transactionSynchronizationManager; this.goCache = goCache; this.stageStatusListeners = new ArrayList<>(Arrays.asList(stageStatusListeners)); } public void addStageStatusListener(StageStatusListener listener) { stageStatusListeners.add(listener); } public Stage getStageByBuild(JobInstance jobInstance) { return getStageByBuild(jobInstance.getId()); } public Stage getStageByBuild(long buildId) { return stageDao.getStageByBuild(buildId); } public String stageNameByStageId(long stageId) { return stageDao.stageNameByStageId(stageId); } public Stage stageById(long id) { return stageDao.stageById(id); } public Stage getStageByIdWithBuilds(long id) { return stageDao.stageByIdWithBuilds(id); } public Stage findStageWithIdentifier(String pipelineName, int pipelineCounter, String stageName, String stageCounter, 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 findStageWithIdentifier(new StageIdentifier(pipelineName, pipelineCounter, stageName, stageCounter)); } public Stage findStageWithIdentifier(StageIdentifier identifier) { return stageDao.findStageWithIdentifier(identifier); } public StageSummaryModel findStageSummaryByIdentifier(StageIdentifier stageId, Username username, LocalizedOperationResult result) { if (!securityService.hasViewPermissionForPipeline(username, stageId.getPipelineName())) { result.unauthorized(LocalizedMessage.cannotViewPipeline(stageId.getPipelineName()), HealthStateType.general(HealthStateScope.forPipeline(stageId.getPipelineName()))); return null; } Stages stages = stageDao.getAllRunsOfStageForPipelineInstance(stageId.getPipelineName(), stageId.getPipelineCounter(), stageId.getStageName()); for (Stage stage : stages) { if (stage.getIdentifier().getStageCounter().equals(stageId.getStageCounter())) { StageSummaryModel summaryModel = new StageSummaryModel(stage, stages, stageDao, null); return summaryModel; } } result.notFound(LocalizedMessage.stageNotFound(stageId), HealthStateType.general(HealthStateScope.GLOBAL)); return null; } public synchronized void cancelStage(final Stage stage) { cancel(stage); notifyStageStatusChangeListeners(stage); transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { stageStatusTopic.post(new StageStatusMessage(stage.getIdentifier(), stage.stageState(), stage.getResult(), UserHelper.getUserName())); } }); } private void cancel(final Stage stage) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { for (JobInstance job : stage.getJobInstances()) { jobInstanceService.cancelJob(job); } updateStageWithoutNotifications(stage); } }); } public DurationBeans getBuildDurations(String pipelineName, Stage stage) { DurationBeans durationBeans = new DurationBeans(); for (JobInstance job : stage.getJobInstances()) { durationBeans.add(getDuration(pipelineName, stage.getName(), job)); } return durationBeans; } public DurationBean getBuildDuration(String pipelineName, String stageName, JobInstance job) { return getDuration(pipelineName, stageName, job); } private DurationBean getDuration(String pipelineName, String stageName, JobInstance job) { if (job.isCompleted()) { // Calculating duration is an expensive query; only do so when the stage is building. return new DurationBean(job.getId(), 0L); } Long duration = stageDao.getDurationOfLastSuccessfulOnAgent(pipelineName, stageName, job); return new DurationBean(job.getId(), duration == null ? 0L : duration); } public Stage stageByIdWithModifications(long stageId) { return stageDao.stageById(stageId); } public Stage mostRecentPassed(String pipelineName, String stageName) { return stageDao.mostRecentPassed(pipelineName, stageName); } public int getCount(String pipelineName, String stageName) { return stageDao.getCount(pipelineName, stageName); } public Stage save(final Pipeline pipeline, final Stage stage) { return (Stage) transactionTemplate.execute(new TransactionCallback() { public Object doInTransaction(TransactionStatus status) { stage.building(); final Stage savedStage = persistStage(pipeline, stage); persistJobs(savedStage); notifyStageStatusChangeListeners(savedStage); return savedStage; } }); } private void notifyStageStatusChangeListeners(final Stage savedStage) { transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { StageStatusListener[] prototype = new StageStatusListener[0]; for (StageStatusListener stageStatusListener : stageStatusListeners.toArray(prototype)) { try { stageStatusListener.stageStatusChanged(savedStage); } catch (Throwable e) { LOGGER.error("error notifying listener for stage " + savedStage, e); } } } }); } // Because current stage order is defined by existing order(for rerun) or Max(order)+1 (for the first run), so we // need to synchronize this method call to make sure no concurrent issues. private synchronized Stage persistStage(Pipeline pipeline, Stage stage) { long pipelineId = pipeline.getId(); stage.setOrderId(resolveStageOrder(pipelineId, stage.getName())); Stage savedStage = stageDao.save(pipeline, stage); savedStage.setIdentifier(new StageIdentifier(pipeline.getName(), pipeline.getCounter(), pipeline.getLabel(), stage.getName(), String.valueOf(stage.getCounter()))); for (JobInstance jobInstance : savedStage.getJobInstances()) { jobInstance.setIdentifier(new JobIdentifier(pipeline, savedStage, jobInstance)); } return savedStage; } private void persistJobs(Stage stage) { for (JobInstance job : stage.getJobInstances()) { jobInstanceService.save(stage.getIdentifier(), stage.getId(), job); } } //stage order definition: 1) if stage has been scheduled, copy existing order 2) if not, increase the max existing // stage order in current pipeline by 1, as current stage's order private Integer resolveStageOrder(long pipelineId, String stageName) { Integer order = getStageOrderInPipeline(pipelineId, stageName); if (order == null) { order = getMaxStageOrderInPipeline(pipelineId) + 1; } return order; } private Integer getStageOrderInPipeline(long pipelineId, String stageName) { return stageDao.getStageOrderInPipeline(pipelineId, stageName); } private int getMaxStageOrderInPipeline(long pipelineId) { return stageDao.getMaxStageOrder(pipelineId); } public void updateResult(final Stage stage) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { updateStageWithoutNotifications(stage); notifyStageStatusChangeListeners(stage); } }); } private void updateStageWithoutNotifications(final Stage stage) { stage.calculateResult(); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { clearCachedCompletedStageFeeds(stage.getIdentifier().getPipelineName()); } }); stageDao.updateResult(stage, stage.getResult()); } }); } public Stage mostRecentStageWithBuilds(String pipelineName, StageConfig stageConfig) { Stage stage = findLatestStage(pipelineName, CaseInsensitiveString.str(stageConfig.name())); if (stage == null) { return NullStage.createNullStage(stageConfig); } stage.setJobInstances(jobInstanceService.currentJobsOfStage(pipelineName, stageConfig)); return stage; } public Stage findLatestStage(String pipelineName, String stageName) { return stageStatusCache.currentStage(new StageConfigIdentifier(pipelineName, stageName)); } public FeedEntries feed(String pipelineName, Username username) { String key = cacheKeyForLatestStageFeedForPipeline(pipelineName); List<StageFeedEntry> feedEntries = (List<StageFeedEntry>) goCache.get(key); if (feedEntries == null) { synchronized (key) { feedEntries = (List<StageFeedEntry>) goCache.get(key);//Double check locking is done because the query is expensive (takes about 2 seconds) if (feedEntries == null) { feedEntries = stageDao.findCompletedStagesFor(pipelineName, FeedModifier.Latest, -1, FEED_PAGE_SIZE); populateAuthorsAndMingleCards(feedEntries, pipelineName, username); goCache.put(key, feedEntries); } } } return cloner.deepClone(new FeedEntries(new ArrayList<>(feedEntries))); } private String cacheKeyForLatestStageFeedForPipeline(String pipelineName) { return String.format("%s_latestStageFeedForPipeline_%s", getClass().getName(), pipelineName).intern(); } private void clearCachedCompletedStageFeeds(String pipelineName) { String key = cacheKeyForLatestStageFeedForPipeline(pipelineName); synchronized (key) { goCache.remove(key); } } public FeedEntries feedBefore(long entryId, String pipelineName, Username username) { List<StageFeedEntry> stageEntries = stageDao.findCompletedStagesFor(pipelineName, FeedModifier.Before, entryId, FEED_PAGE_SIZE); populateAuthorsAndMingleCards(stageEntries, pipelineName, username); return new FeedEntries(new ArrayList<>(stageEntries)); } private void populateAuthorsAndMingleCards(List<StageFeedEntry> stageEntries, String pipelineName, Username username) { List<Long> pipelineIds = new ArrayList<>(); for (StageFeedEntry stageEntry : stageEntries) { pipelineIds.add(stageEntry.getPipelineId()); } CruiseConfig config = goConfigService.currentCruiseConfig(); Map<Long, List<ModificationForPipeline>> revisionsPerPipeline = changesetService.modificationsOfPipelines(pipelineIds, pipelineName, username); for (StageFeedEntry stageEntry : stageEntries) { List<ModificationForPipeline> revs = revisionsPerPipeline.get(stageEntry.getPipelineId()); for (ModificationForPipeline rev : revs) { Author author = rev.getAuthor(); if (author != null) { stageEntry.addAuthor(author); } String pipelineForRev = rev.getPipelineId().getPipelineName(); if (config.hasPipelineNamed(new CaseInsensitiveString(pipelineForRev))) { PipelineConfig pipelineConfig = config.pipelineConfigByName(new CaseInsensitiveString(pipelineForRev)); MingleConfig mingleConfig = pipelineConfig.getMingleConfig(); Set<String> cardNos = rev.getCardNumbersFromComments(); if (mingleConfig.isDefined()) { for (String cardNo : cardNos) { stageEntry.addCard(new MingleCard(mingleConfig, cardNo)); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("pipeline not found: " + pipelineForRev); } } } } } public StageSummaryModels findStageHistoryForChart(String pipelineName, String stageName, int pageNumber, int pageSize, Username username) { int total = stageDao.getTotalStageCountForChart(pipelineName, stageName); Pagination pagination = Pagination.pageByNumber(pageNumber, total, pageSize); List<Stage> stages = stageDao.findStageHistoryForChart(pipelineName, stageName, pageSize, pagination.getOffset()); StageSummaryModels stageSummaryModels = new StageSummaryModels(); for (Stage forStage : stages) { StageSummaryModel stageSummaryByIdentifier = new StageSummaryModel(forStage, null, stageDao, forStage.getIdentifier()); if (!stageSummaryByIdentifier.getStage().getState().completed()) { continue; } stageSummaryModels.add(stageSummaryByIdentifier); } stageSummaryModels.setPagination(pagination); return stageSummaryModels; } public StageHistoryPage findStageHistoryPage(Stage stage, int pageSize) { return stageDao.findStageHistoryPage(stage, pageSize); } public StageHistoryPage findStageHistoryPageByNumber(String pipelineName, String stageName, int pageNumber, int pageSize) { return stageDao.findStageHistoryPageByNumber(pipelineName, stageName, pageNumber, pageSize); } public StageInstanceModels findDetailedStageHistoryByOffset(String pipelineName, String stageName, 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 stageDao.findDetailedStageHistoryByOffset(pipelineName, stageName, pagination); } public Long findStageIdByLocator(String locator) { String[] parts = locator.split("/"); String pipelineName = parts[0]; String counterOrLabel = parts[1]; if (counterOrLabel.matches(".+\\[.+\\]")) { counterOrLabel = counterOrLabel.substring(0, counterOrLabel.indexOf("[")); } String stageName = parts[2]; String stageCounter = parts[3]; Pipeline pipeline = pipelineDao.findPipelineByCounterOrLabel(pipelineName, counterOrLabel); return stageDao.findStageIdByPipelineAndStageNameAndCounter(pipeline.getId(), stageName, stageCounter); } /** * @return Listeners * @deprecated Used only in tests */ List<StageStatusListener> getStageStatusListeners() { return stageStatusListeners; } /** * @deprecated don't call this directly, go through ScheduleService.cancelJob so that stageLevel synchronization is done */ public void cancelJob(final JobInstance jobInstance) { changeJob(new JobOperation() { public void invoke() { jobInstanceService.cancelJob(jobInstance); } }, jobInstance.getIdentifier()); } /** * @deprecated don't call this directly, go through ScheduleService.failJob so that stageLevel synchronization is done */ public void failJob(final JobInstance jobInstance) { changeJob(new JobOperation() { public void invoke() { jobInstanceService.failJob(jobInstance); } }, jobInstance.getIdentifier()); } private void changeJob(final JobOperation jobOperation, final JobIdentifier identifier) { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { jobOperation.invoke(); stageDao.clearCachedStage(identifier.getStageIdentifier()); Stage stage = stageDao.findStageWithIdentifier(identifier.getStageIdentifier()); updateStageWithoutNotifications(stage); notifyStageStatusChangeListeners(stage); } }); } public List<StageIdentifier> findRunForStage(StageIdentifier stageIdentifier) { String pipelineName = stageIdentifier.getPipelineName(); String stageName = stageIdentifier.getStageName(); double toNaturalOrder = pipelineDao.findPipelineByNameAndCounter(pipelineName, stageIdentifier.getPipelineCounter()).getNaturalOrder(); Pipeline pipelineThatLastPassed = pipelineDao.findEarlierPipelineThatPassedForStage(pipelineName, stageName, toNaturalOrder); double fromNaturalOrder = pipelineThatLastPassed != null ? pipelineThatLastPassed.getNaturalOrder() : 0.0; List<StageIdentifier> finalIds = new ArrayList<>(); List<StageIdentifier> failedStages = stageDao.findFailedStagesBetween(pipelineName, stageName, fromNaturalOrder, toNaturalOrder); if (failedStages.isEmpty() || !failedStages.get(0).equals(stageIdentifier)) { return finalIds; } for (StageIdentifier identifier : failedStages) { finalIds.add(identifier); } return finalIds; } public boolean isStageActive(String pipelineName, String stageName) { return stageDao.isStageActive(pipelineName, stageName); } public boolean isAnyStageActiveForPipeline(String pipelineName, int counter) { return stageDao.findAllStagesFor(pipelineName, counter).isAnyStageActive(); } public List<Stage> oldestStagesWithDeletableArtifacts() { return stageDao.oldestStagesHavingArtifacts(); } public void markArtifactsDeletedFor(Stage stage) { stageDao.markArtifactsDeletedFor(stage); } public List<StageIdentity> findLatestStageInstances() { return stageDao.findLatestStageInstances(); } public interface JobOperation { void invoke(); } }