Skip to content

Commit 69dad8a

Browse files
authored
Merge pull request #5184 from getsentry/feat/cache-tracing-retrieve
feat(spring7): [Cache Tracing 8] Add retrieve() overrides for reactive/async cache support
2 parents 7381353 + 41872ad commit 69dad8a

File tree

3 files changed

+242
-0
lines changed

3 files changed

+242
-0
lines changed

sentry-spring-7/api/sentry-spring-7.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ public final class io/sentry/spring7/cache/SentryCacheWrapper : org/springframew
129129
public fun invalidate ()Z
130130
public fun put (Ljava/lang/Object;Ljava/lang/Object;)V
131131
public fun putIfAbsent (Ljava/lang/Object;Ljava/lang/Object;)Lorg/springframework/cache/Cache$ValueWrapper;
132+
public fun retrieve (Ljava/lang/Object;)Ljava/util/concurrent/CompletableFuture;
133+
public fun retrieve (Ljava/lang/Object;Ljava/util/function/Supplier;)Ljava/util/concurrent/CompletableFuture;
132134
}
133135

134136
public abstract interface annotation class io/sentry/spring7/checkin/SentryCheckIn : java/lang/annotation/Annotation {

sentry-spring-7/src/main/java/io/sentry/spring7/cache/SentryCacheWrapper.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import io.sentry.SpanStatus;
88
import java.util.Arrays;
99
import java.util.concurrent.Callable;
10+
import java.util.concurrent.CompletableFuture;
1011
import java.util.concurrent.atomic.AtomicBoolean;
12+
import java.util.function.Supplier;
1113
import org.jetbrains.annotations.ApiStatus;
1214
import org.jetbrains.annotations.NotNull;
1315
import org.jetbrains.annotations.Nullable;
@@ -104,6 +106,76 @@ public SentryCacheWrapper(final @NotNull Cache delegate, final @NotNull IScopes
104106
}
105107
}
106108

