/* * Copyright 2016 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.dao; import com.ibatis.sqlmap.client.SqlMapClient; import com.opensymphony.oscache.base.Cache; import com.rits.cloning.Cloner; import com.thoughtworks.go.config.ArtifactPlan; import com.thoughtworks.go.config.ArtifactPropertiesGenerator; import com.thoughtworks.go.config.Resource; import com.thoughtworks.go.database.Database; import com.thoughtworks.go.domain.*; import com.thoughtworks.go.server.cache.GoCache; import com.thoughtworks.go.server.domain.JobStatusListener; import com.thoughtworks.go.server.persistence.ArtifactPlanRepository; import com.thoughtworks.go.server.persistence.ArtifactPropertiesGeneratorRepository; import com.thoughtworks.go.server.persistence.ResourceRepository; import com.thoughtworks.go.server.service.JobInstanceService; import com.thoughtworks.go.server.transaction.SqlMapClientDaoSupport; import com.thoughtworks.go.server.transaction.TransactionSynchronizationManager; import com.thoughtworks.go.server.transaction.TransactionTemplate; import com.thoughtworks.go.server.ui.SortOrder; import com.thoughtworks.go.server.util.SqlUtil; import com.thoughtworks.go.util.StringUtil; import com.thoughtworks.go.util.SystemEnvironment; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.stereotype.Component; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionSynchronizationAdapter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static com.thoughtworks.go.util.IBatisUtil.arguments; @SuppressWarnings("unchecked") @Component public class JobInstanceSqlMapDao extends SqlMapClientDaoSupport implements JobInstanceDao, JobStatusListener { private static final Logger LOG = Logger.getLogger(JobInstanceSqlMapDao.class); private Cache cache; private TransactionSynchronizationManager transactionSynchronizationManager; private GoCache goCache; private TransactionTemplate transactionTemplate; private EnvironmentVariableDao environmentVariableDao; private JobAgentMetadataDao jobAgentMetadataDao; private Cloner cloner = new Cloner(); private ResourceRepository resourceRepository; private ArtifactPlanRepository artifactPlanRepository; private ArtifactPropertiesGeneratorRepository artifactPropertiesGeneratorRepository; @Autowired public JobInstanceSqlMapDao(EnvironmentVariableDao environmentVariableDao, GoCache goCache, TransactionTemplate transactionTemplate, SqlMapClient sqlMapClient, Cache cache, TransactionSynchronizationManager transactionSynchronizationManager, SystemEnvironment systemEnvironment, Database database, ResourceRepository resourceRepository, ArtifactPlanRepository artifactPlanRepository, ArtifactPropertiesGeneratorRepository artifactPropertiesGeneratorRepository, JobAgentMetadataDao jobAgentMetadataDao) { super(goCache, sqlMapClient, systemEnvironment, database); this.environmentVariableDao = environmentVariableDao; this.goCache = goCache; this.transactionTemplate = transactionTemplate; this.cache = cache; this.transactionSynchronizationManager = transactionSynchronizationManager; this.resourceRepository = resourceRepository; this.artifactPlanRepository = artifactPlanRepository; this.artifactPropertiesGeneratorRepository = artifactPropertiesGeneratorRepository; this.jobAgentMetadataDao = jobAgentMetadataDao; } public JobInstance buildByIdWithTransitions(long buildInstanceId) { String cacheKey = cacheKeyforJobInstanceWithTransitions(buildInstanceId); synchronized (cacheKey) { JobInstance instance = (JobInstance) goCache.get(cacheKey); if (instance == null) { instance = job(buildInstanceId, "buildByIdWithTransitions"); goCache.put(cacheKey, instance); } return cloner.deepClone(instance); } } private String cacheKeyforJobInstanceWithTransitions(long jobId) { return (getClass().getName() + "_jobInstanceWithTranistionIds_" + jobId).intern(); } public JobInstance buildById(long buildId) { return job(buildId, "buildById"); } private JobInstance job(long buildId, String queryName) { JobInstance instance = (JobInstance) getSqlMapClientTemplate().queryForObject(queryName, buildId); if (instance == null) { throw new DataRetrievalFailureException("Could not load build with id " + buildId); } if (instance.getIdentifier() == null) { throw new RuntimeException("Identifier must not be null!"); } return instance; } public List<ActiveJob> activeJobs() { return getActiveJobs(getActiveJobIds()); } private List<ActiveJob> getActiveJobs(List<Long> activeJobIds) { List<ActiveJob> activeJobs = new ArrayList<>(); for (Long activeJobId : activeJobIds) { ActiveJob job = getActiveJob(activeJobId); if (job != null) { activeJobs.add(job); } } return activeJobs; } private ActiveJob getActiveJob(Long activeJobId) { String activeJobKey = cacheKeyForActiveJob(activeJobId); ActiveJob activeJob = (ActiveJob) goCache.get(activeJobKey); if (activeJob == null) { synchronized (activeJobKey) { activeJob = (ActiveJob) goCache.get(activeJobKey); if (activeJob == null) { activeJob = _getActiveJob(activeJobId); if (activeJob != null) { // could have changed to not active and consquently no match found cacheActiveJob(activeJob); } } } } return activeJob;//TODO: clone it, caller may mutate } private List<Long> getActiveJobIds() { String idsCacheKey = cacheKeyForActiveJobIds(); List<Long> activeJobIds = (List<Long>) goCache.get(idsCacheKey); synchronized (idsCacheKey) { if (activeJobIds == null) { activeJobIds = getSqlMapClientTemplate().queryForList("getActiveJobIds"); goCache.put(idsCacheKey, activeJobIds); } } return activeJobIds; } private ActiveJob _getActiveJob(Long id) { return (ActiveJob) getSqlMapClientTemplate().queryForObject("getActiveJobById", arguments("id", id).asMap()); } private void cacheActiveJob(ActiveJob activeJob) { goCache.put(cacheKeyForActiveJob(activeJob.getId()), cloner.deepClone(activeJob));//TODO: we should clone while serving the object out, and not while adding it to cache } public JobInstance mostRecentJobWithTransitions(JobIdentifier job) { Long buildId = findOriginalJobIdentifier(job.getStageIdentifier(), job.getBuildName()).getBuildId(); return buildByIdWithTransitions(buildId); } public JobInstance save(long stageId, JobInstance jobInstance) { jobInstance.setStageId(stageId); getSqlMapClientTemplate().insert("insertBuild", jobInstance); updateStateAndResult(jobInstance); JobPlan plan = jobInstance.getPlan(); if (plan != null) { save(jobInstance.getId(), plan); } return jobInstance; } public void save(long jobId, JobPlan plan) { for (Resource resource : plan.getResources()) { resourceRepository.saveCopyOf(jobId, resource); } for (ArtifactPropertiesGenerator generator : plan.getPropertyGenerators()) { artifactPropertiesGeneratorRepository.saveCopyOf(jobId, generator); } for (ArtifactPlan artifactPlan : plan.getArtifactPlans()) { artifactPlanRepository.saveCopyOf(jobId, artifactPlan); } environmentVariableDao.save(jobId, EnvironmentVariableSqlMapDao.EnvironmentVariableType.Job, plan.getVariables()); if (plan.requiresElasticAgent()){ jobAgentMetadataDao.save(new JobAgentMetadata(jobId, plan.getElasticProfile())); } } public JobPlan loadPlan(long jobId) { DefaultJobPlan plan = (DefaultJobPlan) getSqlMapClientTemplate().queryForObject("select-job-plan", jobId); loadJobPlanAssociatedEntities(plan); return plan; } private void loadJobPlanAssociatedEntities(DefaultJobPlan plan) { plan.setPlans(artifactPlanRepository.findByBuildId(plan.getJobId())); plan.setGenerators(artifactPropertiesGeneratorRepository.findByBuildId(plan.getJobId())); plan.setResources(resourceRepository.findByBuildId(plan.getJobId())); plan.setVariables(environmentVariableDao.load(plan.getJobId(), EnvironmentVariableSqlMapDao.EnvironmentVariableType.Job)); plan.setTriggerVariables(environmentVariableDao.load(plan.getPipelineId(), EnvironmentVariableSqlMapDao.EnvironmentVariableType.Trigger)); JobAgentMetadata jobAgentMetadata = jobAgentMetadataDao.load(plan.getJobId()); if (jobAgentMetadata != null){ plan.setElasticProfile(jobAgentMetadata.elasticProfile()); } } public JobIdentifier findOriginalJobIdentifier(StageIdentifier stageIdentifier, String jobName) { String key = cacheKeyForOriginalJobIdentifier(stageIdentifier, jobName); JobIdentifier jobIdentifier = (JobIdentifier) goCache.get(key); if (jobIdentifier == null) { synchronized (key) { jobIdentifier = (JobIdentifier) goCache.get(key); if (jobIdentifier == null) { Map params = arguments("pipelineName", stageIdentifier.getPipelineName()). and("pipelineCounter", stageIdentifier.getPipelineCounter()). and("pipelineLabel", stageIdentifier.getPipelineLabel()). and("stageName", stageIdentifier.getStageName()). and("stageCounter", Integer.parseInt(stageIdentifier.getStageCounter())). and("jobName", jobName).asMap(); jobIdentifier = (JobIdentifier) getSqlMapClientTemplate().queryForObject("findJobId", params); goCache.put(key, jobIdentifier); } } } return cloner.deepClone(jobIdentifier); } private String cacheKeyForOriginalJobIdentifier(StageIdentifier stageIdentifier, String jobName) { return (getClass().getName() + "_originalJobIdentifier_" + StringUtil.escapeAndJoinStrings( stageIdentifier.getPipelineName(), stageIdentifier.getPipelineLabel(), String.valueOf(stageIdentifier.getPipelineCounter()), stageIdentifier.getStageName(), stageIdentifier.getStageCounter()) + "_job_" + jobName.toLowerCase()).intern(); } public List<JobIdentifier> getBuildingJobs() { return buildingJobs(getActiveJobIds()); } public List<JobInstance> completedJobsOnAgent(String uuid, JobInstanceService.JobHistoryColumns jobHistoryColumns, SortOrder order, int offset, int limit) { Map params = arguments("uuid", uuid). and("offset", offset). and("limit", limit). and("column", jobHistoryColumns.getColumnName()). and("order", order.toString()).asMap(); return (List<JobInstance>) getSqlMapClientTemplate().queryForList("completedJobsOnAgent", params); } public int totalCompletedJobsOnAgent(String uuid) { return (Integer) getSqlMapClientTemplate().queryForObject("totalCompletedJobsOnAgent", arguments("uuid", uuid).asMap()); } private List<JobIdentifier> buildingJobs(List<Long> activeJobIds) { List<JobIdentifier> buildingJobs = new ArrayList<>(); for (Long activeJobId : activeJobIds) { JobIdentifier jobIdentifier = (JobIdentifier) getSqlMapClientTemplate().queryForObject("getBuildingJobIdentifier", activeJobId); if (jobIdentifier != null) { buildingJobs.add(jobIdentifier); } } return buildingJobs; } public JobInstance updateAssignedInfo(final JobInstance jobInstance) { return (JobInstance) transactionTemplate.execute(new TransactionCallback() { public Object doInTransaction(TransactionStatus status) { getSqlMapClientTemplate().update("updateAssignedInfo", jobInstance); updateStateAndResult(jobInstance); return jobInstance; } }); } public JobInstance updateStateAndResult(final JobInstance jobInstance) { return (JobInstance) transactionTemplate.execute(new TransactionCallback() { public Object doInTransaction(TransactionStatus status) { transactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() { @Override public void afterCommit() { // Methods not extracted in order to make synchronization visible. synchronized (cacheKeyForJobPlan(jobInstance.getId())) { removeCachedJobPlan(jobInstance); } synchronized (cacheKeyForActiveJobIds()) { goCache.remove(cacheKeyForActiveJobIds()); } String activeJobKey = cacheKeyForActiveJob(jobInstance.getId()); synchronized (activeJobKey) { goCache.remove(activeJobKey); } removeCachedJobInstance(jobInstance); } }); logIfJobIsCompleted(jobInstance); updateStatus(jobInstance); updateResult(jobInstance); return jobInstance; } }); } private void removeCachedJobInstance(JobInstance jobInstance) { String cacheKeyOfJob = cacheKeyforJobInstanceWithTransitions(jobInstance.getId()); synchronized (cacheKeyOfJob) { goCache.remove(cacheKeyOfJob); } } private void removeCachedJobPlan(JobInstance jobInstance) { goCache.remove(cacheKeyForJobPlan(jobInstance.getId())); } private void logIfJobIsCompleted(JobInstance jobInstance) { JobState currentState = getCurrentState(jobInstance.getId()); if(currentState.isCompleted()) { String message = String.format( "State change for a completed Job is not allowed. Job %s is currently State=%s, Result=%s", jobInstance.getIdentifier(), jobInstance.getState(), jobInstance.getResult()); LOG.warn(message, new Exception().fillInStackTrace()); } } private JobState getCurrentState(long jobId) { String state = (String) getSqlMapClientTemplate().queryForObject("currentJobState", jobId); if (state==null) { return JobState.Unknown; } return JobState.valueOf(state); } private void updateStatus(JobInstance jobInstance) { getSqlMapClientTemplate().update("updateStatus", jobInstance); saveTransitions(jobInstance); } private void updateResult(JobInstance job) { getSqlMapClientTemplate().update("updateResult", job); } public boolean isValid(String pipelineName, String stageName, String buildName) { return (Boolean) getSqlMapClientTemplate().queryForObject("isValid", arguments("pipelineName", pipelineName) .and("stageName", stageName) .and("buildName", buildName).asMap() ); } public void ignore(JobInstance job) { getSqlMapClientTemplate().update("ignoreBuildById", job.getId()); } public JobInstances latestCompletedJobs(String pipelineName, String stageName, String jobConfigName, int count) { Map params = new HashMap(); params.put("pipelineName", pipelineName); params.put("stageName", stageName); params.put("jobConfigName", jobConfigName); params.put("count", count); List<JobInstance> results = (List<JobInstance>) getSqlMapClientTemplate().queryForList("latestCompletedJobs", params); return new JobInstances(results); } public int getJobHistoryCount(String pipelineName, String stageName, String jobName) { Map<String, Object> toGet = arguments("pipelineName", pipelineName).and("stageName", stageName).and("jobConfigName", jobName).asMap(); Integer count = (Integer) getSqlMapClientTemplate().queryForObject("getJobHistoryCount", toGet); return count; } public JobInstances findJobHistoryPage(String pipelineName, String stageName, String jobConfigName, int count, int offset) { Map params = new HashMap(); params.put("pipelineName", pipelineName); params.put("stageName", stageName); params.put("jobConfigName", jobConfigName); params.put("count", count); params.put("offset", offset); List<JobInstance> results = (List<JobInstance>) getSqlMapClientTemplate().queryForList("findJobHistoryPage", params); return new JobInstances(results); } public List<JobPlan> orderedScheduledBuilds() { List<Long> jobIds = (List<Long>) getSqlMapClientTemplate().queryForList("scheduledPlanIds"); List<JobPlan> plans = new ArrayList<>(); for (Long jobId : jobIds) { String cacheKey = cacheKeyForJobPlan(jobId); synchronized (cacheKey) { JobPlan jobPlan = (JobPlan) goCache.get(cacheKey); if (jobPlan == null) { jobPlan = _loadJobPlan(jobId); } if (jobPlan != null) { jobPlan = cloner.deepClone(jobPlan); goCache.put(cacheKey, jobPlan); plans.add(jobPlan); } } } return plans; } private JobPlan _loadJobPlan(Long jobId) { DefaultJobPlan jobPlan = (DefaultJobPlan) getSqlMapClientTemplate().queryForObject("scheduledPlan", arguments("id", jobId).asMap()); if (jobPlan == null) { return null; } loadJobPlanAssociatedEntities(jobPlan); return jobPlan; } private String cacheKeyForJobPlan(Long jobId) { return (getClass().getName() + "_jobPlan_" + jobId).intern(); } private String cacheKeyForActiveJob(Long jobId) { return (getClass().getName() + "_activeJob_" + jobId).intern(); } private String cacheKeyForActiveJobIds() { return (getClass().getName() + "_activeJobIds").intern(); } public JobInstance getLatestInProgressBuildByAgentUuid(String uuid) { JobInstance job = (JobInstance) getSqlMapClientTemplate().queryForObject("getLatestInProgressBuildOnAgent", uuid); return job; } public JobInstances findHungJobs(List<String> liveAgentIdList) { String sqlValue = SqlUtil.joinWithQuotesForSql(liveAgentIdList.toArray()); List<JobInstance> list = getSqlMapClientTemplate().queryForList("getHungJobs", arguments("liveAgentIdList", sqlValue).asMap()); return new JobInstances(list); } public int getNumberOfActiveBuildsOnRemoteAgent(List<String> localAgentIds) { String sqlValue = SqlUtil.joinWithQuotesForSql(localAgentIds.toArray()); Integer count = (Integer) getSqlMapClientTemplate().queryForObject("getNumberOfActiveBuildsOnRemoteAgent", arguments("localAgentIds", sqlValue).asMap()); return count; } private void saveTransitions(JobInstance jobInstance) { for (JobStateTransition transition : jobInstance.getTransitions()) { if (!transition.hasId()) { saveTransition(jobInstance, transition); } } if (jobInstance.getIdentifier() != null) { String pipelineName = jobInstance.getIdentifier().getPipelineName(); String stageName = jobInstance.getIdentifier().getStageName(); cache.flushEntry(jobInstance.getBuildDurationKey(pipelineName, stageName)); } } private void saveTransition(JobInstance jobInstance, JobStateTransition transition) { transition.setJobId(jobInstance.getId()); transition.setStageId(jobInstance.getStageId()); getSqlMapClientTemplate().insert("insertTransition", transition); } @Override public void jobStatusChanged(final JobInstance job) { if(job.isRescheduled()) { goCache.remove(cacheKeyForOriginalJobIdentifier(job.getIdentifier().getStageIdentifier(), job.getName())); } } }