Skip to content

Commit 084562d

Browse files
authored
Wrap Runnable for usage with Sentry (#4332)
* Wrap runnable * changelog * test * api
1 parent 32564c8 commit 084562d

File tree

4 files changed

+105
-0
lines changed

4 files changed

+105
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Add `SentryWrapper.wrapRunnable` to wrap `Runnable` for use with Sentry ([#4332](https://github.com/getsentry/sentry-java/pull/4332))
8+
59
### Fixes
610

711
- Fix TTFD measurement when API called too early ([#4297](https://github.com/getsentry/sentry-java/pull/4297))

sentry/api/sentry.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3541,6 +3541,7 @@ public final class io/sentry/SentryUUID {
35413541
public final class io/sentry/SentryWrapper {
35423542
public fun <init> ()V
35433543
public static fun wrapCallable (Ljava/util/concurrent/Callable;)Ljava/util/concurrent/Callable;
3544+
public static fun wrapRunnable (Ljava/lang/Runnable;)Ljava/lang/Runnable;
35443545
public static fun wrapSupplier (Ljava/util/function/Supplier;)Ljava/util/function/Supplier;
35453546
}
35463547

sentry/src/main/java/io/sentry/SentryWrapper.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,23 @@ public static <U> Supplier<U> wrapSupplier(final @NotNull Supplier<U> supplier)
5757
}
5858
};
5959
}
60+
61+
/**
62+
* Helper method to wrap {@link Runnable}
63+
*
64+
* <p>Forks current and isolation scope before execution and restores previous state afterwards.
65+
* This prevents reused threads (e.g. from thread-pools) from getting an incorrect state.
66+
*
67+
* @param runnable - the {@link Runnable} to be wrapped
68+
* @return the wrapped {@link Runnable}
69+
*/
70+
public static Runnable wrapRunnable(final @NotNull Runnable runnable) {
71+
final IScopes newScopes = Sentry.forkedScopes("SentryWrapper.wrapRunnable");
72+
73+
return () -> {
74+
try (ISentryLifecycleToken ignore = newScopes.makeCurrent()) {
75+
runnable.run();
76+
}
77+
};
78+
}
6079
}

sentry/src/test/java/io/sentry/SentryWrapperTest.kt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,4 +195,85 @@ class SentryWrapperTest {
195195
assertEquals(threadedScopes, Sentry.getCurrentScopes())
196196
}.get()
197197
}
198+
199+
@Test
200+
fun `wrapped runnable isolates Scopes`() {
201+
val capturedEvents = mutableListOf<SentryEvent>()
202+
203+
initForTest {
204+
it.dsn = dsn
205+
it.beforeSend = SentryOptions.BeforeSendCallback { event, hint ->
206+
capturedEvents.add(event)
207+
event
208+
}
209+
}
210+
211+
Sentry.addBreadcrumb("MyOriginalBreadcrumbBefore")
212+
Sentry.captureMessage("OriginalMessageBefore")
213+
println(Thread.currentThread().name)
214+
215+
val future1 = executor.submit(
216+
SentryWrapper.wrapRunnable {
217+
Thread.sleep(20)
218+
Sentry.addBreadcrumb("MyClonedBreadcrumb")
219+
Sentry.captureMessage("ClonedMessage")
220+
"Result 1"
221+
}
222+
)
223+
224+
val future2 = executor.submit(
225+
SentryWrapper.wrapRunnable {
226+
Thread.sleep(10)
227+
Sentry.addBreadcrumb("MyClonedBreadcrumb2")
228+
Sentry.captureMessage("ClonedMessage2")
229+
"Result 2"
230+
}
231+
)
232+
233+
Sentry.addBreadcrumb("MyOriginalBreadcrumb")
234+
Sentry.captureMessage("OriginalMessage")
235+
236+
future1.get()
237+
future2.get()
238+
239+
val mainEvent = capturedEvents.firstOrNull { it.message?.formatted == "OriginalMessage" }
240+
val clonedEvent = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage" }
241+
val clonedEvent2 = capturedEvents.firstOrNull { it.message?.formatted == "ClonedMessage2" }
242+
243+
assertEquals(2, mainEvent?.breadcrumbs?.size)
244+
assertEquals(2, clonedEvent?.breadcrumbs?.size)
245+
assertEquals(2, clonedEvent2?.breadcrumbs?.size)
246+
}
247+
248+
@Test
249+
fun `scopes is reset to state within the thread after isolated runnable is done`() {
250+
initForTest {
251+
it.dsn = dsn
252+
}
253+
254+
val mainScopes = Sentry.getCurrentScopes()
255+
val threadedScopes = Sentry.getCurrentScopes().forkedCurrentScope("test")
256+
257+
executor.submit {
258+
Sentry.setCurrentScopes(threadedScopes)
259+
}.get()
260+
261+
assertEquals(mainScopes, Sentry.getCurrentScopes())
262+
263+
val runnableFuture =
264+
executor.submit(
265+
SentryWrapper.wrapRunnable {
266+
assertNotEquals(mainScopes, Sentry.getCurrentScopes())
267+
assertNotEquals(threadedScopes, Sentry.getCurrentScopes())
268+
"Result 1"
269+
}
270+
)
271+
272+
runnableFuture.get()
273+
274+
executor.submit {
275+
assertNotEquals(mainScopes, Sentry.getCurrentScopes())
276+
assertEquals(threadedScopes, Sentry.getCurrentScopes())
277+
}.get()
278+
}
198279
}

0 commit comments

Comments
 (0)