Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
96 changes: 96 additions & 0 deletions rollbar-java/src/main/java/com/rollbar/notifier/Rollbar.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +29,10 @@ public class Rollbar extends RollbarBase<Void, Config> {

private static volatile Rollbar notifier;

private final Object shutdownHookLock = new Object();

private Thread shutdownHook;

/**
* Constructor.
*
Expand All @@ -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.
*
* <p>
* 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.
* </p>
*/
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<Sender> {
@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.
*
Expand Down Expand Up @@ -652,6 +745,9 @@ public void log(ThrowableWrapper error, Map<String, Object> 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);
}

Expand Down
41 changes: 41 additions & 0 deletions rollbar-java/src/main/java/com/rollbar/notifier/config/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,45 @@ public interface Config extends CommonConfig {
* @return the proxy.
*/
Proxy proxy();

/**
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*
* @return true to flush buffered payloads on JVM shutdown, false otherwise.
*/
default boolean flushOnShutdown() {
return true;
}

/**
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*
* @return the shutdown flush timeout in milliseconds.
*/
default long shutdownTimeoutMillis() {
return 2000L;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -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();
}

Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -496,6 +515,50 @@ public ConfigBuilder compressPayload(boolean compress) {
return this;
}

/**
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*
* @param flushOnShutdown true to flush buffered payloads on JVM shutdown.
* @return the builder instance.
*/
public ConfigBuilder flushOnShutdown(boolean flushOnShutdown) {
this.flushOnShutdown = flushOnShutdown;
return this;
}

/**
* <p>
* 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.
* </p>
* <p>
* 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.
* </p>
*
* @param shutdownTimeoutMillis the shutdown flush timeout in milliseconds.
* @return the builder instance.
*/
public ConfigBuilder shutdownTimeoutMillis(long shutdownTimeoutMillis) {
this.shutdownTimeoutMillis = shutdownTimeoutMillis;
return this;
}

/**
* <p>
* Maximum Telemetry events sent in a payload, only for the default TelemetryEventTracker, if
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading