Skip to content

Commit 47a2359

Browse files
committed
GH-491: Fix NPE in the MetricsRetryListener when label is null
Fixes: #491 The Micrometer tag cannot be with `null` value. When `RetryCallback` does not provide a proper `getLabel()` implementation, the `MetricsRetryListener` fails with a `NullPointerException` * Fix `MetricsRetryListener.close()` to fallback to the `callback.getClass().getName()` if `callback.getLabel() == null` * Cover behavior in the new `RetryMetricsTests.labelFallbackToClassName()`
1 parent 936e720 commit 47a2359

File tree

2 files changed

+28
-4
lines changed

2 files changed

+28
-4
lines changed

src/main/java/org/springframework/retry/support/MetricsRetryListener.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.Collections;
2020
import java.util.IdentityHashMap;
2121
import java.util.Map;
22+
import java.util.Objects;
2223
import java.util.function.Function;
2324

2425
import io.micrometer.core.instrument.MeterRegistry;
@@ -43,7 +44,8 @@
4344
* <p>
4445
* The registered {@value #TIMER_NAME} {@link Timer} has these tags by default:
4546
* <ul>
46-
* <li>{@code name} - {@link RetryCallback#getLabel()}</li>
47+
* <li>{@code name} - {@link RetryCallback#getLabel()} if not null, otherwise
48+
* {@code RetryCallback#getClass().getName()}</li>
4749
* <li>{@code retry.count} - the number of attempts - 1; essentially the successful first
4850
* call means no counts</li>
4951
* <li>{@code exception} - the thrown back to the caller (after all the retry attempts)
@@ -113,7 +115,8 @@ public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T
113115
Assert.state(sample != null,
114116
() -> String.format("No 'Timer.Sample' registered for '%s'. Was the 'open()' called?", context));
115117

116-
Tags retryTags = Tags.of("name", callback.getLabel())
118+
String label = Objects.requireNonNullElse(callback.getLabel(), callback.getClass().getName());
119+
Tags retryTags = Tags.of("name", label)
117120
.and("retry.count", "" + context.getRetryCount())
118121
.and(this.customTags)
119122
.and(this.customTagsProvider.apply(context))

src/test/java/org/springframework/retry/support/RetryMetricsTests.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,9 +26,12 @@
2626
import org.springframework.beans.factory.annotation.Autowired;
2727
import org.springframework.context.annotation.Bean;
2828
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.retry.RetryCallback;
30+
import org.springframework.retry.RetryContext;
2931
import org.springframework.retry.RetryException;
3032
import org.springframework.retry.annotation.EnableRetry;
3133
import org.springframework.retry.annotation.Retryable;
34+
import org.springframework.retry.policy.SimpleRetryPolicy;
3235
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
3336
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
3437

@@ -49,6 +52,9 @@ public class RetryMetricsTests {
4952
@Autowired
5053
Service service;
5154

55+
@Autowired
56+
MetricsRetryListener metricsRetryListener;
57+
5258
@Test
5359
void metricsAreCollectedForRetryable() {
5460
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
@@ -87,6 +93,21 @@ void metricsAreCollectedForRetryable() {
8793
executor.destroy();
8894
}
8995

96+
@Test
97+
void labelFallbackToClassName() {
98+
SimpleRetryPolicy simpleRetryPolicy = new SimpleRetryPolicy();
99+
RetryContext retryContext = simpleRetryPolicy.open(null);
100+
RetryCallback<Object, Throwable> retryCallback = context -> null;
101+
this.metricsRetryListener.open(retryContext, retryCallback);
102+
this.metricsRetryListener.close(retryContext, retryCallback, null);
103+
104+
assertThat(this.meterRegistry.get(MetricsRetryListener.TIMER_NAME)
105+
.tags(Tags.of("name", retryCallback.getClass().getName(), "retry.count", "0", "exception", "none"))
106+
.timer()
107+
.count()).isEqualTo(1);
108+
109+
}
110+
90111
@Configuration(proxyBeanMethods = false)
91112
@EnableRetry
92113
public static class TestConfiguration {

0 commit comments

Comments
 (0)