22
33import static com .linkedin .davinci .kafka .consumer .AggKafkaConsumerService .getKeyLevelLockMaxPoolSizeBasedOnServerConfig ;
44import static com .linkedin .davinci .kafka .consumer .LeaderFollowerStateType .LEADER ;
5+ import static com .linkedin .davinci .kafka .consumer .PartitionConsumptionState .ACTIVE_KEY_COUNT_NOT_TRACKED ;
56import static com .linkedin .venice .VeniceConstants .REWIND_TIME_DECIDED_BY_SERVER ;
67import 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>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 ) {
0 commit comments