/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/search/trunk/search-impl/impl/src/java/org/sakaiproject/search/optimize/shared/impl/DbJournalOptimizationManager.java $ * $Id: DbJournalOptimizationManager.java 105078 2012-02-24 23:00:38Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.search.optimize.shared.impl; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.cluster.api.ClusterService; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.search.indexer.api.IndexJournalException; import org.sakaiproject.search.indexer.api.LockTimeoutException; import org.sakaiproject.search.journal.api.JournalErrorException; import org.sakaiproject.search.journal.api.JournalManager; import org.sakaiproject.search.journal.api.JournalManagerState; import org.sakaiproject.search.journal.impl.JournalSettings; import org.sakaiproject.search.optimize.api.NoOptimizationRequiredException; import org.sakaiproject.search.transaction.api.IndexTransaction; import org.sakaiproject.search.transaction.api.IndexTransactionException; /** * @author ieb */ public class DbJournalOptimizationManager implements JournalManager { /*************************************************************************** * @author ieb */ private static final Log log = LogFactory.getLog(DbJournalOptimizationManager.class); private DataSource datasource; private ClusterService clusterService; private String serverId; private JournalSettings journalSettings; private ServerConfigurationService serverConfigurationService; public void destroy() { } public void init() { serverId = serverConfigurationService.getServerId(); } /** * @see org.sakaiproject.search.journal.api.JournalManager#commitSave(org.sakaiproject.search.journal.api.JournalManagerState) */ public void commitSave(JournalManagerState jms) throws IndexJournalException { OptimizeJournalManagerStateImpl ojms = (OptimizeJournalManagerStateImpl) jms; PreparedStatement success = null; PreparedStatement updateTarget = null; Connection connection = null; try { System.err.println("+++++++++++++++++++++COMMIT+++++++++++++++"); connection = datasource.getConnection(); // set the target to committed and then delete the rest // so the final segment becomes commtted with a writer id of // this+txiD, // and all merging-prepare states in this transaction are removed. updateTarget = connection .prepareStatement("update search_journal set status = 'committed', txts = ? where txid = ? "); updateTarget.clearParameters(); updateTarget.setLong(1, System.currentTimeMillis()); updateTarget.setLong(2, ojms.oldestSavePoint); int i = updateTarget.executeUpdate(); // clear out all others success = connection .prepareStatement("delete from search_journal where indexwriter = ? and status = 'merging-prepare' "); success.clearParameters(); success.setString(1, ojms.indexWriter); success.executeUpdate(); connection.commit(); log.info("Shared Journal Mege Committed into SavePoint " + ojms.oldestSavePoint); } catch (Exception ex) { try { connection.rollback(); } catch (Exception ex2) { log.debug(ex2); } throw new IndexJournalException("Failed to commit index ", ex); } finally { try { updateTarget.close(); } catch (Exception ex) { log.debug(ex); } try { success.close(); } catch (Exception ex) { log.debug(ex); } try { connection.close(); } catch (Exception ex) { log.debug(ex); } } } /** * @see org.sakaiproject.search.journal.api.JournalManager#getNextSavePoint(long) */ public long getNextSavePoint(long savePoint) throws JournalErrorException { return 0; } /** * @throws LockTimeoutException * @see org.sakaiproject.search.journal.api.JournalManager#prepareSave(long) */ public JournalManagerState prepareSave(long transactionId) throws IndexJournalException { PreparedStatement getJournalSavePointPst = null; PreparedStatement getEarlierSavePoint = null; PreparedStatement getEarlierSavePoint2 = null; PreparedStatement lockEarlierSavePoints = null; PreparedStatement listMergeSet = null; PreparedStatement listJournal = null; OptimizeJournalManagerStateImpl jms = new OptimizeJournalManagerStateImpl(); ResultSet rs = null; Connection connection = null; try { connection = datasource.getConnection(); getJournalSavePointPst = connection .prepareStatement("select serverid, jid from search_node_status order by jid asc "); getEarlierSavePoint = connection .prepareStatement("select serverid, jid from search_node_status order by jid asc "); jms.indexWriter = serverId + ":" + transactionId; jms.transactionId = transactionId; jms.oldestSavePoint = 0; List<String> servers = clusterService.getServers(); // workout the oldest SavePoint that has not yet been merged // by any running cluster node. // this assumes that all the cluster nodes are running. // Any that are not running will have to be restarted in a clean // state // so that they update from scratch. getJournalSavePointPst.clearParameters(); rs = getJournalSavePointPst.executeQuery(); long oldestActiveSavepoint = 0; while (oldestActiveSavepoint == 0 && rs.next()) { String server = rs.getString(1); log.debug("Got Server " + server + " with savePoint " + rs.getLong(2)); for (String s : servers) { int dash = s.lastIndexOf('-'); if (dash > 0) { s = s.substring(0, dash); } if (server.equals(s)) { oldestActiveSavepoint = rs.getLong(2); log.debug(" Match against " + s); break; } else { log.debug("No Match against " + s); } } } rs.close(); //SRCh-38 we will also select on the old mispelled value for backward compatability getEarlierSavePoint2 = connection .prepareStatement("select min(txid),max(txid) from search_journal where txid < ? and (status = 'commited' or status = 'committed') "); getEarlierSavePoint2.clearParameters(); getEarlierSavePoint2.setLong(1, oldestActiveSavepoint); rs = getEarlierSavePoint2.executeQuery(); jms.oldestSavePoint = 0; long earliestSavePoint = 0; if (rs.next()) { earliestSavePoint = rs.getLong(1); jms.oldestSavePoint = rs.getLong(2); } if (jms.oldestSavePoint <= 0) { throw new NoOptimizationRequiredException("Oldest savePoint is 0"); } rs.close(); long nshared = jms.oldestSavePoint - earliestSavePoint; // no point in going further there are not enough segments. if (nshared < journalSettings.getMinimumOptimizeSavePoints()) { throw new NoOptimizationRequiredException( "Insuficient Journal Entries prior to savepoint " + jms.oldestSavePoint + " to optimize, found " + nshared); } // too many ? if ( nshared > 2*journalSettings.getMinimumOptimizeSavePoints() ) { // adjust the oldestSavePoint // the number will be less than this if there are holes jms.oldestSavePoint = earliestSavePoint + 2*journalSettings.getMinimumOptimizeSavePoints(); // adjust for a potential hole getEarlierSavePoint2.setLong(1, jms.oldestSavePoint); rs = getEarlierSavePoint2.executeQuery(); jms.oldestSavePoint = 0; earliestSavePoint = 0; if (rs.next()) { earliestSavePoint = rs.getLong(1); jms.oldestSavePoint = rs.getLong(2); } if (jms.oldestSavePoint <= 0) { throw new NoOptimizationRequiredException("Oldest savePoint is 0"); } rs.close(); } log.debug("Optimizing shared Journal Storage to savepoint " + jms.oldestSavePoint); // this requires read committed transaction issolation and WILL NOT // work on HSQL lockEarlierSavePoints = connection .prepareStatement("update search_journal set indexwriter = ?, status = 'merging-prepare', txts = ? where txid <= ? and (status = 'commited' or status = 'committed' ) "); lockEarlierSavePoints.clearParameters(); lockEarlierSavePoints.setString(1, jms.indexWriter); lockEarlierSavePoints.setLong(2, System.currentTimeMillis()); lockEarlierSavePoints.setLong(3, jms.oldestSavePoint); int i = 0; try { i = lockEarlierSavePoints.executeUpdate(); } catch (SQLException lockTimepout) { throw new LockTimeoutException(lockTimepout.getMessage(), lockTimepout); } listJournal = connection .prepareStatement("select txid, indexwriter, status, txts from search_journal"); if (log.isDebugEnabled()) { listJournal.clearParameters(); rs = listJournal.executeQuery(); while (rs.next()) { log.debug("TX[" + rs.getLong(1) + "];indexwriter[" + rs.getString(2) + "];status[" + rs.getString(3) + "];timestamp[" + rs.getLong(4) + "]"); } rs.close(); } if (i < journalSettings.getMinimumOptimizeSavePoints()) { throw new NoOptimizationRequiredException( "Insuficient Journal Entries prior to savepoint " + jms.oldestSavePoint + " to optimize, found " + i); } log.info("Locked " + i + " savePoints "); listJournal.clearParameters(); rs = listJournal.executeQuery(); while (rs.next()) { log.info("TX[" + rs.getLong(1) + "];indexwriter[" + rs.getString(2) + "];status[" + rs.getString(3) + "];timestamp[" + rs.getLong(4) + "]"); } rs.close(); jms.mergeList = new ArrayList<Long>(); listMergeSet = connection .prepareStatement("select txid from search_journal where indexwriter = ? order by txid asc "); listMergeSet.clearParameters(); listMergeSet.setString(1, jms.indexWriter); rs = listMergeSet.executeQuery(); while (rs.next()) { jms.mergeList.add(rs.getLong(1)); } log.info("Retrieved " + jms.mergeList.size() + " locked savePoints "); connection.commit(); } catch (IndexJournalException ijex) { try { connection.rollback(); } catch (Exception ex2) { log.debug(ex2); } throw ijex; } catch (Exception ex) { try { connection.rollback(); } catch (Exception ex2) { log.debug(ex); } if (ex instanceof LockTimeoutException) { throw (LockTimeoutException) ex; } else { throw new IndexJournalException( "Failed to lock savePoints to this node ", ex); } } finally { try { rs.close(); } catch (Exception ex) { log.debug(ex); } try { getJournalSavePointPst.close(); } catch (Exception ex) { log.debug(ex); } try { getEarlierSavePoint.close(); } catch (Exception ex) { log.debug(ex); } try { getEarlierSavePoint2.close(); } catch (Exception ex) { log.debug(ex); } try { if (lockEarlierSavePoints != null) lockEarlierSavePoints.close(); } catch (Exception ex) { log.debug(ex); } try { if (listJournal != null) listJournal.close(); } catch (Exception ex) { log.debug(ex); } try { if (listMergeSet != null) listMergeSet.close(); } catch (Exception ex) { log.debug(ex); } try { connection.close(); } catch (Exception ex2) { log.debug(ex2); } } return jms; } /** * @see org.sakaiproject.search.journal.api.JournalManager#rollbackSave(org.sakaiproject.search.journal.api.JournalManagerState) */ public void rollbackSave(JournalManagerState jms) { OptimizeJournalManagerStateImpl ojms = (OptimizeJournalManagerStateImpl) jms; PreparedStatement updateTarget = null; Connection connection = null; try { connection = datasource.getConnection(); // set the target to committed and then delete the rest // so the final segment becomes commtted with a writer id of // this+txiD, // and all merging-prepare states in this transaction are removed. updateTarget = connection .prepareStatement("update search_journal set status = 'committed', txts = ? where indexwriter = ? and status = 'merging-prepare' "); updateTarget.clearParameters(); updateTarget.setLong(1, System.currentTimeMillis()); updateTarget.setString(2, ojms.indexWriter); int i = updateTarget.executeUpdate(); connection.commit(); log .info("Rolled Back Failed Shared Index operation a retry will happen on annother node soon "); } catch (Exception ex) { try { connection.rollback(); } catch (Exception ex2) { log.error("Rollback Of shared Journal Merge Failed ", ex); } } finally { try { updateTarget.close(); } catch (Exception ex) { log.debug(ex); } try { connection.close(); } catch (Exception ex) { log.debug(ex); } } } /** * @throws IndexJournalException * @throws IndexTransactionException * @see org.sakaiproject.search.journal.api.JournalManager#doOpenTransaction(org.sakaiproject.search.transaction.api.IndexTransaction) */ public void doOpenTransaction(IndexTransaction transaction) throws IndexJournalException { Statement countJournals = null; Connection connection = null; ResultSet rs = null; try { connection = datasource.getConnection(); countJournals = connection.createStatement(); long nSavePoints = 0; Map<String, String> mergingMap = new HashMap<String, String>(); long transactionId = transaction.getTransactionId(); String thisWriter = serverId + ":" + transactionId; try { rs = countJournals .executeQuery("select txid, indexwriter, status, txts from search_journal"); while (rs.next()) { long txid = rs.getLong(1); String indexwriter = rs.getString(2); String status = rs.getString(3); long ts = rs.getLong(4); if ("merging-prepare".equals(status)) { mergingMap.put(indexwriter, indexwriter); } else if ("commited".equals(status) || "committed".equals(status)) { nSavePoints++; } } rs.close(); } catch (Exception ex) { log .info("Optimzation of central journal is in progress on annother node, no optimization possible on this node "); } if (mergingMap.size() > 1) { StringBuilder sb = new StringBuilder(); sb .append("\tMore than One shares segments merge appears to be active in the \n"); sb.append("\tcluster, you Must investigate the search_journal table\n"); sb.append("\tA list of index Writers Follows\n\t===================\n"); for (String iw : mergingMap.values()) { sb.append("\t").append(iw); if (iw.equals(thisWriter)) { sb .append("\tThis node is currently optimizing the shared segments,"); sb .append("\tThis is an error as only one copy of this node should be "); sb.append("\tActive in the cluster"); sb.append("see http://jira.sakaiproject.org/browse/SRCH-38"); } else if (iw.startsWith(serverId)) { sb .append("\tThis node is currently optimizing the shared segments,"); sb .append("\tThis is an error as only one copy of this node should be "); sb.append("\tActive in the cluster"); sb.append("see http://jira.sakaiproject.org/browse/SRCH-38"); } } sb.append("\t==========================\n"); log.error(sb.toString()); throw new NoOptimizationRequiredException( "Merge already in progress, possible error"); } else if (mergingMap.size() == 1) { StringBuilder sb = new StringBuilder(); for (String iw : mergingMap.values()) { if (iw.equals(thisWriter)) { sb .append("This node already merging shared segments, index writer " + iw); sb .append("\tThis node is currently optimizing the shared segments,"); sb .append("\tThis is an error as only one copy of this node should be "); sb.append("\tActive in the cluster"); sb.append("see http://jira.sakaiproject.org/browse/SRCH-38"); } else if (iw.startsWith(serverId)) { sb .append("This node already merging shared segments, index writer " + iw); sb .append("\tThis node is currently optimizing the shared segments,"); sb .append("\tThis is an error as only one copy of this node should be "); sb.append("\tActive in the cluster"); sb.append("see http://jira.sakaiproject.org/browse/SRCH-38"); } } if (sb.length() == 0) { log .info("There is annother node performing shared index merge, this node will continue with other operations "); throw new NoOptimizationRequiredException( "Merge already in progress, normal"); } else { log.error(sb.toString()); throw new NoOptimizationRequiredException( "Merge already in progress, possible error"); } } else if (nSavePoints < journalSettings.getMinimumOptimizeSavePoints()) { throw new NoOptimizationRequiredException("Insufficient items to optimze"); } } catch (NoOptimizationRequiredException nop) { throw nop; } catch (Exception ex) { log.warn("Failed to count available journals for optimization ", ex); } finally { try { rs.close(); } catch (Exception ex) { log.debug(ex); } try { countJournals.close(); } catch (Exception ex) { log.debug(ex); } try { connection.close(); } catch (Exception ex) { log.debug(ex); } } } /** * @return the clusterService */ public ClusterService getClusterService() { return clusterService; } /** * @param clusterService * the clusterService to set */ public void setClusterService(ClusterService clusterService) { this.clusterService = clusterService; } /** * @return the datasource */ public DataSource getDatasource() { return datasource; } /** * @param datasource * the datasource to set */ public void setDatasource(DataSource datasource) { this.datasource = datasource; } /** * @return the serverConfigurationService */ public ServerConfigurationService getServerConfigurationService() { return serverConfigurationService; } /** * @param serverConfigurationService * the serverConfigurationService to set */ public void setServerConfigurationService( ServerConfigurationService serverConfigurationService) { this.serverConfigurationService = serverConfigurationService; } /** * @return the journalSettings */ public JournalSettings getJournalSettings() { return journalSettings; } /** * @param journalSettings * the journalSettings to set */ public void setJournalSettings(JournalSettings journalSettings) { this.journalSettings = journalSettings; } }