Skip to content

Commit

Permalink
Add SentryWrapper for Callable and Supplier Interface (#2720)
Browse files Browse the repository at this point in the history
* introduce sentry wrapper class with static methods to wrap callable and supplier interface

* add changelog entry

* Format code

* add tests

* api dump

* merge changelog

* CR

* format

---------

Co-authored-by: Sentry Github Bot <bot+github-bot@sentry.io>
  • Loading branch information
lbloder and getsentry-bot committed May 26, 2023
1 parent e5c250b commit 16cd2b6
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 0 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- Add SentryWrapper for Callable and Supplier Interface ([#2720](https://github.com/getsentry/sentry-java/pull/2720))

## 6.20.0

### Features
Expand Down
6 changes: 6 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,12 @@ public final class io/sentry/SentryTracer : io/sentry/ITransaction {
public fun updateEndDate (Lio/sentry/SentryDate;)Z
}

public final class io/sentry/SentryWrapper {
public fun <init> ()V
public static fun wrapCallable (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Callable;
public static fun wrapSupplier (Ljava/util/function/Supplier;)Ljava/util/function/Supplier;
}

public final class io/sentry/Session : io/sentry/JsonSerializable, io/sentry/JsonUnknown {
public fun <init> (Lio/sentry/Session$State;Ljava/util/Date;Ljava/util/Date;ILjava/lang/String;Ljava/util/UUID;Ljava/lang/Boolean;Ljava/lang/Long;Ljava/lang/Double;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Lio/sentry/protocol/User;Ljava/lang/String;Ljava/lang/String;)V
Expand Down
67 changes: 67 additions & 0 deletions sentry/src/main/java/io/sentry/SentryWrapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.sentry;

import java.util.concurrent.Callable;
import java.util.function.Supplier;
import org.jetbrains.annotations.NotNull;

/**
* Helper class that provides wrappers around:
*
* <ul>
* <li>{@link Callable}
* <li>{@link Supplier}
* </ul>
*
* that clones the Hub before execution and restores it afterwards. This prevents reused threads
* (e.g. from thread-pools) from getting an incorrect state.
*/
public final class SentryWrapper {

/**
* Helper method to wrap {@link Callable}
*
* <p>Clones the Hub before execution and restores it afterwards. This prevents reused threads
* (e.g. from thread-pools) from getting an incorrect state.
*
* @param callable - the {@link Callable} to be wrapped
* @return the wrapped {@link Callable}
* @param <U> - the result type of the {@link Callable}
*/
public static <U> Callable<U> wrapCallable(final @NotNull Callable<U> callable) {
final IHub oldState = Sentry.getCurrentHub();
final IHub newHub = oldState.clone();

return () -> {
Sentry.setCurrentHub(newHub);
try {
return callable.call();
} finally {
Sentry.setCurrentHub(oldState);
}
};
}

/**
* Helper method to wrap {@link Supplier}
*
* <p>Clones the Hub before execution and restores it afterwards. This prevents reused threads
* (e.g. from thread-pools) from getting an incorrect state.
*
* @param supplier - the {@link Supplier} to be wrapped
* @return the wrapped {@link Supplier}
* @param <U> - the result type of the {@link Supplier}
*/
public static <U> Supplier<U> wrapSupplier(final @NotNull Supplier<U> supplier) {
final IHub oldState = Sentry.getCurrentHub();
final IHub newHub = oldState.clone();

return () -> {
Sentry.setCurrentHub(newHub);
try {
return supplier.get();
} finally {
Sentry.setCurrentHub(oldState);
}
};
}
}
118 changes: 118 additions & 0 deletions sentry/src/test/java/io/sentry/SentryWrapperTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package io.sentry

import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executors
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals

class SentryWrapperTest {

private val dsn = "http://key@localhost/proj"
private val executor = Executors.newSingleThreadExecutor()

@BeforeTest
@AfterTest
fun beforeTest() {
Sentry.close()
SentryCrashLastRunState.getInstance().reset()
}

@Test
fun `wrapped supply async isolates Hubs`() {
val capturedEvents = mutableListOf<SentryEvent>()

Sentry.init {
it.dsn = dsn
it.beforeSend = SentryOptions.BeforeSendCallback { event, hint ->
capturedEvents.add(event)
event
}
}

Sentry.addBreadcrumb("MyOriginalBreadcrumbBefore")
Sentry.captureMessage("OriginalMessageBefore")

val callableFuture =
CompletableFuture.supplyAsync(
SentryWrapper.wrapSupplier {
Sentry.addBreadcrumb("MyClonedBreadcrumb")
Sentry.captureMessage("ClonedMessage")
"Result 1"
},
executor
)

val callableFuture2 =
CompletableFuture.supplyAsync(
SentryWrapper.wrapSupplier {
Sentry.addBreadcrumb("MyClonedBreadcrumb2")
Sentry.captureMessage("ClonedMessage2")
"Result 2"
},
executor
)

Sentry.addBreadcrumb("MyOriginalBreadcrumb")
Sentry.captureMessage("OriginalMessage")

callableFuture.join()
callableFuture2.join()

val mainEvent = capturedEvents.firstOrNull { it.message?.formatted == "OriginalMessage" }
val clonedEvent = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage" }
val clonedEvent2 = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage2" }

assertEquals(2, mainEvent?.breadcrumbs?.size)
assertEquals(2, clonedEvent?.breadcrumbs?.size)
assertEquals(2, clonedEvent2?.breadcrumbs?.size)
}

@Test
fun `wrapped callable isolates Hubs`() {
val capturedEvents = mutableListOf<SentryEvent>()

Sentry.init {
it.dsn = dsn
it.beforeSend = SentryOptions.BeforeSendCallback { event, hint ->
capturedEvents.add(event)
event
}
}

Sentry.addBreadcrumb("MyOriginalBreadcrumbBefore")
Sentry.captureMessage("OriginalMessageBefore")
println(Thread.currentThread().name)

val future1 = executor.submit(
SentryWrapper.wrapCallable {
Sentry.addBreadcrumb("MyClonedBreadcrumb")
Sentry.captureMessage("ClonedMessage")
"Result 1"
}
)

val future2 = executor.submit(
SentryWrapper.wrapCallable {
Sentry.addBreadcrumb("MyClonedBreadcrumb2")
Sentry.captureMessage("ClonedMessage2")
"Result 2"
}
)

Sentry.addBreadcrumb("MyOriginalBreadcrumb")
Sentry.captureMessage("OriginalMessage")

future1.get()
future2.get()

val mainEvent = capturedEvents.firstOrNull { it.message?.formatted == "OriginalMessage" }
val clonedEvent = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage" }
val clonedEvent2 = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage2" }

assertEquals(2, mainEvent?.breadcrumbs?.size)
assertEquals(2, clonedEvent?.breadcrumbs?.size)
assertEquals(2, clonedEvent2?.breadcrumbs?.size)
}
}

0 comments on commit 16cd2b6

Please sign in to comment.