-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Protect uncaught_exceptions virtual table against the crash path #5028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
||
|
|
@@ -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) | ||
|
|
@@ -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) | ||
| { | ||
| 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 | ||
|
|
@@ -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) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 */ } | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 theCollections.synchronizedListjavadoc: