From 4d663e3be6bee3cc12a7e786751dd5e7a51d1506 Mon Sep 17 00:00:00 2001 From: Igor Melnichenko Date: Tue, 4 Aug 2026 21:41:28 +0300 Subject: [PATCH] Do not poison the auth state when a token wait is interrupted BackgroundIdentity.unwrap returned null after an InterruptedException. SyncLogin.validate feeds that value straight into updateState, which CAS-ed the shared state reference to null and then threw a NullPointerException from next.init(). The reference stayed null, so every following getToken() call - on any thread - failed with a NullPointerException and authentication for the whole transport was permanently dead after a single interrupt. Throw instead of returning null, keeping the interrupt flag set. The state stays in SyncLogin, so the next getToken() re-checks the same login future and recovers on its own. updateState now also rejects a null state explicitly, so a future regression fails loudly at the call site. Co-Authored-By: Claude Opus 5 (1M context) --- .../ydb/core/auth/BackgroundIdentity.java | 5 +- .../ydb/core/auth/BackgroundIdentityTest.java | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 core/src/test/java/tech/ydb/core/auth/BackgroundIdentityTest.java diff --git a/core/src/main/java/tech/ydb/core/auth/BackgroundIdentity.java b/core/src/main/java/tech/ydb/core/auth/BackgroundIdentity.java index 882ef7ea9..048c1ef35 100644 --- a/core/src/main/java/tech/ydb/core/auth/BackgroundIdentity.java +++ b/core/src/main/java/tech/ydb/core/auth/BackgroundIdentity.java @@ -2,6 +2,7 @@ import java.time.Clock; import java.time.Instant; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -72,6 +73,7 @@ public void close() { } private State updateState(State current, State next) { + Objects.requireNonNull(next, "next state cannot be null"); if (state.compareAndSet(current, next)) { next.init(); } @@ -92,7 +94,8 @@ private T unwrap(CompletableFuture future) { } catch (InterruptedException ex) { logger.error("updating of authentication token was interrupted", ex); Thread.currentThread().interrupt(); - return null; + // returning null here would poison the state reference and break every following getToken() + throw new RuntimeException("authentication update was interrupted", ex); } } diff --git a/core/src/test/java/tech/ydb/core/auth/BackgroundIdentityTest.java b/core/src/test/java/tech/ydb/core/auth/BackgroundIdentityTest.java new file mode 100644 index 000000000..4a9ed0221 --- /dev/null +++ b/core/src/test/java/tech/ydb/core/auth/BackgroundIdentityTest.java @@ -0,0 +1,68 @@ +package tech.ydb.core.auth; + +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Assert; +import org.junit.Test; + +public class BackgroundIdentityTest { + private final Instant now = Instant.EPOCH; + private final Clock clock = Clock.fixed(now, ZoneId.of("UTC")); + + private static class MockedRpc implements BackgroundIdentity.Rpc { + private final CompletableFuture tokenFuture = new CompletableFuture<>(); + + @Override + public CompletableFuture getTokenAsync() { + return tokenFuture; + } + + @Override + public int getTimeoutSeconds() { + return 60; + } + } + + @Test(timeout = 30_000) + public void interruptedGetTokenDoesNotBreakIdentity() throws InterruptedException { + MockedRpc rpc = new MockedRpc(); + BackgroundIdentity identity = new BackgroundIdentity(clock, rpc); + + AtomicReference caught = new AtomicReference<>(); + Thread reader = new Thread(() -> { + try { + identity.getToken(); + } catch (Throwable th) { + caught.set(th); + } + }); + + // the login never answers, so the reader blocks in the sync state and gets interrupted there + reader.start(); + // the reader may not have reached the await yet, interrupting early is handled the same way + reader.interrupt(); + reader.join(); + + Assert.assertNotNull("interrupted getToken must report a failure", caught.get()); + Assert.assertFalse( + "interrupt must not surface as a NullPointerException", + caught.get() instanceof NullPointerException + ); + + // the identity must recover once the login completes + rpc.getTokenAsync().complete( + new BackgroundIdentity.Rpc.Token( + "token-value", + now.plus(Duration.ofHours(2)), + now.plus(Duration.ofHours(1)) + ) + ); + + Assert.assertEquals("token-value", identity.getToken()); + } +}