|
41 | 41 | import java.util.List; |
42 | 42 | import java.util.Locale; |
43 | 43 | import java.util.Map; |
| 44 | +import java.util.concurrent.RejectedExecutionException; |
44 | 45 | import java.util.concurrent.atomic.AtomicReference; |
45 | 46 | import org.jetbrains.annotations.ApiStatus; |
46 | 47 | import org.jetbrains.annotations.NotNull; |
@@ -244,48 +245,72 @@ public static SentryId captureEnvelopeNonTerminating(final @NotNull byte[] envel |
244 | 245 | return null; |
245 | 246 | } |
246 | 247 |
|
| 248 | + final @NotNull EnvelopeEventState eventState; |
247 | 249 | try { |
248 | | - final @NotNull ISerializer serializer = options.getSerializer(); |
249 | | - final @NotNull EnvelopeEventState eventState = eventStateOf(envelope, serializer); |
250 | | - |
251 | | - if (eventState != EnvelopeEventState.NONE) { |
252 | | - updateSessionNonTerminating(eventState == EnvelopeEventState.UNHANDLED); |
253 | | - } |
254 | | - |
255 | | - return scopes.captureEnvelope(envelope); |
| 250 | + eventState = eventStateOf(envelope, options.getSerializer()); |
256 | 251 | } catch (Exception e) { |
257 | | - options.getLogger().log(SentryLevel.ERROR, "Failed to capture envelope", e); |
| 252 | + // getEvent reads through a Callable, whose call() declares Exception |
| 253 | + options.getLogger().log(SentryLevel.ERROR, "Failed to inspect envelope events", e); |
| 254 | + return null; |
258 | 255 | } |
259 | | - return null; |
| 256 | + |
| 257 | + if (eventState != EnvelopeEventState.NONE) { |
| 258 | + updateSessionNonTerminating(eventState == EnvelopeEventState.UNHANDLED); |
| 259 | + } |
| 260 | + |
| 261 | + return scopes.captureEnvelope(envelope); |
260 | 262 | } |
261 | 263 |
|
262 | 264 | /** |
263 | | - * Mutates and persists the current session for a non-terminating hybrid error. The write stays |
264 | | - * inside {@code withSession} so the mutation and the persist are one critical section. Persisting |
265 | | - * outside it lets a concurrent caller's older snapshot land last and drop the unhandled marker. |
| 265 | + * Flags and persists the current session for a non-terminating hybrid error. |
266 | 266 | * |
267 | | - * @param crashed {@code true} if the error was unhandled ({@code mechanism.handled=false}) |
| 267 | + * @param unhandled {@code true} if the error was unhandled ({@code mechanism.handled=false}) |
268 | 268 | */ |
269 | | - private static void updateSessionNonTerminating(final boolean crashed) { |
| 269 | + private static void updateSessionNonTerminating(final boolean unhandled) { |
270 | 270 | final @NotNull IScopes scopes = ScopesAdapter.getInstance(); |
271 | 271 | final @NotNull SentryOptions options = scopes.getOptions(); |
272 | 272 | scopes.configureScope( |
273 | | - scope -> { |
274 | | - scope.withSession( |
275 | | - session -> { |
276 | | - if (session != null) { |
277 | | - final boolean updated = |
278 | | - crashed |
| 273 | + scope -> |
| 274 | + scope.withSession( |
| 275 | + session -> { |
| 276 | + if (session == null) { |
| 277 | + options.getLogger().log(INFO, "Session is null on updateSessionNonTerminating"); |
| 278 | + return; |
| 279 | + } |
| 280 | + if (session.isTerminated()) { |
| 281 | + options |
| 282 | + .getLogger() |
| 283 | + .log(INFO, "Session already terminated, not recording the error."); |
| 284 | + return; |
| 285 | + } |
| 286 | + final boolean recorded = |
| 287 | + unhandled |
279 | 288 | ? session.recordNonTerminatingUnhandledError() |
280 | 289 | : session.update(null, null, true, null); |
281 | | - if (updated && options.getEnvelopeDiskCache() instanceof EnvelopeCache) { |
282 | | - ((EnvelopeCache) options.getEnvelopeDiskCache()).persistCurrentSession(session); |
| 290 | + if (recorded) { |
| 291 | + schedulePersistSession(options, session.clone()); |
283 | 292 | } |
284 | | - } else { |
285 | | - options.getLogger().log(INFO, "Session is null on updateSessionNonTerminating"); |
286 | | - } |
287 | | - }); |
288 | | - }); |
| 293 | + })); |
| 294 | + } |
| 295 | + |
| 296 | + /** |
| 297 | + * This function is mostly called from Flutter where capturing envelope is run in a background |
| 298 | + * thread. |
| 299 | + * |
| 300 | + * <p>Nonetheless we should run this in the same executor service as the deleteCurrentSessionFile |
| 301 | + * function for consistency. |
| 302 | + */ |
| 303 | + private static void schedulePersistSession( |
| 304 | + final @NotNull SentryOptions options, final @NotNull Session session) { |
| 305 | + if (!(options.getEnvelopeDiskCache() instanceof EnvelopeCache)) { |
| 306 | + return; |
| 307 | + } |
| 308 | + final @NotNull EnvelopeCache cache = (EnvelopeCache) options.getEnvelopeDiskCache(); |
| 309 | + try { |
| 310 | + options.getExecutorService().submit(() -> cache.persistCurrentSession(session)); |
| 311 | + } catch (RejectedExecutionException e) { |
| 312 | + options.getLogger().log(WARNING, "Submission of session persisting rejected.", e); |
| 313 | + } |
289 | 314 | } |
290 | 315 |
|
291 | 316 | /** What the events inside an envelope amount to, from the session's point of view. */ |
@@ -320,11 +345,6 @@ private enum EnvelopeEventState { |
320 | 345 | return errored ? EnvelopeEventState.ERRORED : EnvelopeEventState.NONE; |
321 | 346 | } |
322 | 347 |
|
323 | | - /** |
324 | | - * Reads an envelope from the given bytes. {@link io.sentry.IEnvelopeReader#read(InputStream)} |
325 | | - * declares {@link IOException} and additionally rejects malformed payloads with an unchecked |
326 | | - * {@link IllegalArgumentException}. |
327 | | - */ |
328 | 348 | private static @Nullable SentryEnvelope readEnvelope( |
329 | 349 | final @NotNull SentryOptions options, final @NotNull byte[] envelopeData) { |
330 | 350 | try (final InputStream envelopeInputStream = new ByteArrayInputStream(envelopeData)) { |
@@ -427,22 +447,26 @@ private static Session updateSession( |
427 | 447 | final @NotNull AtomicReference<Session> sessionRef = new AtomicReference<>(); |
428 | 448 | scopes.configureScope( |
429 | 449 | scope -> { |
430 | | - final @Nullable Session session = scope.getSession(); |
431 | | - if (session != null) { |
432 | | - final boolean updated = session.update(status, null, crashedOrErrored, null); |
433 | | - // if we have an uncaughtExceptionHint we can end the session. |
434 | | - if (updated) { |
435 | | - if (session.getStatus() == Session.State.Crashed) { |
436 | | - session.end(); |
437 | | - // Session needs to be removed from the scope, otherwise it will be send twice |
438 | | - // standalone and with the crash event |
439 | | - scope.clearSession(); |
440 | | - } |
441 | | - sessionRef.set(session); |
442 | | - } |
443 | | - } else { |
444 | | - options.getLogger().log(INFO, "Session is null on updateSession"); |
445 | | - } |
| 450 | + scope.withSession( |
| 451 | + session -> { |
| 452 | + if (session != null) { |
| 453 | + final boolean updated = session.update(status, null, crashedOrErrored, null); |
| 454 | + // if we have an uncaughtExceptionHint we can end the session. |
| 455 | + if (updated) { |
| 456 | + if (session.getStatus() == Session.State.Crashed) { |
| 457 | + session.end(); |
| 458 | + // Session needs to be removed from the scope, otherwise it will be send twice |
| 459 | + // standalone and with the crash event |
| 460 | + scope.clearSession(); |
| 461 | + } |
| 462 | + // fromSession serializes lazily, on the transport thread, so handing out the |
| 463 | + // live session would race a later mutation |
| 464 | + sessionRef.set(session.clone()); |
| 465 | + } |
| 466 | + } else { |
| 467 | + options.getLogger().log(INFO, "Session is null on updateSession"); |
| 468 | + } |
| 469 | + }); |
446 | 470 | }); |
447 | 471 | return sessionRef.get(); |
448 | 472 | } |
|
0 commit comments