/*
* Copyright 2013 Jive Software, 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.jivesoftware.os.amza.service.storage.filer;
import com.jivesoftware.os.amza.api.filer.IAppendOnly;
import com.jivesoftware.os.amza.api.filer.IReadable;
import com.jivesoftware.os.amza.service.filer.FileBackedMemMappedByteBufferFactory;
import com.jivesoftware.os.amza.api.filer.HeapFiler;
import com.jivesoftware.os.amza.service.filer.SingleAutoGrowingByteBufferBackedFiler;
import com.jivesoftware.os.mlogger.core.MetricLogger;
import com.jivesoftware.os.mlogger.core.MetricLoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.RandomAccessFile;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
public class DiskBackedWALFiler implements WALFiler {
private static final MetricLogger LOG = MetricLoggerFactory.getLogger();
private static final long BUFFER_SEGMENT_SIZE = 1024L * 1024 * 1024;
private final String fileName;
private final String mode;
private RandomAccessFile randomAccessFile;
private FileChannel channel;
private final boolean useMemMap;
private final AtomicLong size;
private final IAppendOnly appendOnly;
private SingleAutoGrowingByteBufferBackedFiler memMapFiler;
private final AtomicLong memMapFilerLength = new AtomicLong(-1);
private final Object fileLock = new Object();
private final Object memMapLock = new Object();
private final AtomicBoolean closed = new AtomicBoolean(false);
public DiskBackedWALFiler(String fileName, String mode, boolean useMemMap, int appendBufferSize) throws IOException {
this.fileName = fileName;
this.mode = mode;
this.useMemMap = useMemMap;
this.randomAccessFile = new RandomAccessFile(fileName, mode);
this.channel = randomAccessFile.getChannel();
this.size = new AtomicLong(randomAccessFile.length());
this.appendOnly = createAppendOnly(appendBufferSize);
this.memMapFiler = createMemMap();
}
public String getFileName() {
return fileName;
}
@Override
public IReadable reader(IReadable current, long requiredLength, boolean fallBackToChannelReader, int bufferSize) throws IOException {
if (!useMemMap) {
if (current != null) {
return current;
} else {
DiskBackedWALFilerChannelReader reader = new DiskBackedWALFilerChannelReader(this, channel, closed);
return bufferSize > 0 ? new HeapBufferedReadable(reader, bufferSize) : reader;
}
}
// quick check to see if the current filer is big enough
long length = size.get();
if (current != null && current.length() <= length && current.length() >= requiredLength) {
return current;
}
synchronized (memMapLock) {
// double check current filer
length = size.get();
if (current != null && current.length() <= length && current.length() >= requiredLength) {
return current;
}
if (fallBackToChannelReader && memMapFilerLength.get() < requiredLength) {
// mmap is too small, fall back to channel reader
DiskBackedWALFilerChannelReader reader = new DiskBackedWALFilerChannelReader(this, channel, closed);
return bufferSize > 0 ? new HeapBufferedReadable(reader, bufferSize) : reader;
}
memMapFiler.seek(length);
memMapFilerLength.set(length);
}
return memMapFiler.duplicateAll();
}
@Override
public IAppendOnly appender() throws IOException {
return appendOnly;
}
private IAppendOnly createAppendOnly(int bufferSize) throws IOException {
HeapFiler filer = bufferSize > 0 ? new HeapFiler(bufferSize) : null;
randomAccessFile.seek(size.get());
return new IAppendOnly() {
@Override
public void write(byte b) throws IOException {
if (filer != null) {
filer.write(b);
if (filer.length() > bufferSize) {
flush(false);
}
} else {
DiskBackedWALFiler.this.write(b);
}
}
@Override
public void write(byte[] b, int _offset, int _len) throws IOException {
if (filer != null) {
filer.write(b, _offset, _len);
if (filer.length() > bufferSize) {
flush(false);
}
} else {
DiskBackedWALFiler.this.write(b, _offset, _len);
}
}
@Override
public void flush(boolean fsync) throws IOException {
if (filer != null && filer.length() > 0) {
long length = filer.length();
DiskBackedWALFiler.this.write(filer.leakBytes(), 0, (int) length);
filer.reset();
}
DiskBackedWALFiler.this.flush(fsync);
}
@Override
public void close() throws IOException {
closed.compareAndSet(false, true);
if (filer != null) {
filer.reset();
}
}
@Override
public Object lock() {
return DiskBackedWALFiler.this;
}
@Override
public long length() throws IOException {
return DiskBackedWALFiler.this.length() + (filer != null ? filer.length() : 0);
}
@Override
public long getFilePointer() throws IOException {
return length();
}
};
}
private SingleAutoGrowingByteBufferBackedFiler createMemMap() throws IOException {
if (useMemMap) {
FileBackedMemMappedByteBufferFactory byteBufferFactory = new FileBackedMemMappedByteBufferFactory(new File(fileName), BUFFER_SEGMENT_SIZE);
return new SingleAutoGrowingByteBufferBackedFiler(-1L, BUFFER_SEGMENT_SIZE, byteBufferFactory);
} else {
return null;
}
}
@Override
public String toString() {
return "DiskBackedWALFiler{"
+ "fileName=" + fileName
+ ", useMemMap=" + useMemMap
+ ", size=" + size
+ '}';
}
@Override
public void close() throws IOException {
closed.compareAndSet(false, true);
randomAccessFile.close();
if (memMapFiler != null) {
memMapFiler.close();
}
}
@Override
public long length() throws IOException {
return size.get();
}
private void write(byte b) throws IOException {
while (!closed.get()) {
try {
randomAccessFile.write(b);
size.addAndGet(1);
break;
} catch (ClosedChannelException e) {
ensureOpen();
}
}
}
private void write(byte[] b, int _offset, int _len) throws IOException {
while (!closed.get()) {
try {
randomAccessFile.write(b, _offset, _len);
size.addAndGet(_len);
break;
} catch (ClosedChannelException e) {
ensureOpen();
}
}
}
private void flush(boolean fsync) throws IOException {
if (fsync) {
while (!closed.get()) {
try {
randomAccessFile.getFD().sync();
break;
} catch (ClosedChannelException e) {
ensureOpen();
}
}
}
}
@Override
public void truncate(long truncatedSize) throws IOException {
// should only be called with a write AND a read lock
while (!closed.get()) {
try {
randomAccessFile.setLength(truncatedSize);
size.set(randomAccessFile.length());
synchronized (memMapLock) {
memMapFiler = createMemMap();
memMapFilerLength.set(-1);
}
break;
} catch (ClosedChannelException e) {
ensureOpen();
}
}
}
private void ensureOpen() throws IOException {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (!channel.isOpen()) {
if (closed.get()) {
throw new IOException("The WAL filer has been closed");
}
synchronized (fileLock) {
if (!channel.isOpen()) {
LOG.warn("File channel is closed and must be reopened for {}", fileName);
try {
randomAccessFile.close();
} catch (IOException e) {
LOG.error("Failed to close existing random access file while reacquiring channel");
}
randomAccessFile = new RandomAccessFile(fileName, mode);
channel = randomAccessFile.getChannel();
randomAccessFile.seek(size.get());
}
}
}
}
FileChannel getFileChannel() throws IOException {
ensureOpen();
return channel;
}
@Override
public Object lock() {
return this;
}
}