/*********************************************************************************************************************** * Copyright (C) 2010-2013 by the Stratosphere project (http://stratosphere.eu) * * 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 eu.stratosphere.nephele.services.iomanager; import java.io.IOException; import java.lang.Thread.UncaughtExceptionHandler; import java.util.List; import java.util.Random; import java.util.concurrent.LinkedBlockingQueue; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import eu.stratosphere.core.memory.MemorySegment; /** * The facade for the provided I/O manager services. * */ public final class IOManager implements UncaughtExceptionHandler { /** * Logging. */ private static final Log LOG = LogFactory.getLog(IOManager.class); /** * The default temp paths for anonymous Channels. */ private final String[] paths; /** * A random number generator for the anonymous ChannelIDs. */ private final Random random; /** * The writer thread used for asynchronous block oriented channel writing. */ private final WriterThread[] writers; /** * The reader threads used for asynchronous block oriented channel reading. */ private final ReaderThread[] readers; /** * The number of the next path to use. */ private volatile int nextPath; /** * A boolean flag indicating whether the close() has already been invoked. */ private volatile boolean isClosed = false; // ------------------------------------------------------------------------- // Constructors / Destructors // ------------------------------------------------------------------------- /** * Constructs a new IOManager, writing channels to the system directory. */ public IOManager() { this(System.getProperty("java.io.tmpdir")); } /** * Constructs a new IOManager. * * @param path The base directory path for files underlying channels. */ public IOManager(String tempDir) { this(new String[] {tempDir}); } /** * Constructs a new IOManager. * * @param path * the basic directory path for files underlying anonymous * channels. */ public IOManager(String[] paths) { this.paths = paths; this.random = new Random(); this.nextPath = 0; // start a write worker thread for each directory this.writers = new WriterThread[paths.length]; for (int i = 0; i < this.writers.length; i++) { final WriterThread t = new WriterThread(); this.writers[i] = t; t.setName("IOManager writer thread #" + (i + 1)); t.setDaemon(true); t.setUncaughtExceptionHandler(this); t.start(); } // start a reader worker thread for each directory this.readers = new ReaderThread[paths.length]; for (int i = 0; i < this.readers.length; i++) { final ReaderThread t = new ReaderThread(); this.readers[i] = t; t.setName("IOManager reader thread #" + (i + 1)); t.setDaemon(true); t.setUncaughtExceptionHandler(this); t.start(); } } /** * Close method. Shuts down the reader and writer threads immediately, not waiting for their * pending requests to be served. This method waits until the threads have actually ceased their * operation. */ public synchronized final void shutdown() { if (!this.isClosed) { this.isClosed = true; // close writing and reading threads with best effort and log problems // --------------------------------- writer shutdown ---------------------------------- for (int i = 0; i < this.readers.length; i++) { try { this.writers[i].shutdown(); } catch (Throwable t) { LOG.error("Error while shutting down IO Manager writer thread.", t); } } // --------------------------------- reader shutdown ---------------------------------- for (int i = 0; i < this.readers.length; i++) { try { this.readers[i].shutdown(); } catch (Throwable t) { LOG.error("Error while shutting down IO Manager reader thread.", t); } } // ------------------------ wait until shutdown is complete --------------------------- try { for (int i = 0; i < this.readers.length; i++) { this.writers[i].join(); } for (int i = 0; i < this.readers.length; i++) { this.readers[i].join(); } } catch (InterruptedException iex) {} } } /** * Utility method to check whether the IO manager has been properly shut down. The IO manager is considered * to be properly shut down when it is closed and its threads have ceased operation. * * @return True, if the IO manager has properly shut down, false otherwise. */ public final boolean isProperlyShutDown() { boolean readersShutDown = true; for (int i = 0; i < this.readers.length; i++) { readersShutDown &= this.readers[i].getState() == Thread.State.TERMINATED; } boolean writersShutDown = true; for (int i = 0; i < this.writers.length; i++) { readersShutDown &= this.writers[i].getState() == Thread.State.TERMINATED; } return this.isClosed && writersShutDown && readersShutDown; } /* (non-Javadoc) * @see java.lang.Thread.UncaughtExceptionHandler#uncaughtException(java.lang.Thread, java.lang.Throwable) */ @Override public void uncaughtException(Thread t, Throwable e) { LOG.fatal("IO Thread '" + t.getName() + "' terminated due to an exception. Closing I/O Manager.", e); shutdown(); } // ------------------------------------------------------------------------ // Channel Instantiations // ------------------------------------------------------------------------ /** * Creates a new {@link Channel.ID} in one of the temp directories. Multiple * invocations of this method spread the channels evenly across the different directories. * * @return A channel to a temporary directory. */ public Channel.ID createChannel() { final int num = getNextPathNum(); return new Channel.ID(this.paths[num], num, this.random); } /** * Creates a new {@link Channel.Enumerator}, spreading the channels in a round-robin fashion * across the temporary file directories. * * @return An enumerator for channels. */ public Channel.Enumerator createChannelEnumerator() { return new Channel.Enumerator(this.paths, this.random); } // ------------------------------------------------------------------------ // Reader / Writer instantiations // ------------------------------------------------------------------------ /** * Creates a block channel writer that writes to the given channel. The writer writes asynchronously (write-behind), * accepting write request, carrying them out at some time and returning the written segment to the given queue * afterwards. * * @param channelID The descriptor for the channel to write to. * @param returnQueue The queue to put the written buffers into. * @return A block channel writer that writes to the given channel. * @throws IOException Thrown, if the channel for the writer could not be opened. */ public BlockChannelWriter createBlockChannelWriter(Channel.ID channelID, LinkedBlockingQueue<MemorySegment> returnQueue) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelWriter(channelID, this.writers[channelID.getThreadNum()].requestQueue, returnQueue, 1); } /** * Creates a block channel writer that writes to the given channel. The writer writes asynchronously (write-behind), * accepting write request, carrying them out at some time and returning the written segment to the given queue * afterwards. * <p> * The writer will collect a specified number of write requests and carry them out * in one, effectively writing one block in the size of multiple memory pages. * Note that this means that no memory segment will reach the return queue before * the given number of requests are collected, so the number of buffers used with * the writer should be greater than the number of requests to combine. Ideally, * the number of memory segments used is a multiple of the number of requests to * combine. * * @param channelID The descriptor for the channel to write to. * @param returnQueue The queue to put the written buffers into. * @param numRequestsToCombine The number of write requests to combine to one I/O request. * @return A block channel writer that writes to the given channel. * @throws IOException Thrown, if the channel for the writer could not be opened. */ public BlockChannelWriter createBlockChannelWriter(Channel.ID channelID, LinkedBlockingQueue<MemorySegment> returnQueue, int numRequestsToCombine) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelWriter(channelID, this.writers[channelID.getThreadNum()].requestQueue, returnQueue, numRequestsToCombine); } /** * Creates a block channel writer that writes to the given channel. The writer writes asynchronously (write-behind), * accepting write request, carrying them out at some time and returning the written segment its return queue afterwards. * * @param channelID The descriptor for the channel to write to. * @return A block channel writer that writes to the given channel. * @throws IOException Thrown, if the channel for the writer could not be opened. */ public BlockChannelWriter createBlockChannelWriter(Channel.ID channelID) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelWriter(channelID, this.writers[channelID.getThreadNum()].requestQueue, new LinkedBlockingQueue<MemorySegment>(), 1); } /** * Creates a block channel writer that writes to the given channel. The writer writes asynchronously (write-behind), * accepting write request, carrying them out at some time and returning the written segment its return queue afterwards. * <p> * The writer will collect a specified number of write requests and carry them out * in one, effectively writing one block in the size of multiple memory pages. * Note that this means that no memory segment will reach the return queue before * the given number of requests are collected, so the number of buffers used with * the writer should be greater than the number of requests to combine. Ideally, * the number of memory segments used is a multiple of the number of requests to * combine. * * @param channelID The descriptor for the channel to write to. * @param numRequestsToCombine The number of write requests to combine to one I/O request. * @return A block channel writer that writes to the given channel. * @throws IOException Thrown, if the channel for the writer could not be opened. */ public BlockChannelWriter createBlockChannelWriter(Channel.ID channelID, int numRequestsToCombine) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelWriter(channelID, this.writers[channelID.getThreadNum()].requestQueue, new LinkedBlockingQueue<MemorySegment>(), numRequestsToCombine); } /** * Creates a block channel reader that reads blocks from the given channel. The reader reads asynchronously, * such that a read request is accepted, carried out at some (close) point in time, and the full segment * is pushed to the given queue. * * @param channelID The descriptor for the channel to write to. * @param returnQueue The queue to put the full buffers into. * @return A block channel reader that reads from the given channel. * @throws IOException Thrown, if the channel for the reader could not be opened. */ public BlockChannelReader createBlockChannelReader(Channel.ID channelID, LinkedBlockingQueue<MemorySegment> returnQueue) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelReader(channelID, this.readers[channelID.getThreadNum()].requestQueue, returnQueue, 1); } /** * Creates a block channel reader that reads blocks from the given channel. The reader reads asynchronously, * such that a read request is accepted, carried out at some (close) point in time, and the full segment * is pushed to the given queue. * <p> * The reader will collect a specified number of read requests and carry them out * in one, effectively reading one block in the size of multiple memory pages. * Note that this means that no memory segment will reach the return queue before * the given number of requests are collected, so the number of buffers used with * the reader should be greater than the number of requests to combine. Ideally, * the number of memory segments used is a multiple of the number of requests to * combine. * * @param channelID The descriptor for the channel to write to. * @param returnQueue The queue to put the full buffers into. * @param numRequestsToCombine The number of read requests to combine to one I/O request. * @return A block channel reader that reads from the given channel. * @throws IOException Thrown, if the channel for the reader could not be opened. */ public BlockChannelReader createBlockChannelReader(Channel.ID channelID, LinkedBlockingQueue<MemorySegment> returnQueue, int numRequestsToCombine) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelReader(channelID, this.readers[channelID.getThreadNum()].requestQueue, returnQueue, numRequestsToCombine); } /** * Creates a block channel reader that reads blocks from the given channel. The reader reads asynchronously, * such that a read request is accepted, carried out at some (close) point in time, and the full segment * is pushed to the reader's return queue. * * @param channelID The descriptor for the channel to write to. * @return A block channel reader that reads from the given channel. * @throws IOException Thrown, if the channel for the reader could not be opened. */ public BlockChannelReader createBlockChannelReader(Channel.ID channelID) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelReader(channelID, this.readers[channelID.getThreadNum()].requestQueue, new LinkedBlockingQueue<MemorySegment>(), 1); } /** * Creates a block channel reader that reads blocks from the given channel. The reader reads asynchronously, * such that a read request is accepted, carried out at some (close) point in time, and the full segment * is pushed to the reader's return queue. * <p> * The reader will collect a specified number of read requests and carry them out * in one, effectively reading one block in the size of multiple memory pages. * Note that this means that no memory segment will reach the return queue before * the given number of requests are collected, so the number of buffers used with * the reader should be greater than the number of requests to combine. Ideally, * the number of memory segments used is a multiple of the number of requests to * combine. * * @param channelID The descriptor for the channel to write to. * @param numRequestsToCombine The number of write requests to combine to one I/O request. * @return A block channel reader that reads from the given channel. * @throws IOException Thrown, if the channel for the reader could not be opened. */ public BlockChannelReader createBlockChannelReader(Channel.ID channelID, int numRequestsToCombine) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BlockChannelReader(channelID, this.readers[channelID.getThreadNum()].requestQueue, new LinkedBlockingQueue<MemorySegment>(), numRequestsToCombine); } /** * Creates a block channel reader that reads all blocks from the given channel directly in one bulk. * The reader draws segments to read the blocks into from a supplied list, which must contain as many * segments as the channel has blocks. After the reader is done, the list with the full segments can be * obtained from the reader. * <p> * If a channel is not to be read in one bulk, but in multiple smaller batches, a * {@link BlockChannelReader} should be used. * * @param channelID The descriptor for the channel to write to. * @param targetSegments The list to take the segments from into which to read the data. * @param numBlocks The number of blocks in the channel to read. * @return A block channel reader that reads from the given channel. * @throws IOException Thrown, if the channel for the reader could not be opened. */ public BulkBlockChannelReader createBulkBlockChannelReader(Channel.ID channelID, List<MemorySegment> targetSegments, int numBlocks) throws IOException { if (this.isClosed) { throw new IllegalStateException("I/O-Manger is closed."); } return new BulkBlockChannelReader(channelID, this.readers[channelID.getThreadNum()].requestQueue, targetSegments, numBlocks); } // ======================================================================== // Utilities // ======================================================================== private final int getNextPathNum() { final int next = this.nextPath; final int newNext = next + 1; this.nextPath = newNext >= this.paths.length ? 0 : newNext; return next; } // ======================================================================== // I/O Worker Threads // ======================================================================== /** * A worker thread for asynchronous read. * */ private static final class ReaderThread extends Thread { protected final RequestQueue<ReadRequest> requestQueue; private volatile boolean alive; // --------------------------------------------------------------------- // Constructors / Destructors // --------------------------------------------------------------------- protected ReaderThread() { this.requestQueue = new RequestQueue<ReadRequest>(); this.alive = true; } /** * Shuts the thread down. This operation does not wait for all pending requests to be served, halts the thread * immediately. All buffers of pending requests are handed back to their channel readers and an exception is * reported to them, declaring their request queue as closed. */ protected void shutdown() { if (this.alive) { // shut down the thread try { this.alive = false; this.requestQueue.close(); this.interrupt(); } catch (Throwable t) {} } // notify all pending write requests that the thread has been shut down IOException ioex = new IOException("IO-Manager has been closed."); while (!this.requestQueue.isEmpty()) { ReadRequest request = this.requestQueue.poll(); request.requestDone(ioex); } } // --------------------------------------------------------------------- // Main loop // --------------------------------------------------------------------- @Override public void run() { while (this.alive) { // get the next buffer. ignore interrupts that are not due to a shutdown. ReadRequest request = null; while (request == null) { try { request = this.requestQueue.take(); } catch (InterruptedException iex) { if (!this.alive) { // exit return; } } } // remember any IO exception that occurs, so it can be reported to the writer IOException ioex = null; try { // read buffer from the specified channel request.read(); } catch (IOException e) { ioex = e; } catch (Throwable t) { ioex = new IOException("The buffer could not be read: " + t.getMessage(), t); IOManager.LOG.error("I/O reading thread encountered an error" + t.getMessage() == null ? "." : ": ", t); } // invoke the processed buffer handler of the request issuing reader object request.requestDone(ioex); } // end while alive } } // end reading thread /** * A worker thread that asynchronously writes the buffers to disk. */ private static final class WriterThread extends Thread { protected final RequestQueue<WriteRequest> requestQueue; private volatile boolean alive; // --------------------------------------------------------------------- // Constructors / Destructors // --------------------------------------------------------------------- protected WriterThread() { this.requestQueue = new RequestQueue<WriteRequest>(); this.alive = true; } /** * Shuts the thread down. This operation does not wait for all pending requests to be served, halts the thread * immediately. All buffers of pending requests are handed back to their channel writers and an exception is * reported to them, declaring their request queue as closed. */ protected void shutdown() { if (this.alive) { // shut down the thread try { this.alive = false; this.requestQueue.close(); this.interrupt(); } catch (Throwable t) {} // notify all pending write requests that the thread has been shut down IOException ioex = new IOException("Writer thread has been closed."); while (!this.requestQueue.isEmpty()) { WriteRequest request = this.requestQueue.poll(); request.requestDone(ioex); } } } // --------------------------------------------------------------------- // Main loop // --------------------------------------------------------------------- @Override public void run() { while (this.alive) { WriteRequest request = null; // get the next buffer. ignore interrupts that are not due to a shutdown. while (request == null) { try { request = requestQueue.take(); } catch (InterruptedException iex) { if (!this.alive) { // exit return; } } } // remember any IO exception that occurs, so it can be reported to the writer IOException ioex = null; try { // write buffer to the specified channel request.write(); } catch (IOException e) { ioex = e; } catch (Throwable t) { ioex = new IOException("The buffer could not be written: " + t.getMessage(), t); IOManager.LOG.error("I/O reading thread encountered an error" + t.getMessage() == null ? "." : ": ", t); } // invoke the processed buffer handler of the request issuing writer object request.requestDone(ioex); } // end while alive } }; // end writer thread }