Skip to content

Commit e9f7b7f

Browse files
committed
move active key count initialization from first record to SOP
- initializeActiveKeyCount() at SOP sets -1→0, ensuring mid-batch config enablement (config OFF at SOP, ON after restart) stays at -1 (not tracked) rather than producing an inaccurate partial count - incrementActiveKeyCountForBatchRecord skips when count < 0 (not initialized) - Renamed finalizeActiveKeyCountForBatchPush → cleanupBatchKeyCountState (only releases dedup state, no longer manipulates count) - Added tests: mid-batch config enablement, initializeActiveKeyCount idempotency - Fixed SpotBugs ICAST_INTEGER_MULTIPLY_CAST_TO_LONG in testConcurrency
1 parent 426b4b3 commit e9f7b7f

6 files changed

Lines changed: 91 additions & 24 deletions

File tree

clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/PartitionConsumptionState.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,17 @@ public void incrementActiveKeyCount() {
481481
activeKeyCount.incrementAndGet();
482482
}
483483

484+
/**
485+
* Called at SOP to mark this partition as actively counting. Without this, a mid-batch
486+
* restart with a newly enabled config would start counting partway through, producing
487+
* an inaccurate partial count. On restart, SOP is not re-processed (already past the
488+
* checkpoint), so the count stays at {@link #ACTIVE_KEY_COUNT_NOT_TRACKED} and batch
489+
* records are skipped by {@link #incrementActiveKeyCountForBatchRecord}.
490+
*/
491+
public void initializeActiveKeyCount() {
492+
activeKeyCount.compareAndSet(ACTIVE_KEY_COUNT_NOT_TRACKED, 0);
493+
}
494+
484495
/**
485496
* Decrements activeKeyCount. If the count is already at 0, this indicates drift
486497
* (more deletes than creates), so we invalidate to {@link #ACTIVE_KEY_COUNT_NOT_TRACKED}
@@ -496,22 +507,26 @@ public boolean decrementActiveKeyCount() {
496507
/**
497508
* Increments activeKeyCount for a batch PUT, skipping speculative execution duplicates
498509
* (key &lt;= last key). Only PUTs call this — DELETEs are tombstones and excluded.
499-
* First call initializes count from {@link #ACTIVE_KEY_COUNT_NOT_TRACKED} to 1. Checkpoint-safe: partial counts are persisted.
510+
* Requires prior {@link #initializeActiveKeyCount()} at SOP; skips if not initialized
511+
* (mid-batch config enablement). Checkpoint-safe: partial counts are persisted.
500512
*/
501513
public void incrementActiveKeyCountForBatchRecord(byte[] keyBytes) {
514+
if (activeKeyCount.get() < 0) {
515+
return; // Not initialized at SOP — mid-batch config enablement, skip
516+
}
502517
if (lastBatchKeyForDedup != null && ArrayUtils.compareUnsigned(keyBytes, lastBatchKeyForDedup) <= 0) {
503518
return; // Speculative execution duplicate — key order went backwards
504519
}
505520
lastBatchKeyForDedup = keyBytes;
506-
activeKeyCount.compareAndSet(ACTIVE_KEY_COUNT_NOT_TRACKED, 0);
507521
activeKeyCount.incrementAndGet();
508522
}
509523

