/** * Copyright 2005-2012 Akiban Technologies, 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.persistit; import java.io.File; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import com.persistit.exception.BufferSizeUnavailableException; import com.persistit.exception.InUseException; import com.persistit.exception.PersistitException; import com.persistit.exception.ReadOnlyVolumeException; import com.persistit.exception.TruncateVolumeException; import com.persistit.exception.UnderSpecifiedVolumeException; import com.persistit.exception.VolumeAlreadyExistsException; import com.persistit.exception.VolumeClosedException; import com.persistit.exception.VolumeNotFoundException; import com.persistit.exception.WrongVolumeException; import com.persistit.util.Util; /** * <p> * Represent the identity and optionally the service classes that manage a * volume. A newly constructed Volume is "hollow" in the sense that it has no * ability to perform I/O on a backing file or allocate pages. In this state it * represents the identity, but not the content, of the volume. * </p> * <p> * To enable the <code>Volume</code> to act on data, you must supply a * {@link VolumeSpecification} object, either through the constructor or with * {@link #setSpecification}, and then call the {@link #open(Persistit)} method. * </p> * * @author peter */ public class Volume { final static int LOCK_VOLUME_HANDLE = Integer.MAX_VALUE; final static String LOCK_VOLUME_NAME = "_lock"; private final String _name; private long _id; private final AtomicBoolean _closing = new AtomicBoolean(); private final AtomicBoolean _closed = new AtomicBoolean(); private final AtomicInteger _handle = new AtomicInteger(); private final AtomicReference<Object> _appCache = new AtomicReference<Object>(); private VolumeSpecification _specification; private volatile VolumeStorage _storage; private volatile VolumeStatistics _statistics; private volatile VolumeStructure _structure; /* * These two constants are used to identify temporary volumes that may have * been identified in existing journal files due to bug 1018526. They are * used in code to detect and remove these records. Once all existing * Persistit volumes sites have been cleaned up, we can remove these * constants and the logic that depends on them. */ private final static long TEMP_VOLUME_ID_FOR_FIXUP_DETECTION = 12345; private final static String TEMP_VOLUME_NAME_SUFFIX_FOR_FIXUP_DETECTION = "_temporary_volume"; public static boolean isValidPageSize(final int pageSize) { for (int b = 1024; b <= 16384; b *= 2) { if (b == pageSize) { return true; } } return false; } static Volume createTemporaryVolume(final Persistit persistit, final int pageSize, final File tempDirectory) throws PersistitException { final Volume volume = new Volume( Thread.currentThread().getName() + TEMP_VOLUME_NAME_SUFFIX_FOR_FIXUP_DETECTION, TEMP_VOLUME_ID_FOR_FIXUP_DETECTION); volume.openInternal(persistit, pageSize); volume._storage = new VolumeStorageT2(persistit, volume, tempDirectory); volume._storage.create(); return volume; } static Volume createLockVolume(final Persistit persistit, final int pageSize, final File tempDirectory) throws PersistitException { final Volume volume = new Volume(LOCK_VOLUME_NAME, 0); volume.setHandle(Volume.LOCK_VOLUME_HANDLE); volume.openInternal(persistit, pageSize); volume._storage = new VolumeStorageL2(persistit, volume, tempDirectory); volume._storage.create(); return volume; } /** * Construct a hollow Volume - used by JournalManager * * @param name * @param id */ Volume(final String name, final long id) { _name = name; _id = id; _specification = new VolumeSpecification(name); } /** * Construct a hollow Volume with its specification. * * @param specification */ Volume(final VolumeSpecification specification) { this(specification.getName(), specification.getId()); _specification = specification; } private void checkNull(final Object o, final String delegate) { if (o == null) { throw new IllegalStateException(this + " has no " + delegate); } } private void checkClosing() throws VolumeClosedException { if (_closing.get()) { throw new VolumeClosedException(); } } VolumeSpecification getSpecification() { final VolumeSpecification s = _specification; checkNull(s, "VolumeSpecification"); return s; } VolumeStorage getStorage() { final VolumeStorage s = _storage; checkNull(s, "VolumeStorage"); return s; } VolumeStatistics getStatistics() { final VolumeStatistics s = _statistics; checkNull(s, "VolumeStatistics"); return s; } VolumeStructure getStructure() { return _structure; } void setSpecification(final VolumeSpecification specification) { if (_specification != null) { throw new IllegalStateException("Volume " + this + " already has a VolumeSpecification"); } if (specification.getName().equals(_name) && (specification.getId() == _id || specification.getId() == 0)) { _specification = specification; } else { throw new IllegalStateException("Volume " + this + " is incompatible with " + specification); } } void overwriteSpecification(final VolumeSpecification specification) { _specification = null; setSpecification(specification); } void closing() { _closing.set(true); } /** * Release all resources for this <code>Volume</code> and invalidate all its * buffers in the {@link BufferPool}. Exchanges based on this * <code>Volume</code> may no longer be used after this call. Waits up to * {@value com.persistit.SharedResource#DEFAULT_MAX_WAIT_TIME} milliseconds * for other threads to relinquish access to the volume. * * @throws PersistitException */ public void close() throws PersistitException { close(SharedResource.DEFAULT_MAX_WAIT_TIME); } /** * Release all resources for this <code>Volume</code> and invalidate all its * buffers in the {@link BufferPool}. Exchanges based on this * <code>Volume</code> may no longer be used after this call. * * @param timeout * Maximum time in milliseconds to wait for other threads to * relinquish access to the volume. * @throws PersistitException */ public void close(final long timeout) throws PersistitException { closing(); final long expiration = System.currentTimeMillis() + timeout; for (;;) { // // Prevents read/write operations from starting while the // volume is being closed. // final VolumeStorage storage = getStorage(); if (!storage.claim(true, timeout)) { throw new InUseException("Unable to acquire claim on " + this); } if (_closed.get()) { break; } try { // // BufferPool#invalidate may fail and return false if other // threads hold claims on pages of this volume. In that case we // need to back off all locks and retry // if (getStructure().getPool().invalidate(this)) { getStructure().close(); getStorage().close(); getStatistics().reset(); _closed.set(true); break; } } finally { storage.release(); } if (System.currentTimeMillis() >= expiration) { throw new InUseException("Unable invalidate all pages on " + this); } Util.sleep(Persistit.SHORT_DELAY); } } /** * Remove all data from this volume. This is equivalent to deleting and then * recreating the <code>Volume</code> except that it does not actually * delete and close the file. Instead, this method truncates the file, * rewrites the head page, and invalidates all buffers belonging to this * <code>Volume</code> in the {@link BufferPool}. * * @throws PersistitException */ public void truncate() throws PersistitException { if (isReadOnly()) { throw new ReadOnlyVolumeException(); } if (!isTemporary() && !getSpecification().isCreate() && !getSpecification().isCreateOnly()) { throw new TruncateVolumeException(); } for (;;) { // // Prevents read/write operations from starting while the // volume is being closed. // if (getStorage().claim(true, 0)) { try { // // BufferPool#invalidate may fail and return false if other // threads hold claims on pages of this volume. In that case // we need to back off all locks and retry // if (getStructure().getPool().invalidate(this)) { getStructure().truncate(); getStorage().truncate(); getStatistics().reset(); break; } } finally { getStorage().release(); } } Util.sleep(Persistit.SHORT_DELAY); } } /** * Delete the file backing this <code>Volume</code>. * * @return <code>true</code> if the file existed and was successfully * deleted. * @throws PersistitException */ public boolean delete() throws PersistitException { if (!isClosed()) { throw new IllegalStateException("Volume must be closed before deletion"); } return getStorage().delete(); } /** * Returns the path name by which this volume was opened. * * @return The path name */ public String getPath() { return getStorage().getPath(); } /** * Returns the absolute file by which this volume was opened. * * @return The file */ public File getAbsoluteFile() { return getSpecification().getAbsoluteFile(); } /** * @return page address of the first unused page in this <code>Volume</code> */ public long getNextAvailablePage() { return getStorage().getNextAvailablePage(); } /** * @return number of pages allocated to this <code>Volume</code>. Note that * this method reflects the current length of the volume file, but * on sparse file systems the disk space needed to store new pages * may not yet have been allocated. */ public long getExtendedPageCount() { return getStorage().getExtentedPageCount(); } BufferPool getPool() { return getStructure().getPool(); } Tree getDirectoryTree() { return getStructure().getDirectoryTree(); } boolean isTemporary() { if (_storage == null) { /* * TODO - Temporary code to detect temporary volumes on existing * systems to resolve side-effects of bug 1018526. */ return _id == TEMP_VOLUME_ID_FOR_FIXUP_DETECTION && _name.endsWith(TEMP_VOLUME_NAME_SUFFIX_FOR_FIXUP_DETECTION); } return getStorage().isTemp(); } boolean isLockVolume() { return getHandle() == LOCK_VOLUME_HANDLE; } /** * @return The size in bytes of one page in this <code>Volume</code>. */ public int getPageSize() { return getStructure().getPageSize(); } /** * Looks up by name and returns a <code>NewTree</code> within this * <code>Volume</code>. If no such tree exists, this method either creates a * new tree or returns null depending on whether the * <code>createIfNecessary</code> parameter is <code>true</code>. * * @param name * The tree name * * @param createIfNecessary * Determines whether this method will create a new tree if there * is no tree having the specified name. * * @return The <code>NewTree</code>, or <code>null</code> if * <code>createIfNecessary</code> is false and there is no such tree * in this <code>Volume</code>. * * @throws PersistitException */ public Tree getTree(final String name, final boolean createIfNecessary) throws PersistitException { checkClosing(); return getStructure().getTree(name, createIfNecessary); } /** * Returns an array of all currently defined <code>Tree</code> names. * * @return The array * * @throws PersistitException */ public String[] getTreeNames() throws PersistitException { checkClosing(); return getStructure().getTreeNames(); } /** * Return a TreeInfo structure for a tree by the specified name. If there is * no such tree, then return <code>null</code>. * * @param tree * name * @return an information structure for the Management interface. */ Management.TreeInfo getTreeInfo(final String name) { try { final Tree tree = getTree(name, false); if (tree != null) { return new Management.TreeInfo(tree); } else { return null; } } catch (final PersistitException pe) { return null; } } /** * Indicate whether this <code>Volume</code> has been opened or created, * i.e., whether a backing volume file has been created or opened. * * @return <code>true</code> if there is a backing volume file. */ public boolean isOpened() { return _storage != null && _storage.isOpened(); } /** * Indicate whether this <code>Volume</code> has been closed. * * @return <code>true</code> if this Volume is closed. */ public boolean isClosed() { return _closing.get(); } /** * Indicate whether this <code>Volume</code> prohibits updates. * * @return <code>true</code> if this Volume prohibits updates. */ public boolean isReadOnly() { return getStorage().isReadOnly(); } /** * Open an existing Volume file or create a new one, depending on the * settings of the {@link VolumeSpecification}. * * @throws PersistitException */ synchronized void open(final Persistit persistit) throws PersistitException { checkClosing(); if (isOpened()) { return; } if (_specification == null) { throw new IllegalStateException("Missing VolumeSpecification"); } if (_storage != null) { throw new IllegalStateException("This volume has already been opened"); } if (_specification.getPageSize() <= 0) { throw new UnderSpecifiedVolumeException(getName()); } if (persistit.getBufferPool(_specification.getPageSize()) == null) { throw new BufferSizeUnavailableException(getName()); } final boolean exists = VolumeHeader.verifyVolumeHeader(_specification, persistit.getCurrentTimestamp()); _structure = new VolumeStructure(persistit, this, _specification.getPageSize()); _storage = new VolumeStorageV2(persistit, this); _statistics = new VolumeStatistics(); boolean opened = false; try { if (exists) { if (_specification.isCreateOnly()) { throw new VolumeAlreadyExistsException(_specification.getPath()); } _storage.open(); opened = true; } else { if (!_specification.isCreate()) { throw new VolumeNotFoundException(_specification.getPath()); } _storage.create(); opened = true; } } finally { if (!opened) { _structure = null; _storage = null; _statistics = null; } } persistit.addVolume(this); } private void openInternal(final Persistit persistit, final int pageSize) throws PersistitException { checkClosing(); if (_storage != null) { throw new IllegalStateException("This volume has already been opened"); } if (persistit.getBufferPool(pageSize) == null) { throw new BufferSizeUnavailableException("There is no buffer pool for pages of size " + pageSize); } _structure = new VolumeStructure(persistit, this, pageSize); _statistics = new VolumeStatistics(); } public String getName() { return _name; } public long getId() { return _id; } void setId(final long id) { if (id != 0 && _id != 0 && _id != id) { throw new IllegalStateException("Volume " + this + " already has id=" + _id); } _id = id; } void verifyId(final long id) throws WrongVolumeException { if (id != 0 && _id != 0 && id != _id) { throw new WrongVolumeException(this + "id " + _id + " does not match expected id " + id); } } /** * Store an Object with this Volume for the convenience of an application. * * @param appCache * the object to be cached for application convenience. */ public void setAppCache(final Object appCache) { _appCache.set(appCache); } /** * @return the object cached for application convenience */ public Object getAppCache() { return _appCache.get(); } /** * @return The handle value used to identify this Tree in the journal */ public int getHandle() { return _handle.get(); } /** * Set the handle used to identify this Tree in the journal. May be invoked * only once. * * @param handle * @return the handle * @throws IllegalStateException * if the handle has already been set */ int setHandle(final int handle) { if (!_handle.compareAndSet(0, handle)) { throw new IllegalStateException("Volume handle already set"); } return handle; } /** * Resets the handle to zero. Intended for use only by tests. */ void resetHandle() { _handle.set(0); } @Override public String toString() { final VolumeSpecification specification = _specification; if (specification != null) { return specification.summary(); } else { return _name; } } @Override public int hashCode() { return _name.hashCode(); } @Override public boolean equals(final Object o) { if (o instanceof Volume) { final Volume volume = (Volume) o; return volume.getName().equals(getName()) && volume.getId() == getId(); } return false; } }