blob: e3055c4c2ee6ca71937c796ec026303be20bc781 [file] [log] [blame]
/*
* 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.cassandra.db.virtual;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.google.common.annotations.VisibleForTesting;
import org.apache.cassandra.db.DecoratedKey;
import org.apache.cassandra.db.marshal.Int32Type;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.TimestampType;
import org.apache.cassandra.db.marshal.UTF8Type;
import org.apache.cassandra.dht.LocalPartitioner;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.utils.Clock;
import org.apache.cassandra.utils.logging.AbstractVirtualTableAppender;
public class ExceptionsTable extends AbstractMutableVirtualTable
{
public static final String EXCEPTIONS_TABLE_NAME = "uncaught_exceptions";
public static final String EXCEPTION_CLASS_COLUMN_NAME = "exception_class";
public static final String EXCEPTION_LOCATION_COLUMN_NAME = "exception_location";
public static final String COUNT_COLUMN_NAME = "count";
public static final String LAST_MESSAGE_COLUMN_NAME = "last_message";
public static final String LAST_STACKTRACE_COLUMN_NAME = "last_stacktrace";
public static final String LAST_OCCURRENCE_COLUMN_NAME = "last_occurrence";
/**
* Buffer of uncaught exceptions which happened while virtual table was not initialized.
*/
static final List<ExceptionRow> preInitialisationBuffer = Collections.synchronizedList(new ArrayList<>());
@VisibleForTesting
static volatile ExceptionsTable INSTANCE;
// please be sure operations on this structure are thread-safe
@VisibleForTesting
final BoundedMap buffer;
ExceptionsTable(String keyspace)
{
// for starters capped to 1k, I do not think we need to make this configurable (yet).
this(keyspace, 1000);
}
ExceptionsTable(String keyspace, int maxSize)
{
super(TableMetadata.builder(keyspace, EXCEPTIONS_TABLE_NAME)
.comment("View into uncaught exceptions")
.kind(TableMetadata.Kind.VIRTUAL)
.partitioner(new LocalPartitioner(UTF8Type.instance))
.addPartitionKeyColumn(EXCEPTION_CLASS_COLUMN_NAME, UTF8Type.instance)
.addClusteringColumn(EXCEPTION_LOCATION_COLUMN_NAME, UTF8Type.instance)
.addRegularColumn(COUNT_COLUMN_NAME, Int32Type.instance)
.addRegularColumn(LAST_MESSAGE_COLUMN_NAME, UTF8Type.instance)
.addRegularColumn(LAST_STACKTRACE_COLUMN_NAME, ListType.getInstance(UTF8Type.instance, false))
.addRegularColumn(LAST_OCCURRENCE_COLUMN_NAME, TimestampType.instance)
.build());
this.buffer = new BoundedMap(maxSize);
}
public void flush()
{
for (ExceptionRow row : preInitialisationBuffer)
add(row.exceptionClass, row.exceptionLocation, row.message, row.stackTrace, row.occurrence.getTime());
preInitialisationBuffer.clear();
}
@Override
public DataSet data()
{
SimpleDataSet result = new SimpleDataSet(metadata());
synchronized (buffer)
{
for (Map.Entry<String, LinkedHashMap<String, ExceptionRow>> partition : buffer.entrySet())
{
for (Map.Entry<String, ExceptionRow> entry : partition.getValue().entrySet())
populateRow(result, partition.getKey(), entry.getKey(), entry.getValue());
}
}
return result;
}
@Override
public DataSet data(DecoratedKey partitionKey)
{
SimpleDataSet result = new SimpleDataSet(metadata());
synchronized (buffer)
{
String exceptionClass = UTF8Type.instance.getSerializer().deserialize(partitionKey.getKey());
LinkedHashMap<String, ExceptionRow> partition = buffer.get(exceptionClass);
if (partition != null)
{
for (Map.Entry<String, ExceptionRow> row : partition.entrySet())
populateRow(result, exceptionClass, row.getKey(), row.getValue());
}
}
return result;
}
private void populateRow(SimpleDataSet result, String exceptionClass, String exceptionLocation, ExceptionRow row)
{
result.row(exceptionClass, exceptionLocation)
.column(COUNT_COLUMN_NAME, row.count)
.column(LAST_MESSAGE_COLUMN_NAME, row.message)
.column(LAST_STACKTRACE_COLUMN_NAME, row.stackTrace)
.column(LAST_OCCURRENCE_COLUMN_NAME, row.occurrence);
}
@Override
public void truncate()
{
synchronized (buffer)
{
buffer.clear();
}
}
static List<String> extractStacktrace(StackTraceElement[] stackTraceArray)
{
List<String> result = new ArrayList<>(stackTraceArray.length);
for (StackTraceElement element : stackTraceArray)
result.add(element.toString());
return result;
}
public static void persist(Throwable t)
{
if (INSTANCE == null)
INSTANCE = AbstractVirtualTableAppender.getVirtualTable(ExceptionsTable.class, EXCEPTIONS_TABLE_NAME);
Throwable toPersist = t;
while (toPersist.getCause() != null)
toPersist = toPersist.getCause();
List<String> stackTrace = extractStacktrace(toPersist.getStackTrace());
long now = Clock.Global.currentTimeMillis();
if (INSTANCE != null)
{
INSTANCE.add(toPersist.getClass().getName(),
stackTrace.isEmpty() ? "unknown" : stackTrace.get(0),
toPersist.getMessage(),
stackTrace,
now);
}
else
{
preInitialisationBuffer.add(new ExceptionRow(toPersist.getClass().getName(),
stackTrace.isEmpty() ? "unknown" : stackTrace.get(0),
0,
toPersist.getMessage(),
stackTrace,
now));
}
}
/**
* Adds entry to internal buffer.
*
* @param exceptionClass exception class of uncaught exception
* @param exceptionLocation location where that exception was thrown
* @param message message of given exception
* @param stackTrace whole stacktrace of given exception
* @param occurrenceTime time when given exception ocurred
*/
private void add(String exceptionClass,
String exceptionLocation,
String message,
List<String> stackTrace,
long occurrenceTime)
{
synchronized (buffer)
{
Map<String, ExceptionRow> exceptionRowWithLocation = buffer.computeIfAbsent(exceptionClass, (classToAdd) -> new LinkedHashMap<>());
ExceptionRow exceptionRow = exceptionRowWithLocation.get(exceptionLocation);
if (exceptionRow == null)
{
// exception class and location can be null for value as we have it as part of keys already
exceptionRow = new ExceptionRow(null, null, 1, message, stackTrace, occurrenceTime);
exceptionRowWithLocation.put(exceptionLocation, exceptionRow);
// not important, can be null
// we need to do this, because if we add into a map which is
// a value of some buffer key, we might exceed the number
// of overall entries in the buffer
buffer.removeEldestEntry(null);
}
else
{
exceptionRow.count += 1;
exceptionRow.message = message;
exceptionRow.stackTrace = stackTrace;
exceptionRow.occurrence = new Date(occurrenceTime);
}
}
}
static final class ExceptionRow
{
final String exceptionClass;
final String exceptionLocation;
int count;
String message;
List<String> stackTrace;
Date occurrence;
/**
* @param exceptionClass exception class of uncaught exception
* @param exceptionLocation location where that exception was thrown
* @param message message of given exception
* @param stackTrace whole stacktrace of given exception
* @param occurrenceTime time when given exception ocurred, in milliseconds from epoch
*/
ExceptionRow(String exceptionClass,
String exceptionLocation,
int count,
String message,
List<String> stackTrace,
long occurrenceTime)
{
this.exceptionClass = exceptionClass;
this.exceptionLocation = exceptionLocation;
this.count = count;
this.stackTrace = stackTrace;
this.message = message;
this.occurrence = new Date(occurrenceTime);
}
}
@VisibleForTesting
static class BoundedMap extends LinkedHashMap<String, LinkedHashMap<String, ExceptionRow>>
{
private final int maxSize;
public BoundedMap(int maxSize)
{
if (maxSize <= 0)
throw new IllegalArgumentException("maxSize has to be bigger than 0");
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<String, LinkedHashMap<String, ExceptionRow>> eldest)
{
if (computeSize() > maxSize)
{
String oldestExceptionClass = null;
String oldestExceptionLocation = null;
long oldestLastOccurrence = Long.MAX_VALUE;
for (Map.Entry<String, LinkedHashMap<String, ExceptionRow>> entry : entrySet())
{
for (Map.Entry<String, ExceptionRow> entryInEntry : entry.getValue().entrySet())
{
long currentLastOccurrence = entryInEntry.getValue().occurrence.getTime();
if (currentLastOccurrence < oldestLastOccurrence)
{
oldestExceptionLocation = entryInEntry.getKey();
oldestExceptionClass = entry.getKey();
oldestLastOccurrence = currentLastOccurrence;
}
}
}
if (oldestLastOccurrence < Long.MAX_VALUE)
{
LinkedHashMap<String, ExceptionRow> aMap = get(oldestExceptionClass);
if (aMap.size() == 1)
remove(oldestExceptionClass);
else
aMap.remove(oldestExceptionLocation);
}
}
// always returning false as per method's contract saying that
// overrides might modify the map directly but in that case it must return false
return false;
}
private int computeSize()
{
int size = 0;
for (LinkedHashMap<String, ExceptionRow> value : values())
size += value.size();
return size;
}
}
}