sepuri sai krishna created CASSANDRA-21575:
----------------------------------------------

             Summary: Message ids become negative once the id counter wraps, 
inflating every message header
                 Key: CASSANDRA-21575
                 URL: https://issues.apache.org/jira/browse/CASSANDRA-21575
             Project: Apache Cassandra
          Issue Type: Bug
          Components: Messaging/Internode
            Reporter: sepuri sai krishna
            Assignee: sepuri sai krishna


{{Message.nextId()}} draws from a shared {{AtomicInteger}} and widens the 
result to a {{long}}:

{code:java}
private static final AtomicInteger nextId = new AtomicInteger(0);

private static long nextId()
{
    long id;
    do
    {
        id = nextId.incrementAndGet();
    }
    while (id == NO_ID);

    return id;
}
{code}

Once the counter passes {{Integer.MAX_VALUE}} it wraps to 
{{Integer.MIN_VALUE}}, and the negative
{{int}} sign-extends into a negative {{long}}. {{Message.Serializer}} writes 
the id as an unsigned
vint:

{code:java}
out.writeUnsignedVInt(header.id);
{code}

A negative long has all its high bits set, so it always encodes at the maximum 
width of 9 bytes:

|| id || wire bytes ||
| 1 | 1 |
| 2147483647 | 5 |
| -2147483648 (first wrap) | 9 |
| -1 | 9 |

That is an extra 4 bytes on the header of every internode message, sustained 
for the 2^31 messages
until the counter cycles back through the positive range, and then again on 
every subsequent wrap.
A node sending 50k messages/sec reaches the first wrap in roughly 12 hours, so 
this is steady state
for any long-running cluster rather than an edge case.

Nothing depends on the sign of an id. They are opaque correlation keys, used 
only for matching
responses to callbacks in {{RequestCallbacks}}, so keeping them non-negative is 
behaviour preserving.
The wire format is unchanged; only the range of values written changes.

h3. Coupled defect in ForwardingInfo

{{ForwardingInfo}} is the {{FORWARD_TO}} parameter used for inter-DC write 
forwarding. It writes ids
with the 64-bit form and sizes them the same way, but reads them back with the 
32-bit form:

{code:java}
// serialize
out.writeUnsignedVInt(ids[i]);
// serializedSize
size += computeUnsignedVIntSize(ids[i]);
// deserialize
ids[i] = in.readUnsignedVInt32();   // mismatch
{code}

This is the only place in the codebase that reads a message id at 32 bits; 
every other reader uses
{{readUnsignedVInt}}/{{getUnsignedVInt}}. It is currently harmless only by 
accident: ids always fit in
{{int}} today, because they come from an {{int}} counter, so the 
{{checkedCast}} inside
{{readUnsignedVInt32}} happens to succeed even for the sign-extended negative 
values.

The same narrowing is present on 4.0 and 4.1, spelled differently:

{code:java}
ids[i] = version >= VERSION_40 ? Ints.checkedCast(in.readUnsignedVInt()) : 
in.readInt();
{code}

so both parts of this ticket apply from 4.0 onwards. The attached patch is 
against trunk; happy to
prepare backports for whichever branches the reviewer wants this on.

That makes it a blocker for the fix rather than a separate cleanup. Once ids 
span the full unsigned
32-bit range, this read throws:

{noformat}
fixed nextId, post-wrap   id=2147483648   ForwardingInfo.deserialize: 
VIntOutOfRangeException
fixed nextId, max         id=4294967295   ForwardingInfo.deserialize: 
VIntOutOfRangeException
{noformat}

which would break inter-DC write forwarding. Both changes therefore belong in 
the same commit.

h3. Fix

Mask the counter so ids stay in {{[0, 2^32)}}. This covers the same number of 
distinct values, keeps
them non-negative, and bounds the encoding at 5 bytes. Change 
{{ForwardingInfo.deserialize}} to use
{{readUnsignedVInt}}, matching its own serialize and serializedSize.

h3. Tests

The existing {{ForwardingInfoTest}} only ever used ids 44-49, which is why the 
width mismatch was
never exercised. Added:

* {{ForwardingInfoTest.testLargeMessageIdsRoundTrip}} - round-trips ids across 
the whole unsigned
32-bit range, including {{Integer.MAX_VALUE + 1}} and {{0xFFFFFFFF}}, asserting 
{{serializedSize}}
matches the bytes written. Fails with {{VIntOutOfRangeException: 2147483648 is 
out of range for a
32-bit integer}} without the {{ForwardingInfo}} fix.
* {{MessageTest.testIdsRemainUnsignedAcrossCounterWrap}} - asserts ids stay 
non-negative, stay within
32 unsigned bits, and encode within 5 bytes across the wrap boundary.
* {{MessageTest.testLargeIdRoundTrips}} - round-trips a message header carrying 
ids from the upper
half of the range, for every supported messaging version.




--
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