510-
/** Called at EOP. Sets {@link #ACTIVE_KEY_COUNT_NOT_TRACKED} to 0 for empty partitions so the metric reports 0. */
511-
public void finalizeActiveKeyCountForBatchPush() {
512-
if (activeKeyCount.get() == ACTIVE_KEY_COUNT_NOT_TRACKED) {
513-
activeKeyCount.set(0);
514-
}
524+
/**
525+
* Called at EOP. Releases dedup state. Empty partitions are already at 0 from
526+
* {@link #initializeActiveKeyCount()} at SOP. If SOP was missed (mid-batch config enablement),
527+
* the count stays at {@link #ACTIVE_KEY_COUNT_NOT_TRACKED} — no partial baseline is created.
528+
*/
529+
public void cleanupBatchKeyCountState() {
515530
lastBatchKeyForDedup = null; // Release reference; dedup is only needed during batch
516531
}
517532

clients/da-vinci-client/src/main/java/com/linkedin/davinci/kafka/consumer/StoreIngestionTask.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3793,6 +3793,10 @@ private void processStartOfPush(
37933793
beginBatchWrite(persistedStoreVersionState.sorted, partitionConsumptionState);
37943794
partitionConsumptionState.setStartOfPushTimestamp(startOfPushKME.producerMetadata.messageTimestamp);
37953795

3796+
// Initialize active key count at SOP
3797+
if (serverConfig.isActiveKeyCountForAllBatchPushEnabled()) {
3798+
partitionConsumptionState.initializeActiveKeyCount();
3799+
}
37963800
}
37973801

37983802
@VisibleForTesting
@@ -3872,11 +3876,11 @@ protected void processEndOfPush(
38723876
*/
38733877
partitionConsumptionState.finalizeExpectedChecksum();
38743878

3875-
// Finalize batch key count (sets ACTIVE_KEY_COUNT_NOT_TRACKED→0 for empty partitions).
3879+
// Release batch key count dedup state (no longer needed after EOP).
38763880
if (serverConfig.isActiveKeyCountForAllBatchPushEnabled()) {
3877-
partitionConsumptionState.finalizeActiveKeyCountForBatchPush();
3881+
partitionConsumptionState.cleanupBatchKeyCountState();
38783882
LOGGER.info(
3879-
"Active key count finalized at EOP for replica: {}, activeKeyCount: {}",
3883+
"Active key count at EOP for replica: {}, activeKeyCount: {}",
38803884
partitionConsumptionState.getReplicaId(),
38813885
partitionConsumptionState.getActiveKeyCount());
38823886
}

clients/da-vinci-client/src/test/java/com/linkedin/davinci/kafka/consumer/ActiveKeyCountScenarioTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public void testCrashRecoveryWithConfigCombinations(
133133
String desc) {
134134
PartitionConsumptionState pcs = freshPcs();
135135
if (batchCountingEnabled) {
136+
pcs.initializeActiveKeyCount(); // SOP
136137
for (int i = 0; i < 30; i++) {
137138
pcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(i));
138139
}
@@ -143,7 +144,7 @@ public void testCrashRecoveryWithConfigCombinations(
143144
for (int i = 30; i < 50; i++) {
144145
restarted.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(i));
145146
}
146-
restarted.finalizeActiveKeyCountForBatchPush();
147+
restarted.cleanupBatchKeyCountState();
147148
assertEquals(restarted.getActiveKeyCount(), 50L, desc);
148149
} else {
149150
assertEquals(restarted.getActiveKeyCount(), -1L, desc);

clients/da-vinci-client/src/test/java/com/linkedin/davinci/kafka/consumer/ActiveKeyCountTest.java

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -295,15 +295,19 @@ public Object[][] followerSignalSkippedCases() {
295295
public void testBatchKeyCountAndFinalize() {
296296
PartitionConsumptionState localPcs = freshPcs();
297297
assertEquals(localPcs.getActiveKeyCount(), -1L);
298+
localPcs.initializeActiveKeyCount();
299+
assertEquals(localPcs.getActiveKeyCount(), 0L);
298300
for (int i = 0; i < 5; i++) {
299301
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(i));
300302
}
301303
assertEquals(localPcs.getActiveKeyCount(), 5L);
302-
localPcs.finalizeActiveKeyCountForBatchPush();
304+
localPcs.cleanupBatchKeyCountState();
303305
assertEquals(localPcs.getActiveKeyCount(), 5L);
304-
// Empty batch finalize yields 0
306+
// Empty batch: initializeActiveKeyCount at SOP sets -1→0, finalize is a no-op
305307
PartitionConsumptionState emptyPcs = freshPcs();
306-
emptyPcs.finalizeActiveKeyCountForBatchPush();
308+
emptyPcs.initializeActiveKeyCount();
309+
assertEquals(emptyPcs.getActiveKeyCount(), 0L);
310+
emptyPcs.cleanupBatchKeyCountState();
307311
assertEquals(emptyPcs.getActiveKeyCount(), 0L);
308312
// Verify increment/decrement works post-finalize (RT signals)
309313
localPcs.incrementActiveKeyCount();
@@ -315,6 +319,7 @@ public void testBatchKeyCountAndFinalize() {
315319
@Test
316320
public void testBatchDedupSkipsDuplicateKeys() {
317321
PartitionConsumptionState localPcs = freshPcs();
322+
localPcs.initializeActiveKeyCount();
318323
for (int i = 0; i < 5; i++) {
319324
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(i));
320325
}
@@ -335,14 +340,15 @@ public void testBatchDedupSkipsDuplicateKeys() {
335340
@Test
336341
public void testFinalizeDoesNotOverwriteRTSignals() {
337342
PartitionConsumptionState localPcs = freshPcs();
343+
localPcs.initializeActiveKeyCount();
338344
for (int i = 0; i < 10; i++) {
339345
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(i));
340346
}
341-
localPcs.finalizeActiveKeyCountForBatchPush();
347+
localPcs.cleanupBatchKeyCountState();
342348
localPcs.incrementActiveKeyCount(); // RT adjustment -> 11
343349
assertEquals(localPcs.getActiveKeyCount(), 11L);
344350
// Second finalize (e.g., from duplicate EOP) does NOT overwrite RT signals
345-
localPcs.finalizeActiveKeyCountForBatchPush();
351+
localPcs.cleanupBatchKeyCountState();
346352
assertEquals(localPcs.getActiveKeyCount(), 11L);
347353
}
348354

@@ -790,9 +796,9 @@ public void testProcessEndOfPush() throws Exception {
790796
kme.producerMetadata.messageTimestamp = System.currentTimeMillis();
791797
sitMock.processEndOfPush(kme, mock(PubSubPosition.class), mockPcs, new EndOfPush());
792798
if (enabled) {
793-
verify(mockPcs).finalizeActiveKeyCountForBatchPush();
799+
verify(mockPcs).cleanupBatchKeyCountState();
794800
} else {
795-
verify(mockPcs, never()).finalizeActiveKeyCountForBatchPush();
801+
verify(mockPcs, never()).cleanupBatchKeyCountState();
796802
}
797803
}
798804
}
@@ -991,12 +997,51 @@ public void testDecrementUnderflowInvalidatesAndPreservesInvalidState() {
991997
public void testDecrementUnderflowAfterEmptyBatch() {
992998
PartitionConsumptionState localPcs = freshPcs();
993999

994-
// Simulate empty batch push → finalize sets -1 to 0
995-
localPcs.finalizeActiveKeyCountForBatchPush();
1000+
// Simulate empty batch push: SOP initializes -1→0, finalize is no-op
1001+
localPcs.initializeActiveKeyCount();
9961002
assertEquals(localPcs.getActiveKeyCount(), 0);
9971003

9981004
// RT DELETE signal on empty batch: underflow invalidates to -1 (drift)
9991005
Assert.assertFalse(localPcs.decrementActiveKeyCount());
10001006
assertEquals(localPcs.getActiveKeyCount(), -1);
10011007
}
1008+
1009+
@Test
1010+
public void testMidBatchConfigEnablementSkipsCounting() {
1011+
// Simulate: PCS restored from checkpoint mid-batch (SOP already processed without config ON)
1012+
// activeKeyCount is ACTIVE_KEY_COUNT_NOT_TRACKED (-1) because SOP didn't initialize it
1013+
PartitionConsumptionState localPcs = freshPcs();
1014+
assertEquals(localPcs.getActiveKeyCount(), -1);
1015+
1016+
// Batch records arrive with config now ON, but initializeActiveKeyCount was never called (SOP missed)
1017+
// incrementActiveKeyCountForBatchRecord should skip (count stays -1)
1018+
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(0));
1019+
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(1));
1020+
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(2));
1021+
assertEquals(localPcs.getActiveKeyCount(), -1);
1022+
1023+
// EOP finalize: count stays -1 (no partial baseline created). Finalize only releases
1024+
// dedup state; it does not set -1→0 since initializeActiveKeyCount was never called at SOP.
1025+
localPcs.cleanupBatchKeyCountState();
1026+
assertEquals(localPcs.getActiveKeyCount(), -1);
1027+
}
1028+
1029+
@Test
1030+
public void testInitializeActiveKeyCountIdempotent() {
1031+
PartitionConsumptionState localPcs = freshPcs();
1032+
assertEquals(localPcs.getActiveKeyCount(), -1);
1033+
1034+
// First init: -1 → 0
1035+
localPcs.initializeActiveKeyCount();
1036+
assertEquals(localPcs.getActiveKeyCount(), 0);
1037+
1038+
// Count some records
1039+
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(0));
1040+
localPcs.incrementActiveKeyCountForBatchRecord(ActiveKeyCountTestUtils.sortedKeyBytes(1));
1041+
assertEquals(localPcs.getActiveKeyCount(), 2);
1042+
1043+
// Second init (e.g., duplicate SOP): no-op because compareAndSet(-1, 0) fails (count is 2)
1044+
localPcs.initializeActiveKeyCount();
1045+
assertEquals(localPcs.getActiveKeyCount(), 2);
1046+
}
10021047
}

