CASSANDRA-21191: [CEP-59] Implementation of In-Band Connection Draining (Graceful Disconnect) - #4953
CASSANDRA-21191: [CEP-59] Implementation of In-Band Connection Draining (Graceful Disconnect)#4953AshenScribe wants to merge 5 commits into
Conversation
AshenScribe
left a comment
There was a problem hiding this comment.
StorageService.drain() was synchronized on the same monitor as drain(boolean). Pre-CEP-59, drain() called drain(false) directly on the same thread, so reentrancy made this safe. Now drain() hands a callback to gracefulDisconnect(...), which invokes it asynchronously (Netty close-listener or scheduler thread) — so drain(false) can run on a different thread than the one blocked in drain()'s await(). Since that thread still holds the monitor while waiting, drain(false) can never acquire it → deadlock whenever a client is still connected at drain time.
Fix: drop synchronized from drain(), use a dedicated ReentrantLock scoped only to drain() (preserves "one drain at a time"), leave drain(boolean)'s own synchronized untouched.
There was a problem hiding this comment.
Pull request overview
Implements CEP-59 graceful native-protocol disconnection for controlled Cassandra shutdowns.
Changes:
- Adds capability advertisement, event registration, and connection draining.
- Integrates draining with shutdown operations and metrics.
- Adds configuration, documentation, and tests.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
test/unit/org/apache/cassandra/service/StorageServiceTest.java |
Tests graceful-disconnect settings. |
test/unit/org/apache/cassandra/config/DatabaseDescriptorTest.java |
Tests configuration defaults and updates. |
test/unit/org/apache/cassandra/concurrent/DebuggableScheduledThreadPoolExecutorTest.java |
Propagates the new drain exception. |
test/distributed/org/apache/cassandra/distributed/test/GracefulDisconnectTest.java |
Adds distributed feature tests. |
src/java/org/apache/cassandra/transport/SimpleClient.java |
Handles graceful-disconnect events. |
src/java/org/apache/cassandra/transport/Server.java |
Tracks subscribers and stops accepting connections. |
src/java/org/apache/cassandra/transport/messages/StartupMessage.java |
Defines the capability key. |
src/java/org/apache/cassandra/transport/messages/OptionsMessage.java |
Advertises feature support. |
src/java/org/apache/cassandra/transport/InitialConnectionHandler.java |
Advertises support before negotiation. |
src/java/org/apache/cassandra/transport/Event.java |
Defines the protocol event. |
src/java/org/apache/cassandra/tools/nodetool/Drain.java |
Handles drain timeout errors. |
src/java/org/apache/cassandra/tools/NodeProbe.java |
Propagates drain timeouts. |
src/java/org/apache/cassandra/service/StorageServiceMBean.java |
Exposes settings and timeout contract. |
src/java/org/apache/cassandra/service/StorageService.java |
Orchestrates graceful shutdown. |
src/java/org/apache/cassandra/service/NativeTransportService.java |
Exposes subscribed channels. |
src/java/org/apache/cassandra/metrics/ClientMetrics.java |
Adds draining metrics. |
src/java/org/apache/cassandra/config/DatabaseDescriptor.java |
Exposes configuration accessors. |
src/java/org/apache/cassandra/config/Config.java |
Defines feature configuration. |
doc/modules/cassandra/pages/managing/operating/graceful_disconnect.adoc |
Documents operation and compatibility. |
conf/cassandra.yaml |
Documents stable configuration defaults. |
conf/cassandra_latest.yaml |
Enables the feature in latest configuration. |
Suppressed comments (1)
src/java/org/apache/cassandra/transport/SimpleClient.java:85
- Removing this suppression makes Netty
Promisefail theIllegalImportcheck in.build/checkstyle.xml:104. Restore the established import-level suppression.
import io.netty.util.concurrent.Promise;
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| import java.util.UUID; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.CopyOnWriteArrayList; | ||
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.BlockingQueue; | ||
| import java.util.concurrent.ConcurrentLinkedQueue; | ||
| import java.util.concurrent.SynchronousQueue; // checkstyle: permit this import | ||
| import java.util.concurrent.SynchronousQueue; |
| supportedOptions.put(StartupMessage.CQL_VERSION, cqlVersions); | ||
| supportedOptions.put(StartupMessage.COMPRESSION, compressions); | ||
| supportedOptions.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions()); | ||
| supportedOptions.put(StartupMessage.GRACEFUL_DISCONNECT, List.of(String.valueOf(DatabaseDescriptor.getGracefulDisconnectEnabled()))); |
| if (!drainComplete.await(DatabaseDescriptor.getGracefulDisconnectGracePeriod(), MILLISECONDS)) | ||
| throw new TimeoutException("Timed out waiting for drain to complete after graceful disconnect"); |
| if (connectedChannels.decrementAndGet() == 0) | ||
| { | ||
| timeoutTask.cancel(false); | ||
| runOnceAction.run(); |
| public boolean graceful_disconnect_enabled = false; | ||
|
|
||
| public volatile DurationSpec.LongMillisecondsBound graceful_disconnect_grace_period = new DurationSpec.LongMillisecondsBound(5000); |
There was a problem hiding this comment.
Isn't this the default configuration itself?
| @Test | ||
| public void testGracefulDisconnectEnabled() | ||
| { | ||
| Assertions.assertThat(StorageService.instance.getGracefulDisconnectEnabled()).isFalse(); |
| AtomicBoolean actionStarted = new AtomicBoolean(false); | ||
| AtomicInteger connectedChannels = new AtomicInteger(channelGroup.size()); |
| if (bindChannel != null && bindChannel.isOpen()) | ||
| { | ||
| logger.info("Stopping native transport acceptor on {}", bindChannel.localAddress()); | ||
| // syncUninterruptibly ensures we wait for the port to actually close | ||
| bindChannel.close().syncUninterruptibly(); |
|
|
||
| == Solution | ||
|
|
||
| Introduce an in-band signal — `GRACEFUL_DISCONNECT` — so that the server can notify clients (that have subscribed) connection before closing it. This gives drivers time to: |
SiyaoIsHiding
left a comment
There was a problem hiding this comment.
Preliminary review, yet to dig into the core mechanism.
The most convincing test is to put together a client-server scenario, where it throws an error when graceful disconnect is disabled, and does not throw an error when graceful disconnect is enabled. This is the whole purpose of CEP-59.
But the existing tests are only about configs, whether server sends event or start draining, etc. We need the test to prove the error is gone.
If the existing integration test harness is not enough to put together such scenario, then a manual testing is needed.
| connection.setCompressor(Compressor.LZ4Compressor.instance); | ||
| } | ||
| if (version.isGreaterOrEqualTo(ProtocolVersion.V5)) | ||
| options.put(StartupMessage.GRACEFUL_DISCONNECT, "GRACEFUL_DISCONNECT"); |
There was a problem hiding this comment.
graceful disconnect is opted in by REGISTER, not in startup message
| supportedOptions.put(StartupMessage.CQL_VERSION, cqlVersions); | ||
| supportedOptions.put(StartupMessage.COMPRESSION, compressions); | ||
| supportedOptions.put(StartupMessage.PROTOCOL_VERSIONS, ProtocolVersion.supportedVersions()); | ||
| supportedOptions.put(StartupMessage.GRACEFUL_DISCONNECT, List.of(String.valueOf(DatabaseDescriptor.getGracefulDisconnectEnabled()))); |
| } | ||
| }); | ||
| if (!drainComplete.await(DatabaseDescriptor.getGracefulDisconnectGracePeriod(), MILLISECONDS)) | ||
| throw new TimeoutException("Timed out waiting for drain to complete after graceful disconnect"); |
There was a problem hiding this comment.
Why does drain throws a TimeoutException when graceful period passes? it should at most log a warning and proceed IMO
| @VisibleForTesting | ||
| Gauge<Integer> connectedNativeClients; | ||
|
|
||
| public AtomicInteger connectionsDraining; |
|
@SiyaoIsHiding I need a java driver supporting cep 59. I am unable to get one, so further testing can't be done by me. |
|
@AshenScribe I sent you a working java driver and a working native protocol on Feb 25, I also hopped on an half hour meeting with you around the same time when I showed you how to do necessary end-to-end testing. |
|
Hi @SiyaoIsHiding yes I remember that meeting. I'll do so. As of drivers, since shanzita took over the java driver, I was waiting for her branch of native protocol, as that will be the official code to be merged now, which I received just yesterday. |
|
She didn't have a full server side implementation to test against until last month, either. You two needs each other to do end-to-end testing, so you both will need to test against WIP code, and you should expect changes through out the PR review process. |
| import java.util.UUID; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.CopyOnWriteArrayList; | ||
| import org.apache.cassandra.utils.concurrent.CountDownLatch |
There was a problem hiding this comment.
I think you missed a semicolon at the end of this import.
There was a problem hiding this comment.
yup, I also skipped the import style check skip patching it now.
There was a problem hiding this comment.
Also wait some moment before reviewing the StorageService class, I am going to change the approach there.
bdeaf90 to
c0641da
Compare
|
added new logic for draining.
|
|
Here are a list of different ways for a node to disconnect and whether graceful disconnect should kick in.
If everyone agree with the above behavior, this table should go into the CEP and the operating manual. Assuming we want the above behavior, I'd suggest we add graceful disconnect to
|
c0641da to
d9a366e
Compare
|
@SiyaoIsHiding I inspected the files to find out call stack. I made this call stack graph. Let me know if this aligns with your understand of drain flow. So here only drain change is in Transport layer. |
d9a366e to
406f620
Compare
|
I have added necessary test cases in the codebase. Now starting manual testing. using this branch for server side, for Java Driver and native-protocol and stable java driver and stable native-protocol as un supported client. To avoid redundant test cases across topologies (single vs multi node) and scale(flag true/false and driver supporting/non supportign), the 16 theoretical permutations cut down to 4 scenarios (and a mixed-driver scenario). Needed Tests Redundant test cases |
|
The chart looks generally correct to me! Re: Re: the four testing scenarios you listed, you should check client side logs and see whether they are expected, too, apart from the server side logs. Re: single node v.s. multi node, I personally prefer testing against multi node cluster because the primary purpose of graceful disconnect is that during rolling restarts, i.e. one node out of several is temporary down, the client application should be able to send the queries to other nodes so the client app can continue running, i.e. every request can get a response in time. If you use only single node and it's temporarily down, even if you have graceful disconnect, the client app will need to stop anyway because there just isn't any node that can give responses, most likely throwing AllNodesFailedException. |
|
If I understand correctly: grace_disconnect is a connection-local event, so the server behaves identically whether it's a single node or a multi-node cluster. once the server emits GRACEFUL_DISCONNECT, routing subsequent requests to other healthy nodes in topology is entirely the driver's responsibility. The FSM introduced traverses on two possible paths WAITING_FOR_CLIENTS → COMPLETE when all clients cooperate or WAITING_FOR_CLIENTS → FORCE_CLOSING not all cooperate, so not that difficult of a path. I'll keep looking for better logic though. |
|
Both your goal and Shanzita's goal is to eliminate errors on the driver's side, cuz that's this CEP's goal. So, you should check client side logs too cuz even if the server log shows what you expected, are you 100% sure the client side errors are gone? If you see the client side error persisting, are you 100% sure it's not the server side that's not behaving as you expected? Re: multi-node, when you check the client side logs of your testing scenarios you will see multi-node clusters are easiest to check whether the client side errors are gone, cuz it's the difference of an application exiting or not. It's ultimately your choice tho. |
406f620 to
35e6cf1
Compare
|
Please refer to https://github.com/SiyaoIsHiding/cassandra/blob/feature/cep-59/metrics/src/java/org/apache/cassandra/transport/Server.java |
|
I appreciate the newCloseFuture().awaitUninterruptibly(...) approach, I'll adapot it in my implementation. |
| SCHEMA_CHANGE(ProtocolVersion.V3), | ||
| TRACE_COMPLETE(ProtocolVersion.V4); | ||
| TRACE_COMPLETE(ProtocolVersion.V4), | ||
| GRACEFUL_DISCONNECT(ProtocolVersion.V5); |
There was a problem hiding this comment.
There's no logical connection between the protocol version bound here and support for graceful disconnect; in theory most (all?) protocol versions could support it without issue. But changing this to even v4 would require some additional testing and it's not abundantly clear there's a huge population in the intersection of v4 + graceful disconnect.
It's also something we can change if we get pushback. A very reasonable compromise is to roll out the initial version with explicit support for v5 only and expand that to v4 if users register complaints.
I'm stealing most of this from some observations made by @SiyaoIsHiding in direct conversation recently. I believe she's going to be posting a follow-up to this effect in the not-too-distant future... I'm just noting it here since she asked me to take a look at this code. :)
SiyaoIsHiding
left a comment
There was a problem hiding this comment.
I'd propose we support Graceful Disconnect from protocol v4 and up, instead of v5, so quite some changes in this PR need to be updated, unless people disagree.
We need to update the file native_protocol_v4.spec and native_protocol_v5.spec too.
We also need to remove the redundant stopNativeTransport() call in CassandraDaemon.destroyClientTransports() as https://issues.apache.org/jira/projects/CASSANDRA/issues/CASSANDRA-21641.
And please avoid force-pushing when the PR is still under active review, so it's easier for reviewers to see what have changes since last review.
| return server; | ||
| } | ||
|
|
||
| public ChannelGroup getChannelsSubscribedToGracefulDisconnect() |
There was a problem hiding this comment.
Add
// for testing purposeCuz it's not used anywhere in src.
There was a problem hiding this comment.
Ig we must use @VisibleForTesting annotation
| probe.drain(); | ||
| } catch (IOException | InterruptedException | ExecutionException e) | ||
| } | ||
| catch (IOException | InterruptedException | ExecutionException e) |
There was a problem hiding this comment.
What's the purpose of this change
| public boolean graceful_disconnect_enabled = false; | ||
|
|
||
| public volatile DurationSpec.LongMillisecondsBound graceful_disconnect_grace_period = new DurationSpec.LongMillisecondsBound(5000); |
| @@ -0,0 +1,153 @@ | |||
| /* | |||
There was a problem hiding this comment.
Please refer to https://github.com/SiyaoIsHiding/cassandra/blob/feature/cep-59/metrics/src/java/org/apache/cassandra/transport/Server.java
On simplier draining logic.
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Queue; |
There was a problem hiding this comment.
Please revert the formatting changes in this file and do not move things around, e.g. that Builder, so it's easier for PR reviewers to review what's actually relevant to CEP-59
| @@ -239,4 +239,20 @@ public void testConnectedClientsAndAuthMetrics() throws SSLException | |||
| assertEquals(0, clientMetrics.encryptedConnectedNativeClients.getValue().intValue()); | |||
| assertEquals(0, passwordConnections.getValue().intValue()); | |||
| } | |||
|
|
|||
| @Test | |||
| public void testGracefulDisconnectMetrics() | |||
There was a problem hiding this comment.
This metrics test doesn't cover anything in the core mechanism. Please refer to other client metrics tests.
74fc95e to
3ee100e
Compare
|
the most recent force push was for in complete responses to changes requested.. |
| if (type == Event.Type.GRACEFUL_DISCONNECT && !DatabaseDescriptor.getGracefulDisconnectEnabled()) | ||
| return; |
There was a problem hiding this comment.
Should we or should we not consider case of a buggy client sending GRACEFUL_DISCONNECT in register regardless of false value of that flag in supported options.
47acc4d to
a325bad
Compare

Cassandra Jira 21191