/* $HeadURL:: $ * $Id$ * * Copyright (c) 2009-2010 DuraSpace * http://duraspace.org * * In collaboration with Topaz Inc. * http://www.topazproject.org * * 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 org.akubraproject.txn; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import javax.transaction.Status; import javax.transaction.Synchronization; import javax.transaction.Transaction; import com.google.common.collect.MapMaker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.akubraproject.Blob; import org.akubraproject.BlobStore; import org.akubraproject.BlobStoreConnection; import org.akubraproject.DuplicateBlobException; import org.akubraproject.MissingBlobException; import org.akubraproject.UnsupportedIdException; import org.akubraproject.impl.AbstractBlob; import org.akubraproject.impl.AbstractBlobStoreConnection; /** * A basic superclass for transactional store connections. This implements the common blob-handling * parts of a transactional connection, leaving subclasses to implement the transactional * management of the id-mappings. * * <p>Subclasses must implement {@link #getRealId getRealId}, {@link #remNameEntry remNameEntry}, * {@link #addNameEntry addNameEntry}, {@link BlobStoreConnection#listBlobIds listBlobIds}, and * override {@link #close close}; in addition they may want to override {@link * #beforeCompletion beforeCompletion} and/or {@link #afterCompletion afterCompletion} for pre- * and post-commit/rollback processing. * * <p>The subclass is expected to implement id mapping, mapping upper-level blob-id's to underlying * blob-id's; these mappings are managed via the <code>remNameEntry</code> and * <code>addNameEntry</code> method, and <code>getRealId</code> is used to query the mapping. * * @author Ronald Tschalär */ public abstract class AbstractTransactionalConnection extends AbstractBlobStoreConnection implements Synchronization { private static final Logger logger = LoggerFactory.getLogger(AbstractTransactionalConnection.class); /** the underlying blob-store that actually stores the blobs */ protected final BlobStoreConnection bStoreCon; /** the transaction this connection belongs to */ protected final Transaction tx; /** Whether or not the current transaction has been completed yet */ protected boolean isCompleted = false; /** the list of underlying id's of added blobs */ protected final List<URI> newBlobs = new ArrayList<URI>(); /** the list of underlying id's of deleted blobs */ protected final List<URI> delBlobs = new ArrayList<URI>(); /** a cache of blobs */ protected final Map<URI, Blob> blobCache = new MapMaker().weakValues().concurrencyLevel(1).<URI, Blob>makeMap(); /** * Create a new transactional connection. * * @param owner the blob-store we belong to * @param bStore the underlying blob-store to use * @param tx the transaction we belong to * @param hints A set of hints for the <code>openConnection</code> on the wrapped store * @throws IOException if an error occurs initializing this connection */ protected AbstractTransactionalConnection(BlobStore owner, BlobStore bStore, Transaction tx, Map<String, String> hints) throws IOException { super(owner); this.bStoreCon = bStore.openConnection(null, hints); this.tx = tx; try { tx.registerSynchronization(this); } catch (Exception e) { throw new IOException("Error registering txn synchronization", e); } if (logger.isDebugEnabled()) logger.debug("opened connection " + this); } @Override public Blob getBlob(URI blobId, Map<String, String> hints) throws IOException { ensureOpen(); if (blobId != null) validateId(blobId); else blobId = (URI) createBlob(null, hints)[0]; Blob b = blobCache.get(blobId); if (b == null) blobCache.put(blobId, b = new TxnBlob(blobId, hints)); return b; } @Override public void sync() throws IOException { ensureOpen(); bStoreCon.sync(); } private Object[] createBlob(URI blobId, Map<String, String> hints) throws IOException { if (blobId == null) throw new UnsupportedOperationException("id-generation is not currently supported"); if (logger.isDebugEnabled()) logger.debug("creating blob '" + blobId + "' (" + this + ")"); Blob res = bStoreCon.getBlob(blobId , hints); if (res.exists()) { if (logger.isDebugEnabled()) logger.debug("duplicate id - retrying with generated id"); res = bStoreCon.getBlob(null, hints); } boolean added = false; try { addNameEntry(blobId, res.getId()); addBlob(blobId, res.getId()); added = true; } finally { if (!added) { try { res.delete(); } catch (Throwable t) { logger.warn("Error removing created blob during exception handling: lower-blob-id = '" + res.getId() + "'", t); } } } if (logger.isDebugEnabled()) logger.debug("created blob '" + blobId + "' with underlying id '" + res.getId() + "' (" + this + ")"); return new Object[] { blobId, res }; } private void renameBlob(URI oldBlobId, URI newBlobId, URI storeId) throws DuplicateBlobException, IOException, MissingBlobException { if (logger.isDebugEnabled()) logger.debug("renaming blob '" + oldBlobId + "' to '" + newBlobId + "' (" + this + ")"); if (getRealId(newBlobId) != null) throw new DuplicateBlobException(newBlobId); remNameEntry(oldBlobId, storeId); addNameEntry(newBlobId, storeId); } private void removeBlob(URI blobId, URI storeId) throws IOException { if (logger.isDebugEnabled()) logger.debug("removing blob '" + blobId + "' (" + this + ")"); if (storeId == null) return; remNameEntry(blobId, storeId); remBlob(blobId, storeId); if (logger.isDebugEnabled()) logger.debug("removed blob '" + blobId + "' with underlying id '" + storeId + "' (" + this + ")"); } /** * Check whether we can store this id. * * @param blobId the upper level blob-id * @throws UnsupportedIdException if the id cannot be stored */ protected void validateId(URI blobId) throws UnsupportedIdException { } /** * Look up the underlying store's blob-id for the given upper-level blob-id. * * @param blobId the upper level blob-id * @return the underlying blob-id that <var>blobId</var> maps to, or null if no such mapping * exists (i.e. <var>blobId</var> is not a known upper-level blob-id) * @throws IOException if an error occurred looking up the id */ protected abstract URI getRealId(URI blobId) throws IOException; /** * Remove an id mapping. * * @param ourId the upper-level blob-id * @param storeId the underlying store's blob-id * @throws IOException if an error occurred removing the mapping or the mapping does not exist */ protected abstract void remNameEntry(URI ourId, URI storeId) throws IOException; /** * Add an id mapping. * * @param ourId the upper-level blob-id to map * @param storeId the underlying store's blob-id to map <var>ourId</var> to * @throws IOException if an error occurred adding the mapping or the mapping already exists */ protected abstract void addNameEntry(URI ourId, URI storeId) throws IOException; /** * Remove a blob from the underlying store. This implementation just updates the {@link #newBlobs} * and {@link #delBlobs} lists; actual blob deletion is deferred till commit. * * @param ourId the upper-level blob-id * @param storeId the underlying store's blob-id * @throws IOException if an error occurred removing the blob or the blob does not exist */ protected void remBlob(URI ourId, URI storeId) throws IOException { if (newBlobs.contains(storeId)) { newBlobs.remove(storeId); bStoreCon.getBlob(storeId, null).delete(); } else { delBlobs.add(storeId); } } /** * Add a blob to the underlying store. This implementation just updates the {@link #newBlobs} * list; actual blob writing is done via the blob itself.. * * @param ourId the upper-level blob-id * @param storeId the underlying store's blob-id * @throws IOException if an error occurred removing the blob or the blob does not exist */ protected void addBlob(URI ourId, URI storeId) throws IOException { newBlobs.add(storeId); } /** * Invoked before the transaction is completed, i.e. before a rollback or commit is started. * Whether or not this is called on a rollback may vary. * * @see Synchronization#beforeCompletion */ public void beforeCompletion() { try { bStoreCon.sync(); } catch (UnsupportedOperationException uoe) { logger.warn("Sync'ing underlying connection '" + bStoreCon + "' not supported", uoe); } catch (IOException ioe) { throw new RuntimeException("Error sync'ing underlying connection " + bStoreCon, ioe); } } /** * Invoked after the transaction has completed, i.e. after a rollback or commit has finished. * This is always callled. * * <p>Subclasses that override this must make sure to invoke <code>super.afterCompletion</code> * so that the cleanup code in this implementation is run. This implementation cleans up deleted * or added blobs (depending on the outcome of the transaction). * * @see Synchronization#afterCompletion */ public void afterCompletion(int status) { if (isCompleted) // I've seen BTM call us twice here after a timeout and rollback. return; isCompleted = true; try { if (status == Status.STATUS_COMMITTED) { for (URI blobId : delBlobs) { try { bStoreCon.getBlob(blobId, null).delete(); } catch (IOException ioe) { logger.error("Error deleting removed blob after commit: blobId = '" + blobId + "'", ioe); } } } else { for (URI blobId : newBlobs) { try { bStoreCon.getBlob(blobId, null).delete(); } catch (IOException ioe) { logger.error("Error deleting added blob after rollback: blobId = '" + blobId + "'", ioe); } } } } finally { bStoreCon.close(); } } /** * A transactional blob implementation. This blob caches underlying infos such as the * store-id and the store-blob, and hence only works properly in conjunction with the * blob-cache which guarantees only one instance of this class per blob-id at any given * time. */ protected class TxnBlob extends AbstractBlob { private final Map<String, String> hints; private boolean needToCopy; private URI storeId; private Blob storeBlob = null; public TxnBlob(URI blobId, Map<String, String> hints) throws IOException { super(AbstractTransactionalConnection.this, blobId); this.hints = hints; storeId = getRealId(blobId); needToCopy = true; } @Override public URI getCanonicalId() { return getId(); } @Override public boolean exists() throws IOException { check(false, false); return (storeId != null); } @Override public void delete() throws IOException { check(false, false); removeBlob(getId(), storeId); storeBlob = null; storeId = null; } @Override public Blob moveTo(URI blobId, Map<String, String> hints) throws IOException { check(true, false); TxnBlob dest = (TxnBlob) getConnection().getBlob(blobId, hints); renameBlob(getId(), blobId, storeId); dest.storeBlob = storeBlob; dest.storeId = storeId; storeBlob = null; storeId = null; return dest; } @Override public long getSize() throws IOException { getStoreBlob(); return storeBlob.getSize(); } @Override public InputStream openInputStream() throws IOException { getStoreBlob(); return storeBlob.openInputStream(); } @Override public OutputStream openOutputStream(long estimatedSize, boolean overwrite) throws IOException, DuplicateBlobException { check(false, !overwrite); if (needToCopy || storeId == null) { if (storeId != null) removeBlob(getId(), storeId); storeBlob = (Blob) createBlob(getId(), hints)[1]; storeId = storeBlob.getId(); needToCopy = false; } else { getStoreBlob(); } return storeBlob.openOutputStream(estimatedSize, true); } private void getStoreBlob() throws IOException, MissingBlobException { check(true, false); if (storeBlob == null) storeBlob = bStoreCon.getBlob(storeId, hints); } private void check(boolean mustExist, boolean mustNotExist) throws IllegalStateException, MissingBlobException, DuplicateBlobException { ensureOpen(); if (mustExist && storeId == null) throw new MissingBlobException(getId()); if (mustNotExist && storeId != null) throw new DuplicateBlobException(getId()); } } }