/*
* Copyright 2012-2017 the original author or authors.
*
* 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 org.glowroot.agent.embedded.util;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.util.List;
import java.util.Map;
import javax.annotation.concurrent.GuardedBy;
import com.google.common.base.Charsets;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
import com.google.common.io.CountingOutputStream;
import com.google.common.primitives.Longs;
import com.google.protobuf.AbstractMessage;
import com.google.protobuf.MessageLite;
import com.google.protobuf.Parser;
import com.ning.compress.lzf.LZFInputStream;
import com.ning.compress.lzf.LZFOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.glowroot.common.util.OnlyUsedByTests;
import org.glowroot.common.util.SizeLimitBypassingParser;
public class CappedDatabase {
private static final Logger logger = LoggerFactory.getLogger(CappedDatabase.class);
private final File file;
private final Object lock = new Object();
@GuardedBy("lock")
private final CappedDatabaseOutputStream out;
private final Thread shutdownHookThread;
@GuardedBy("lock")
private RandomAccessFile inFile;
private volatile boolean closed = false;
private final Ticker ticker;
private final Map<String, CappedDatabaseStats> statsByType = Maps.newHashMap();
public CappedDatabase(File file, int requestedSizeKb, Ticker ticker) throws IOException {
this.file = file;
this.ticker = ticker;
out = new CappedDatabaseOutputStream(file, requestedSizeKb);
inFile = new RandomAccessFile(file, "r");
shutdownHookThread = new ShutdownHookThread();
Runtime.getRuntime().addShutdownHook(shutdownHookThread);
}
public long writeMessage(final AbstractMessage message, String type) throws IOException {
return write(type, new Copier() {
@Override
public void copyTo(OutputStream writer) throws IOException {
message.writeTo(writer);
}
});
}
public long writeMessages(final List<? extends AbstractMessage> messages, String type)
throws IOException {
return write(type, new Copier() {
@Override
public void copyTo(OutputStream writer) throws IOException {
for (AbstractMessage message : messages) {
message.writeDelimitedTo(writer);
}
}
});
}
public CappedDatabaseStats getStats(String type) {
CappedDatabaseStats stats = statsByType.get(type);
if (stats == null) {
return new CappedDatabaseStats();
}
return stats;
}
@OnlyUsedByTests
long write(final ByteSource byteSource, String type) throws IOException {
return write(type, new Copier() {
@Override
public void copyTo(OutputStream out) throws IOException {
byteSource.copyTo(out);
}
});
}
private long write(String type, Copier copier) throws IOException {
synchronized (lock) {
if (closed) {
return -1;
}
long startTick = ticker.read();
out.startBlock();
NonClosingCountingOutputStream countingStreamAfterCompression =
new NonClosingCountingOutputStream(out);
CountingOutputStream countingStreamBeforeCompression =
new CountingOutputStream(new LZFOutputStream(countingStreamAfterCompression));
copier.copyTo(countingStreamBeforeCompression);
countingStreamBeforeCompression.close();
long endTick = ticker.read();
CappedDatabaseStats stats = statsByType.get(type);
if (stats == null) {
stats = new CappedDatabaseStats();
statsByType.put(type, stats);
}
stats.record(countingStreamBeforeCompression.getCount(),
countingStreamAfterCompression.getCount(), endTick - startTick);
return out.endBlock();
}
}
public <T extends /*@NonNull*/ AbstractMessage> /*@Nullable*/ T readMessage(long cappedId,
Parser<T> parser) throws IOException {
boolean overwritten;
boolean inTheFuture;
synchronized (lock) {
overwritten = out.isOverwritten(cappedId);
inTheFuture = cappedId >= out.getCurrIndex();
}
if (overwritten) {
return null;
}
if (inTheFuture) {
// this can happen when the glowroot folder is copied for analysis without shutting down
// the JVM and glowroot.capped.db is copied first, then new data is written to
// glowroot.capped.db and the new capped ids are written to glowroot.h2.db and then
// glowroot.h2.db is copied with capped ids that do not exist in the copied
// glowroot.capped.db
return null;
}
// it's important to wrap CappedBlockInputStream in a BufferedInputStream to prevent
// lots of small reads from the underlying RandomAccessFile
final int bufferSize = 32768;
InputStream input = new LZFInputStream(
new BufferedInputStream(new CappedBlockInputStream(cappedId), bufferSize));
try {
return parser.parseFrom(input);
} catch (Exception e) {
if (!out.isOverwritten(cappedId)) {
logger.error(e.getMessage(), e);
}
return null;
} finally {
input.close();
}
}
public <T extends /*@NonNull*/MessageLite> List<T> readMessages(long cappedId, Parser<T> parser)
throws IOException {
boolean overwritten;
boolean inTheFuture;
synchronized (lock) {
overwritten = out.isOverwritten(cappedId);
inTheFuture = cappedId >= out.getCurrIndex();
}
if (overwritten) {
return ImmutableList.of();
}
if (inTheFuture) {
// this can happen when the glowroot folder is copied for analysis without shutting down
// the JVM and glowroot.capped.db is copied first, then new data is written to
// glowroot.capped.db and the new capped ids are written to glowroot.h2.db and then
// glowroot.h2.db is copied with capped ids that do not exist in the copied
// glowroot.capped.db
return ImmutableList.of();
}
// it's important to wrap CappedBlockInputStream in a BufferedInputStream to prevent
// lots of small reads from the underlying RandomAccessFile
final int bufferSize = 32768;
InputStream input = new LZFInputStream(
new BufferedInputStream(new CappedBlockInputStream(cappedId), bufferSize));
SizeLimitBypassingParser<T> sizeLimitBypassingParser =
new SizeLimitBypassingParser<T>(parser);
List<T> messages = Lists.newArrayList();
try {
T message;
while ((message = sizeLimitBypassingParser.parseDelimitedFrom(input)) != null) {
messages.add(message);
}
} catch (Exception e) {
if (!out.isOverwritten(cappedId)) {
logger.error(e.getMessage(), e);
}
return ImmutableList.of();
} finally {
input.close();
}
return messages;
}
@OnlyUsedByTests
CharSource read(long cappedId) {
return new CappedBlockCharSource(cappedId);
}
boolean isExpired(long cappedId) {
synchronized (lock) {
return out.isOverwritten(cappedId);
}
}
public long getSmallestNonExpiredId() {
synchronized (lock) {
return out.getSmallestNonOverwrittenId();
}
}
public void resize(int newSizeKb) throws IOException {
synchronized (lock) {
if (closed) {
return;
}
inFile.close();
out.resize(newSizeKb);
inFile = new RandomAccessFile(file, "r");
}
}
@OnlyUsedByTests
public void close() throws IOException {
synchronized (lock) {
closed = true;
out.close();
inFile.close();
}
Runtime.getRuntime().removeShutdownHook(shutdownHookThread);
}
@OnlyUsedByTests
private class CappedBlockCharSource extends CharSource {
private final long cappedId;
private CappedBlockCharSource(long cappedId) {
this.cappedId = cappedId;
}
@Override
public Reader openStream() throws IOException {
// it's important to wrap CappedBlockInputStream in a BufferedInputStream to prevent
// lots of small reads from the underlying RandomAccessFile
final int bufferSize = 32768;
return new InputStreamReader(new LZFInputStream(
new BufferedInputStream(new CappedBlockInputStream(cappedId), bufferSize)),
Charsets.UTF_8);
}
}
private class CappedBlockInputStream extends InputStream {
private final long cappedId;
private long blockLength = -1;
private long blockIndex;
private CappedBlockInputStream(long cappedId) {
this.cappedId = cappedId;
}
@Override
public int read(byte[] bytes, int off, int len) throws IOException {
if (blockIndex == blockLength) {
return -1;
}
synchronized (lock) {
if (out.isOverwritten(cappedId)) {
throw new CappedBlockRolledOverMidReadException("Block rolled over mid-read");
}
if (blockLength == -1) {
long filePosition = out.convertToFilePosition(cappedId);
inFile.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES + filePosition);
blockLength = inFile.readLong();
}
long filePosition = out.convertToFilePosition(
cappedId + CappedDatabaseOutputStream.BLOCK_HEADER_SKIP_BYTES + blockIndex);
inFile.seek(CappedDatabaseOutputStream.HEADER_SKIP_BYTES + filePosition);
long blockRemaining = blockLength - blockIndex;
long fileRemaining = out.getSizeKb() * 1024L - filePosition;
int numToRead = (int) Longs.min(len, blockRemaining, fileRemaining);
inFile.readFully(bytes, off, numToRead);
blockIndex += numToRead;
return numToRead;
}
}
@Override
public int read(byte[] bytes) throws IOException {
// this is never called since CappedBlockInputStream is always wrapped in a
// BufferedInputStream
return read(bytes, 0, bytes.length);
}
@Override
public int read() throws IOException {
throw new UnsupportedOperationException(
"CappedBlockInputStream should always be wrapped in a BufferedInputStream");
}
}
private class ShutdownHookThread extends Thread {
@Override
public void run() {
try {
// update flag outside of lock in case there is a backlog of threads already
// waiting on the lock (once the flag is set, any threads in the backlog that
// haven't acquired the lock will abort quickly once they do obtain the lock)
closed = true;
synchronized (lock) {
out.close();
inFile.close();
}
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
private interface Copier {
void copyTo(OutputStream out) throws IOException;
}
@SuppressWarnings("serial")
private static class CappedBlockRolledOverMidReadException extends IOException {
public CappedBlockRolledOverMidReadException(String message) {
super(message);
}
}
private static class NonClosingCountingOutputStream extends FilterOutputStream {
private long count;
private NonClosingCountingOutputStream(OutputStream out) {
super(out);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
count += len;
}
@Override
public void write(int b) throws IOException {
out.write(b);
count++;
}
@Override
public void close() {}
private long getCount() {
return count;
}
}
}