diff --git a/CHANGELOG.md b/CHANGELOG.md index c6f830a3476..7f2b745f91a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Improvements +- Handle `RejectedExecutionException` everywhere ([#4747](https://github.com/getsentry/sentry-java/pull/4747)) - Mark `SentryEnvelope` as not internal ([#4748](https://github.com/getsentry/sentry-java/pull/4748)) ## 8.22.0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 9409c29b0d8..1e37916aaee 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -4,18 +4,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; -import io.sentry.DateUtils; -import io.sentry.EventProcessor; -import io.sentry.Hint; -import io.sentry.IpAddressUtils; -import io.sentry.NoOpLogger; -import io.sentry.SentryAttributeType; -import io.sentry.SentryBaseEvent; -import io.sentry.SentryEvent; -import io.sentry.SentryLevel; -import io.sentry.SentryLogEvent; -import io.sentry.SentryLogEventAttributeValue; -import io.sentry.SentryReplayEvent; +import io.sentry.*; import io.sentry.android.core.internal.util.AndroidThreadChecker; import io.sentry.android.core.performance.AppStartMetrics; import io.sentry.android.core.performance.TimeSpan; @@ -37,6 +26,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -47,7 +37,7 @@ final class DefaultAndroidEventProcessor implements EventProcessor { private final @NotNull BuildInfoProvider buildInfoProvider; private final @NotNull SentryAndroidOptions options; - private final @NotNull Future deviceInfoUtil; + private final @Nullable Future deviceInfoUtil; private final @NotNull LazyEvaluator deviceFamily = new LazyEvaluator<>(() -> ContextUtils.getFamily(NoOpLogger.getInstance())); @@ -65,9 +55,16 @@ public DefaultAndroidEventProcessor( // don't ref. to method reference, theres a bug on it // noinspection Convert2MethodRef // some device info performs disk I/O, but it's result is cached, let's pre-cache it + @Nullable Future deviceInfoUtil; final @NotNull ExecutorService executorService = Executors.newSingleThreadExecutor(); - this.deviceInfoUtil = - executorService.submit(() -> DeviceInfoUtil.getInstance(this.context, options)); + try { + deviceInfoUtil = + executorService.submit(() -> DeviceInfoUtil.getInstance(this.context, options)); + } catch (RejectedExecutionException e) { + deviceInfoUtil = null; + options.getLogger().log(SentryLevel.WARNING, "Device info caching task rejected.", e); + } + this.deviceInfoUtil = deviceInfoUtil; executorService.shutdown(); } @@ -181,12 +178,16 @@ private void setDevice( final boolean errorEvent, final boolean applyScopeData) { if (event.getContexts().getDevice() == null) { - try { - event - .getContexts() - .setDevice(deviceInfoUtil.get().collectDeviceInformation(errorEvent, applyScopeData)); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); + if (deviceInfoUtil != null) { + try { + event + .getContexts() + .setDevice(deviceInfoUtil.get().collectDeviceInformation(errorEvent, applyScopeData)); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); + } + } else { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info"); } mergeOS(event); } @@ -194,12 +195,17 @@ private void setDevice( private void mergeOS(final @NotNull SentryBaseEvent event) { final OperatingSystem currentOS = event.getContexts().getOperatingSystem(); - try { - final OperatingSystem androidOS = deviceInfoUtil.get().getOperatingSystem(); - // make Android OS the main OS using the 'os' key - event.getContexts().setOperatingSystem(androidOS); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve os system", e); + + if (deviceInfoUtil != null) { + try { + final OperatingSystem androidOS = deviceInfoUtil.get().getOperatingSystem(); + // make Android OS the main OS using the 'os' key + event.getContexts().setOperatingSystem(androidOS); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve os system", e); + } + } else { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info"); } if (currentOS != null) { @@ -284,10 +290,14 @@ private void setPackageInfo(final @NotNull SentryBaseEvent event, final @NotNull setDist(event, versionCode); @Nullable DeviceInfoUtil deviceInfoUtil = null; - try { - deviceInfoUtil = this.deviceInfoUtil.get(); - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); + if (this.deviceInfoUtil != null) { + try { + deviceInfoUtil = this.deviceInfoUtil.get(); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); + } + } else { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info"); } ContextUtils.setAppPackageInfo(packageInfo, buildInfoProvider, deviceInfoUtil, app); @@ -331,16 +341,20 @@ private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { } private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) { - try { - final ContextUtils.SideLoadedInfo sideLoadedInfo = deviceInfoUtil.get().getSideLoadedInfo(); - if (sideLoadedInfo != null) { - final @NotNull Map tags = sideLoadedInfo.asTags(); - for (Map.Entry entry : tags.entrySet()) { - event.setTag(entry.getKey(), entry.getValue()); + if (deviceInfoUtil != null) { + try { + final ContextUtils.SideLoadedInfo sideLoadedInfo = deviceInfoUtil.get().getSideLoadedInfo(); + if (sideLoadedInfo != null) { + final @NotNull Map tags = sideLoadedInfo.asTags(); + for (Map.Entry entry : tags.entrySet()) { + event.setTag(entry.getKey(), entry.getValue()); + } } + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error getting side loaded info.", e); } - } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error getting side loaded info.", e); + } else { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info"); } } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 77b9b94731c..ce11b19f738 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -5,10 +5,7 @@ import io.sentry.hints.SessionStartHint; import io.sentry.logger.ILoggerApi; import io.sentry.logger.LoggerApi; -import io.sentry.protocol.Feedback; -import io.sentry.protocol.SentryId; -import io.sentry.protocol.SentryTransaction; -import io.sentry.protocol.User; +import io.sentry.protocol.*; import io.sentry.transport.RateLimiter; import io.sentry.util.HintUtils; import io.sentry.util.Objects; @@ -16,6 +13,7 @@ import io.sentry.util.TracingUtils; import java.io.Closeable; import java.util.List; +import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -449,8 +447,18 @@ public void close(final boolean isRestarting) { getOptions().getConnectionStatusProvider().close(); final @NotNull ISentryExecutorService executorService = getOptions().getExecutorService(); if (isRestarting) { - executorService.submit( - () -> executorService.close(getOptions().getShutdownTimeoutMillis())); + try { + executorService.submit( + () -> executorService.close(getOptions().getShutdownTimeoutMillis())); + } catch (RejectedExecutionException e) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Failed to submit executor service shutdown task during restart. Shutting down synchronously.", + e); + executorService.close(getOptions().getShutdownTimeoutMillis()); + } } else { executorService.close(getOptions().getShutdownTimeoutMillis()); } diff --git a/sentry/src/main/java/io/sentry/SentryExecutorService.java b/sentry/src/main/java/io/sentry/SentryExecutorService.java index 3fe262b5538..873e4744e3c 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -54,11 +54,11 @@ public SentryExecutorService() { } @Override - public @NotNull Future submit(final @NotNull Runnable runnable) { + public @NotNull Future submit(final @NotNull Runnable runnable) + throws RejectedExecutionException { if (executorService.getQueue().size() < MAX_QUEUE_SIZE) { return executorService.submit(runnable); } - // TODO: maybe RejectedExecutionException? if (options != null) { options .getLogger() @@ -68,11 +68,11 @@ public SentryExecutorService() { } @Override - public @NotNull Future submit(final @NotNull Callable callable) { + public @NotNull Future submit(final @NotNull Callable callable) + throws RejectedExecutionException { if (executorService.getQueue().size() < MAX_QUEUE_SIZE) { return executorService.submit(callable); } - // TODO: maybe RejectedExecutionException? if (options != null) { options .getLogger() @@ -82,11 +82,11 @@ public SentryExecutorService() { } @Override - public @NotNull Future schedule(final @NotNull Runnable runnable, final long delayMillis) { + public @NotNull Future schedule(final @NotNull Runnable runnable, final long delayMillis) + throws RejectedExecutionException { if (executorService.getQueue().size() < MAX_QUEUE_SIZE) { return executorService.schedule(runnable, delayMillis, TimeUnit.MILLISECONDS); } - // TODO: maybe RejectedExecutionException? if (options != null) { options .getLogger() @@ -122,20 +122,30 @@ public boolean isClosed() { @SuppressWarnings({"FutureReturnValueIgnored"}) @Override public void prewarm() { - executorService.submit( - () -> { - try { - // schedule a bunch of dummy runnables in the future that will never execute to trigger - // queue growth and then purge the queue - for (int i = 0; i < INITIAL_QUEUE_SIZE; i++) { - final Future future = executorService.schedule(dummyRunnable, 365L, TimeUnit.DAYS); - future.cancel(true); + try { + executorService.submit( + () -> { + try { + // schedule a bunch of dummy runnables in the future that will never execute to + // trigger + // queue growth and then purge the queue + for (int i = 0; i < INITIAL_QUEUE_SIZE; i++) { + final Future future = + executorService.schedule(dummyRunnable, 365L, TimeUnit.DAYS); + future.cancel(true); + } + executorService.purge(); + } catch (RejectedExecutionException ignored) { + // ignore } - executorService.purge(); - } catch (RejectedExecutionException ignored) { - // ignore - } - }); + }); + } catch (RejectedExecutionException e) { + if (options != null) { + options + .getLogger() + .log(SentryLevel.WARNING, "Prewarm task rejected from " + executorService, e); + } + } } private static final class SentryExecutorServiceThreadFactory implements ThreadFactory { diff --git a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java index 92fa3ce0a5c..fdbfea2a3a2 100644 --- a/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java +++ b/sentry/src/main/java/io/sentry/backpressure/BackpressureMonitor.java @@ -7,6 +7,7 @@ import io.sentry.SentryOptions; import io.sentry.util.AutoClosableReentrantLock; import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -79,7 +80,13 @@ private void reschedule(final int delay) { final @NotNull ISentryExecutorService executorService = sentryOptions.getExecutorService(); if (!executorService.isClosed()) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - latestScheduledRun = executorService.schedule(this, delay); + try { + latestScheduledRun = executorService.schedule(this, delay); + } catch (RejectedExecutionException e) { + sentryOptions + .getLogger() + .log(SentryLevel.WARNING, "Backpressure monitor reschedule task rejected", e); + } } } } diff --git a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java index 3d760263f38..369f24f75de 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java @@ -15,6 +15,7 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -76,7 +77,14 @@ private void maybeSchedule(boolean forceSchedule, boolean immediately) { || latestScheduledFlush.isCancelled()) { hasScheduled = true; final int flushAfterMs = immediately ? 0 : FLUSH_AFTER_MS; - scheduledFlush = executorService.schedule(new BatchRunnable(), flushAfterMs); + try { + scheduledFlush = executorService.schedule(new BatchRunnable(), flushAfterMs); + } catch (RejectedExecutionException e) { + hasScheduled = false; + options + .getLogger() + .log(SentryLevel.WARNING, "Logs batch processor flush task rejected", e); + } } } } diff --git a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java index f8d35a6888a..e5565a279fc 100644 --- a/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java +++ b/sentry/src/main/java/io/sentry/transport/QueuedThreadPoolExecutor.java @@ -8,6 +8,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; @@ -65,7 +66,14 @@ public QueuedThreadPoolExecutor( public Future submit(final @NotNull Runnable task) { if (isSchedulingAllowed()) { unfinishedTasksCount.increment(); - return super.submit(task); + try { + return super.submit(task); + } catch (RejectedExecutionException e) { + unfinishedTasksCount.decrement(); + lastRejectTimestamp = dateProvider.now(); + logger.log(SentryLevel.WARNING, "Submit rejected by thread pool executor", e); + return new CancelledFuture<>(); + } } else { lastRejectTimestamp = dateProvider.now(); // if the thread pool is full, we don't cache it