/** * 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 java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedChannelException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.channels.spi.AbstractInterruptibleChannel; /** * <p> * A {@link FileChannel} implementation that provides different semantics on * interrupt. This class wraps an actual FileChannelImpl instance obtained from * a {@link RandomAccessFile} and delegates all supported operations to that * FileChannelImpl. If a blocking I/O operation, e.g., a read or write, is * interrupted, the actual FileChannel instance is closed by its * {@link AbstractInterruptibleChannel} superclass. The result is that the * interrupted thread receives a {@link ClosedByInterruptException}, and other * threads concurrently or subsequently reading or writing the same channel * receive {@link ClosedChannelException}s. * </p> * <p> * However, this mediator class class catches the * <code>ClosedChannelException</code>, and implicitly opens a FileChanel if the * client did not actually call {@link #close()}. If the operation that threw * the <code>ClosedChannelException</code> was on an interrupted thread then the * method that received the exception throws a {@link InterruptedIOException} * after re-opening the channel. Otherwise the method retries the operation * using the new channel. * </p> * <p> * To maintain the <code>FileChannel</code> contract, methods of this class may * only throw an <code>IOException</code>. Therefore, to signify a interrupt, * this method throws <code>InteruptedIOException</code> rather than * <code>InterruptedException</code>. * </p> * <p> * A number of methods of <code>FileChannel</code> including all methods that * depend on the channel's file position, are unsupported and throw * {@link UnsupportedOperationException}s. * </p> * * @author peter * */ class MediatedFileChannel extends FileChannel { private final static String LOCK_EXTENSION = ".lck"; final File _file; final File _lockFile; final String _mode; volatile FileChannel _channel; volatile FileChannel _lockChannel; MediatedFileChannel(final String path, final String mode) throws IOException { this(new File(path), mode); } MediatedFileChannel(final File file, final String mode) throws IOException { _file = file; _lockFile = new File(file.getParentFile(), file.getName() + LOCK_EXTENSION); _mode = mode; openChannel(); } /** * Handles <code>ClosedChannelException</code> and its subclasses * <code>AsynchronousCloseException</code> and * <code>ClosedByInterruptException</code>. Empirically we determined (and * by reading {@link AbstractInterruptibleChannel}) that an interrupted * thread can throw either <code>ClosedChannelException</code> or * <code>ClosedByInterruptException</code>. Therefore we simply use the * interrupted state of the thread itself to determine whether the Exception * occurred due to an interrupt on the current thread. * * @param cce * A ClosedChannelException * @throws IOException * if (a) the attempt to reopen a new channel fails, or (b) the * current thread was in fact interrupted. */ private void handleClosedChannelException(final ClosedChannelException cce) throws IOException { /* * The ClosedChannelException may have occurred because the client * actually called close. In that event throwing the original exception * is correct. */ if (!isOpen()) { throw cce; } /* * Open a new inner FileChannel */ openChannel(); /* * Behavior depends on whether this thread was originally the * interrupted thread. If so then throw an InterruptedIOException which * wraps the original exception. Otherwise return normally so that the * while-loops in the methods below can retry the I/O operation using * the new FileChannel. */ if (Thread.interrupted()) { final InterruptedIOException iioe = new InterruptedIOException(); iioe.initCause(cce); throw iioe; } } /** * Attempt to open a real FileChannel. This method is synchronized and * checks the status of the existing channel because multiple threads might * receive AsynchronousCloseException * * @throws IOException */ private synchronized void openChannel() throws IOException { if (isOpen() && (_channel == null || !_channel.isOpen())) { _channel = new RandomAccessFile(_file, _mode).getChannel(); } } /* * -------------------------------- * * Implementations of these FileChannel methods simply delegate to the inner * FileChannel. But they retry upon receiving a ClosedChannelException * caused by an I/O operation on a different thread having been interrupted. * * -------------------------------- */ @Override public void force(final boolean metaData) throws IOException { while (true) { try { _channel.force(metaData); break; } catch (final ClosedChannelException e) { handleClosedChannelException(e); } } } @Override public int read(final ByteBuffer byteBuffer, final long position) throws IOException { final int offset = byteBuffer.position(); while (true) { try { return _channel.read(byteBuffer, position); } catch (final ClosedChannelException e) { handleClosedChannelException(e); } byteBuffer.position(offset); } } @Override public long size() throws IOException { while (true) { try { return _channel.size(); } catch (final ClosedChannelException e) { handleClosedChannelException(e); } } } @Override public FileChannel truncate(final long size) throws IOException { while (true) { try { return _channel.truncate(size); } catch (final ClosedChannelException e) { handleClosedChannelException(e); } } } @Override public synchronized FileLock tryLock(final long position, final long size, final boolean shared) throws IOException { if (_lockChannel == null) { try { _lockChannel = new RandomAccessFile(_lockFile, "rw").getChannel(); } catch (final IOException ioe) { if (!shared) { throw ioe; } else { /* * Read-only volume, probably failed to create a lock file * due to permissions. We'll assume that means no other * process could be modifying the corresponding volume file. */ } } } return _lockChannel.tryLock(position, size, shared); } @Override public int write(final ByteBuffer byteBuffer, final long position) throws IOException { final int offset = byteBuffer.position(); while (true) { try { return _channel.write(byteBuffer, position); } catch (final ClosedChannelException e) { handleClosedChannelException(e); } byteBuffer.position(offset); } } /** * Implement closing of this <code>MediatedFileChannel</code> by closing the * real channel and setting the <code>_reallyClosed</code> flag. The flag * prevents another thread from performing an {@link #openChannel()} * operation after this thread has closed the channel. */ @Override protected synchronized void implCloseChannel() throws IOException { try { IOException exception = null; try { if (_lockChannel != null) { _lockFile.delete(); _lockChannel.close(); } } catch (final IOException e) { exception = e; } try { if (_channel != null) { _channel.close(); } } catch (final IOException e) { exception = e; } if (exception != null) { throw exception; } } catch (final ClosedChannelException e) { // ignore - whatever, the channel is closed } } /* * -------------------------------- * * Persistit does not use these methods and so they are Unsupported. Note * that it would be difficult to support the relative read/write methods * because the channel size is unavailable after it is closed. Therefore a * client of this class must maintain its own position counter and cannot * use the relative-addressing calls. * * -------------------------------- */ @Override public FileLock lock(final long position, final long size, final boolean shared) throws IOException { throw new UnsupportedOperationException(); } @Override public MappedByteBuffer map(final MapMode arg0, final long arg1, final long arg2) throws IOException { throw new UnsupportedOperationException(); } @Override public long position() throws IOException { throw new UnsupportedOperationException(); } @Override public FileChannel position(final long arg0) throws IOException { throw new UnsupportedOperationException(); } @Override public int read(final ByteBuffer byteBuffer) throws IOException { throw new UnsupportedOperationException(); } @Override public long read(final ByteBuffer[] arg0, final int arg1, final int arg2) throws IOException { throw new UnsupportedOperationException(); } @Override public long transferFrom(final ReadableByteChannel arg0, final long arg1, final long arg2) throws IOException { throw new UnsupportedOperationException(); } @Override public long transferTo(final long arg0, final long arg1, final WritableByteChannel arg2) throws IOException { throw new UnsupportedOperationException(); } @Override public int write(final ByteBuffer byteBuffer) throws IOException { throw new UnsupportedOperationException(); } @Override public long write(final ByteBuffer[] arg0, final int arg1, final int arg2) throws IOException { throw new UnsupportedOperationException(); } /* * -------------------------------- * * Method and interface intended solely for unit tests. * * -------------------------------- */ /** * Method used by unit tests to rewire the FileChannel delegation. The * replacement delegate simulates various IOExceptions. The FileChannel * provided as an argument must implement TestChannelInjector so that this * method can hook it up properly. * * @param channel */ void injectChannelForTests(final FileChannel channel) { ((TestChannelInjector) channel).setChannel(_channel); _channel = channel; } /** * Interface implemented by an error-injecting FileChannel subclass used in * unit tests. * * @author peter * */ interface TestChannelInjector { void setChannel(FileChannel channel); } }