Skip to content

Commit ffcdc2f

Browse files
committed
address review comments: invalidation, metrics, and correctness fixes
- Corrupt/multi-byte kcs signal invalidates count instead of silently ignoring - Strict length==1 check on follower signal header (rejects multi-byte) - Decrement underflow invalidates to ACTIVE_KEY_COUNT_NOT_TRACKED (not clamp to 0) - keyExists failure in Tier 3 catches VeniceException, invalidates, returns false - Leader propagates KEY_COUNT_INVALIDATE_SIGNAL (value=0) to followers on invalidation - Follower handles invalidate signal, corrupt signal, and underflow consistently - Added ACTIVE_KEY_COUNT_NOT_TRACKED constant, replaced all inline -1 sentinels - Added OTel COUNTER metric (ACTIVE_KEY_COUNT_INVALIDATION) via AggVersionedIngestionStats - Added Tehuti AsyncGauge (active_key_count) and LongAdderRateGauge (active_key_count_invalidation) - Added activeKeyCountBeforeAliveCheck snapshot for mid-record invalidation detection - Rate-limited all error logs via REDUNDANT_LOGGING_FILTER - Updated all Javadocs to reflect 3 signal types (+1/-1/0) and invalidation semantics - Added tests: underflow, invalidate signal, multi-byte, keyExists failure, OTel gauge after invalidation
1 parent dd0459b commit ffcdc2f

16 files changed

Lines changed: 373 additions & 47 deletions

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

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.linkedin.davinci.kafka.consumer.AggKafkaConsumerService.getKeyLevelLockMaxPoolSizeBasedOnServerConfig;
44
import static com.linkedin.davinci.kafka.consumer.LeaderFollowerStateType.LEADER;
5+
import static com.linkedin.davinci.kafka.consumer.PartitionConsumptionState.ACTIVE_KEY_COUNT_NOT_TRACKED;
56
import static com.linkedin.venice.VeniceConstants.REWIND_TIME_DECIDED_BY_SERVER;
67
import static com.linkedin.venice.writer.VeniceWriter.APP_DEFAULT_LOGICAL_TS;
78

@@ -110,14 +111,19 @@ public class ActiveActiveStoreIngestionTask extends LeaderFollowerStoreIngestion
110111
private final boolean addRmdToBatchPushForHybridStores;
111112
/** @see ConfigKeys#SERVER_ACTIVE_KEY_COUNT_FOR_HYBRID_STORE_ENABLED */
112113
private final boolean activeKeyCountForHybridStoreEnabled;
114+
/** @see #computeActiveKeyCountSignal for signal semantics. */
113115
static final byte KEY_CREATED_SIGNAL_VALUE = 1;
114116
static final byte KEY_DELETED_SIGNAL_VALUE = -1;
117+
static final byte KEY_COUNT_INVALIDATE_SIGNAL_VALUE = 0;
115118
static final PubSubMessageHeader KEY_CREATED_SIGNAL =
116119
new PubSubMessageHeader(StoreIngestionTask.KEY_COUNT_SIGNAL_HEADER, new byte[] { KEY_CREATED_SIGNAL_VALUE });
117120
static final PubSubMessageHeader KEY_DELETED_SIGNAL =
118121
new PubSubMessageHeader(StoreIngestionTask.KEY_COUNT_SIGNAL_HEADER, new byte[] { KEY_DELETED_SIGNAL_VALUE });
122+
static final PubSubMessageHeader KEY_COUNT_INVALIDATE_SIGNAL = new PubSubMessageHeader(
123+
StoreIngestionTask.KEY_COUNT_SIGNAL_HEADER,
124+
new byte[] { KEY_COUNT_INVALIDATE_SIGNAL_VALUE });
119125

