diff --git a/components/context/src/main/java/datadog/context/ContextScope.java b/components/context/src/main/java/datadog/context/ContextScope.java index 7788a077615..d21d32a5146 100644 --- a/components/context/src/main/java/datadog/context/ContextScope.java +++ b/components/context/src/main/java/datadog/context/ContextScope.java @@ -8,4 +8,19 @@ public interface ContextScope extends AutoCloseable { /** Detaches the context from the execution unit. */ @Override void close(); + + /** + * Closes the given scope, tolerating a {@code null} scope. + * + *

Useful in advice that attaches a scope on enter and closes it on exit: if the enter advice + * throws before assigning the scope, the exit advice would otherwise NPE on a null scope, masking + * the original failure. + * + * @param scope the scope to close; can be {@code null}. + */ + static void close(ContextScope scope) { + if (scope != null) { + scope.close(); + } + } } diff --git a/components/context/src/test/java/datadog/context/ContextScopeTest.java b/components/context/src/test/java/datadog/context/ContextScopeTest.java new file mode 100644 index 00000000000..00299a57b23 --- /dev/null +++ b/components/context/src/test/java/datadog/context/ContextScopeTest.java @@ -0,0 +1,35 @@ +package datadog.context; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ContextScopeTest { + + private static final class RecordingScope implements ContextScope { + private boolean closed; + + @Override + public Context context() { + return null; + } + + @Override + public void close() { + closed = true; + } + } + + @Test + void staticCloseToleratesNullScope() { + assertDoesNotThrow(() -> ContextScope.close(null)); + } + + @Test + void staticCloseClosesNonNullScope() { + RecordingScope scope = new RecordingScope(); + ContextScope.close(scope); + assertTrue(scope.closed); + } +}