[ 
https://issues.apache.org/jira/browse/CASSANDRA-21508?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

C. Scott Andreas updated CASSANDRA-21508:
-----------------------------------------
    Description: 
When the coordinator sheds a request that has waited in the 
Native-Transport-Requests (NTR) queue longer than 
{{{}native_transport_timeout{}}}, it returns an {{OverloadedException}} frame 
without setting the response stream id. The frame goes out on stream id 0 
instead of the timed-out request's stream id. Under load this misroutes an 
error to an unrelated in-flight request on the same connection, and can 
escalate into silent client-side data corruption (a value from one query 
decoded against another query's column definitions) when the native protocol v5 
skip-metadata optimization is in effect.

*Severity*

Silent read corruption. A single mis-stamped error frame can cause an unrelated 
query's rows to be decoded against the wrong column definitions, producing 
either a decode exception or a plausible-but-wrong value returned in response 
to a query.

*Root Cause*

Dispatcher.processRequest, Dispatcher.java:366-369:

{{if (queueTime > 
DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS))}}
{{{}}
{{    ClientMetrics.instance.markTimedOutBeforeProcessing();}}
{{    return ErrorMessage.fromException(new OverloadedException("Query timed 
out before it could start"));}}
{{}   // <-- no setStreamId(request.getStreamId())}}

 

This is the only response-producing exit in {{Dispatcher}} that does not set 
the stream id. The other exits set it explicitly ({{{}Dispatcher.java:113{}}}, 
{{{}:423{}}}, {{{}:448{}}}).
 * {{ErrorMessage.fromException(e, null)}} initializes {{streamId = 0}} 
({{{}ErrorMessage.java:415{}}}).
 * {{OverloadedException}} extends {{RequestExecutionException}} (not 
{{{}WrappedException{}}}), so no stream id is recovered from the throwable.
 * {{Message.streamId}} defaults to 0.
 * The outer {{processRequest}} ({{{}Dispatcher.java:433-437{}}}) returns this 
response via the normal (non-exceptional) path, so the catch block that would 
have called {{setStreamId}} ({{{}:448{}}}) never runs.
 * The frame is flushed and {{Message.encode}} uses {{{}getStreamId() == 0{}}}.

Result: the OVERLOADED error reaches the client on stream id 0, not on the 
stream id of the request that actually timed out.

{{OverloadedException}} encodes as a normal, non-fatal error, so the client 
demultiplexes it by stream id (the channel is not torn down).{*}{{*}}

 

*Mechanism for triggering client-side corruption:*

Two independent requests are involved:
 * {*}Request A{*}: the one that sat in the NTR queue past the deadline. The 
server sheds it and emits the OVERLOADED error, mis-stamped with stream id 0.
 * {*}Request B{*}: a different, healthy request that the client currently has 
on stream id 0. The server is still processing it normally.

On a busy connection stream id 0 is almost always in use, because the DataStax 
java-driver allocates the lowest free id first 
({{{}StreamIdGenerator.acquire(){}}} uses {{{}ids.nextClearBit(0){}}}). The 
sequence:
 # The client receives OVERLOADED on stream id 0 and applies it to request B. B 
is completed (failed or retried) and the client performs a normal, reusable 
release of stream id 0. This differs from a client-side timeout, which would 
orphan the id and keep it reserved; here the client is led to believe B 
genuinely finished.
 # The client reuses stream id 0 for a new request C (a different prepared 
statement, with its own cached column definitions).
 # Request B's real rows response arrives on stream id 0 and is delivered to C.
 # Under skip-metadata the rows frame carries no column metadata 
({{{}ResultSet.java{}}}: the column count is written unconditionally, but the 
name/type loop is skipped under the no-metadata flag), so the driver decodes 
B's row bytes positionally against C's cached column definitions.

When B's and C's column orders differ, each value lands one or more columns 
away from where it belongs. Type-incompatible positions throw in the codec (for 
example {{Invalid boolean value, expecting 1 byte but got 8}} when an 8-byte 
value lands in a boolean slot, or a {{MalformedInputException}} when non-UTF-8 
bytes land in a text slot). Type-compatible positions decode cleanly and return 
the wrong value, which the application can then persist.

The stream id 0 misroute happens at the transport layer, below result metadata. 
A schema or metadata change is not required to trigger it.

 

*Diagram:*

{{        Client (java-driver)                        Server (coordinator, 
overloaded)}}
{{        --------------------                        
--------------------------------}}
{{  B = healthy query              --- stream 0 --->  B queued, still 
processing ...}}
{{  A = other query                --- stream 1 --->  A sits in NTR queue too 
long}}
{{                                                    -> load-sheds A: sends an 
overload}}
{{                                                       error but does not set 
stream id}}
{{                                 <-- error ---------    => stamped stream id 
0   [defect]}}
{{                                     (stream 0)}}
{{  inFlight[0] == B  => error delivered to B,}}
{{  B completed, stream 0 released (normal reuse,}}
{{  not an orphan)}}
{{  C = new query                  --- reuse 0 --->   B's real read finally 
finishes ...}}
{{                                 <-- rows -----------   B's rows sent on 
stream 0 (correct}}
{{                                     (stream 0)          from the server's 
view)}}
{{  inFlight[0] == C  => B's rows decoded against}}
{{  C's column definitions (skip-metadata: no}}
{{  column info in the frame)}}
{{        => a value from B lands in the wrong column of C}}

 

*Steps to Reproduce*

Extreme load produces this naturally (NTR queue backlog on a saturated 
coordinator, correlating with the OVERLOADED errors and a client-side timeout 
storm). It can be reproduced deterministically with the in-JVM dtest below:
 * Start a 1-node cluster with {{native_transport_max_threads=1}} and 
{{{}native_transport_timeout=500ms{}}}.
 * Slow down SELECT execution (a ByteBuddy interceptor on 
{{{}SelectStatement.execute{}}}, as in {{{}OverloadTest.SlowSelect{}}}) so a 
request queued behind a running one crosses the deadline.
 * On one {{SimpleClient}} connection, saturate the single NTR worker and pile 
several requests behind it. On a second connection, send a request on a 
non-zero stream id (42) that queues past the deadline and is shed.
 * Observe that the response is an {{OverloadedException}} and that its stream 
id is 0, not 42.

Run:
{{ant test-jvm-dtest-some 
-Dtest.name=org.apache.cassandra.distributed.test.StreamIdMisrouteTest}}
 

*Expected*

The load-shed OVERLOADED error carries the stream id of the request that timed 
out, so the client attributes it to the correct request and never frees a 
stream id that still has an in-flight response.

*Actual*

The OVERLOADED error carries stream id 0. On a busy connection it is delivered 
to an unrelated in-flight request, whose stream id is then freed and reused, so 
a later legitimate rows response is decoded against the wrong query's column 
definitions.

*Simplest Fix*

Set the stream id on the load-shed path, matching every other 
response-producing exit in {{{}Dispatcher{}}}:
if (queueTime > 
DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS))

{ ClientMetrics.instance.markTimedOutBeforeProcessing(); Message.Response 
response = ErrorMessage.fromException(new OverloadedException("Query timed out 
before it could start")); response.setStreamId(request.getStreamId()); return 
response; }

 
With this change the shed error is delivered to the request that actually timed 
out, and no unrelated stream id is freed early.
 
This response path is overly brittle, though. As it's designed today, each 
return site is responsible for setting the streamId on the response, which is 
easy to miss.
 
We should consider: * {*}Cassandra{*}{*}:{*} Setting the streamId centrally in 
the Dispatcher instance's processRequest method to ensure all paths are guarded 
(suggestion from Benedict).
 * *Cassandra:* Initialize the streamId to an invalid sentinel value and assert 
that it is properly set before returning to the client (suggestion from 
Benedict).
 * *Java* *Driver:* Altering the driver to stop using streamId=0 to avoid this 
behavior from older/unpatched server versions.
 * *Java Driver:* Validate value count versus column definitions count on the 
skip-metadata decode path so a rows/definitions mismatch fails loudly instead 
of silently mis-decoding.
 * *Protocol:* Sending a digest of schema and/or metadata with responses for 
clients to validate, ensuring that the version of the schema rows are decoded 
against matches between the client and server.
 * *Other drivers:* Review behavior and apply changes as appropriate.

 

*Reproduction Test*

The dtest below asserts the shed error comes back on stream id 0. Applying the 
fix above changes the observed stream id from 0 to the request's own id (42), 
so the test fails when the bug is fixed and passes while it is present, which 
confirms it exercises the real path.

Verified locally:
 * Unmodified source: {{tests=2 errors=0 failures=0}} (both pass).
 * With the fix applied: {{loadShedErrorIsStampedWithStreamIdZero}} fails with 
{{{}expected:<0> but was:<42>{}}}; the decode test still passes.

Place at 
{{test/distributed/org/apache/cassandra/distributed/test/StreamIdMisrouteTest.java}}

[dtest attached].

  was:
When the coordinator sheds a request that has waited in the 
Native-Transport-Requests (NTR) queue longer than 
{{{}native_transport_timeout{}}}, it returns an {{OverloadedException}} frame 
without setting the response stream id. The frame goes out on stream id 0 
instead of the timed-out request's stream id. Under load this misroutes an 
error to an unrelated in-flight request on the same connection, and can 
escalate into silent client-side data corruption (a value from one query 
decoded against another query's column definitions) when the native protocol v5 
skip-metadata optimization is in effect.

*Severity*

Silent read corruption. A single mis-stamped error frame can cause an unrelated 
query's rows to be decoded against the wrong column definitions, producing 
either a decode exception or a plausible-but-wrong value returned in response 
to a query.

*Root Cause*

Dispatcher.processRequest, Dispatcher.java:366-369:

{{{}if (queueTime > 
DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS)){}}}{{{}{ 
ClientMetrics.instance.markTimedOutBeforeProcessing(); return 
ErrorMessage.fromException(new OverloadedException("Query timed out before it 
could start")); }{}}}{{{}// <-- no setStreamId(request.getStreamId()){}}}
{{ }}

This is the only response-producing exit in {{Dispatcher}} that does not set 
the stream id. The other exits set it explicitly ({{{}Dispatcher.java:113{}}}, 
{{{}:423{}}}, {{{}:448{}}}).
 * {{ErrorMessage.fromException(e, null)}} initializes {{streamId = 0}} 
({{{}ErrorMessage.java:415{}}}).
 * {{OverloadedException}} extends {{RequestExecutionException}} (not 
{{{}WrappedException{}}}), so no stream id is recovered from the throwable.
 * {{Message.streamId}} defaults to 0.
 * The outer {{processRequest}} ({{{}Dispatcher.java:433-437{}}}) returns this 
response via the normal (non-exceptional) path, so the catch block that would 
have called {{setStreamId}} ({{{}:448{}}}) never runs.
 * The frame is flushed and {{Message.encode}} uses {{{}getStreamId() == 0{}}}.

Result: the OVERLOADED error reaches the client on stream id 0, not on the 
stream id of the request that actually timed out.

{{OverloadedException}} encodes as a normal, non-fatal error, so the client 
demultiplexes it by stream id (the channel is not torn down).{*}{{*}}

 

*Mechanism for triggering client-side corruption:*

Two independent requests are involved:
 * {*}Request A{*}: the one that sat in the NTR queue past the deadline. The 
server sheds it and emits the OVERLOADED error, mis-stamped with stream id 0.
 * {*}Request B{*}: a different, healthy request that the client currently has 
on stream id 0. The server is still processing it normally.

On a busy connection stream id 0 is almost always in use, because the DataStax 
java-driver allocates the lowest free id first 
({{{}StreamIdGenerator.acquire(){}}} uses {{{}ids.nextClearBit(0){}}}). The 
sequence:
 # The client receives OVERLOADED on stream id 0 and applies it to request B. B 
is completed (failed or retried) and the client performs a normal, reusable 
release of stream id 0. This differs from a client-side timeout, which would 
orphan the id and keep it reserved; here the client is led to believe B 
genuinely finished.
 # The client reuses stream id 0 for a new request C (a different prepared 
statement, with its own cached column definitions).
 # Request B's real rows response arrives on stream id 0 and is delivered to C.
 # Under skip-metadata the rows frame carries no column metadata 
({{{}ResultSet.java{}}}: the column count is written unconditionally, but the 
name/type loop is skipped under the no-metadata flag), so the driver decodes 
B's row bytes positionally against C's cached column definitions.

When B's and C's column orders differ, each value lands one or more columns 
away from where it belongs. Type-incompatible positions throw in the codec (for 
example {{Invalid boolean value, expecting 1 byte but got 8}} when an 8-byte 
value lands in a boolean slot, or a {{MalformedInputException}} when non-UTF-8 
bytes land in a text slot). Type-compatible positions decode cleanly and return 
the wrong value, which the application can then persist.

The stream id 0 misroute happens at the transport layer, below result metadata. 
A schema or metadata change is not required to trigger it.

 

*Diagram:*

{{        Client (java-driver)                        Server (coordinator, 
overloaded)}}
{{        --------------------                        
--------------------------------}}
{{  B = healthy query              --- stream 0 --->  B queued, still 
processing ...}}
{{  A = other query                --- stream 1 --->  A sits in NTR queue too 
long}}
{{                                                    -> load-sheds A: sends an 
overload}}
{{                                                       error but does not set 
stream id}}
{{                                 <-- error ---------    => stamped stream id 
0   [defect]}}
{{                                     (stream 0)}}
{{  inFlight[0] == B  => error delivered to B,}}
{{  B completed, stream 0 released (normal reuse,}}
{{  not an orphan)}}
{{  C = new query                  --- reuse 0 --->   B's real read finally 
finishes ...}}
{{                                 <-- rows -----------   B's rows sent on 
stream 0 (correct}}
{{                                     (stream 0)          from the server's 
view)}}
{{  inFlight[0] == C  => B's rows decoded against}}
{{  C's column definitions (skip-metadata: no}}
{{  column info in the frame)}}
{{        => a value from B lands in the wrong column of C}}

 

*Steps to Reproduce*

Extreme load produces this naturally (NTR queue backlog on a saturated 
coordinator, correlating with the OVERLOADED errors and a client-side timeout 
storm). It can be reproduced deterministically with the in-JVM dtest below:
 * Start a 1-node cluster with {{native_transport_max_threads=1}} and 
{{{}native_transport_timeout=500ms{}}}.
 * Slow down SELECT execution (a ByteBuddy interceptor on 
{{{}SelectStatement.execute{}}}, as in {{{}OverloadTest.SlowSelect{}}}) so a 
request queued behind a running one crosses the deadline.
 * On one {{SimpleClient}} connection, saturate the single NTR worker and pile 
several requests behind it. On a second connection, send a request on a 
non-zero stream id (42) that queues past the deadline and is shed.
 * Observe that the response is an {{OverloadedException}} and that its stream 
id is 0, not 42.

Run:
{{ant test-jvm-dtest-some 
-Dtest.name=org.apache.cassandra.distributed.test.StreamIdMisrouteTest}}
 

*Expected*

The load-shed OVERLOADED error carries the stream id of the request that timed 
out, so the client attributes it to the correct request and never frees a 
stream id that still has an in-flight response.

*Actual*

The OVERLOADED error carries stream id 0. On a busy connection it is delivered 
to an unrelated in-flight request, whose stream id is then freed and reused, so 
a later legitimate rows response is decoded against the wrong query's column 
definitions.

*Simplest Fix*

Set the stream id on the load-shed path, matching every other 
response-producing exit in {{{}Dispatcher{}}}:
if (queueTime > 
DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS))

{ ClientMetrics.instance.markTimedOutBeforeProcessing(); Message.Response 
response = ErrorMessage.fromException(new OverloadedException("Query timed out 
before it could start")); response.setStreamId(request.getStreamId()); return 
response; }

 
With this change the shed error is delivered to the request that actually timed 
out, and no unrelated stream id is freed early.
 
This response path is overly brittle, though. As it's designed today, each 
return site is responsible for setting the streamId on the response, which is 
easy to miss.
 
We should consider: * {*}Cassandra{*}{*}:{*} Setting the streamId centrally in 
the Dispatcher instance's processRequest method to ensure all paths are guarded 
(suggestion from Benedict).
 * *Cassandra:* Initialize the streamId to an invalid sentinel value and assert 
that it is properly set before returning to the client (suggestion from 
Benedict).
 * *Java* *Driver:* Altering the driver to stop using streamId=0 to avoid this 
behavior from older/unpatched server versions.
 * *Java Driver:* Validate value count versus column definitions count on the 
skip-metadata decode path so a rows/definitions mismatch fails loudly instead 
of silently mis-decoding.
 * *Protocol:* Sending a digest of schema and/or metadata with responses for 
clients to validate, ensuring that the version of the schema rows are decoded 
against matches between the client and server.
 * *Other drivers:* Review behavior and apply changes as appropriate.

 

*Reproduction Test*

The dtest below asserts the shed error comes back on stream id 0. Applying the 
fix above changes the observed stream id from 0 to the request's own id (42), 
so the test fails when the bug is fixed and passes while it is present, which 
confirms it exercises the real path.

Verified locally:
 * Unmodified source: {{tests=2 errors=0 failures=0}} (both pass).
 * With the fix applied: {{loadShedErrorIsStampedWithStreamIdZero}} fails with 
{{{}expected:<0> but was:<42>{}}}; the decode test still passes.

Place at 
{{test/distributed/org/apache/cassandra/distributed/test/StreamIdMisrouteTest.java}}

[dtest attached].


> Coordinator load-shedding returns OverloadedException without setting 
> streamId, misrouting query responses
> ----------------------------------------------------------------------------------------------------------
>
>                 Key: CASSANDRA-21508
>                 URL: https://issues.apache.org/jira/browse/CASSANDRA-21508
>             Project: Apache Cassandra
>          Issue Type: Bug
>          Components: Messaging/Client
>            Reporter: C. Scott Andreas
>            Priority: Normal
>         Attachments: StreamIdMisrouteTest.java
>
>
> When the coordinator sheds a request that has waited in the 
> Native-Transport-Requests (NTR) queue longer than 
> {{{}native_transport_timeout{}}}, it returns an {{OverloadedException}} frame 
> without setting the response stream id. The frame goes out on stream id 0 
> instead of the timed-out request's stream id. Under load this misroutes an 
> error to an unrelated in-flight request on the same connection, and can 
> escalate into silent client-side data corruption (a value from one query 
> decoded against another query's column definitions) when the native protocol 
> v5 skip-metadata optimization is in effect.
> *Severity*
> Silent read corruption. A single mis-stamped error frame can cause an 
> unrelated query's rows to be decoded against the wrong column definitions, 
> producing either a decode exception or a plausible-but-wrong value returned 
> in response to a query.
> *Root Cause*
> Dispatcher.processRequest, Dispatcher.java:366-369:
> {{if (queueTime > 
> DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS))}}
> {{{}}
> {{    ClientMetrics.instance.markTimedOutBeforeProcessing();}}
> {{    return ErrorMessage.fromException(new OverloadedException("Query timed 
> out before it could start"));}}
> {{}   // <-- no setStreamId(request.getStreamId())}}
>  
> This is the only response-producing exit in {{Dispatcher}} that does not set 
> the stream id. The other exits set it explicitly 
> ({{{}Dispatcher.java:113{}}}, {{{}:423{}}}, {{{}:448{}}}).
>  * {{ErrorMessage.fromException(e, null)}} initializes {{streamId = 0}} 
> ({{{}ErrorMessage.java:415{}}}).
>  * {{OverloadedException}} extends {{RequestExecutionException}} (not 
> {{{}WrappedException{}}}), so no stream id is recovered from the throwable.
>  * {{Message.streamId}} defaults to 0.
>  * The outer {{processRequest}} ({{{}Dispatcher.java:433-437{}}}) returns 
> this response via the normal (non-exceptional) path, so the catch block that 
> would have called {{setStreamId}} ({{{}:448{}}}) never runs.
>  * The frame is flushed and {{Message.encode}} uses {{{}getStreamId() == 
> 0{}}}.
> Result: the OVERLOADED error reaches the client on stream id 0, not on the 
> stream id of the request that actually timed out.
> {{OverloadedException}} encodes as a normal, non-fatal error, so the client 
> demultiplexes it by stream id (the channel is not torn down).{*}{{*}}
>  
> *Mechanism for triggering client-side corruption:*
> Two independent requests are involved:
>  * {*}Request A{*}: the one that sat in the NTR queue past the deadline. The 
> server sheds it and emits the OVERLOADED error, mis-stamped with stream id 0.
>  * {*}Request B{*}: a different, healthy request that the client currently 
> has on stream id 0. The server is still processing it normally.
> On a busy connection stream id 0 is almost always in use, because the 
> DataStax java-driver allocates the lowest free id first 
> ({{{}StreamIdGenerator.acquire(){}}} uses {{{}ids.nextClearBit(0){}}}). The 
> sequence:
>  # The client receives OVERLOADED on stream id 0 and applies it to request B. 
> B is completed (failed or retried) and the client performs a normal, reusable 
> release of stream id 0. This differs from a client-side timeout, which would 
> orphan the id and keep it reserved; here the client is led to believe B 
> genuinely finished.
>  # The client reuses stream id 0 for a new request C (a different prepared 
> statement, with its own cached column definitions).
>  # Request B's real rows response arrives on stream id 0 and is delivered to 
> C.
>  # Under skip-metadata the rows frame carries no column metadata 
> ({{{}ResultSet.java{}}}: the column count is written unconditionally, but the 
> name/type loop is skipped under the no-metadata flag), so the driver decodes 
> B's row bytes positionally against C's cached column definitions.
> When B's and C's column orders differ, each value lands one or more columns 
> away from where it belongs. Type-incompatible positions throw in the codec 
> (for example {{Invalid boolean value, expecting 1 byte but got 8}} when an 
> 8-byte value lands in a boolean slot, or a {{MalformedInputException}} when 
> non-UTF-8 bytes land in a text slot). Type-compatible positions decode 
> cleanly and return the wrong value, which the application can then persist.
> The stream id 0 misroute happens at the transport layer, below result 
> metadata. A schema or metadata change is not required to trigger it.
>  
> *Diagram:*
> {{        Client (java-driver)                        Server (coordinator, 
> overloaded)}}
> {{        --------------------                        
> --------------------------------}}
> {{  B = healthy query              --- stream 0 --->  B queued, still 
> processing ...}}
> {{  A = other query                --- stream 1 --->  A sits in NTR queue too 
> long}}
> {{                                                    -> load-sheds A: sends 
> an overload}}
> {{                                                       error but does not 
> set stream id}}
> {{                                 <-- error ---------    => stamped stream 
> id 0   [defect]}}
> {{                                     (stream 0)}}
> {{  inFlight[0] == B  => error delivered to B,}}
> {{  B completed, stream 0 released (normal reuse,}}
> {{  not an orphan)}}
> {{  C = new query                  --- reuse 0 --->   B's real read finally 
> finishes ...}}
> {{                                 <-- rows -----------   B's rows sent on 
> stream 0 (correct}}
> {{                                     (stream 0)          from the server's 
> view)}}
> {{  inFlight[0] == C  => B's rows decoded against}}
> {{  C's column definitions (skip-metadata: no}}
> {{  column info in the frame)}}
> {{        => a value from B lands in the wrong column of C}}
>  
> *Steps to Reproduce*
> Extreme load produces this naturally (NTR queue backlog on a saturated 
> coordinator, correlating with the OVERLOADED errors and a client-side timeout 
> storm). It can be reproduced deterministically with the in-JVM dtest below:
>  * Start a 1-node cluster with {{native_transport_max_threads=1}} and 
> {{{}native_transport_timeout=500ms{}}}.
>  * Slow down SELECT execution (a ByteBuddy interceptor on 
> {{{}SelectStatement.execute{}}}, as in {{{}OverloadTest.SlowSelect{}}}) so a 
> request queued behind a running one crosses the deadline.
>  * On one {{SimpleClient}} connection, saturate the single NTR worker and 
> pile several requests behind it. On a second connection, send a request on a 
> non-zero stream id (42) that queues past the deadline and is shed.
>  * Observe that the response is an {{OverloadedException}} and that its 
> stream id is 0, not 42.
> Run:
> {{ant test-jvm-dtest-some 
> -Dtest.name=org.apache.cassandra.distributed.test.StreamIdMisrouteTest}}
>  
> *Expected*
> The load-shed OVERLOADED error carries the stream id of the request that 
> timed out, so the client attributes it to the correct request and never frees 
> a stream id that still has an in-flight response.
> *Actual*
> The OVERLOADED error carries stream id 0. On a busy connection it is 
> delivered to an unrelated in-flight request, whose stream id is then freed 
> and reused, so a later legitimate rows response is decoded against the wrong 
> query's column definitions.
> *Simplest Fix*
> Set the stream id on the load-shed path, matching every other 
> response-producing exit in {{{}Dispatcher{}}}:
> if (queueTime > 
> DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS))
> { ClientMetrics.instance.markTimedOutBeforeProcessing(); Message.Response 
> response = ErrorMessage.fromException(new OverloadedException("Query timed 
> out before it could start")); response.setStreamId(request.getStreamId()); 
> return response; }
>  
> With this change the shed error is delivered to the request that actually 
> timed out, and no unrelated stream id is freed early.
>  
> This response path is overly brittle, though. As it's designed today, each 
> return site is responsible for setting the streamId on the response, which is 
> easy to miss.
>  
> We should consider: * {*}Cassandra{*}{*}:{*} Setting the streamId centrally 
> in the Dispatcher instance's processRequest method to ensure all paths are 
> guarded (suggestion from Benedict).
>  * *Cassandra:* Initialize the streamId to an invalid sentinel value and 
> assert that it is properly set before returning to the client (suggestion 
> from Benedict).
>  * *Java* *Driver:* Altering the driver to stop using streamId=0 to avoid 
> this behavior from older/unpatched server versions.
>  * *Java Driver:* Validate value count versus column definitions count on the 
> skip-metadata decode path so a rows/definitions mismatch fails loudly instead 
> of silently mis-decoding.
>  * *Protocol:* Sending a digest of schema and/or metadata with responses for 
> clients to validate, ensuring that the version of the schema rows are decoded 
> against matches between the client and server.
>  * *Other drivers:* Review behavior and apply changes as appropriate.
>  
> *Reproduction Test*
> The dtest below asserts the shed error comes back on stream id 0. Applying 
> the fix above changes the observed stream id from 0 to the request's own id 
> (42), so the test fails when the bug is fixed and passes while it is present, 
> which confirms it exercises the real path.
> Verified locally:
>  * Unmodified source: {{tests=2 errors=0 failures=0}} (both pass).
>  * With the fix applied: {{loadShedErrorIsStampedWithStreamIdZero}} fails 
> with {{{}expected:<0> but was:<42>{}}}; the decode test still passes.
> Place at 
> {{test/distributed/org/apache/cassandra/distributed/test/StreamIdMisrouteTest.java}}
> [dtest attached].



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to