120-
/** RMD bytes (ts=0) with superset schema ID prepended. For chunk manifests and DELETEs. */
126+
/** RMD bytes (ts=0) with superset schema ID prepended. For chunk manifests (superset schema ID known at construction). */
121127
private final byte[] defaultBatchRmdWithSchemaIdPrefix;
122128
/** RMD bytes (ts=0) without schema ID prefix. For non-chunked PUTs (schema ID varies). */
123129
private final byte[] defaultBatchRmdBytes;
@@ -619,6 +625,7 @@ private PubSubMessageProcessedResult processActiveActiveMessage(
619625
oldValueProvider,
620626
oldValueByteBufferProvider,
621627
false,
628+
ACTIVE_KEY_COUNT_NOT_TRACKED, // ignored update — no signal computed
622629
rmdWithValueSchemaID,
623630
valueManifestContainer,
624631
null,
@@ -653,6 +660,10 @@ private PubSubMessageProcessedResult processActiveActiveMessage(
653660
final ByteBuffer updatedRmdBytes =
654661
rmdSerDe.serializeRmdRecord(mergeConflictResult.getValueSchemaId(), mergeConflictResult.getRmdRecord());
655662

663+
// Snapshot count before wasOldValueAlive, which may invalidate it on keyExists failure.
664+
// Used by computeActiveKeyCountSignal to detect mid-record invalidation and propagate to followers.
665+
long activeKeyCountBeforeAliveCheck = partitionConsumptionState.getActiveKeyCount();
666+
656667
// Must be captured before setTransientRecord() below, which overwrites the transient cache.
657668
boolean oldValueAlive =
658669
wasOldValueAlive(rmdWithValueSchemaID, oldValueByteBufferProvider, partitionConsumptionState, keyBytes);
@@ -680,6 +691,7 @@ private PubSubMessageProcessedResult processActiveActiveMessage(
680691
oldValueProvider,
681692
oldValueByteBufferProvider,
682693
oldValueAlive,
694+
activeKeyCountBeforeAliveCheck,
683695
rmdWithValueSchemaID,
684696
valueManifestContainer,
685697
updatedValueBytes,
@@ -867,21 +879,40 @@ private ByteBufferValueRecord<ByteBuffer> getValueBytesForKey(
867879
}
868880

869881
/**
870-
* Computes the active key count signal (+1/-1/none) and returns the VT headers to propagate
871-
* to followers. Updates the PCS active key count directly on the leader.
882+
* Computes the active key count signal and returns the VT headers to propagate to followers.
883+
* Updates the PCS active key count directly on the leader.
884+
*
885+
* <p>Three signal types:
886+
* <ul>
887+
* <li>{@code +1} (KEY_CREATED_SIGNAL): dead→alive transition, count incremented.</li>
888+
* <li>{@code -1} (KEY_DELETED_SIGNAL): alive→dead transition, count decremented.</li>
889+
* <li>{@code 0} (KEY_COUNT_INVALIDATE_SIGNAL): count invalidated — emitted when decrement
890+
* underflows (count was 0, drift detected) or when the count was invalidated mid-record
891+
* (e.g., keyExists failure in {@link #isValuePresentForKey}). Tells followers to also
892+
* invalidate. We choose to report {@link PartitionConsumptionState#ACTIVE_KEY_COUNT_NOT_TRACKED}
893+
* rather than continue publishing an inaccurate count — a missing metric is better
894+
* than a wrong one.</li>
895+
* </ul>
872896
*
873897
* <p>Returns {@link EmptyPubSubMessageHeaders#SINGLETON} when no signal is needed (feature
874-
* disabled, no batch baseline, or alive-to-alive/dead-to-dead transition). Returns a freshly
875-
* created {@link PubSubMessageHeaders} with the "kcs" signal header when the key's alive/dead
876-
* state changes. The fresh-per-record creation is safe even though
877-
* {@code VeniceWriter.sendMessage} may mutate the headers (e.g., adding a view-partition
878-
* header) — each record gets its own instance.
898+
* disabled, count already invalidated before this record, or alive-to-alive/dead-to-dead
899+
* transition). Returns a freshly created {@link PubSubMessageHeaders} for each signal — safe
900+
* even though {@code VeniceWriter.sendMessage} may mutate headers.
879901
*/
880902
private PubSubMessageHeaders computeActiveKeyCountSignal(
881903
MergeConflictResultWrapper mergeConflictResultWrapper,
882904
MergeConflictResult mergeConflictResult,
883905
PartitionConsumptionState partitionConsumptionState) {
884-
if (!activeKeyCountForHybridStoreEnabled || partitionConsumptionState.getActiveKeyCount() < 0) {
906+
if (!activeKeyCountForHybridStoreEnabled) {
907+
return EmptyPubSubMessageHeaders.SINGLETON;
908+
}
909+
long currentCount = partitionConsumptionState.getActiveKeyCount();
910+
if (currentCount < 0) {
911+
// Count was invalidated. If it was valid before this record's wasOldValueAlive call,
912+
// the invalidation happened mid-record (e.g., keyExists failure). Propagate to followers.
913+
if (mergeConflictResultWrapper.getActiveKeyCountBeforeAliveCheck() >= 0) {
914+
return new PubSubMessageHeaders().add(KEY_COUNT_INVALIDATE_SIGNAL);
915+
}
885916
return EmptyPubSubMessageHeaders.SINGLETON;
886917
}
887918
boolean wasAlive = mergeConflictResultWrapper.wasOldValueAlive();
@@ -890,8 +921,14 @@ private PubSubMessageHeaders computeActiveKeyCountSignal(
890921
partitionConsumptionState.incrementActiveKeyCount();
891922
return new PubSubMessageHeaders().add(KEY_CREATED_SIGNAL);
892923
} else if (wasAlive && !isAlive) {
893-
partitionConsumptionState.decrementActiveKeyCount();
894-
return new PubSubMessageHeaders().add(KEY_DELETED_SIGNAL);
924+
if (partitionConsumptionState.decrementActiveKeyCount()) {
925+
return new PubSubMessageHeaders().add(KEY_DELETED_SIGNAL);
926+
}
927+
// Underflow detected (count was 0 but got a delete) — count drifted, now invalidated to
928+
// ACTIVE_KEY_COUNT_NOT_TRACKED.
929+
aggVersionedIngestionStats.recordActiveKeyCountInvalidation(storeName, versionNumber);
930+
getHostLevelIngestionStats().recordActiveKeyCountInvalidation();
931+
return new PubSubMessageHeaders().add(KEY_COUNT_INVALIDATE_SIGNAL);
895932
}
896933
return EmptyPubSubMessageHeaders.SINGLETON;
897934
}
@@ -921,7 +958,9 @@ private PubSubMessageHeaders computeActiveKeyCountSignal(
921958
* is the only branch that adds a net-new RocksDB read.</li>
922959
* <li>{@code rmd.ts==0} (batch sentinel): batch PUT, never RT-written — alive.
923960
* <b>Value CF lookup: NO.</b> Residual edge case: reprocessing PUT then DELETE for
924-
* same key during batch leaves stale ts=0 RMD (rare).</li>
961+
* same key during batch leaves stale ts=0 RMD (rare). Impact: at most one incorrect
962+
* +1 or -1 signal per affected key, bounded by the number of batch-PUT-then-DELETE
963+
* sequences for the same key in a single batch push.</li>
925964
* <li>{@code rmd.ts>0}: previously RT-written, could be alive or dead — must check.
926965
* <b>Value CF lookup:</b> Tier 1 (DCR cached) for field-level/tie/UPDATE — free.
927966
* Tier 2 (transient cache) for value-level new-wins — typically free.
@@ -973,8 +1012,8 @@ private boolean wasOldValueAlive(
9731012
* </ol>
9741013
*
9751014
* <p>In steady state with recommended config (addRmdToBatchPush ON), Tier 3 is rarely
976-
* reached: branch 1 (new keys) and branch 3 (batch keys with ts=0) avoid this method
977-
* entirely, and branch 4 (RT keys) typically hits Tier 1 or Tier 2.
1015+
* reached: new keys with rmd==null and batch keys with ts=0 sentinel avoid this method
1016+
* entirely, and RT keys (rmd.ts&gt;0) typically hit Tier 1 or Tier 2.
9781017
*/
9791018
private boolean isValuePresentForKey(
9801019
Lazy<ByteBuffer> oldValueByteBufferProvider,
@@ -989,11 +1028,25 @@ private boolean isValuePresentForKey(
9891028
if (transientRecord != null) {
9901029
return transientRecord.getValue() != null;
9911030
}
992-
// Tier 3: Storage engine existence check (bloom filter → disk if needed).
1031+
// Tier 3: Storage engine existence check (RocksDB disk read).
9931032
// For chunked stores, the actual RocksDB key has a chunking suffix appended.
9941033
byte[] storageKey =
9951034
isChunked() ? ChunkingUtils.KEY_WITH_CHUNKING_SUFFIX_SERIALIZER.serializeNonChunkedKey(key) : key;
996-
return storageEngine.keyExists(partitionConsumptionState.getPartition(), storageKey);
1035+
try {
1036+
return storageEngine.keyExists(partitionConsumptionState.getPartition(), storageKey);
1037+
} catch (VeniceException e) {
1038+
// A transient RocksDB I/O failure must not halt ingestion. Invalidate the count so we stop
1039+
// publishing a wrong number, and return false (assume key absent) to skip the signal.
1040+
partitionConsumptionState.setActiveKeyCount(ACTIVE_KEY_COUNT_NOT_TRACKED);
1041+
aggVersionedIngestionStats.recordActiveKeyCountInvalidation(storeName, versionNumber);
1042+
getHostLevelIngestionStats().recordActiveKeyCountInvalidation();
1043+
String msg =
1044+
"keyExists failed for replica " + partitionConsumptionState.getReplicaId() + "; invalidating activeKeyCount.";
1045+
if (!REDUNDANT_LOGGING_FILTER.isRedundantException(msg)) {
1046+
LOGGER.error(msg, e);
1047+
}
1048+
return false;
1049+
}
9971050
}
9981051

9991052
ByteBuffer getCurrentValueFromTransientRecord(PartitionConsumptionState.TransientRecord transientRecord) {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class MergeConflictResultWrapper {
2323

2424
/** Whether a live value existed before this write's conflict resolution (for key count signal). */
2525
private final boolean oldValueAlive;
26+
/** Snapshot of activeKeyCount before wasOldValueAlive ran. Used to detect mid-record invalidation. */
27+
private final long activeKeyCountBeforeAliveCheck;
2628

2729
// Serialized and potentially compressed updated value bytes
2830
private final ByteBuffer updatedValueBytes;
@@ -39,6 +41,7 @@ public MergeConflictResultWrapper(
3941
Lazy<ByteBufferValueRecord<ByteBuffer>> oldValueProvider,
4042
Lazy<ByteBuffer> oldValueByteBufferProvider,
4143
boolean oldValueAlive,
44+
long activeKeyCountBeforeAliveCheck,
4245
RmdWithValueSchemaId oldRmdWithValueSchemaId,
4346
ChunkedValueManifestContainer oldValueManifestContainer,
4447
ByteBuffer updatedValueBytes,
@@ -48,6 +51,7 @@ public MergeConflictResultWrapper(
4851
this.oldValueProvider = oldValueProvider;
4952
this.oldValueByteBufferProvider = oldValueByteBufferProvider;
5053
this.oldValueAlive = oldValueAlive;
54+
this.activeKeyCountBeforeAliveCheck = activeKeyCountBeforeAliveCheck;
5155
this.oldRmdWithValueSchemaId = oldRmdWithValueSchemaId;
5256
this.oldValueManifestContainer = oldValueManifestContainer;
5357
this.updatedValueBytes = updatedValueBytes;
@@ -87,6 +91,11 @@ public boolean wasOldValueAlive() {
8791
return oldValueAlive;
8892
}
8993

94+
/** Returns the activeKeyCount snapshot taken before wasOldValueAlive ran (for invalidation detection). */
95+
public long getActiveKeyCountBeforeAliveCheck() {
96+
return activeKeyCountBeforeAliveCheck;
97+
}
98+
9099
public RmdWithValueSchemaId getOldRmdWithValueSchemaId() {
91100
return oldRmdWithValueSchemaId;
92101
}

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,17 @@ enum LatchStatus {
344344
*/
345345
private final Map<String, HeartbeatKey> cachedHeartbeatKeys;
346346

347+
/** Sentinel value indicating the active key count is not tracked or has been invalidated. */
348+
public static final long ACTIVE_KEY_COUNT_NOT_TRACKED = -1;
349+
347350
/**
348351
* Active logical key count. Batch phase: incremented per logical PUT. RT phase (hybrid A/A):
349-
* adjusted by +1/-1 signals from conflict resolution. -1 = not tracked (no batch baseline);
350-
* RT signals are skipped when -1. AtomicLong for AA/WC parallel processing.
352+
* adjusted by +1/-1 signals from conflict resolution. {@link #ACTIVE_KEY_COUNT_NOT_TRACKED} = not tracked
353+
* (no batch baseline); RT signals are skipped when not tracked. AtomicLong for AA/WC parallel processing.
351354
*/
352-
private final AtomicLong activeKeyCount = new AtomicLong(-1);
353-
/** Last PUT key for batch dedup. Not persisted. @see #incrementActiveKeyCountForBatchRecord */
355+
private final AtomicLong activeKeyCount = new AtomicLong(ACTIVE_KEY_COUNT_NOT_TRACKED);
356+
/** Last PUT key for batch dedup. Not persisted; used by {@link #incrementActiveKeyCountForBatchRecord}.
357+
* Single-threaded: only accessed from the drainer thread during batch (pre-EOP). */
354358
private byte[] lastBatchKeyForDedup;
355359

356360
/** Lazily allocated per-partition detector for partial-update amplification. */
@@ -471,33 +475,41 @@ public void incrementActiveKeyCount() {
471475
activeKeyCount.incrementAndGet();
472476
}
473477

474-
public void decrementActiveKeyCount() {
475-
activeKeyCount.decrementAndGet();
478+
/**
479+
* Decrements activeKeyCount. If the count is already at 0, this indicates drift
480+
* (more deletes than creates), so we invalidate to {@link #ACTIVE_KEY_COUNT_NOT_TRACKED}
481+
* rather than continue tracking with a wrong baseline. If already not tracked, stays not tracked.
482+
*
483+
* @return true if the count was successfully decremented, false if invalidated or already invalid
484+
*/
485+
public boolean decrementActiveKeyCount() {
486+
long prev = activeKeyCount.getAndUpdate(v -> v > 0 ? v - 1 : ACTIVE_KEY_COUNT_NOT_TRACKED);
487+
return prev > 0;
476488
}
477489

478490
/**
479491
* Increments activeKeyCount for a batch PUT, skipping speculative execution duplicates
480492
* (key &lt;= last key). Only PUTs call this — DELETEs are tombstones and excluded.
481-
* First call initializes count from -1 to 1. Checkpoint-safe: partial counts are persisted.
493+
* First call initializes count from {@link #ACTIVE_KEY_COUNT_NOT_TRACKED} to 1. Checkpoint-safe: partial counts are persisted.
482494
*/
483495
public void incrementActiveKeyCountForBatchRecord(byte[] keyBytes) {
484496
if (lastBatchKeyForDedup != null && ArrayUtils.compareUnsigned(keyBytes, lastBatchKeyForDedup) <= 0) {
485497
return; // Speculative execution duplicate — key order went backwards
486498
}
487499
lastBatchKeyForDedup = keyBytes;
488-
activeKeyCount.compareAndSet(-1, 0);
500+
activeKeyCount.compareAndSet(ACTIVE_KEY_COUNT_NOT_TRACKED, 0);
489501
activeKeyCount.incrementAndGet();
490502
}
491503

492-
/** Called at EOP. Sets -1 to 0 for empty partitions so the metric reports 0. */
504+
/** Called at EOP. Sets {@link #ACTIVE_KEY_COUNT_NOT_TRACKED} to 0 for empty partitions so the metric reports 0. */
493505
public void finalizeActiveKeyCountForBatchPush() {
494-
if (activeKeyCount.get() == -1) {
506+
if (activeKeyCount.get() == ACTIVE_KEY_COUNT_NOT_TRACKED) {
495507
activeKeyCount.set(0);
496508
}
497509
lastBatchKeyForDedup = null; // Release reference; dedup is only needed during batch
498510
}
499511

500-
// --- HLL unique ingested key count methods (from main) ---
512+
// --- HLL unique ingested key count methods ---
501513

502514
private void restoreUniqueKeyCountHllFromCheckpoint(int lgK, ByteBuffer hllBytes) {
503515
byte[] bytes = new byte[hllBytes.remaining()];

0 commit comments

Comments
 (0)