Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions src/java/org/apache/cassandra/db/virtual/ExceptionsTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ public class ExceptionsTable extends AbstractMutableVirtualTable
*/
static final List<ExceptionRow> preInitialisationBuffer = Collections.synchronizedList(new ArrayList<>());

/**
* Upper bound on {@link #preInitialisationBuffer}. Bounds heap retention if uncaught exceptions storm during early
* startup (before virtual tables are registered), or in offline/tool contexts that never register virtual tables
* and thus never call {@link #flush()}. Kept in line with the live buffer's default cap.
*/
@VisibleForTesting
static final int PRE_INITIALISATION_BUFFER_CAPACITY = 1000;

@VisibleForTesting
static volatile ExceptionsTable INSTANCE;

Expand All @@ -62,7 +70,7 @@ public class ExceptionsTable extends AbstractMutableVirtualTable
ExceptionsTable(String keyspace)
{
// for starters capped to 1k, I do not think we need to make this configurable (yet).
this(keyspace, 1000);
this(keyspace, PRE_INITIALISATION_BUFFER_CAPACITY);
}

ExceptionsTable(String keyspace, int maxSize)
Expand All @@ -84,10 +92,19 @@ public class ExceptionsTable extends AbstractMutableVirtualTable

public void flush()
{
for (ExceptionRow row : preInitialisationBuffer)
add(row.exceptionClass, row.exceptionLocation, row.message, row.stackTrace, row.occurrence.getTime());
// Drain under the list's monitor and iterate a private copy: preInitialisationBuffer is a synchronizedList,
// whose contract requires holding its monitor while iterating. A concurrent persist() on another thread could
// otherwise add() during iteration and trigger a ConcurrentModificationException, which would propagate out of
// setupVirtualKeyspaces() and abort node startup.
List<ExceptionRow> drained;
synchronized (preInitialisationBuffer)

@frankgh frankgh Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iteration must be synchronized for arrays wrapped with Collections.synchronizedList. Here's an excerpt from the Collections.synchronizedList javadoc:

     * It is imperative that the user manually synchronize on the returned
     * list when traversing it via {@link Iterator}, {@link Spliterator}
     * or {@link Stream}:
     * <pre>
     *  List list = Collections.synchronizedList(new ArrayList());
     *      ...
     *  synchronized (list) {
     *      Iterator i = list.iterator(); // Must be in synchronized block
     *      while (i.hasNext())
     *          foo(i.next());
     *  }
     * </pre>

{
drained = new ArrayList<>(preInitialisationBuffer);
preInitialisationBuffer.clear();
}

preInitialisationBuffer.clear();
for (ExceptionRow row : drained)
add(row.exceptionClass, row.exceptionLocation, row.message, row.stackTrace, row.occurrence.getTime());
}

@Override
Expand Down Expand Up @@ -178,12 +195,19 @@ public static void persist(Throwable t)
}
else
{
preInitialisationBuffer.add(new ExceptionRow(toPersist.getClass().getName(),
stackTrace.isEmpty() ? "unknown" : stackTrace.get(0),
0,
toPersist.getMessage(),
stackTrace,
now));
// Bound retention (see PRE_INITIALISATION_BUFFER_CAPACITY): keep the earliest entries, which are usually the
// most diagnostic, and drop once full rather than growing without limit. Guard the size check and the add
// together under the list monitor so concurrent persist() calls cannot race past the cap.
synchronized (preInitialisationBuffer)
{
if (preInitialisationBuffer.size() < PRE_INITIALISATION_BUFFER_CAPACITY)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want to avoid this growing bigger than the 1000 limit we had set above in the ctor in the original patch

preInitialisationBuffer.add(new ExceptionRow(toPersist.getClass().getName(),
stackTrace.isEmpty() ? "unknown" : stackTrace.get(0),
0,
toPersist.getMessage(),
stackTrace,
now));
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ public static void uncaughtException(Thread thread, Throwable t)
try { StorageMetrics.uncaughtExceptions.inc(); } catch (Throwable ignore) { /* might not be initialised */ }
logger.error("Exception in thread {}", thread, t);
Tracing.trace("Exception in thread {}", thread, t);
ExceptionsTable.persist(t);
// Recording the exception for observability must never preempt the stability handling below (the
// disk_failure_policy / OOM "die" actions in inspectThrowable). Guard it like the StorageMetrics increment above.
try { ExceptionsTable.persist(t); } catch (Throwable ignore) { /* observability only, must not throw here */ }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should never allow a failure here prevent us from processing the exception, we follow the same pattern as line 74

for (Throwable t2 = t; t2 != null; t2 = t2.getCause())
{
// make sure error gets logged exactly once.
Expand Down
19 changes: 19 additions & 0 deletions test/unit/org/apache/cassandra/db/virtual/ExceptionsTableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,25 @@ public void testEmptyStacktrace()
});
}

@Test
public void testPreInitialisationBufferIsBounded()
{
doWithVTable(100, table ->
{
// Do not register the table, so INSTANCE stays null and every persist() lands in the pre-initialisation
// buffer. Persisting well past the cap must not grow the buffer without bound.
ExceptionsTable.INSTANCE = null;
ExceptionsTable.preInitialisationBuffer.clear();

int overCap = ExceptionsTable.PRE_INITIALISATION_BUFFER_CAPACITY + 50;
for (int i = 0; i < overCap; i++)
ExceptionsTable.persist(new MyUncaughtException("boom " + i));

assertEquals(ExceptionsTable.PRE_INITIALISATION_BUFFER_CAPACITY,
ExceptionsTable.preInitialisationBuffer.size());
});
}

private List<UntypedResultSet.Row> rows(String query)
{
return execute(query).stream().collect(toList());
Expand Down