From f25a914e267fefad5b98eec395c559a9195b0803 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 21 Sep 2026 03:13:15 -0300 Subject: [PATCH 1/3] fix(java): flush buffered payloads on JVM shutdown --- .../java/com/rollbar/notifier/Rollbar.java | 96 +++++++++++++ .../com/rollbar/notifier/config/Config.java | 41 ++++++ .../notifier/config/ConfigBuilder.java | 79 +++++++++++ .../notifier/shutdown/SenderShutdownHook.java | 94 +++++++++++++ .../notifier/RollbarShutdownHookTest.java | 99 ++++++++++++++ .../shutdown/SenderShutdownHookTest.java | 128 ++++++++++++++++++ 6 files changed, 537 insertions(+) create mode 100644 rollbar-java/src/main/java/com/rollbar/notifier/shutdown/SenderShutdownHook.java create mode 100644 rollbar-java/src/test/java/com/rollbar/notifier/RollbarShutdownHookTest.java create mode 100644 rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java b/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java index b9265745..9bc8dba6 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java @@ -5,6 +5,9 @@ import com.rollbar.notifier.config.Config; import com.rollbar.notifier.config.ConfigBuilder; import com.rollbar.notifier.config.ConfigProvider; +import com.rollbar.notifier.provider.Provider; +import com.rollbar.notifier.sender.Sender; +import com.rollbar.notifier.shutdown.SenderShutdownHook; import com.rollbar.notifier.uncaughtexception.RollbarUncaughtExceptionHandler; import com.rollbar.notifier.util.BodyFactory; import com.rollbar.notifier.util.ObjectsUtils; @@ -26,6 +29,10 @@ public class Rollbar extends RollbarBase { private static volatile Rollbar notifier; + private final Object shutdownHookLock = new Object(); + + private Thread shutdownHook; + /** * Constructor. * @@ -41,9 +48,95 @@ public Rollbar(Config config) { if (config.handleUncaughtErrors()) { this.handleUncaughtErrors(); } + if (config.flushOnShutdown()) { + this.registerShutdownHook(config); + } processAppPackages(config); } + /** + * Registers a JVM shutdown hook that flushes buffered payloads before the process exits. + * + *

+ * Mirrors {@link #handleUncaughtErrors()} in being driven from the configuration at + * construction time, so replacing the configuration later via + * {@link #configure(ConfigProvider)} does not add or remove the hook. + *

+ */ + private void registerShutdownHook(Config config) { + if (config.sender() == null) { + return; + } + + Thread hook = new SenderShutdownHook(new CurrentSenderProvider(), + config.shutdownTimeoutMillis()); + try { + Runtime.getRuntime().addShutdownHook(hook); + synchronized (shutdownHookLock) { + this.shutdownHook = hook; + } + LOGGER.debug("Registered the Rollbar shutdown hook."); + } catch (IllegalStateException e) { + // The JVM is already shutting down, so there is nothing left to flush later. + LOGGER.debug("The JVM is already shutting down, the Rollbar shutdown hook was not " + + "registered."); + } catch (SecurityException e) { + LOGGER.warn("No permission to register the Rollbar shutdown hook. Payloads buffered when " + + "the JVM exits will not be sent.", e); + } + } + + /** + * Resolves the sender from the configuration in force when the hook actually runs, so that a + * configuration replaced through {@link #configure(ConfigProvider)} after the hook was + * registered does not leave a stale sender being flushed. + */ + private class CurrentSenderProvider implements Provider { + @Override + public Sender provide() { + configReadLock.lock(); + try { + return config.sender(); + } finally { + configReadLock.unlock(); + } + } + } + + /** + * The registered shutdown hook, or null if none was registered. Visible for testing. + */ + Thread shutdownHook() { + synchronized (shutdownHookLock) { + return this.shutdownHook; + } + } + + /** + * Removes the shutdown hook, if one was registered. Called when the notifier is closed + * explicitly, both to avoid flushing twice and to let the hook be garbage collected. + */ + private void unregisterShutdownHook() { + Thread hook; + synchronized (shutdownHookLock) { + hook = this.shutdownHook; + this.shutdownHook = null; + } + + if (hook == null) { + return; + } + + try { + Runtime.getRuntime().removeShutdownHook(hook); + } catch (IllegalStateException e) { + // Shutdown is already in progress, the hook may be running right now. Nothing to do. + LOGGER.debug("The JVM is already shutting down, the Rollbar shutdown hook was not removed."); + } catch (SecurityException e) { + LOGGER.debug("No permission to remove the Rollbar shutdown hook."); + } + } + /** * Method to initialize the library managed notifier instance. * @@ -652,6 +745,9 @@ public void log(ThrowableWrapper error, Map custom, String descr } public void close(boolean wait) throws Exception { + // Dropped first: the notifier is being shut down explicitly, so the hook would either flush a + // sender that is already closed or duplicate the work being done here. + unregisterShutdownHook(); this.config.sender().close(wait); } diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/config/Config.java b/rollbar-java/src/main/java/com/rollbar/notifier/config/Config.java index 4257bb7c..b154f7cc 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/config/Config.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/config/Config.java @@ -23,4 +23,45 @@ public interface Config extends CommonConfig { * @return the proxy. */ Proxy proxy(); + + /** + *

+ * If set to true (the default), a JVM shutdown hook is registered that flushes any payloads + * still buffered in the {@link Sender sender} before the process exits. + *

+ *

+ * The default sender buffers payloads in memory and drains them on a background daemon thread + * every few seconds, so without this hook any occurrence captured shortly before the JVM + * terminates is discarded. That includes short-lived processes and, more importantly, + * {@code SIGTERM} during a rolling deploy or container eviction. + *

+ *

+ * Set to false if the application manages the notifier lifecycle itself, for example by + * calling {@link Rollbar#close(boolean)} explicitly, or when payloads are already persisted by + * a durable queue. + *

+ * + * @return true to flush buffered payloads on JVM shutdown, false otherwise. + */ + default boolean flushOnShutdown() { + return true; + } + + /** + *

+ * The maximum time, in milliseconds, that the shutdown hook installed by + * {@link #flushOnShutdown()} will spend flushing buffered payloads before letting the JVM + * continue to exit. Ignored when {@link #flushOnShutdown()} is false. + *

+ *

+ * This is a hard bound and not a target: shutdown always proceeds once it elapses, even if + * payloads remain unsent. It exists because a flush performs blocking network I/O, and an + * unbounded one would be able to stall JVM shutdown indefinitely. + *

+ * + * @return the shutdown flush timeout in milliseconds. + */ + default long shutdownTimeoutMillis() { + return 2000L; + } } diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java b/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java index d91e3705..1be31d73 100644 --- a/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java +++ b/rollbar-java/src/main/java/com/rollbar/notifier/config/ConfigBuilder.java @@ -33,6 +33,17 @@ */ public class ConfigBuilder { + /** + * Whether a JVM shutdown hook that flushes buffered payloads is registered by default. + */ + public static final boolean DEFAULT_FLUSH_ON_SHUTDOWN = true; + + /** + * The default hard bound, in milliseconds, on the shutdown flush. Kept short because it delays + * JVM exit, and the flush only has to drain what the background sender has not sent yet. + */ + public static final long DEFAULT_SHUTDOWN_TIMEOUT_MILLIS = 2000L; + protected String accessToken; protected String endpoint; @@ -89,6 +100,10 @@ public class ConfigBuilder { protected boolean compressPayload; + protected boolean flushOnShutdown; + + protected long shutdownTimeoutMillis; + private int maximumTelemetryData = RollbarTelemetryEventTracker.MAXIMUM_CAPACITY_FOR_TELEMETRY_EVENTS; @@ -104,6 +119,8 @@ protected ConfigBuilder(String accessToken) { this.handleUncaughtErrors = true; this.enabled = true; this.compressPayload = true; + this.flushOnShutdown = DEFAULT_FLUSH_ON_SHUTDOWN; + this.shutdownTimeoutMillis = DEFAULT_SHUTDOWN_TIMEOUT_MILLIS; this.defaultLevels = new DefaultLevels(); } @@ -140,6 +157,8 @@ private ConfigBuilder(Config config) { this.defaultLevels = new DefaultLevels(config); this.truncateLargePayloads = config.truncateLargePayloads(); this.compressPayload = config.compressPayload(); + this.flushOnShutdown = config.flushOnShutdown(); + this.shutdownTimeoutMillis = config.shutdownTimeoutMillis(); this.maximumTelemetryData = config.maximumTelemetryData(); this.telemetryEventTracker = config.telemetryEventTracker(); } @@ -496,6 +515,50 @@ public ConfigBuilder compressPayload(boolean compress) { return this; } + /** + *

+ * If set to true (the default), a JVM shutdown hook is registered that flushes any payloads + * still buffered in the sender before the process exits. + *

+ *

+ * The default sender buffers payloads in memory and drains them on a background daemon thread + * every few seconds, so without this hook an occurrence captured shortly before the JVM + * terminates is discarded. That covers short-lived processes as well as {@code SIGTERM} during + * a rolling deploy or container eviction. + *

+ *

+ * Set to false when the application manages the notifier lifecycle itself, for example by + * calling {@link com.rollbar.notifier.Rollbar#close(boolean)}, or when a durable queue already + * persists payloads across restarts. + *

+ * + * @param flushOnShutdown true to flush buffered payloads on JVM shutdown. + * @return the builder instance. + */ + public ConfigBuilder flushOnShutdown(boolean flushOnShutdown) { + this.flushOnShutdown = flushOnShutdown; + return this; + } + + /** + *

+ * The maximum time, in milliseconds, that the shutdown hook will spend flushing buffered + * payloads before letting the JVM continue to exit. Default: 2000. Ignored when + * {@link #flushOnShutdown(boolean)} is false. + *

+ *

+ * This is a hard bound rather than a target, and shutdown proceeds once it elapses even if + * payloads remain unsent. Values of zero or less start the flush without waiting for it. + *

+ * + * @param shutdownTimeoutMillis the shutdown flush timeout in milliseconds. + * @return the builder instance. + */ + public ConfigBuilder shutdownTimeoutMillis(long shutdownTimeoutMillis) { + this.shutdownTimeoutMillis = shutdownTimeoutMillis; + return this; + } + /** *

* Maximum Telemetry events sent in a payload, only for the default TelemetryEventTracker, if @@ -620,6 +683,10 @@ private static class ConfigImpl implements Config { private final boolean compressPayload; + private final boolean flushOnShutdown; + + private final long shutdownTimeoutMillis; + private final int maximumTelemetryData; private final TelemetryEventTracker telemetryEventTracker; @@ -657,6 +724,8 @@ private static class ConfigImpl implements Config { this.defaultLevels = builder.defaultLevels; this.truncateLargePayloads = builder.truncateLargePayloads; this.compressPayload = builder.compressPayload; + this.flushOnShutdown = builder.flushOnShutdown; + this.shutdownTimeoutMillis = builder.shutdownTimeoutMillis; this.maximumTelemetryData = builder.maximumTelemetryData; this.telemetryEventTracker = builder.telemetryEventTracker; } @@ -811,6 +880,16 @@ public boolean compressPayload() { return this.compressPayload; } + @Override + public boolean flushOnShutdown() { + return this.flushOnShutdown; + } + + @Override + public long shutdownTimeoutMillis() { + return this.shutdownTimeoutMillis; + } + @Override public int maximumTelemetryData() { return this.maximumTelemetryData; diff --git a/rollbar-java/src/main/java/com/rollbar/notifier/shutdown/SenderShutdownHook.java b/rollbar-java/src/main/java/com/rollbar/notifier/shutdown/SenderShutdownHook.java new file mode 100644 index 00000000..66d72a7a --- /dev/null +++ b/rollbar-java/src/main/java/com/rollbar/notifier/shutdown/SenderShutdownHook.java @@ -0,0 +1,94 @@ +package com.rollbar.notifier.shutdown; + +import com.rollbar.notifier.provider.Provider; +import com.rollbar.notifier.sender.Sender; +import com.rollbar.notifier.util.ObjectsUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A JVM shutdown hook that flushes the payloads still buffered in a {@link Sender sender}. + * + *

+ * The hook is bounded by a timeout. Flushing performs blocking network I/O, and the sender does + * not impose a socket timeout of its own, so an unbounded flush would be able to stall JVM + * shutdown for as long as the remote end kept the connection open. Instead the flush runs on a + * separate daemon thread and this hook waits for it for at most {@code timeoutMillis}; once that + * elapses shutdown continues regardless, and the daemon thread is torn down with the JVM. + *

+ */ +public class SenderShutdownHook extends Thread { + + private static final Logger LOGGER = LoggerFactory.getLogger(SenderShutdownHook.class); + + private static final String HOOK_THREAD_NAME = "rollbar-shutdown-hook"; + + private static final String FLUSH_THREAD_NAME = "rollbar-shutdown-flush"; + + private final Provider senderProvider; + + private final long timeoutMillis; + + /** + * Constructor. + * + * @param senderProvider supplies the sender to flush, resolved when the hook runs rather than + * when it is registered, so that a sender replaced in the meantime through + * {@link com.rollbar.notifier.Rollbar#configure} is the one flushed. Not + * null, though it may return null. + * @param timeoutMillis the maximum time to spend flushing, in milliseconds. Values of zero or + * less disable waiting: the flush is still started, but shutdown does not + * block on it. + */ + public SenderShutdownHook(Provider senderProvider, long timeoutMillis) { + super(HOOK_THREAD_NAME); + ObjectsUtils.requireNonNull(senderProvider, "The sender provider can not be null"); + this.senderProvider = senderProvider; + this.timeoutMillis = timeoutMillis; + } + + @Override + public void run() { + Thread flusher = new Thread(new FlushTask(), FLUSH_THREAD_NAME); + // Daemon, so that failing to finish within the timeout can never keep the JVM alive. + flusher.setDaemon(true); + flusher.start(); + + if (timeoutMillis <= 0) { + return; + } + + try { + flusher.join(timeoutMillis); + } catch (InterruptedException e) { + // Preserve the interrupt for whatever else is running during shutdown. + Thread.currentThread().interrupt(); + return; + } + + if (flusher.isAlive()) { + LOGGER.warn("Timed out after {} ms flushing Rollbar payloads on shutdown. Some occurrences " + + "may not have been sent.", timeoutMillis); + } + } + + private class FlushTask implements Runnable { + @Override + public void run() { + try { + Sender sender = senderProvider.provide(); + if (sender == null) { + return; + } + sender.close(true); + } catch (Exception e) { + LOGGER.error("Error flushing Rollbar payloads on shutdown.", e); + } catch (Throwable e) { + // The JVM is on its way down and this thread has no other handler. Swallowing here keeps + // an error in the notifier from surfacing as a confusing failure during shutdown. + LOGGER.error("Error flushing Rollbar payloads on shutdown.", e); + } + } + } +} diff --git a/rollbar-java/src/test/java/com/rollbar/notifier/RollbarShutdownHookTest.java b/rollbar-java/src/test/java/com/rollbar/notifier/RollbarShutdownHookTest.java new file mode 100644 index 00000000..11c34694 --- /dev/null +++ b/rollbar-java/src/test/java/com/rollbar/notifier/RollbarShutdownHookTest.java @@ -0,0 +1,99 @@ +package com.rollbar.notifier; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.rollbar.notifier.config.Config; +import com.rollbar.notifier.config.ConfigBuilder; +import com.rollbar.notifier.sender.Sender; + +import org.junit.Test; + +/** + * Covers the registration side of the shutdown hook. The flushing behaviour itself is covered by + * {@link com.rollbar.notifier.shutdown.SenderShutdownHookTest}. + */ +public class RollbarShutdownHookTest { + + @Test + public void shouldRegisterTheShutdownHookByDefault() throws Exception { + Rollbar rollbar = new Rollbar(configWith(mock(Sender.class), true)); + try { + assertThat(rollbar.shutdownHook(), is(notNullValue())); + } finally { + rollbar.close(false); + } + } + + @Test + public void shouldNotRegisterTheShutdownHookWhenDisabled() throws Exception { + Rollbar rollbar = new Rollbar(configWith(mock(Sender.class), false)); + try { + assertThat(rollbar.shutdownHook(), is(nullValue())); + } finally { + rollbar.close(false); + } + } + + @Test + public void shouldRemoveTheShutdownHookOnClose() throws Exception { + Rollbar rollbar = new Rollbar(configWith(mock(Sender.class), true)); + Thread hook = rollbar.shutdownHook(); + assertThat(hook, is(notNullValue())); + + rollbar.close(false); + + assertThat(rollbar.shutdownHook(), is(nullValue())); + // Idempotent: the hook is really gone from the JVM registry, not just from our field. + assertThat(Runtime.getRuntime().removeShutdownHook(hook), is(false)); + } + + /** + * Closing explicitly must flush exactly once. If the hook were left registered it would run a + * second flush against an already closed sender while the JVM was exiting. + */ + @Test + public void shouldFlushOnlyOnceWhenClosedExplicitly() throws Exception { + Sender sender = mock(Sender.class); + Rollbar rollbar = new Rollbar(configWith(sender, true)); + + rollbar.close(true); + + verify(sender).close(true); + assertThat(rollbar.shutdownHook(), is(nullValue())); + } + + @Test + public void shouldDefaultToFlushingOnShutdownWithATwoSecondBound() { + Config config = ConfigBuilder.withAccessToken("token").build(); + + assertThat(config.flushOnShutdown(), is(true)); + assertThat(config.shutdownTimeoutMillis(), is(2000L)); + } + + @Test + public void shouldCarryTheSettingsOverWhenRebuildingFromAnExistingConfig() { + Config original = ConfigBuilder.withAccessToken("token") + .flushOnShutdown(false) + .shutdownTimeoutMillis(750L) + .build(); + + Config copy = ConfigBuilder.withConfig(original).build(); + + assertThat(copy.flushOnShutdown(), is(false)); + assertThat(copy.shutdownTimeoutMillis(), is(750L)); + } + + private Config configWith(Sender sender, boolean flushOnShutdown) { + return ConfigBuilder.withAccessToken("token") + .sender(sender) + .handleUncaughtErrors(false) + .flushOnShutdown(flushOnShutdown) + .shutdownTimeoutMillis(100L) + .build(); + } +} diff --git a/rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java b/rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java new file mode 100644 index 00000000..e24d323d --- /dev/null +++ b/rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java @@ -0,0 +1,128 @@ +package com.rollbar.notifier.shutdown; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import com.rollbar.notifier.provider.Provider; +import com.rollbar.notifier.sender.Sender; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.mockito.stubbing.Answer; + +public class SenderShutdownHookTest { + + private static final long TIMEOUT = 500L; + + @Test + public void shouldFlushTheSender() throws Exception { + Sender sender = mock(Sender.class); + + new SenderShutdownHook(providerOf(sender), TIMEOUT).run(); + + verify(sender).close(true); + } + + @Test + public void shouldReturnWithoutWaitingWhenTimeoutIsNotPositive() throws Exception { + Sender sender = mock(Sender.class); + + long start = System.nanoTime(); + new SenderShutdownHook(providerOf(sender), 0L).run(); + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + assertTrue("Expected not to wait, but waited " + elapsedMillis + " ms", elapsedMillis < 200L); + } + + /** + * The sender performs blocking network I/O without a socket timeout of its own, so the hook has + * to give up on its own rather than let a stalled flush hold JVM shutdown open indefinitely. + */ + @Test + public void shouldGiveUpOnceTheTimeoutElapses() throws Exception { + final CountDownLatch released = new CountDownLatch(1); + Sender sender = mock(Sender.class, (Answer) invocation -> { + if ("close".equals(invocation.getMethod().getName())) { + // Block until the test releases us, simulating an unresponsive endpoint. + released.await(10, TimeUnit.SECONDS); + } + return null; + }); + + long start = System.nanoTime(); + new SenderShutdownHook(providerOf(sender), TIMEOUT).run(); + long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); + + released.countDown(); + + assertTrue("Expected to give up at ~" + TIMEOUT + " ms, but took " + elapsedMillis + " ms", + elapsedMillis >= TIMEOUT && elapsedMillis < TIMEOUT * 6); + } + + /** + * A failure inside the notifier must not surface as a confusing error while the JVM is exiting. + */ + @Test + public void shouldNotPropagateFlushFailures() throws Exception { + Sender sender = mock(Sender.class); + doThrow(new IllegalStateException("network down")).when(sender).close(true); + + new SenderShutdownHook(providerOf(sender), TIMEOUT).run(); + + verify(sender).close(true); + } + + @Test + public void shouldFlushOnADaemonThreadSoItCanNeverKeepTheJvmAlive() throws Exception { + final AtomicBoolean daemon = new AtomicBoolean(false); + Sender sender = mock(Sender.class, (Answer) invocation -> { + if ("close".equals(invocation.getMethod().getName())) { + daemon.set(Thread.currentThread().isDaemon()); + } + return null; + }); + + new SenderShutdownHook(providerOf(sender), TIMEOUT).run(); + + assertThat(daemon.get(), is(true)); + } + + /** + * The sender is resolved when the hook runs, not when it is registered, so replacing the + * configuration after registration flushes the sender actually in use. + */ + @Test + public void shouldFlushTheSenderInForceWhenTheHookRuns() throws Exception { + Sender original = mock(Sender.class); + final Sender replacement = mock(Sender.class); + final AtomicBoolean replaced = new AtomicBoolean(false); + + Provider provider = () -> replaced.get() ? replacement : null; + Thread hook = new SenderShutdownHook(provider, TIMEOUT); + + replaced.set(true); + hook.run(); + + verify(replacement).close(true); + verify(original, org.mockito.Mockito.never()).close(true); + } + + @Test + public void shouldDoNothingWhenThereIsNoSender() { + Provider provider = () -> null; + + new SenderShutdownHook(provider, TIMEOUT).run(); + // No exception: a notifier configured without a sender has nothing to flush. + } + + private Provider providerOf(final Sender sender) { + return () -> sender; + } +} From c25209a2bff83dea62fa47140ccd56fb9b323691 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 21 Sep 2026 03:13:32 -0300 Subject: [PATCH 2/3] fix(android): disable the shutdown flush hook --- .../src/main/java/com/rollbar/android/Rollbar.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java index 2200fbdf..ddd70e46 100644 --- a/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java +++ b/rollbar-android/src/main/java/com/rollbar/android/Rollbar.java @@ -486,7 +486,11 @@ public Rollbar(Context context, String accessToken, String environment, .notifier(new NotifierProvider(context)) .environment(environment == null ? DEFAULT_ENVIRONMENT : environment) .sender(sender) - .handleUncaughtErrors(false); // Use the global handler, not the default per thread one. + .handleUncaughtErrors(false) // Use the global handler, not the default per thread one. + // The DiskQueue above already persists payloads before the process dies, and they are + // transmitted on the next app start. Flushing at shutdown would attempt blocking network + // I/O while the app is being torn down, with nothing to gain. + .flushOnShutdown(false); Config config; if (configProvider != null) { From 6050a31abc659b8c410512dd8defbdfa5b93ab53 Mon Sep 17 00:00:00 2001 From: buongarzoni Date: Mon, 21 Sep 2026 03:20:42 -0300 Subject: [PATCH 3/3] test(java): make the late sender lookup test able to fail --- .../com/rollbar/notifier/shutdown/SenderShutdownHookTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java b/rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java index e24d323d..bfca3ee7 100644 --- a/rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java +++ b/rollbar-java/src/test/java/com/rollbar/notifier/shutdown/SenderShutdownHookTest.java @@ -104,7 +104,7 @@ public void shouldFlushTheSenderInForceWhenTheHookRuns() throws Exception { final Sender replacement = mock(Sender.class); final AtomicBoolean replaced = new AtomicBoolean(false); - Provider provider = () -> replaced.get() ? replacement : null; + Provider provider = () -> replaced.get() ? replacement : original; Thread hook = new SenderShutdownHook(provider, TIMEOUT); replaced.set(true);