Skip to content

Commit 7f455ec

Browse files
authored
io.lettuce.core.RedisCommandExecutionException: NOAUTH Authentication required on CLIENT and READONLY command (#3035)
Using custom credentials provider can delay providing of credentials. In this case applyPostHandshake and applyConnectionMetadata got executed before handshake and lead to NOAUTH error in the log for CLIENT command.
1 parent 114755d commit 7f455ec

File tree

3 files changed

+85
-16
lines changed

3 files changed

+85
-16
lines changed

pom.xml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
</developers>
4646

4747
<properties>
48+
<awaitility.version>4.2.2</awaitility.version>
4849
<assertj-core.version>3.25.3</assertj-core.version>
4950
<cdi-api.version>2.0.SP1</cdi-api.version>
5051
<brave.version>5.13.11</brave.version>
@@ -103,7 +104,12 @@
103104

104105
<dependencyManagement>
105106
<dependencies>
106-
107+
<dependency>
108+
<groupId>org.awaitility</groupId>
109+
<artifactId>awaitility</artifactId>
110+
<version>${awaitility.version}</version>
111+
<scope>test</scope>
112+
</dependency>
107113
<dependency>
108114
<groupId>io.netty</groupId>
109115
<artifactId>netty-bom</artifactId>
@@ -327,6 +333,12 @@
327333
<scope>test</scope>
328334
</dependency>
329335

336+
<dependency>
337+
<groupId>org.awaitility</groupId>
338+
<artifactId>awaitility</artifactId>
339+
<scope>test</scope>
340+
</dependency>
341+
330342
<dependency>
331343
<groupId>org.hdrhistogram</groupId>
332344
<artifactId>HdrHistogram</artifactId>

src/main/java/io/lettuce/core/RedisHandshake.java

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,14 @@ public CompletionStage<Void> initialize(Channel channel) {
101101
new RedisConnectionException("Protocol version" + this.requestedProtocolVersion + " not supported"));
102102
}
103103

104-
// post-handshake commands, whose execution failures would cause the connection to be considered
105-
// unsuccessfully established
106-
CompletableFuture<Void> postHandshake = applyPostHandshake(channel);
107-
108-
// post-handshake commands, executed in a 'fire and forget' manner, to avoid having to react to different
109-
// implementations or versions of the server runtime, and whose execution result (whether a success or a
110-
// failure ) should not alter the outcome of the connection attempt
111-
CompletableFuture<Void> connectionMetadata = applyConnectionMetadata(channel).handle((result, error) -> {
112-
if (error != null) {
113-
LOG.debug("Error applying connection metadata", error);
114-
}
115-
return null;
116-
});
117-
118-
return handshake.thenCompose(ignore -> postHandshake).thenCompose(ignore -> connectionMetadata);
104+
return handshake
105+
// post-handshake commands, whose execution failures would cause the connection to be considered
106+
// unsuccessfully established
107+
.thenCompose(ignore -> applyPostHandshake(channel))
108+
// post-handshake commands, executed in a 'fire and forget' manner, to avoid having to react to different
109+
// implementations or versions of the server runtime, and whose execution result (whether a success or a
110+
// failure ) should not alter the outcome of the connection attempt
111+
.thenCompose(ignore -> applyConnectionMetadataSafely(channel));
119112
}
120113

