/*
*
* 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.bookkeeper.bookie;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import org.apache.bookkeeper.util.ZeroBuffer;
import java.util.concurrent.atomic.AtomicLong;
/**
* Provides a buffering layer in front of a FileChannel.
*/
public class BufferedChannel extends BufferedReadChannel {
// The capacity of the write buffer.
protected final int writeCapacity;
// The position of the file channel's write pointer.
protected AtomicLong writeBufferStartPosition = new AtomicLong(0);
// The buffer used to write operations.
protected final ByteBuffer writeBuffer;
// The absolute position of the next write operation.
protected volatile long position;
// make constructor to be public for unit test
public BufferedChannel(FileChannel fc, int capacity) throws IOException {
// Use the same capacity for read and write buffers.
this(fc, capacity, capacity);
}
public BufferedChannel(FileChannel fc, int writeCapacity, int readCapacity) throws IOException {
super(fc, readCapacity);
// Set the read buffer's limit to readCapacity.
this.readBuffer.limit(readCapacity);
this.writeCapacity = writeCapacity;
this.position = fc.position();
this.writeBufferStartPosition.set(position);
this.writeBuffer = ByteBuffer.allocateDirect(writeCapacity);
}
/**
* Write all the data in src to the {@link FileChannel}. Note that this function can
* buffer or re-order writes based on the implementation. These writes will be flushed
* to the disk only when flush() is invoked.
*
* @param src The source ByteBuffer which contains the data to be written.
* @throws IOException if a write operation fails.
*/
synchronized public void write(ByteBuffer src) throws IOException {
int copied = 0;
while(src.remaining() > 0) {
int truncated = 0;
if (writeBuffer.remaining() < src.remaining()) {
truncated = src.remaining() - writeBuffer.remaining();
src.limit(src.limit()-truncated);
}
copied += src.remaining();
writeBuffer.put(src);
src.limit(src.limit()+truncated);
// if we have run out of buffer space, we should flush to the file
if (writeBuffer.remaining() == 0) {
flushInternal();
}
}
position += copied;
}
/**
* Get the position where the next write operation will begin writing from.
* @return
*/
public long position() {
return position;
}
/**
* Get the position of the file channel's write pointer.
* @return
*/
public long getFileChannelPosition() {
return writeBufferStartPosition.get();
}
/**
* Write any data in the buffer to the file. If sync is set to true, force a sync operation so that
* data is persisted to the disk.
* @param shouldForceWrite
* @throws IOException if the write or sync operation fails.
*/
public void flush(boolean shouldForceWrite) throws IOException {
synchronized(this) {
flushInternal();
}
if (shouldForceWrite) {
forceWrite(false);
}
}
/**
* Write any data in the buffer to the file and advance the writeBufferPosition
* Callers are expected to synchronize appropriately
* @throws IOException if the write fails.
*/
private void flushInternal() throws IOException {
writeBuffer.flip();
do {
fileChannel.write(writeBuffer);
} while (writeBuffer.hasRemaining());
writeBuffer.clear();
writeBufferStartPosition.set(fileChannel.position());
}
public long forceWrite(boolean forceMetadata) throws IOException {
// This is the point up to which we had flushed to the file system page cache
// before issuing this force write hence is guaranteed to be made durable by
// the force write, any flush that happens after this may or may
// not be flushed
long positionForceWrite = writeBufferStartPosition.get();
fileChannel.force(forceMetadata);
return positionForceWrite;
}
@Override
synchronized public int read(ByteBuffer dest, long pos) throws IOException {
long prevPos = pos;
while(dest.remaining() > 0) {
// check if it is in the write buffer
if (writeBuffer != null && writeBufferStartPosition.get() <= pos) {
long positionInBuffer = pos - writeBufferStartPosition.get();
long bytesToCopy = writeBuffer.position()-positionInBuffer;
if (bytesToCopy > dest.remaining()) {
bytesToCopy = dest.remaining();
}
if (bytesToCopy == 0) {
throw new IOException("Read past EOF");
}
ByteBuffer src = writeBuffer.duplicate();
src.position((int) positionInBuffer);
src.limit((int) (positionInBuffer+bytesToCopy));
dest.put(src);
pos+= bytesToCopy;
} else if (writeBuffer == null && writeBufferStartPosition.get() <= pos) {
// here we reach the end
break;
// first check if there is anything we can grab from the readBuffer
} else if (readBufferStartPosition <= pos && pos < readBufferStartPosition+readBuffer.capacity()) {
long positionInBuffer = pos - readBufferStartPosition;
long bytesToCopy = readBuffer.capacity()-positionInBuffer;
if (bytesToCopy > dest.remaining()) {
bytesToCopy = dest.remaining();
}
ByteBuffer src = readBuffer.duplicate();
src.position((int) positionInBuffer);
src.limit((int) (positionInBuffer+bytesToCopy));
dest.put(src);
pos += bytesToCopy;
// let's read it
} else {
readBufferStartPosition = pos;
readBuffer.clear();
// make sure that we don't overlap with the write buffer
if (readBufferStartPosition + readBuffer.capacity() >= writeBufferStartPosition.get()) {
readBufferStartPosition = writeBufferStartPosition.get() - readBuffer.capacity();
if (readBufferStartPosition < 0) {
ZeroBuffer.put(readBuffer, (int)-readBufferStartPosition);
}
}
while(readBuffer.remaining() > 0) {
if (fileChannel.read(readBuffer, readBufferStartPosition+readBuffer.position()) <= 0) {
throw new IOException("Short read");
}
}
ZeroBuffer.put(readBuffer);
readBuffer.clear();
}
}
return (int)(pos - prevPos);
}
@Override
synchronized public void clear() {
super.clear();
writeBuffer.clear();
}
}