/** * Copyright 2011-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 static com.persistit.VolumeHeader.changeCreateTime; import static com.persistit.VolumeHeader.changeDirectoryRoot; import static com.persistit.VolumeHeader.changeExtendedPageCount; import static com.persistit.VolumeHeader.changeExtensionPages; import static com.persistit.VolumeHeader.changeFetchCounter; import static com.persistit.VolumeHeader.changeGarbageRoot; import static com.persistit.VolumeHeader.changeGlobalTimestamp; import static com.persistit.VolumeHeader.changeInitialPages; import static com.persistit.VolumeHeader.changeLastExtensionTime; import static com.persistit.VolumeHeader.changeLastReadTime; import static com.persistit.VolumeHeader.changeLastWriteTime; import static com.persistit.VolumeHeader.changeMaximumPages; import static com.persistit.VolumeHeader.changeNextAvailablePage; import static com.persistit.VolumeHeader.changeReadCounter; import static com.persistit.VolumeHeader.changeRemoveCounter; import static com.persistit.VolumeHeader.changeStoreCounter; import static com.persistit.VolumeHeader.changeTraverseCounter; import static com.persistit.VolumeHeader.changeWriteCounter; import static com.persistit.VolumeHeader.getCreateTime; import static com.persistit.VolumeHeader.getDirectoryRoot; import static com.persistit.VolumeHeader.getExtensionPages; import static com.persistit.VolumeHeader.getGarbageRoot; import static com.persistit.VolumeHeader.getGlobalTimestamp; import static com.persistit.VolumeHeader.getId; import static com.persistit.VolumeHeader.getInitialPages; import static com.persistit.VolumeHeader.getLastExtensionTime; import static com.persistit.VolumeHeader.getLastReadTime; import static com.persistit.VolumeHeader.getLastWriteTime; import static com.persistit.VolumeHeader.getMaximumPages; import static com.persistit.VolumeHeader.putId; import static com.persistit.VolumeHeader.putPageSize; import static com.persistit.VolumeHeader.putSignature; import static com.persistit.VolumeHeader.putVersion; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import com.persistit.AlertMonitor.AlertLevel; import com.persistit.AlertMonitor.Event; import com.persistit.exception.InUseException; import com.persistit.exception.InvalidPageAddressException; import com.persistit.exception.PersistitException; import com.persistit.exception.PersistitIOException; import com.persistit.exception.PersistitInterruptedException; import com.persistit.exception.ReadOnlyVolumeException; import com.persistit.exception.VolumeAlreadyExistsException; import com.persistit.exception.VolumeClosedException; import com.persistit.exception.VolumeFullException; import com.persistit.exception.VolumeNotFoundException; /** * Manage all details of file I/O for a <code>Volume</code> backing file for * Version 2.xx storage formats. * * @author peter */ class VolumeStorageV2 extends VolumeStorage { private volatile FileChannel _channel; private volatile FileLock _fileLock; private volatile Buffer _headBuffer; private volatile long _nextAvailablePage; private volatile long _extendedPageCount; private volatile boolean _opened; private volatile boolean _closed; /** * Generate a random positive (non-zero) long value to be used as a * validation of a Volume's identity. * * @return */ VolumeStorageV2(final Persistit persistit, final Volume volume) { super(persistit, volume); } /** * Returns the path name by which this volume was opened. * * @return The path name */ @Override String getPath() { return _volume.getSpecification().getPath(); } /** * Indicate whether this <code>Volume</code> prohibits updates. * * @return <code>true</code> if this Volume prohibits updates. */ @Override boolean isReadOnly() { return _volume.getSpecification().isReadOnly(); } /** * Indicate whether this is a temporary volume * * @return <code>true</code> if this volume is temporary */ @Override boolean isTemp() { return false; } /** * @return the channel used to read and write pages of this volume. */ @Override FileChannel getChannel() { return _channel; } /** * Create a new <code>Volume</code> backing file according to the * {@link Volume}'s volume specification. * * @throws PersistitException */ @Override synchronized void create() throws PersistitException { if (_opened) { throw new IllegalStateException("Volume " + this + " cannot be reopened"); } if (exists()) { throw new VolumeAlreadyExistsException(getPath()); } try { _channel = new MediatedFileChannel(getPath(), "rw"); lockChannel(); truncate(); _opened = true; } catch (final IOException ioe) { throw new PersistitIOException(ioe); } finally { if (!_opened) { try { closeChannel(); } catch (final IOException e) { // Not much to do - we're going to try to delete // the file anyway. } if (getPath() != null) { delete(); } } } } /** * Open an existing <code>Volume</code> backing file. * * @throws PersistitException */ @Override synchronized void open() throws PersistitException { if (_opened) { throw new IllegalStateException("Volume " + this + " cannot be reopened"); } if (!exists()) { throw new VolumeNotFoundException(getPath()); } final VolumeSpecification spec = _volume.getSpecification(); final VolumeStatistics stat = _volume.getStatistics(); final VolumeStructure struc = _volume.getStructure(); try { _channel = new MediatedFileChannel(getPath(), isReadOnly() ? "r" : "rw"); lockChannel(); _nextAvailablePage = 1; // correct value installed below _volume.setId(spec.getId()); _headBuffer = _volume.getStructure().getPool().get(_volume, 0, true, true); _headBuffer.setFixed(); final byte[] bytes = _headBuffer.getBytes(); _nextAvailablePage = VolumeHeader.getNextAvailablePage(bytes); _extendedPageCount = _channel.size() / _volume.getStructure().getPageSize(); final long id = getId(bytes); _volume.verifyId(id); _volume.setId(id); final long now = System.currentTimeMillis(); spec.setInitialPages(getInitialPages(bytes)); spec.setMaximumPages(getMaximumPages(bytes)); spec.setExtensionPages(getExtensionPages(bytes)); stat.setCreateTime(getCreateTime(bytes)); stat.setNextAvailablePage(VolumeHeader.getNextAvailablePage(bytes)); stat.setLastExtensionTime(getLastExtensionTime(bytes)); stat.setLastReadTime(getLastReadTime(bytes)); stat.setLastWriteTime(getLastWriteTime(bytes)); stat.setOpenTime(now); final long directoryRootPage = getDirectoryRoot(bytes); final long garbageRootPage = getGarbageRoot(bytes); struc.init(directoryRootPage, garbageRootPage); final long globalTimestamp = getGlobalTimestamp(bytes); stat.setLastGlobalTimestamp(globalTimestamp); flushMetaData(); _opened = true; } catch (final IOException ioe) { throw new PersistitIOException(ioe); } finally { if (_headBuffer != null) { if (!_opened) { _headBuffer.clearFixed(); _headBuffer.clearValid(); releaseHeadBuffer(); _headBuffer = null; } else { releaseHeadBuffer(); } } } } /** * @return <code>true</code> if a backing file exists on the specified path. * @throws PersistitException */ @Override boolean exists() throws PersistitException { final File file = new File(getPath()); return file.exists() && file.isFile(); } /** * Delete the backing file for this Volume if it exists. * * @return <code>true</code> if there was a file and it was successfully * deleted * @throws PersistitException */ @Override boolean delete() throws PersistitException { final File file = new File(getPath()); return file.exists() && file.isFile() && file.delete(); } /** * Force all file system buffers to disk. * * @throws PersistitIOException */ @Override void force() throws PersistitIOException { try { _channel.force(true); } catch (final IOException ioe) { throw new PersistitIOException(ioe); } } /** * Close the file resources held by this <code>Volume</code>. After this * method is called no further file I/O is possible. * * @throws PersistitException */ @Override void close() throws PersistitException { /* * Exclusive claim here intended to conflict with readPage and writePage */ if (!claim(true)) { throw new InUseException("Unable to acquire claim on " + this); } try { if (_closed) { return; } _closed = true; _headBuffer = null; PersistitException pe = null; try { final FileLock lock = _fileLock; _fileLock = null; if (lock != null) { lock.release(); } } catch (final Exception e) { _persistit.getLogBase().exception.log(e); pe = new PersistitException(e); } try { closeChannel(); } catch (final Exception e) { _persistit.getLogBase().exception.log(e); // has priority over Exception thrown by // releasing file lock. pe = new PersistitException(e); } if (pe != null) { throw pe; } } finally { release(); } } private void closeChannel() throws IOException { final FileChannel channel = _channel; _channel = null; if (channel != null) { channel.close(); } } /** * Truncate an existing volume. This method deletes all existing data in the * volume. It is equivalent to deleting the volume and creating a new one * using the same VolumeSpecifications; however the file is never actually * deleted by this operation. * <p /> * This method assigns a new unique id value to the volume so that journal * records pertaining to its state prior to being truncated are not confused * with the new empty state. Caution: information in the volume is * irrecoverably destroyed by this method. */ @Override void truncate() throws PersistitException { _volume.setId(0); _volume.setId(generateId()); final VolumeSpecification spec = _volume.getSpecification(); final VolumeStatistics stat = _volume.getStatistics(); final VolumeStructure struc = _volume.getStructure(); resize(1); resize(spec.getInitialPages()); final long now = System.currentTimeMillis(); stat.setCreateTime(now); stat.setOpenTime(now); _extendedPageCount = spec.getInitialPages(); _nextAvailablePage = 1; _headBuffer = _volume.getStructure().getPool().get(_volume, 0, true, false); boolean truncated = false; try { _headBuffer.init(Buffer.PAGE_TYPE_HEAD); _headBuffer.setFixed(); initMetaData(_headBuffer.getBytes()); // // Lay down the initial version of the header page so that the // volume file will be valid on restart // final ByteBuffer bb = _headBuffer.getByteBuffer(); bb.limit(_headBuffer.getBufferSize()).position(0); writePage(bb, _headBuffer.getPageAddress()); // // Now create directory root page, etc. // struc.init(0, 0); flushMetaData(); truncated = true; } finally { if (!truncated) { _headBuffer.clearValid(); _headBuffer.clearFixed(); releaseHeadBuffer(); _headBuffer = null; } else { releaseHeadBuffer(); } } } @Override boolean isOpened() { return _opened; } @Override boolean isClosed() { return _closed; } @Override long getExtentedPageCount() { return _extendedPageCount; } @Override long getNextAvailablePage() { return _nextAvailablePage; } @Override void claimHeadBuffer() throws PersistitException { if (!_headBuffer.claim(true)) { throw new InUseException("Unable to acquire claim on " + _headBuffer); } } @Override void releaseHeadBuffer() { _headBuffer.release(); } @Override void readPage(final Buffer buffer) throws PersistitIOException, InvalidPageAddressException, VolumeClosedException, PersistitInterruptedException, InUseException { // non-exclusive claim here intended to conflict with exclusive claim in // close and truncate if (!claim(false)) { throw new InUseException("Unable to acquire claim on " + this); } try { final long page = buffer.getPageAddress(); if (page < 0 || page >= _nextAvailablePage) { throw new InvalidPageAddressException("Page " + page + " out of bounds [0-" + _nextAvailablePage + "]"); } if (_persistit.getJournalManager().readPageFromJournal(buffer)) { return; } try { final ByteBuffer bb = buffer.getByteBuffer(); bb.position(0).limit(buffer.getBufferSize()); int read = 0; while (read < buffer.getBufferSize()) { final long position = page * _volume.getStructure().getPageSize() + bb.position(); final int bytesRead = _channel.read(bb, position); if (bytesRead <= 0) { throw new PersistitIOException("Unable to read bytes at position " + position + " in " + this); } read += bytesRead; } _persistit.getIOMeter().chargeReadPageFromVolume(this._volume, buffer.getPageAddress(), buffer.getBufferSize(), buffer.getIndex()); _volume.getStatistics().bumpReadCounter(); } catch (final IOException ioe) { _persistit.getAlertMonitor().post( new Event(AlertLevel.ERROR, _persistit.getLogBase().readException, ioe, _volume, page, buffer.getIndex()), AlertMonitor.READ_PAGE_CATEGORY); throw new PersistitIOException(ioe); } } finally { release(); } } @Override void writePage(final Buffer buffer) throws PersistitException { /* * Non-exclusive claim here intended to conflict with exclusive claim in * close and truncate */ if (!claim(false)) { throw new InUseException("Unable to acquire claim on " + this); } try { _persistit.getJournalManager().writePageToJournal(buffer); } finally { release(); } } @Override void writePage(final ByteBuffer bb, final long page) throws PersistitIOException, InvalidPageAddressException, ReadOnlyVolumeException, VolumeClosedException { if (page < 0 || page >= _nextAvailablePage) { throw new InvalidPageAddressException("Page " + page + " out of bounds [0-" + _nextAvailablePage + "]"); } if (isReadOnly()) { throw new ReadOnlyVolumeException(getPath()); } try { _channel.write(bb, page * _volume.getStructure().getPageSize()); } catch (final IOException ioe) { _persistit.getAlertMonitor().post( new Event(AlertLevel.ERROR, _persistit.getLogBase().writeException, ioe, _volume, page), AlertMonitor.WRITE_PAGE_CATEGORY); throw new PersistitIOException(ioe); } } @Override long allocNewPage() throws PersistitException { long page = -1; claimHeadBuffer(); try { for (;;) { if (_nextAvailablePage < _extendedPageCount) { page = _nextAvailablePage++; _volume.getStatistics().setNextAvailablePage(page); break; } extend(); } flushMetaData(); } finally { releaseHeadBuffer(); } return page; } @Override void flush() throws PersistitException { claimHeadBuffer(); try { flushMetaData(); } finally { releaseHeadBuffer(); } } @Override void flushMetaData() throws PersistitException { if (!isReadOnly()) { assert _headBuffer.isOwnedAsWriterByMe(); final long timestamp = _persistit.getTimestampAllocator().updateTimestamp(); _volume.getStatistics().setLastGlobalTimestamp(timestamp); _headBuffer.writePageOnCheckpoint(timestamp); if (updateMetaData(_headBuffer.getBytes())) { _headBuffer.setDirtyAtTimestamp(timestamp); } } } @Override void extend(final long pageAddr) throws PersistitException { if (pageAddr >= _extendedPageCount) { extend(); flush(); } } private void lockChannel() throws InUseException, IOException { try { _fileLock = _channel.tryLock(0, Long.MAX_VALUE, isReadOnly()); } catch (final OverlappingFileLockException e) { // Note: OverlappingFileLockException is a RuntimeException throw new InUseException("Volume file " + getPath() + " is locked by another thread in this JVM"); } if (_fileLock == null) { throw new InUseException("Volume file " + getPath() + " is locked by another process"); } } private void initMetaData(final byte[] bytes) { final VolumeStructure struc = _volume.getStructure(); putSignature(bytes); putVersion(bytes); putPageSize(bytes, struc.getPageSize()); putId(bytes, _volume.getId()); changeNextAvailablePage(bytes, _nextAvailablePage); changeExtendedPageCount(bytes, _extendedPageCount); changeCreateTime(bytes, _volume.getStatistics().getCreateTime()); changeInitialPages(bytes, _volume.getSpecification().getInitialPages()); changeMaximumPages(bytes, _volume.getSpecification().getMaximumPages()); changeExtensionPages(bytes, _volume.getSpecification().getExtensionPages()); } @Override boolean updateMetaData(final byte[] bytes) { final VolumeStatistics stat = _volume.getStatistics(); final VolumeStructure struc = _volume.getStructure(); boolean changed = false; changed |= changeNextAvailablePage(bytes, _nextAvailablePage); changed |= changeExtendedPageCount(bytes, _extendedPageCount); changed |= changeDirectoryRoot(bytes, struc.getDirectoryRoot()); changed |= changeGarbageRoot(bytes, struc.getGarbageRoot()); changed |= changeFetchCounter(bytes, stat.getFetchCounter()); changed |= changeTraverseCounter(bytes, stat.getTraverseCounter()); changed |= changeStoreCounter(bytes, stat.getStoreCounter()); changed |= changeRemoveCounter(bytes, stat.getRemoveCounter()); changed |= changeReadCounter(bytes, stat.getReadCounter()); changed |= changeLastExtensionTime(bytes, stat.getLastExtensionTime()); changed |= changeLastReadTime(bytes, stat.getLastReadTime()); // Ugly, but the act of closing the system increments this // counter, leading to an extra write. So basically we // ignore the final write by not setting the changed flag. changeWriteCounter(bytes, stat.getWriteCounter()); changeLastWriteTime(bytes, stat.getLastWriteTime()); changeGlobalTimestamp(bytes, stat.getLastGlobalTimestamp()); return changed; } private void extend() throws PersistitException { final long maximumPages = _volume.getSpecification().getMaximumPages(); final long extensionPages = _volume.getSpecification().getExtensionPages(); if (_extendedPageCount >= maximumPages || extensionPages <= 0) { throw new VolumeFullException(this + " is full: " + _extendedPageCount + " pages"); } // Do not extend past maximum pages final long pageCount = Math.min(_extendedPageCount + extensionPages, maximumPages); resize(pageCount); } private void resize(final long pageCount) throws PersistitException { final long newSize = pageCount * _volume.getStructure().getPageSize(); long currentSize = -1; try { currentSize = _channel.size(); if (currentSize > newSize) { _persistit.getLogBase().extendLonger.log(this, currentSize, newSize); } if (currentSize < newSize) { final ByteBuffer bb = ByteBuffer.allocate(1); bb.position(0).limit(1); _channel.write(bb, newSize - 1); _channel.force(true); _persistit.getLogBase().extendNormal.log(this, currentSize, newSize); } _volume.getStatistics().setLastExtensionTime(System.currentTimeMillis()); _extendedPageCount = pageCount; } catch (final IOException ioe) { _persistit.getAlertMonitor().post( new Event(AlertLevel.ERROR, _persistit.getLogBase().extendException, ioe, _volume.getName(), currentSize, newSize), AlertMonitor.EXTEND_VOLUME_CATEGORY); throw new PersistitIOException(ioe); } } @Override public String toString() { return _volume.toString(); } }