Search before asking
Fluss version
main (development)
Please describe the bug 🐞
Summary
Follow-up to #3951, which covers the same defect in the record-batch classes. Four more classes
in fluss-server override equals() with value semantics but do not override hashCode(). In
each case the superclass is Object and equals() does not delegate to super.equals(), so
hashCode() is the identity hash and byte-for-byte equal instances hash differently, breaking
the general contract of Object.hashCode():
"If two objects are equal according to the equals(Object) method, then calling the
hashCode method on each of the two objects must produce the same integer result."
— java.lang.Object.hashCode(), Java SE 11 API
The four:
Why these four belong together
PendingExpandIsrState.equals() and PendingShrinkIsrState.equals() both compare
lastCommittedState, which is a CommittedIsrState. A correct hashCode() for either pending
state therefore has to fold in CommittedIsrState.hashCode() — so fixing the pending states
without also fixing CommittedIsrState would still leave the composed hash identity-derived and
inconsistent. They have to be done as one change.
LeaderAndIsr, the other field these two compare, already implements both equals() and
hashCode() consistently, so composing it is safe.
Impact
Any HashSet/HashMap keyed on these types would fail to find an equal instance. I checked and
nothing keys them today, so this is a latent trap rather than an active failure. The ISR states
in particular are compared frequently in the replica code, and equality already works — it is
only hashing that is broken, which is exactly the kind of thing that bites silently when someone
later reaches for a Set.
The Checkstyle EqualsHashCode module is not enabled in tools/maven/checkstyle.xml, which is
why CI does not flag any of this.
Solution
Add hashCode() to each class over exactly the fields its own equals() already compares,
using Objects.hash(...) to match the existing idiom in LeaderAndIsr in the same module.
One thing I deliberately did not change, and would like a maintainer's read on:
CommittedIsrState holds both isr and standbyReplicas, but its equals() compares only
isr. I kept hashCode() consistent with the equals() that is there today — Objects.hash(isr) —
rather than widening equals(), since changing equality semantics for ISR state is a behavioural
change that deserves its own discussion. If the omission of standbyReplicas from equals() is
itself unintended, I am happy to open a separate issue for it.
Scope
Two further cases came out of the same sweep and are not included, for reasons worth stating:
TieringSourceEnumeratorState (fluss-flink-common) also has no hashCode(), but its problem
is larger than a missing hash: equals() is implemented as
this.toString().equals(that.toString()) against any non-null argument, so it is asymmetric and
will accept unrelated types. Adding a hashCode() alone would only half-fix it, so it needs its
own issue.
WriteResultForBucket and its subclasses, and the ModifyColumn* classes in the Flink 1.18/1.19
modules, inherit a hashCode() from a base class that hashes a subset of the fields their
equals() compares. That is legal — merely a weaker hash — so I have left them alone.
Are you willing to submit a PR?
Search before asking
Fluss version
main (development)
Please describe the bug 🐞
Summary
Follow-up to #3951, which covers the same defect in the record-batch classes. Four more classes
in
fluss-serveroverrideequals()with value semantics but do not overridehashCode(). Ineach case the superclass is
Objectandequals()does not delegate tosuper.equals(), sohashCode()is the identity hash and byte-for-byte equal instances hash differently, breakingthe general contract of
Object.hashCode():The four:
equals()comparesIsrState.CommittedIsrState(IsrState.java:85)isrIsrState.PendingExpandIsrState(IsrState.java:171)newInSyncReplicaId,sentLeaderAndIsr,lastCommittedStateIsrState.PendingShrinkIsrState(IsrState.java:244)outOfSyncReplicaIds,sentLeaderAndIsr,lastCommittedStateRemoteLogManifestHandle(RemoteLogManifestHandle.java:49)remoteLogManifestPath,remoteLogEndOffsetWhy these four belong together
PendingExpandIsrState.equals()andPendingShrinkIsrState.equals()both comparelastCommittedState, which is aCommittedIsrState. A correcthashCode()for either pendingstate therefore has to fold in
CommittedIsrState.hashCode()— so fixing the pending stateswithout also fixing
CommittedIsrStatewould still leave the composed hash identity-derived andinconsistent. They have to be done as one change.
LeaderAndIsr, the other field these two compare, already implements bothequals()andhashCode()consistently, so composing it is safe.Impact
Any
HashSet/HashMapkeyed on these types would fail to find an equal instance. I checked andnothing keys them today, so this is a latent trap rather than an active failure. The ISR states
in particular are compared frequently in the replica code, and equality already works — it is
only hashing that is broken, which is exactly the kind of thing that bites silently when someone
later reaches for a
Set.The Checkstyle
EqualsHashCodemodule is not enabled intools/maven/checkstyle.xml, which iswhy CI does not flag any of this.
Solution
Add
hashCode()to each class over exactly the fields its ownequals()already compares,using
Objects.hash(...)to match the existing idiom inLeaderAndIsrin the same module.One thing I deliberately did not change, and would like a maintainer's read on:
CommittedIsrStateholds bothisrandstandbyReplicas, but itsequals()compares onlyisr. I kepthashCode()consistent with theequals()that is there today —Objects.hash(isr)—rather than widening
equals(), since changing equality semantics for ISR state is a behaviouralchange that deserves its own discussion. If the omission of
standbyReplicasfromequals()isitself unintended, I am happy to open a separate issue for it.
Scope
Two further cases came out of the same sweep and are not included, for reasons worth stating:
TieringSourceEnumeratorState(fluss-flink-common) also has nohashCode(), but its problemis larger than a missing hash:
equals()is implemented asthis.toString().equals(that.toString())against any non-null argument, so it is asymmetric andwill accept unrelated types. Adding a
hashCode()alone would only half-fix it, so it needs itsown issue.
WriteResultForBucketand its subclasses, and theModifyColumn*classes in the Flink 1.18/1.19modules, inherit a
hashCode()from a base class that hashes a subset of the fields theirequals()compares. That is legal — merely a weaker hash — so I have left them alone.Are you willing to submit a PR?