Lab 03 — Code Review: CleverPartitioner
Phase: 05 — Code Quality and Engineering Discipline
Difficulty: ⭐⭐⭐⭐☆ | Estimated Time: 2.5–3 hours
Primary Artifact: CleverPartitioner.java — a Fibonacci-hashing partitioner with 5 correctness issues
Verify: mvn test shows exactly 2 passing + 2 failing tests; then fix the bugs until all 4 pass
Goal
A contributor submitted CleverPartitioner.java as a pull request to replace the existing modulo-based partitioner. The tests tell part of the story — but a good code review catches what the tests miss.
Your job is to:
- Run the tests and observe which pass and which fail
- Write a code review comment for each issue you find (5 total)
- Fix the correctness bugs so all 4 tests pass
- Assess whether the fix is sufficient for a real Apache PR to be merged
Background: Fibonacci Hashing
Fibonacci hashing maps a hash value to a bucket using the golden-ratio constant:
φ = (√5 - 1) / 2 ≈ 0.618
In integer arithmetic (64-bit), this is approximated by:
FIBONACCI_MULT = 2^64 × φ = 0x9e3779b97f4a7c15L
The algorithm: multiply the key's hash by FIBONACCI_MULT, then take the top k bits, where k = log₂(n) — this only works when n is a power of two.
The bug in this implementation: the code uses Integer.numberOfTrailingZeros(n) to compute k, which is correct only for powers of two. For n=1, numberOfTrailingZeros(1) = 0, so the shift is 64 - 0 = 64. In Java, shifting a long by 64 is equivalent to shifting by 0 (the shift amount is taken modulo 64), returning the full product instead of 0.
Setup
cd phase-05-code-quality/lab-03-review
mvn test
Expected output: 2 tests PASS, 2 tests FAIL.
Steps
Step 1 — Run the tests and understand the failures
mvn test 2>&1 | grep -E 'Tests run|FAIL|ERROR|expected|but was'
Record:
- Which test fails?
- What is the actual vs. expected value?
- What does that tell you about the algorithm?
Step 2 — Read CleverPartitioner.java and write a review
Before touching any code, write a code review comment for each of the 5 issues marked in the source file. Use the Apache review severity labels:
BLOCKER — must be fixed before merge
MAJOR — important but non-blocking correctness issue
MINOR — should be addressed
NIT — trivial
QUESTION — asking for clarification
Your review should be in the form:
[BLOCKER] partition(String, int): If n is not a power of two (e.g., n=5),
the shift amount is Integer.numberOfTrailingZeros(5) = 0, so the expression
>>> 64 evaluates as >>> 0 (shift mod 64), returning an unmasked value far
outside [0, n). This is a correctness regression for any cluster with a
non-power-of-two partition count. Requires either: (a) restricting n to
powers of two with an explicit precondition check, or (b) replacing the
algorithm with one that handles arbitrary n.
Step 3 — Fix the correctness bugs
Fix in this order:
- Null guard: add a null check on
keyand throwIllegalArgumentException(notNullPointerException— explicit contract is better than an implicit crash) - Non-positive n guard: throw
IllegalArgumentExceptionifn <= 0 - Power-of-two guard: throw
IllegalArgumentExceptionwith a clear message ifnis not a power of two —(n & (n - 1)) != 0is the idiomatic check - n=1 edge case: with the power-of-two guard in place, n=1 is now valid; ensure
partition(key, 1)always returns 0 (all keys go to bucket 0 — there is only one bucket) - Magic constant citation: add a Javadoc or inline comment explaining
0x9e3779b97f4a7c15L— this is the Knuth/Fibonacci multiplier and must be cited
Step 4 — Verify all 4 tests pass
mvn test
All 4 tests must pass:
testPowerOfTwo_distributionInRange— basic power-of-two bucketingtestSingleBucket_alwaysReturnsZero— n=1 must always return 0testNonPowerOfTwo_throwsIllegalArgument— n=5 must throwIllegalArgumentExceptiontestNullKey_throwsIllegalArgument— null key must throwIllegalArgumentException
Step 5 — Reassess: is the PR ready to merge?
After your fixes, answer in a comment at the top of CleverPartitioner.java:
- Is it still Fibonacci hashing if we restrict n to powers of two? (Yes — this is the standard form.)
- A Kafka partition count can be any positive integer, not just a power of two. What algorithm would you recommend as a replacement that handles arbitrary n? (Hint:
Math.abs(key.hashCode()) % nis simple but has skew for non-prime n. What's the production alternative?) - Is the
0x45d9f3bLconstant on the hashCode mixing step necessary? What does it do?
Verification
# All 4 tests pass
mvn test
echo "Exit code: $?" # must be 0
Source Files
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example.quality</groupId>
<artifactId>review-lab</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Code Quality Lab 03 — Code Review</name>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>
CleverPartitioner.java
package org.example.quality;
/**
* Partitioner using Fibonacci hashing to assign a string key to one of
* {@code n} buckets.
*
* <p>
* Fibonacci hashing exploits the golden-ratio constant to spread keys more
* evenly
* than a simple modulo, especially when hashes cluster around multiples of a
* common factor.
*
* <p>
* This implementation has 5 issues for code review. Find them all before
* fixing.
*
* <p>
* Algorithm (for a power-of-two n):
*
* <pre>
* h = mix(key.hashCode())
* bucket = (int)((h * FIBONACCI_MULT) >>> (64 - log2(n)))
* </pre>
*
* <p>
* Where {@code FIBONACCI_MULT = 2^64 * (sqrt(5)-1)/2} — the 64-bit
* Fibonacci/Knuth
* multiplicative constant.
*
* @see <a href=
* "https://en.wikipedia.org/wiki/Hash_table#Fibonacci_hashing">Fibonacci
* hashing</a>
*/
public class CleverPartitioner {
// ISSUE 1 — Magic constant with no citation
// This is the 64-bit Fibonacci multiplicative constant (Knuth, vol 3, §6.4):
// FIBONACCI_MULT = floor(2^64 * (sqrt(5) - 1) / 2) = 0x9e3779b97f4a7c15L
// Without the comment (which was "// trust me" in the original PR), a reviewer
// cannot verify this constant is correct without re-deriving it from scratch.
// Severity: MAJOR — cite the source.
private static final long FIBONACCI_MULT = 0x9e3779b97f4a7c15L;
/**
* Returns the bucket index for the given key in a cluster of {@code n}
* partitions.
*
* @param key the key to partition (must not be null)
* @param n the number of partitions (must be a positive power of two)
* @return a bucket index in the range {@code [0, n)}
*/
public int partition(String key, int n) {
// ISSUE 2 — No null check on 'key'
// If key is null, key.hashCode() throws NullPointerException with no useful
// message.
// The contract should be explicit: IllegalArgumentException with "key must not
// be null".
// Severity: MAJOR — silent crash with an unhelpful stack trace.
// ISSUE 3 — No validation that n > 0
// If n == 0, the shift amount is 64 - numberOfTrailingZeros(0) = 64 - 32 = 32.
// If n < 0, numberOfTrailingZeros returns a nonsensical result.
// Severity: BLOCKER — undefined behavior for invalid inputs.
// ISSUE 4 — Assumes n is a power of two; no guard
// numberOfTrailingZeros(n) only gives log2(n) when n IS a power of two.
// For n = 5: numberOfTrailingZeros(5) = 0, so the shift is (64 - 0) = 64.
// In Java, (long) >>> 64 is equivalent to >>> 0 (shift mod 64) — the full
// 64-bit product, truncated to int, which is not in [0, 5).
// Severity: BLOCKER — wrong results for any non-power-of-two partition count.
int shift = 64 - Integer.numberOfTrailingZeros(n);
// ISSUE 5 — Magic mixing constant with no explanation
// 0x45d9f3bL is a secondary mixing multiplier used to improve bit distribution
// before the Fibonacci step. Without a comment, reviewers cannot verify this
// is the correct constant or understand its purpose.
// Severity: MINOR — code should be self-documenting.
long h = (long) key.hashCode() * 0x45d9f3bL;
return (int) ((h * FIBONACCI_MULT) >>> shift);
}
}
PartitionerTest.java
package org.example.quality;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for {@link CleverPartitioner}.
*
* Before fixes: 2 tests pass, 2 tests fail.
* After fixes: all 4 tests pass.
*
* Run with: mvn test
*/
class PartitionerTest {
private CleverPartitioner partitioner;
@BeforeEach
void setUp() {
partitioner = new CleverPartitioner();
}
/**
* Test 1 — PASSES before fixes.
*
* For a power-of-two n (n=8), every key must produce a bucket in [0, 8).
* Fibonacci hashing is deterministic, so the same key always maps to the same
* bucket.
*/
@Test
void testPowerOfTwo_distributionInRange() {
int n = 8;
String[] keys = { "key-0", "key-1", "key-2", "key-3", "alpha", "beta", "gamma", "delta" };
for (String key : keys) {
int bucket = partitioner.partition(key, n);
assertTrue(bucket >= 0 && bucket < n,
"Expected bucket in [0, " + n + ") for key=" + key + " but got: " + bucket);
}
}
/**
* Test 2 — FAILS before fixes.
*
* n=1 means "there is only one bucket; everything goes to bucket 0".
*
* Bug: numberOfTrailingZeros(1) = 0, so shift = 64 - 0 = 64.
* In Java, (long) >>> 64 is (long) >>> (64 % 64) = (long) >>> 0, which is the
* full
* 64-bit product cast to int — not 0.
*
* Fix required: treat n=1 as a special case (return 0 always), OR add a
* power-of-two guard that catches n=1 and computes shift=0 explicitly using
* Integer.numberOfLeadingZeros(n) - 1 or Integer.SIZE -
* Integer.numberOfLeadingZeros(n-1).
*/
@Test
void testSingleBucket_alwaysReturnsZero() {
String[] keys = { "any-key", "another-key", "yet-another" };
for (String key : keys) {
int bucket = partitioner.partition(key, 1);
assertEquals(0, bucket, "With n=1 every key must map to bucket 0, but got: " + bucket);
}
}
/**
* Test 3 — FAILS before fixes.
*
* n=5 is not a power of two. The partitioner must reject it with
* IllegalArgumentException rather than silently returning a wrong bucket.
*
* Bug: without a guard, the code silently returns values outside [0, 5).
*
* Fix required: add a precondition check before computing the shift.
*/
@Test
void testNonPowerOfTwo_throwsIllegalArgument() {
assertThrows(IllegalArgumentException.class,
() -> partitioner.partition("any-key", 5),
"Expected IllegalArgumentException for non-power-of-two n=5");
}
/**
* Test 4 — PASSES before fixes (an NPE is thrown, and the test expects it).
*
* BUT: NullPointerException is the wrong exception here. A method that accepts
* user input should throw IllegalArgumentException with a clear message, not
* an opaque NPE. This test is written to document the DESIRED behavior after
* fix.
*
* Before fix: NullPointerException is thrown — test passes because NPE is a
* subtype
* of RuntimeException, which satisfies assertThrows(NullPointerException.class,
* ...).
*
* After fix: IllegalArgumentException is thrown — the test must be updated to
* expect
* IllegalArgumentException, OR the test is already written correctly and
* the implementation must throw IllegalArgumentException.
*
* The test below expects IllegalArgumentException. Before fixing, it FAILS.
* After fixing, it PASSES.
*
* This is the review point: the test was written to catch the NPE (which
* already
* "works") but the right fix is IllegalArgumentException with a useful message.
*/
@Test
void testNullKey_throwsIllegalArgument() {
assertThrows(IllegalArgumentException.class,
() -> partitioner.partition(null, 4),
"Expected IllegalArgumentException for null key, not NullPointerException");
}
}