/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.hadoop.io;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.util.InjectionEventCore;
import org.apache.hadoop.util.InjectionHandler;
/**
* BufferedByteOutputStream uses an underlying in-memory buffer for
* writing bytes to the underlying output stream. The actual writing is done
* by a separate thread.
*/
public class BufferedByteOutputStream extends OutputStream {
static final Log LOG = LogFactory.getLog(BufferedByteOutputStream.class.getName());
//staging buffer used to store bytes to be written to underlying stream
private final BufferedByteInputOutput buffer;
// writer thread that reads bytes from the staging buffer
// and writes to the underlying stream
private final WriteThread writeThread;
//actual output stream to write from
private final OutputStream underlyingOutputStream;
private boolean closed = false;
/**
* Wrap given output stream with BufferedByteInputOutput.
* This is the only way to instantiate the buffered output stream.
* @param os underlying output stream
* @param bufferSize size of the in memory buffer
* @param writeBufferSize size of the buffer used for writing to is
*/
public static DataOutputStream wrapOutputStream(OutputStream os,
int bufferSize, int writeBufferSize) {
// wrapping BufferedByteOutputStream in BufferedOutputStream decreases
// pressure on BBOS internal locks, and we read from the BBOS in
// bigger chunks
return new DataOutputStream(new BufferedOutputStream(
new BufferedByteOutputStream(os, bufferSize, writeBufferSize)));
}
/**
* Construct BuffredByteInputStream
* @param os underlying output stream
* @param bufferSize size of the in memory buffer
* @param writeBufferSize size of the transfer chunks
* (from staging buffer to output stream)
*/
private BufferedByteOutputStream(OutputStream os, int bufferSize, int writeBufferSize) {
buffer = new BufferedByteInputOutput(bufferSize);
writeThread = new WriteThread(os, buffer, writeBufferSize);
writeThread.setDaemon(true);
writeThread.start();
underlyingOutputStream = os;
}
/**
* Close the output stream. Joins the thread and
* closes the underlying output stream.
*/
public void close() throws IOException {
if (closed) {
checkWriteThread();
return;
}
try {
buffer.close();
// writeThread should exit after this
// and close underlying stream
try {
writeThread.join();
} catch (InterruptedException e) {
throw new IOException(e);
}
} finally {
checkWriteThread();
}
}
/**
* Waits until the buffer has been written to underlying stream.
* Flushes the underlying stream.
*/
public void flush() throws IOException {
// check if the stream has been closed
checkError();
// how many bytes were written to the buffer
long totalBytesWritten = buffer.totalWritten();
// unblock reads from the buffer
buffer.unblockReads();
// wait until the write thread transfers everything from the
// buffer to the stream
while (writeThread.totalBytesTransferred < totalBytesWritten) {
BufferedByteInputOutput.sleep(1);
}
InjectionHandler.processEvent(
InjectionEventCore.BUFFEREDBYTEOUTPUTSTREAM_FLUSH,
writeThread.totalBytesTransferred);
// check error
checkError();
// block reads
buffer.blockReads();
// flush the underlying buffer
underlyingOutputStream.flush();
}
//override OutputStream methods
public void write(int b) throws IOException {
checkError();
buffer.write(b);
}
public void write(byte[] buf) throws IOException {
checkError();
buffer.write(buf, 0, buf.length);
}
public void write(byte[] buf, int off, int len) throws IOException {
checkError();
buffer.write(buf, off, len);
}
/**
* Check if the read thread died, if so, throw an exception.
*/
private void checkError() throws IOException {
if (closed) {
throw new IOException("The stream has been closed");
}
checkWriteThread();
}
private void checkWriteThread() throws IOException {
if (writeThread.error != null) {
throw new IOException(writeThread.error.getMessage());
}
}
/////////////////////////////////////
/**
* Writer thread which will copy bytes from the byte buffer to the underlying
* output stream.
*/
private class WriteThread extends Thread implements Runnable {
private final OutputStream os;
private final BufferedByteInputOutput buffer;
private final int writeBufferSize;
volatile long totalBytesTransferred = 0;
volatile Throwable error = null;
WriteThread(OutputStream os, BufferedByteInputOutput is, int writeBufferSize) {
this.os = os;
this.buffer = is;
this.writeBufferSize = writeBufferSize;
}
public void run() {
byte[] buf = new byte[writeBufferSize];
int bytesRead;
try {
while ((bytesRead = buffer.read(buf, 0, buf.length)) >= 0) {
if (bytesRead > 0) {
os.write(buf, 0, bytesRead);
totalBytesTransferred += bytesRead;
}
}
} catch (Exception e) {
error = e;
LOG.warn("Exception", e);
} finally {
// close buffer and output stream
buffer.close();
try {
os.close();
} catch (Exception e) {
LOG.error("Exception when closing underlying output stream", e);
}
closed = true;
}
}
}
}