clients/da-vinci-client/src/test/java/com/linkedin/davinci/kafka/consumer/ActiveKeyCountTestUtils.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,12 @@ static OffsetRecord checkpoint(PartitionConsumptionState pcs) {
7979

8080
/** Simulates a non-chunked batch push of {@code count} records followed by finalization. */
8181
static void doBatch(PartitionConsumptionState pcs, int count) {
82+
pcs.initializeActiveKeyCount(); // SOP initialization
8283
for (int i = 0; i < count; i++) {
8384
// Synthetic sorted key bytes (ascending order) to pass dedup check
8485
pcs.incrementActiveKeyCountForBatchRecord(sortedKeyBytes(i));
8586
}
86-
pcs.finalizeActiveKeyCountForBatchPush();
87+
pcs.cleanupBatchKeyCountState();
8788
}
8889

8990
/**
@@ -92,12 +93,13 @@ static void doBatch(PartitionConsumptionState pcs, int count) {
9293
* Chunk fragment filtering itself is tested via reflection in ActiveKeyCountTest.
9394
*/
9495
static void doBatchChunked(PartitionConsumptionState pcs, int logicalKeyCount) {
96+
pcs.initializeActiveKeyCount(); // SOP initialization
9597
for (int key = 0; key < logicalKeyCount; key++) {
9698
// Chunk fragments: schemaId == CHUNK -> filtered out by processKafkaDataMessage, NOT counted
9799
// Manifest: schemaId == CHUNKED_VALUE_MANIFEST -> passes filter, counted
98100
pcs.incrementActiveKeyCountForBatchRecord(sortedKeyBytes(key));
99101
}
100-
pcs.finalizeActiveKeyCountForBatchPush();
102+
pcs.cleanupBatchKeyCountState();
101103
}
102104

103105
/** Generates key bytes in ascending unsigned byte order for test dedup checks. */

internal/venice-test-common/src/integrationTest/java/com/linkedin/venice/endToEnd/TestActiveKeyCount.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,8 +671,8 @@ public void testBothExactAndHllCountsWorkSimultaneously() throws Exception {
671671

672672
/**
673673
* Validates that an empty batch push (0 user records) results in an active key count of 0,
674-
* not -1 (untracked). The finalizeActiveKeyCountForBatchPush at EOP transitions -1 to 0
675-
* for empty partitions.
674+
* not -1 (untracked). The initializeActiveKeyCount at SOP transitions -1 to 0,
675+
* and empty partitions stay at 0 through EOP.
676676
*/
677677
@Test(timeOut = TEST_TIMEOUT)
678678
public void testEmptyBatchPushGetsZeroKeyCount() throws Exception {

0 commit comments

Comments
 (0)