-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix ClientOptions race condition during async connection #3891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,6 +97,8 @@ public class RedisClient extends AbstractRedisClient { | |
|
|
||
| private static final RedisURI EMPTY_URI = new RedisURI(); | ||
|
|
||
| private final ThreadLocal<ClientOptions> clientOptionsThreadLocal = new ThreadLocal<>(); | ||
|
|
||
| private final RedisURI redisURI; | ||
|
|
||
| protected RedisClient(ClientResources clientResources, RedisURI redisURI) { | ||
|
|
@@ -276,36 +278,42 @@ private <K, V> ConnectionFuture<StatefulRedisConnection<K, V>> connectStandalone | |
|
|
||
| logger.debug("Trying to get a Redis connection for: {}", redisURI); | ||
|
|
||
| DefaultEndpoint endpoint = createEndpoint(); | ||
| RedisChannelWriter writer = endpoint; | ||
| ClientOptions clientOptions = getOptions(); | ||
| clientOptionsThreadLocal.set(clientOptions); | ||
| try { | ||
| DefaultEndpoint endpoint = createEndpoint(); | ||
| RedisChannelWriter writer = endpoint; | ||
|
|
||
| if (CommandExpiryWriter.isSupported(getOptions())) { | ||
| writer = CommandExpiryWriter.buildCommandExpiryWriter(writer, getOptions(), getResources()); | ||
| } | ||
| if (CommandExpiryWriter.isSupported(clientOptions)) { | ||
| writer = CommandExpiryWriter.buildCommandExpiryWriter(writer, clientOptions, getResources()); | ||
| } | ||
|
|
||
| if (CommandListenerWriter.isSupported(getCommandListeners())) { | ||
| writer = new CommandListenerWriter(writer, getCommandListeners()); | ||
| } | ||
| if (CommandListenerWriter.isSupported(getCommandListeners())) { | ||
| writer = new CommandListenerWriter(writer, getCommandListeners()); | ||
| } | ||
|
|
||
| StatefulRedisConnectionImpl<K, V> connection = newStatefulRedisConnection(writer, endpoint, codec, timeout); | ||
| StatefulRedisConnectionImpl<K, V> connection = newStatefulRedisConnection(writer, endpoint, codec, timeout, | ||
| clientOptions); | ||
|
Comment on lines
+295
to
+296
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For clients that subclass Useful? React with 👍 / 👎. |
||
|
|
||
| ClientOptions clientOptions = getOptions(); | ||
| ConnectionFuture<StatefulRedisConnection<K, V>> future = connectStatefulAsync(connection, endpoint, redisURI, | ||
| () -> new CommandHandler(clientOptions, getResources(), endpoint), false); | ||
| ConnectionFuture<StatefulRedisConnection<K, V>> future = connectStatefulAsync(connection, endpoint, redisURI, | ||
| () -> new CommandHandler(clientOptions, getResources(), endpoint), false, clientOptions); | ||
|
|
||
| future.whenComplete((channelHandler, throwable) -> { | ||
| future.whenComplete((channelHandler, throwable) -> { | ||
|
|
||
| if (throwable != null) { | ||
| connection.closeAsync(); | ||
| } | ||
| }); | ||
| if (throwable != null) { | ||
| connection.closeAsync(); | ||
| } | ||
| }); | ||
|
|
||
| return future; | ||
| return future; | ||
| } finally { | ||
| clientOptionsThreadLocal.remove(); | ||
| } | ||
| } | ||
|
|
||
| @SuppressWarnings("unchecked") | ||
| private <K, V, S> ConnectionFuture<S> connectStatefulAsync(StatefulRedisConnectionImpl<K, V> connection, Endpoint endpoint, | ||
| RedisURI redisURI, Supplier<CommandHandler> commandHandlerSupplier, Boolean isPubSub) { | ||
| RedisURI redisURI, Supplier<CommandHandler> commandHandlerSupplier, Boolean isPubSub, ClientOptions clientOptions) { | ||
|
|
||
| ConnectionBuilder connectionBuilder; | ||
| if (redisURI.isSsl()) { | ||
|
|
@@ -319,15 +327,15 @@ private <K, V, S> ConnectionFuture<S> connectStatefulAsync(StatefulRedisConnecti | |
| ConnectionState state = connection.getConnectionState(); | ||
| state.apply(redisURI); | ||
| state.setDb(redisURI.getDatabase()); | ||
| connection | ||
| .setAuthenticationHandler(createHandler(connection, redisURI.getCredentialsProvider(), isPubSub, getOptions())); | ||
| connection.setAuthenticationHandler( | ||
| createHandler(connection, redisURI.getCredentialsProvider(), isPubSub, clientOptions)); | ||
| connectionBuilder.connection(connection); | ||
| connectionBuilder.clientOptions(getOptions()); | ||
| connectionBuilder.clientOptions(clientOptions); | ||
| connectionBuilder.clientResources(getResources()); | ||
| connectionBuilder.commandHandler(commandHandlerSupplier).endpoint(endpoint); | ||
|
|
||
| connectionBuilder(getSocketAddressSupplier(redisURI), connectionBuilder, connection.getConnectionEvents(), redisURI); | ||
| connectionBuilder.connectionInitializer(createHandshake(state)); | ||
| connectionBuilder.connectionInitializer(createHandshake(state, clientOptions)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a subclass overrides the existing protected Useful? React with 👍 / 👎. |
||
|
|
||
| ConnectionFuture<RedisChannelHandler<K, V>> future = initializeChannelAsync(connectionBuilder); | ||
|
|
||
|
|
@@ -410,29 +418,35 @@ private <K, V> ConnectionFuture<StatefulRedisPubSubConnection<K, V>> connectPubS | |
| assertNotNull(codec); | ||
| checkValidRedisURI(redisURI); | ||
|
|
||
| PubSubEndpoint<K, V> endpoint = createPubSubEndpoint(); | ||
| RedisChannelWriter writer = endpoint; | ||
| ClientOptions clientOptions = getOptions(); | ||
| clientOptionsThreadLocal.set(clientOptions); | ||
| try { | ||
| PubSubEndpoint<K, V> endpoint = createPubSubEndpoint(); | ||
| RedisChannelWriter writer = endpoint; | ||
|
|
||
| if (CommandExpiryWriter.isSupported(getOptions())) { | ||
| writer = CommandExpiryWriter.buildCommandExpiryWriter(writer, getOptions(), getResources()); | ||
| } | ||
| if (CommandExpiryWriter.isSupported(clientOptions)) { | ||
| writer = CommandExpiryWriter.buildCommandExpiryWriter(writer, clientOptions, getResources()); | ||
| } | ||
|
|
||
| if (CommandListenerWriter.isSupported(getCommandListeners())) { | ||
| writer = new CommandListenerWriter(writer, getCommandListeners()); | ||
| } | ||
| if (CommandListenerWriter.isSupported(getCommandListeners())) { | ||
| writer = new CommandListenerWriter(writer, getCommandListeners()); | ||
| } | ||
|
|
||
| StatefulRedisPubSubConnectionImpl<K, V> connection = newStatefulRedisPubSubConnection(endpoint, writer, codec, timeout); | ||
| StatefulRedisPubSubConnectionImpl<K, V> connection = newStatefulRedisPubSubConnection(endpoint, writer, codec, | ||
| timeout, clientOptions); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Subclass factory hooks bypassedHigh Severity Async connect paths now call the new five-argument Additional Locations (2)Reviewed by Cursor Bugbot for commit 49e24f1. Configure here.
Comment on lines
+435
to
+436
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
With subclasses that override only the existing 4-argument Useful? React with 👍 / 👎. |
||
|
|
||
| ClientOptions clientOptions = getOptions(); | ||
| ConnectionFuture<StatefulRedisPubSubConnection<K, V>> future = connectStatefulAsync(connection, endpoint, redisURI, | ||
| () -> new PubSubCommandHandler<>(clientOptions, getResources(), codec, endpoint), true); | ||
| ConnectionFuture<StatefulRedisPubSubConnection<K, V>> future = connectStatefulAsync(connection, endpoint, redisURI, | ||
| () -> new PubSubCommandHandler<>(clientOptions, getResources(), codec, endpoint), true, clientOptions); | ||
|
|
||
| return future.whenComplete((conn, throwable) -> { | ||
| return future.whenComplete((conn, throwable) -> { | ||
|
|
||
| if (throwable != null) { | ||
| conn.close(); | ||
| } | ||
| }); | ||
| if (throwable != null) { | ||
| conn.close(); | ||
| } | ||
| }); | ||
| } finally { | ||
| clientOptionsThreadLocal.remove(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -567,52 +581,59 @@ private <K, V> CompletableFuture<StatefulRedisSentinelConnection<K, V>> connectS | |
| private <K, V> ConnectionFuture<StatefulRedisSentinelConnection<K, V>> doConnectSentinelAsync(RedisCodec<K, V> codec, | ||
| RedisURI redisURI, Duration timeout, ConnectionMetadata metadata) { | ||
|
|
||
| ConnectionBuilder connectionBuilder; | ||
| if (redisURI.isSsl()) { | ||
| SslConnectionBuilder sslConnectionBuilder = SslConnectionBuilder.sslConnectionBuilder(); | ||
| sslConnectionBuilder.ssl(redisURI); | ||
| connectionBuilder = sslConnectionBuilder; | ||
| } else { | ||
| connectionBuilder = ConnectionBuilder.connectionBuilder(); | ||
| } | ||
| connectionBuilder.clientOptions(ClientOptions.copyOf(getOptions())); | ||
| connectionBuilder.clientResources(getResources()); | ||
| ClientOptions clientOptions = getOptions(); | ||
| clientOptionsThreadLocal.set(clientOptions); | ||
| try { | ||
| ConnectionBuilder connectionBuilder; | ||
| if (redisURI.isSsl()) { | ||
| SslConnectionBuilder sslConnectionBuilder = SslConnectionBuilder.sslConnectionBuilder(); | ||
| sslConnectionBuilder.ssl(redisURI); | ||
| connectionBuilder = sslConnectionBuilder; | ||
| } else { | ||
| connectionBuilder = ConnectionBuilder.connectionBuilder(); | ||
| } | ||
| connectionBuilder.clientOptions(ClientOptions.copyOf(clientOptions)); | ||
| connectionBuilder.clientResources(getResources()); | ||
|
|
||
| DefaultEndpoint endpoint = createEndpoint(); | ||
| RedisChannelWriter writer = endpoint; | ||
| DefaultEndpoint endpoint = createEndpoint(); | ||
| RedisChannelWriter writer = endpoint; | ||
|
|
||
| if (CommandExpiryWriter.isSupported(getOptions())) { | ||
| writer = CommandExpiryWriter.buildCommandExpiryWriter(writer, getOptions(), getResources()); | ||
| } | ||
| if (CommandExpiryWriter.isSupported(clientOptions)) { | ||
| writer = CommandExpiryWriter.buildCommandExpiryWriter(writer, clientOptions, getResources()); | ||
| } | ||
|
|
||
| if (CommandListenerWriter.isSupported(getCommandListeners())) { | ||
| writer = new CommandListenerWriter(writer, getCommandListeners()); | ||
| } | ||
| if (CommandListenerWriter.isSupported(getCommandListeners())) { | ||
| writer = new CommandListenerWriter(writer, getCommandListeners()); | ||
| } | ||
|
|
||
| StatefulRedisSentinelConnectionImpl<K, V> connection = newStatefulRedisSentinelConnection(writer, codec, timeout); | ||
| ConnectionState state = connection.getConnectionState(); | ||
| StatefulRedisSentinelConnectionImpl<K, V> connection = newStatefulRedisSentinelConnection(writer, codec, timeout, | ||
| clientOptions); | ||
|
Comment on lines
+609
to
+610
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For subclasses that override the existing 3-argument Useful? React with 👍 / 👎. |
||
| ConnectionState state = connection.getConnectionState(); | ||
|
|
||
| state.apply(redisURI); | ||
| state.apply(metadata); | ||
| state.apply(redisURI); | ||
| state.apply(metadata); | ||
|
|
||
| connectionBuilder.connectionInitializer(createHandshake(state)); | ||
| connectionBuilder.connectionInitializer(createHandshake(state, clientOptions)); | ||
|
|
||
| logger.debug("Connecting to Redis Sentinel, address: " + redisURI); | ||
| logger.debug("Connecting to Redis Sentinel, address: " + redisURI); | ||
|
|
||
| ClientOptions clientOptions = getOptions(); | ||
| connectionBuilder.endpoint(endpoint).commandHandler(() -> new CommandHandler(clientOptions, getResources(), endpoint)) | ||
| .connection(connection); | ||
| connectionBuilder(getSocketAddressSupplier(redisURI), connectionBuilder, connection.getConnectionEvents(), redisURI); | ||
| connectionBuilder.endpoint(endpoint) | ||
| .commandHandler(() -> new CommandHandler(clientOptions, getResources(), endpoint)).connection(connection); | ||
| connectionBuilder(getSocketAddressSupplier(redisURI), connectionBuilder, connection.getConnectionEvents(), | ||
| redisURI); | ||
|
|
||
| ConnectionFuture<?> sync = initializeChannelAsync(connectionBuilder); | ||
| ConnectionFuture<?> sync = initializeChannelAsync(connectionBuilder); | ||
|
|
||
| return sync.thenApply(ignore -> (StatefulRedisSentinelConnection<K, V>) connection).whenComplete((ignore, e) -> { | ||
| return sync.thenApply(ignore -> (StatefulRedisSentinelConnection<K, V>) connection).whenComplete((ignore, e) -> { | ||
|
|
||
| if (e != null) { | ||
| logger.warn("Cannot connect Redis Sentinel at " + redisURI + ": " + e); | ||
| connection.closeAsync(); | ||
| } | ||
| }); | ||
| if (e != null) { | ||
| logger.warn("Cannot connect Redis Sentinel at " + redisURI + ": " + e); | ||
| connection.closeAsync(); | ||
| } | ||
| }); | ||
| } finally { | ||
| clientOptionsThreadLocal.remove(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -645,6 +666,25 @@ public void setOptions(ClientOptions clientOptions) { | |
| */ | ||
| protected <K, V> StatefulRedisPubSubConnectionImpl<K, V> newStatefulRedisPubSubConnection(PubSubEndpoint<K, V> endpoint, | ||
| RedisChannelWriter channelWriter, RedisCodec<K, V> codec, Duration timeout) { | ||
| return newStatefulRedisPubSubConnection(endpoint, channelWriter, codec, timeout, getOptions()); | ||
| } | ||
|
|
||
| /** | ||
| * Create a new instance of {@link StatefulRedisPubSubConnectionImpl} or a subclass. | ||
| * <p> | ||
| * Subclasses of {@link RedisClient} may override that method. | ||
| * | ||
| * @param endpoint the endpoint | ||
| * @param channelWriter the channel writer | ||
| * @param codec codec | ||
| * @param timeout default timeout | ||
| * @param clientOptions the client options | ||
| * @param <K> Key-Type | ||
| * @param <V> Value Type | ||
| * @return new instance of StatefulRedisPubSubConnectionImpl | ||
| */ | ||
| protected <K, V> StatefulRedisPubSubConnectionImpl<K, V> newStatefulRedisPubSubConnection(PubSubEndpoint<K, V> endpoint, | ||
| RedisChannelWriter channelWriter, RedisCodec<K, V> codec, Duration timeout, ClientOptions clientOptions) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PubSub options parameter unusedMedium Severity The new Additional Locations (1)Reviewed by Cursor Bugbot for commit 49e24f1. Configure here.
Comment on lines
+686
to
+687
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This newly added protected overload is an extension point in AGENTS.md reference: AGENTS.md:L145-L146 Useful? React with 👍 / 👎. |
||
| return new StatefulRedisPubSubConnectionImpl<>(endpoint, channelWriter, codec, timeout); | ||
| } | ||
|
|
||
|
|
@@ -662,7 +702,29 @@ protected <K, V> StatefulRedisPubSubConnectionImpl<K, V> newStatefulRedisPubSubC | |
| */ | ||
| protected <K, V> StatefulRedisSentinelConnectionImpl<K, V> newStatefulRedisSentinelConnection( | ||
| RedisChannelWriter channelWriter, RedisCodec<K, V> codec, Duration timeout) { | ||
| return new StatefulRedisSentinelConnectionImpl<>(channelWriter, codec, timeout, getOptions().getJsonParser()); | ||
| ClientOptions clientOptions = clientOptionsThreadLocal.get(); | ||
| if (clientOptions == null) { | ||
| clientOptions = getOptions(); | ||
| } | ||
| return newStatefulRedisSentinelConnection(channelWriter, codec, timeout, clientOptions); | ||
| } | ||
|
|
||
| /** | ||
| * Create a new instance of {@link StatefulRedisSentinelConnectionImpl} or a subclass. | ||
| * <p> | ||
| * Subclasses of {@link RedisClient} may override that method. | ||
| * | ||
| * @param channelWriter the channel writer | ||
| * @param codec codec | ||
| * @param timeout default timeout | ||
| * @param clientOptions the client options | ||
| * @param <K> Key-Type | ||
| * @param <V> Value Type | ||
| * @return new instance of StatefulRedisSentinelConnectionImpl | ||
| */ | ||
| protected <K, V> StatefulRedisSentinelConnectionImpl<K, V> newStatefulRedisSentinelConnection( | ||
| RedisChannelWriter channelWriter, RedisCodec<K, V> codec, Duration timeout, ClientOptions clientOptions) { | ||
| return new StatefulRedisSentinelConnectionImpl<>(channelWriter, codec, timeout, clientOptions.getJsonParser()); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -680,7 +742,30 @@ protected <K, V> StatefulRedisSentinelConnectionImpl<K, V> newStatefulRedisSenti | |
| */ | ||
| protected <K, V> StatefulRedisConnectionImpl<K, V> newStatefulRedisConnection(RedisChannelWriter channelWriter, | ||
| PushHandler pushHandler, RedisCodec<K, V> codec, Duration timeout) { | ||
| return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, getOptions().getJsonParser()); | ||
| ClientOptions clientOptions = clientOptionsThreadLocal.get(); | ||
| if (clientOptions == null) { | ||
| clientOptions = getOptions(); | ||
| } | ||
| return newStatefulRedisConnection(channelWriter, pushHandler, codec, timeout, clientOptions); | ||
| } | ||
|
|
||
| /** | ||
| * Create a new instance of {@link StatefulRedisConnectionImpl} or a subclass. | ||
| * <p> | ||
| * Subclasses of {@link RedisClient} may override that method. | ||
| * | ||
| * @param channelWriter the channel writer | ||
| * @param pushHandler the handler for push notifications | ||
| * @param codec codec | ||
| * @param timeout default timeout | ||
| * @param clientOptions the client options | ||
| * @param <K> Key-Type | ||
| * @param <V> Value Type | ||
| * @return new instance of StatefulRedisConnectionImpl | ||
| */ | ||
| protected <K, V> StatefulRedisConnectionImpl<K, V> newStatefulRedisConnection(RedisChannelWriter channelWriter, | ||
| PushHandler pushHandler, RedisCodec<K, V> codec, Duration timeout, ClientOptions clientOptions) { | ||
| return new StatefulRedisConnectionImpl<>(channelWriter, pushHandler, codec, timeout, clientOptions.getJsonParser()); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Endpoint still races on optionsHigh Severity
Additional Locations (2)Reviewed by Cursor Bugbot for commit 49e24f1. Configure here. |
||
| } | ||
|
|
||
| /** | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although this method captures
ClientOptionsbefore entering the setup block, the endpoint is still built throughcreateEndpoint(), whose default implementation callsgetOptions()again. IfsetOptions()runs in that window, theDefaultEndpointcan pick up newer request-queue/reconnect settings while the writer, handler, auth, and handshake use the captured options, so a single new connection still observes mixed options; pass the captured options into the endpoint factory, and do the same for the Pub/Sub endpoint factory, to close the race.Useful? React with 👍 / 👎.