109+
@Override
110+
public @Nullable CompletableFuture<?> retrieve(final @NotNull Object key) {
111+
final ISpan span = startSpan("cache.get", key);
112+
if (span == null) {
113+
return delegate.retrieve(key);
114+
}
115+
final CompletableFuture<?> result;
116+
try {
117+
result = delegate.retrieve(key);
118+
} catch (Throwable e) {
119+
span.setStatus(SpanStatus.INTERNAL_ERROR);
120+
span.setThrowable(e);
121+
span.finish();
122+
throw e;
123+
}
124+
if (result == null) {
125+
span.setData(SpanDataConvention.CACHE_HIT_KEY, false);
126+
span.setStatus(SpanStatus.OK);
127+
span.finish();
128+
return null;
129+
}
130+
return result.whenComplete(
131+
(value, throwable) -> {
132+
if (throwable != null) {
133+
span.setStatus(SpanStatus.INTERNAL_ERROR);
134+
span.setThrowable(throwable);
135+
} else {
136+
span.setData(SpanDataConvention.CACHE_HIT_KEY, value != null);
137+
span.setStatus(SpanStatus.OK);
138+
}
139+
span.finish();
140+
});
141+
}
142+
143+
@Override
144+
public <T> CompletableFuture<T> retrieve(
145+
final @NotNull Object key, final @NotNull Supplier<CompletableFuture<T>> valueLoader) {
146+
final ISpan span = startSpan("cache.get", key);
147+
if (span == null) {
148+
return delegate.retrieve(key, valueLoader);
149+
}
150+
final AtomicBoolean loaderInvoked = new AtomicBoolean(false);
151+
final CompletableFuture<T> result;
152+
try {
153+
result =
154+
delegate.retrieve(
155+
key,
156+
() -> {
157+
loaderInvoked.set(true);
158+
return valueLoader.get();
159+
});
160+
} catch (Throwable e) {
161+
span.setStatus(SpanStatus.INTERNAL_ERROR);
162+
span.setThrowable(e);
163+
span.finish();
164+
throw e;
165+
}
166+
return result.whenComplete(
167+
(value, throwable) -> {
168+
if (throwable != null) {
169+
span.setStatus(SpanStatus.INTERNAL_ERROR);
170+
span.setThrowable(throwable);
171+
} else {
172+
span.setData(SpanDataConvention.CACHE_HIT_KEY, !loaderInvoked.get());
173+
span.setStatus(SpanStatus.OK);
174+
}
175+
span.finish();
176+
});
177+
}
178+
107179
@Override
108180
public void put(final @NotNull Object key, final @Nullable Object value) {
109181
final ISpan span = startSpan("cache.put", key);

sentry-spring-7/src/test/kotlin/io/sentry/spring7/cache/SentryCacheWrapperTest.kt

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import io.sentry.SpanDataConvention
77
import io.sentry.SpanStatus
88
import io.sentry.TransactionContext
99
import java.util.concurrent.Callable
10+
import java.util.concurrent.CompletableFuture
11+
import java.util.function.Supplier
1012
import kotlin.test.BeforeTest
1113
import kotlin.test.Test
1214
import kotlin.test.assertEquals
@@ -137,6 +139,172 @@ class SentryCacheWrapperTest {
137139
assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
138140
}
139141

142+
// -- retrieve(Object key) --
143+
144+
@Test
145+
fun `retrieve creates span with cache hit true when future resolves with value`() {
146+
val tx = createTransaction()
147+
val wrapper = SentryCacheWrapper(delegate, scopes)
148+
whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture("value"))
149+
150+
val result = wrapper.retrieve("myKey")
151+
152+
assertEquals("value", result!!.get())
153+
assertEquals(1, tx.spans.size)
154+
val span = tx.spans.first()
155+
assertEquals("cache.get", span.operation)
156+
assertEquals("myKey", span.description)
157+
assertEquals(SpanStatus.OK, span.status)
158+
assertEquals(true, span.getData(SpanDataConvention.CACHE_HIT_KEY))
159+
assertTrue(span.isFinished)
160+
}
161+
162+
@Test
163+
fun `retrieve creates span with cache hit false when future resolves with null`() {
164+
val tx = createTransaction()
165+
val wrapper = SentryCacheWrapper(delegate, scopes)
166+
whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture(null))
167+
168+
val result = wrapper.retrieve("myKey")
169+
170+
assertNull(result!!.get())
171+
assertEquals(1, tx.spans.size)
172+
assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
173+
assertTrue(tx.spans.first().isFinished)
174+
}
175+
176+
@Test
177+
fun `retrieve creates span with cache hit false when delegate returns null`() {
178+
val tx = createTransaction()
179+
val wrapper = SentryCacheWrapper(delegate, scopes)
180+
whenever(delegate.retrieve("myKey")).thenReturn(null)
181+
182+
val result = wrapper.retrieve("myKey")
183+
184+
assertNull(result)
185+
assertEquals(1, tx.spans.size)
186+
val span = tx.spans.first()
187+
assertEquals(false, span.getData(SpanDataConvention.CACHE_HIT_KEY))
188+
assertEquals(SpanStatus.OK, span.status)
189+
assertTrue(span.isFinished)
190+
}
191+
192+
@Test
193+
fun `retrieve sets error status when future completes exceptionally`() {
194+
val tx = createTransaction()
195+
val wrapper = SentryCacheWrapper(delegate, scopes)
196+
val exception = RuntimeException("async cache error")
197+
whenever(delegate.retrieve("myKey"))
198+
.thenReturn(CompletableFuture<Any>().also { it.completeExceptionally(exception) })
199+
200+
val result = wrapper.retrieve("myKey")
201+
202+
assertFailsWith<Exception> { result!!.get() }
203+
assertEquals(1, tx.spans.size)
204+
val span = tx.spans.first()
205+
assertEquals(SpanStatus.INTERNAL_ERROR, span.status)
206+
assertEquals(exception, span.throwable)
207+
assertTrue(span.isFinished)
208+
}
209+
210+
@Test
211+
fun `retrieve sets error status when delegate throws synchronously`() {
212+
val tx = createTransaction()
213+
val wrapper = SentryCacheWrapper(delegate, scopes)
214+
val exception = RuntimeException("sync error")
215+
whenever(delegate.retrieve("myKey")).thenThrow(exception)
216+
217+
assertFailsWith<RuntimeException> { wrapper.retrieve("myKey") }
218+
219+
assertEquals(1, tx.spans.size)
220+
val span = tx.spans.first()
221+
assertEquals(SpanStatus.INTERNAL_ERROR, span.status)
222+
assertEquals(exception, span.throwable)
223+
assertTrue(span.isFinished)
224+
}
225+
226+
@Test
227+
fun `retrieve does not create span when tracing is disabled`() {
228+
options.isEnableCacheTracing = false
229+
val tx = createTransaction()
230+
val wrapper = SentryCacheWrapper(delegate, scopes)
231+
whenever(delegate.retrieve("myKey")).thenReturn(CompletableFuture.completedFuture("value"))
232+
233+
wrapper.retrieve("myKey")
234+
235+
verify(delegate).retrieve("myKey")
236+
assertEquals(0, tx.spans.size)
237+
}
238+
239+
// -- retrieve(Object key, Supplier<CompletableFuture<T>>) --
240+
241+
@Test
242+
fun `retrieve with loader creates span with cache hit true when loader not invoked`() {
243+
val tx = createTransaction()
244+
val wrapper = SentryCacheWrapper(delegate, scopes)
245+
// Simulate cache hit: delegate returns value without invoking the loader
246+
whenever(delegate.retrieve(eq("myKey"), any<Supplier<CompletableFuture<String>>>()))
247+
.thenReturn(CompletableFuture.completedFuture("cached"))
248+
249+
val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") }
250+
251+
assertEquals("cached", result.get())
252+
assertEquals(1, tx.spans.size)
253+
assertEquals(true, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
254+
assertTrue(tx.spans.first().isFinished)
255+
}
256+
257+
@Test
258+
fun `retrieve with loader creates span with cache hit false when loader invoked`() {
259+
val tx = createTransaction()
260+
val wrapper = SentryCacheWrapper(delegate, scopes)
261+
// Simulate cache miss: delegate invokes the loader supplier
262+
whenever(delegate.retrieve(eq("myKey"), any<Supplier<CompletableFuture<String>>>()))
263+
.thenAnswer { invocation ->
264+
val loader = invocation.getArgument<Supplier<CompletableFuture<String>>>(1)
265+
loader.get()
266+
}
267+
268+
val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") }
269+
270+
assertEquals("loaded", result.get())
271+
assertEquals(1, tx.spans.size)
272+
assertEquals(false, tx.spans.first().getData(SpanDataConvention.CACHE_HIT_KEY))
273+
assertTrue(tx.spans.first().isFinished)
274+
}
275+
276+
@Test
277+
fun `retrieve with loader sets error status when future completes exceptionally`() {
278+
val tx = createTransaction()
279+
val wrapper = SentryCacheWrapper(delegate, scopes)
280+
val exception = RuntimeException("async loader error")
281+
whenever(delegate.retrieve(eq("myKey"), any<Supplier<CompletableFuture<String>>>()))
282+
.thenReturn(CompletableFuture<String>().also { it.completeExceptionally(exception) })
283+
284+
val result = wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") }
285+
286+
assertFailsWith<Exception> { result.get() }
287+
assertEquals(1, tx.spans.size)
288+
val span = tx.spans.first()
289+
assertEquals(SpanStatus.INTERNAL_ERROR, span.status)
290+
assertEquals(exception, span.throwable)
291+
assertTrue(span.isFinished)
292+
}
293+
294+
@Test
295+
fun `retrieve with loader does not create span when tracing is disabled`() {
296+
options.isEnableCacheTracing = false
297+
val tx = createTransaction()
298+
val wrapper = SentryCacheWrapper(delegate, scopes)
299+
whenever(delegate.retrieve(eq("myKey"), any<Supplier<CompletableFuture<String>>>()))
300+
.thenReturn(CompletableFuture.completedFuture("cached"))
301+
302+
wrapper.retrieve("myKey") { CompletableFuture.completedFuture("loaded") }
303+
304+
verify(delegate).retrieve(eq("myKey"), any<Supplier<CompletableFuture<String>>>())
305+
assertEquals(0, tx.spans.size)
306+
}
307+
140308
// -- put --
141309

142310
@Test

0 commit comments

Comments
 (0)