/* * 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.persistence; import com.thoughtworks.go.database.Database; import com.thoughtworks.go.database.QueryExtensions; import java.math.BigInteger; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import com.thoughtworks.go.domain.PipelineTimelineEntry; import com.thoughtworks.go.server.cache.GoCache; import com.thoughtworks.go.server.domain.PipelineTimeline; import com.thoughtworks.go.server.domain.user.PipelineSelections; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.hibernate.HibernateException; import org.hibernate.SQLQuery; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.orm.hibernate3.HibernateCallback; import org.springframework.orm.hibernate3.support.HibernateDaoSupport; import org.springframework.stereotype.Component; /** * @understands how to store and retrieve piplines from the database */ @Component public class PipelineRepository extends HibernateDaoSupport { private static final Logger LOGGER = Logger.getLogger(PipelineRepository.class); private final QueryExtensions queryExtensions; private GoCache goCache; @Autowired public PipelineRepository(SessionFactory sessionFactory, GoCache goCache, Database databaseStrategy) { this.goCache = goCache; this.queryExtensions = databaseStrategy.getQueryExtensions(); setSessionFactory(sessionFactory); } public static int updateNaturalOrderForPipeline(Session session, Long pipelineId, double naturalOrder) { String sql = "UPDATE pipelines SET naturalOrder = :naturalOrder WHERE id = :pipelineId"; SQLQuery query = session.createSQLQuery(sql); query.setLong("pipelineId", pipelineId); query.setDouble("naturalOrder", naturalOrder); return query.executeUpdate(); } @SuppressWarnings({"unchecked"}) public void updatePipelineTimeline(final PipelineTimeline pipelineTimeline, final List<PipelineTimelineEntry> tempEntriesForRollback) { getHibernateTemplate().execute(new HibernateCallback() { private static final int PIPELINE_NAME = 0; private static final int ID = 1; private static final int COUNTER = 2; private static final int MODIFIED_TIME = 3; private static final int FINGERPRINT = 4; private static final int NATURAL_ORDER = 5; private static final int REVISION = 6; private static final int FOLDER = 7; private static final int MOD_ID = 8; private static final int PMR_ID = 9; public Object doInHibernate(Session session) throws HibernateException, SQLException { LOGGER.info("Start updating pipeline timeline"); List<Object[]> matches = retrieveTimeline(session, pipelineTimeline); List<PipelineTimelineEntry> newPipelines = populateFrom(matches); addEntriesToPipelineTimeline(newPipelines, pipelineTimeline, tempEntriesForRollback); updateNaturalOrdering(session, newPipelines); LOGGER.info("Pipeline timeline updated"); return null; } private void updateNaturalOrdering(Session session, List<PipelineTimelineEntry> pipelines) { for (PipelineTimelineEntry pipeline : pipelines) { if (pipeline.hasBeenUpdated()) { updateNaturalOrderForPipeline(session, pipeline.getId(), pipeline.naturalOrder()); } } } private List<Object[]> loadTimeline(SQLQuery query) { long startedAt = System.currentTimeMillis(); List<Object[]> matches = (List<Object[]>) query.list(); long duration = System.currentTimeMillis() - startedAt; if (duration > 1000) { LOGGER.warn("updating in memory pipeline-timeline took: " + duration + " ms"); } return matches; } private List<Object[]> retrieveTimeline(Session session, PipelineTimeline pipelineTimeline) { SQLQuery query = session.createSQLQuery(queryExtensions.retrievePipelineTimeline()); query.setLong(0, pipelineTimeline.maximumId()); List<Object[]> matches = loadTimeline(query); sortTimeLineByPidAndPmrId(matches); return matches; } private void sortTimeLineByPidAndPmrId(List<Object[]> matches) { Collections.sort(matches, new Comparator<Object[]>() { @Override public int compare(Object[] m1, Object[] m2) { long id1 = id(m1); long id2 = id(m2); if (id1 == id2) { return (int) (pmrId(m1) - pmrId(m2)); } return (int) (id1 - id2); } }); } private List<PipelineTimelineEntry> populateFrom(List<Object[]> matches) { ArrayList<PipelineTimelineEntry> newPipelines = new ArrayList<>(); if (matches.isEmpty()) { return newPipelines; } Map<String, List<PipelineTimelineEntry.Revision>> revisions = new HashMap<>(); String name = null; long curId = -1; Integer counter = null; double naturalOrder = 0.0; PipelineTimelineEntry entry = null; for (int i = 0; i < matches.size(); i++) { Object[] row = matches.get(i); long id = id(row); if (curId != id) { name = pipelineName(row); curId = id; counter = counter(row); revisions = new HashMap<>(); naturalOrder = naturalOrder(row); } String fingerprint = fingerprint(row); if (!revisions.containsKey(fingerprint)) { revisions.put(fingerprint, new ArrayList<>()); } revisions.get(fingerprint).add(rev(row)); int nextI = i + 1; if (((nextI < matches.size() && id(matches.get(nextI)) != curId) ||//new pipeline instance starts in next record, so capture this one nextI == matches.size())) {//this is the last record, so capture it entry = new PipelineTimelineEntry(name, curId, counter, revisions, naturalOrder); newPipelines.add(entry); } } return newPipelines; } private String folder(Object[] row) { return (String) row[FOLDER]; } private PipelineTimelineEntry.Revision rev(Object[] row) { return new PipelineTimelineEntry.Revision(modifiedTime(row), stringRevision(row), folder(row), modId(row)); } private long pmrId(Object[] row) { return ((BigInteger) row[PMR_ID]).longValue(); } private long modId(Object[] row) { return ((BigInteger) row[MOD_ID]).longValue(); } private double naturalOrder(Object[] row) { return (Double) row[NATURAL_ORDER]; } private Date modifiedTime(Object[] row) { return (Date) row[MODIFIED_TIME]; } private String stringRevision(Object[] row) { return (String) row[REVISION]; } private String fingerprint(Object[] row) { return String.valueOf(row[FINGERPRINT]); } private String pipelineName(Object[] row) { return (String) row[PIPELINE_NAME]; } private int counter(Object[] row) { return row[COUNTER] == null ? -1 : ((BigInteger) row[COUNTER]).intValue(); } private long id(Object[] first) { return ((BigInteger) first[ID]).longValue(); } }); } private void addEntriesToPipelineTimeline(List<PipelineTimelineEntry> newEntries, PipelineTimeline pipelineTimeline, List<PipelineTimelineEntry> tempEntriesForRollback) { for (PipelineTimelineEntry newEntry : newEntries) { tempEntriesForRollback.add(newEntry); pipelineTimeline.add(newEntry); } } public long saveSelectedPipelines(PipelineSelections pipelineSelections) { removePipelineSelectionFromCacheForUserId(pipelineSelections); removePipelineSelectionFromCacheForCookie(pipelineSelections); getHibernateTemplate().saveOrUpdate(pipelineSelections); return pipelineSelections.getId(); } public PipelineSelections findPipelineSelectionsById(long id) { PipelineSelections pipelineSelections; String key = pipelineSelectionForCookieKey(id); if (goCache.isKeyInCache(key)) { return (PipelineSelections) goCache.get(key); } synchronized (key) { if (goCache.isKeyInCache(key)) { return (PipelineSelections) goCache.get(key); } pipelineSelections = getHibernateTemplate().get(PipelineSelections.class, id); goCache.put(key, pipelineSelections); return pipelineSelections; } } public PipelineSelections findPipelineSelectionsById(String id) { if (StringUtils.isEmpty(id)) { return null; } return findPipelineSelectionsById(Long.parseLong(id)); } public PipelineSelections findPipelineSelectionsByUserId(Long userId) { if (userId == null) { return null; } PipelineSelections pipelineSelections; String key = pipelineSelectionForUserIdKey(userId); if (goCache.isKeyInCache(key)) { return (PipelineSelections) goCache.get(key); } synchronized (key) { if (goCache.isKeyInCache(key)) { return (PipelineSelections) goCache.get(key); } List list = getHibernateTemplate().find("FROM PipelineSelections WHERE userId = ?", new Object[]{userId}); if (list.isEmpty()) { pipelineSelections = null; } else { pipelineSelections = (PipelineSelections) list.get(0); } goCache.put(key, pipelineSelections); return pipelineSelections; } } private void removePipelineSelectionFromCacheForCookie(PipelineSelections pipelineSelections) { String pipelineSelectionCookieKey = pipelineSelectionForCookieKey(pipelineSelections.getId()); synchronized (pipelineSelectionCookieKey) { goCache.remove(pipelineSelectionCookieKey); } } private void removePipelineSelectionFromCacheForUserId(PipelineSelections pipelineSelections) { String pipelineSelectionUserIdKey = pipelineSelectionForUserIdKey(pipelineSelections.userId()); synchronized (pipelineSelectionUserIdKey) { goCache.remove(pipelineSelectionUserIdKey); } } String pipelineSelectionForUserIdKey(Long userId) { return (PipelineRepository.class.getName() + "_userIdPipelineSelection_" + userId).intern(); } String pipelineSelectionForCookieKey(long id) { return (PipelineRepository.class.getName() + "_cookiePipelineSelection_" + id).intern(); } }