// Copyright (C) 2009 Google 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.google.enterprise.connector.instantiator; import com.google.enterprise.connector.persist.ConnectorNotFoundException; import com.google.enterprise.connector.traversal.BatchResult; import com.google.enterprise.connector.traversal.BatchResultRecorder; import com.google.enterprise.connector.traversal.BatchTimeout; import com.google.enterprise.connector.traversal.TraversalStateStore; import java.util.logging.Logger; /** * Coordinate operations that apply to a running batch with other changes that * affect this [@link {@link ConnectorCoordinatorImpl}. * <p> * The {@link ConnectorCoordinatorImpl} monitor is used to guard batch * operations. * <p> * To avoid long held locks the {@link ConnectorCoordinatorImpl} monitor is * not held while a batch runs or even between the time a batch is canceled * and the time its background processing completes. Therefore, a lingering * batch may attempt to record completion information, modify the checkpoint * or timeout after the lingering batch has been canceled. These operations * may even occur after a new batch has started. To avoid corrupting the * {@link ConnectorCoordinatorImpl} state this class employs the batchKey * protocol to disable completion operations that are performed on behalf of * lingering batches. Here is how the protocol works. * <OL> * <LI>To start a batch starts while holding the * {@link ConnectorCoordinatorImpl} monitor assign the batch a unique key. * Store the key in ConnectorCoordinator.this.currentBatchKey. Also create a * {@link BatchCoordinator} with BatchCoordinator.requiredBatchKey set to the * key for the batch. * <LI>To cancel a batch while holding the ConnectorCoordinatorImpl monitor, * null out ConnectorCoordinator.this.currentBatchKey. * <LI>The {@link BatchCoordinator} performs all completion operations for a * batch and prevents operations on behalf of non current batches. To check * while holding the {@link ConnectorCoordinatorImpl} monitor it * verifies that * BatchCoordinator.requiredBatchKey equals * ConnectorCoordinator.this.currentBatchKey. * </OL> */ class BatchCoordinator implements TraversalStateStore, BatchResultRecorder, BatchTimeout { private static final Logger LOGGER = Logger.getLogger(BatchCoordinator.class.getName()); private String cachedState; private final Object requiredBatchKey; private final ConnectorCoordinatorImpl connectorCoordinator; /** * Creates a BatchCoordinator */ BatchCoordinator(ConnectorCoordinatorImpl connectorCoordinator) throws ConnectorNotFoundException { this.requiredBatchKey = connectorCoordinator.currentBatchKey; this.cachedState = connectorCoordinator.getConnectorState(); this.connectorCoordinator = connectorCoordinator; } public String getTraversalState() { synchronized (connectorCoordinator) { if (connectorCoordinator.currentBatchKey == requiredBatchKey) { return cachedState; } else { throw new BatchCompletedException(); } } } public void storeTraversalState(String state) { synchronized (connectorCoordinator) { // Make sure our batch is still valid and that nobody has modified // the checkpoint while we were away. try { if ((connectorCoordinator.currentBatchKey == requiredBatchKey) && isCheckpointUnmodified()) { connectorCoordinator.setConnectorState(state); cachedState = state; } else { throw new BatchCompletedException(); } } catch (ConnectorNotFoundException cnfe) { // Connector disappeared while we were away. // Don't try to store results. throw new BatchCompletedException(); } } } public void recordResult(BatchResult result) { synchronized (connectorCoordinator) { if (connectorCoordinator.currentBatchKey == requiredBatchKey) { connectorCoordinator.recordResult(result); } else { LOGGER.fine("Ignoring a BatchResult returned from a " + "prevously canceled traversal batch. Connector = " + connectorCoordinator.getConnectorName() + " result = " + result + " batchKey = " + requiredBatchKey); } } } public void timeout() { synchronized (connectorCoordinator) { if (connectorCoordinator.currentBatchKey == requiredBatchKey) { connectorCoordinator.resetBatch(); } else { LOGGER.warning("Ignoring Timeout for previously prevously canceled" + " or completed traversal batch. Connector = " + connectorCoordinator.getConnectorName() + " batchKey = "+ requiredBatchKey); } } } // Returns true if the stored traversal state has not been modified since // we started, false if the persisted state does not match our cache. private boolean isCheckpointUnmodified() throws ConnectorNotFoundException { String currentState = connectorCoordinator.getConnectorState(); if (currentState == cachedState) { return true; } else { return (cachedState != null) && cachedState.equals(currentState); } } // TODO(strellis): Add this Exception to throws for BatchRecorder, // TraversalStateStore, BatchTimeout interfaces and catch this // specific exception rather than IllegalStateException. private static class BatchCompletedException extends IllegalStateException { } }