121114
private CompletionStage<?> tryHandshakeResp3(Channel channel) {
@@ -271,6 +264,15 @@ private CompletableFuture<Void> applyPostHandshake(Channel channel) {
271264
return dispatch(channel, postHandshake);
272265
}
273266

267+
private CompletionStage<Void> applyConnectionMetadataSafely(Channel channel) {
268+
return applyConnectionMetadata(channel).handle((result, error) -> {
269+
if (error != null) {
270+
LOG.debug("Error applying connection metadata", error);
271+
}
272+
return null;
273+
});
274+
}
275+
274276
private CompletableFuture<Void> applyConnectionMetadata(Channel channel) {
275277

276278
List<AsyncCommand<?, ?, ?>> postHandshake = new ArrayList<>();

src/test/java/io/lettuce/core/RedisHandshakeUnitTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
package io.lettuce.core;
22

33
import static io.lettuce.TestTags.UNIT_TEST;
4+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
45
import static org.assertj.core.api.Assertions.*;
56

67
import java.nio.ByteBuffer;
78
import java.util.List;
89
import java.util.Map;
910
import java.util.concurrent.CompletionStage;
1011

12+
import org.awaitility.Awaitility;
1113
import org.junit.jupiter.api.Tag;
1214
import org.junit.jupiter.api.Test;
1315

1416
import io.lettuce.core.output.CommandOutput;
1517
import io.lettuce.core.protocol.AsyncCommand;
1618
import io.lettuce.core.protocol.ProtocolVersion;
1719
import io.netty.channel.embedded.EmbeddedChannel;
20+
import reactor.core.publisher.Mono;
21+
import reactor.core.publisher.Sinks;
1822

1923
/**
2024
* Unit tests for {@link RedisHandshake}.
@@ -106,6 +110,42 @@ void handshakeFireAndForgetPostHandshake() {
106110
assertThat(handshakeInit.toCompletableFuture().isCompletedExceptionally()).isFalse();
107111
}
108112

113+
@Test
114+
void handshakeDelayedCredentialProvider() {
115+
116+
DelayedRedisCredentialsProvider cp = new DelayedRedisCredentialsProvider();
117+
// RedisCredentialsProvider cp = () -> Mono.just(RedisCredentials.just("foo",
118+
// "bar")).delayElement(Duration.ofMillis(3));
119+
EmbeddedChannel channel = new EmbeddedChannel(true, false);
120+
121+
ConnectionMetadata connectionMetdata = new ConnectionMetadata();
122+
connectionMetdata.setLibraryName("library-name");
123+
connectionMetdata.setLibraryVersion("library-version");
124+
125+
ConnectionState state = new ConnectionState();
126+
state.setCredentialsProvider(cp);
127+
state.apply(connectionMetdata);
128+
RedisHandshake handshake = new RedisHandshake(null, false, state);
129+
CompletionStage<Void> handshakeInit = handshake.initialize(channel);
130+
cp.completeCredentials(RedisCredentials.just("foo", "bar"));
131+
132+
Awaitility.await().atMost(50, MILLISECONDS) // Wait up to 5 seconds
133+
.pollInterval(5, MILLISECONDS) // Poll every 50 milliseconds
134+
.until(() -> !channel.outboundMessages().isEmpty());
135+
136+
AsyncCommand<String, String, Map<String, String>> hello = channel.readOutbound();
137+
helloResponse(hello.getOutput());
138+
hello.complete();
139+
140+
List<AsyncCommand<String, String, Map<String, String>>> postHandshake = channel.readOutbound();
141+
postHandshake.get(0).getOutput().setError(ERR_UNKNOWN_COMMAND);
142+
postHandshake.get(0).completeExceptionally(new RedisException(ERR_UNKNOWN_COMMAND));
143+
postHandshake.get(0).complete();
144+
145+
assertThat(postHandshake.size()).isEqualTo(2);
146+
assertThat(handshakeInit.toCompletableFuture().isCompletedExceptionally()).isFalse();
147+
}
148+
109149
@Test
110150
void shouldParseVersionWithCharacters() {
111151

@@ -136,4 +176,19 @@ private static void helloResponse(CommandOutput<String, String, Map<String, Stri
136176
output.set(ByteBuffer.wrap("1.2.3".getBytes()));
137177
}
138178

179+
static class DelayedRedisCredentialsProvider implements RedisCredentialsProvider {
180+
181+
private final Sinks.One<RedisCredentials> credentialsSink = Sinks.one();
182+
183+
@Override
184+
public Mono<RedisCredentials> resolveCredentials() {
185+
return credentialsSink.asMono();
186+
}
187+
188+
public void completeCredentials(RedisCredentials credentials) {
189+
credentialsSink.tryEmitValue(credentials);
190+
}
191+
192+
}
193+
139194
}

0 commit comments

Comments
 (0)