Skip to content

Commit d81d23e

Browse files
authored
[server][protocol] Add active unique key count tracking for store versions (linkedin#2676)
Introduces an exact per-partition active key count and exposed via OTel and Tehuti metrics. The count is built in three stages: a batch baseline that counts all valid PUTs during ingestion, an optional RMD for batch push for hybrid stores to distinguish batch keys without extra reads, and hybrid ingestion continuing to build on top of batch baseline. This design ensures accuracy while minimizing I/O by avoiding redundant value column family reads and delegating computation primarily to the leader. - Exact per-partition count via `AtomicLong` in `PartitionConsumptionState` - Persisted with offsets in `PartitionState v22` (PR `linkedin#2761`) - Metrics: - OTel `ASYNC_GAUGE` → `ingestion.key.active_count` - Tehuti `AsyncGauge` → `active_key_count` # Approach - Controlled by 3 configs ## Batch RMD (Hybrid) - Config: `server.add.rmd.to.batch.push.for.hybrid.stores` - Add `ts=0` RMD for batch `PUT`s only - Helps with zero-I/O detection of batch keys ## Batch Baseline - Config: `server.active.key.count.for.all.batch.push.enabled` - Count `PUT`s during SOP→EOP - Filters: chunk fragments + duplicates - SOP: `-1 → 0`; missed SOP ⇒ stays `-1` - Persist via `syncOffset()` ## RT Signals - Config: `server.active.key.count.for.hybrid.store.enabled` - Leader computes alive state change - Emits `+1/-1/0` via `"kcs"` header - Followers mirror without DCR or value lookup # Key Decisions - `ACTIVE_KEY_COUNT_NOT_TRACKED = -1` sentinel - Any inconsistency ⇒ invalidate the counting for that version - `decrementActiveKeyCount()` detects underflow - `keyExists` failure ⇒ invalidate, continue ingestion - Batch `DELETE` has no `ts=0` RMD - 3-tier lookup: cache → transient → `keyExists`
1 parent 6574798 commit d81d23e

33 files changed

Lines changed: 3762 additions & 44 deletions

File tree

build.gradle

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ subprojects {
329329
// when actually using the new protocol. Example to pin KME to v12 when introducing v13:
330330
// project(':internal:venice-common').file('src/main/resources/avro/KafkaMessageEnvelope/v12', PathValidation.DIRECTORY)
331331
def versionOverrides = [
332-
project(':internal:venice-common').file('src/main/resources/avro/PartitionState/v21', PathValidation.DIRECTORY)
333332
]
334333

335334
def schemaDirs = [sourceDir]

clients/da-vinci-client/src/main/java/com/linkedin/davinci/config/VeniceServerConfig.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,15 @@
6969
import static com.linkedin.venice.ConfigKeys.SERVER_AA_WC_INGESTION_STORAGE_LOOKUP_THREAD_POOL_SIZE;
7070
import static com.linkedin.venice.ConfigKeys.SERVER_AA_WC_WORKLOAD_PARALLEL_PROCESSING_ENABLED;
7171
import static com.linkedin.venice.ConfigKeys.SERVER_AA_WC_WORKLOAD_PARALLEL_PROCESSING_THREAD_POOL_SIZE;
72+
import static com.linkedin.venice.ConfigKeys.SERVER_ACTIVE_KEY_COUNT_FOR_ALL_BATCH_PUSH_ENABLED;
73+
import static com.linkedin.venice.ConfigKeys.SERVER_ACTIVE_KEY_COUNT_FOR_HYBRID_STORE_ENABLED;
7274
import static com.linkedin.venice.ConfigKeys.SERVER_ADAPTIVE_THROTTLER_ENABLED;
7375
import static com.linkedin.venice.ConfigKeys.SERVER_ADAPTIVE_THROTTLER_MULTI_GET_LATENCY_THRESHOLD;
7476
import static com.linkedin.venice.ConfigKeys.SERVER_ADAPTIVE_THROTTLER_READ_COMPUTE_GET_LATENCY_THRESHOLD;
7577
import static com.linkedin.venice.ConfigKeys.SERVER_ADAPTIVE_THROTTLER_SIGNAL_IDLE_THRESHOLD;
7678
import static com.linkedin.venice.ConfigKeys.SERVER_ADAPTIVE_THROTTLER_SIGNAL_REFRESH_INTERVAL_IN_SECONDS;
7779
import static com.linkedin.venice.ConfigKeys.SERVER_ADAPTIVE_THROTTLER_SINGLE_GET_LATENCY_THRESHOLD;
80+
import static com.linkedin.venice.ConfigKeys.SERVER_ADD_RMD_TO_BATCH_PUSH_FOR_HYBRID_STORES;
7881
import static com.linkedin.venice.ConfigKeys.SERVER_BATCH_REPORT_END_OF_INCREMENTAL_PUSH_STATUS_ENABLED;
7982
import static com.linkedin.venice.ConfigKeys.SERVER_BLOB_TRANSFER_ADAPTIVE_THROTTLER_ENABLED;
8083
import static com.linkedin.venice.ConfigKeys.SERVER_BLOB_TRANSFER_ADAPTIVE_THROTTLER_UPDATE_PERCENTAGE;
@@ -722,6 +725,9 @@ public class VeniceServerConfig extends VeniceClusterConfig {
722725
private final int lagMonitorCleanupCycle;
723726
private final boolean readQuotaInitializationFallbackEnabled;
724727
private final boolean ingestionProgressLoggingEnabled;
728+
private final boolean addRmdToBatchPushForHybridStores;
729+
private final boolean activeKeyCountForAllBatchPushEnabled;
730+
private final boolean activeKeyCountForHybridStoreEnabled;
725731
private final int partialUpdateLargeResultLogThresholdBytes;
726732
private final long partialUpdateAmplificationReportIntervalMs;
727733

@@ -1246,6 +1252,12 @@ public VeniceServerConfig(VeniceProperties serverProperties, Map<String, Map<Str
12461252
this.readQuotaInitializationFallbackEnabled =
12471253
serverProperties.getBoolean(SERVER_READ_QUOTA_INITIALIZATION_FALLBACK_ENABLED, true);
12481254
this.ingestionProgressLoggingEnabled = serverProperties.getBoolean(POSITIONAL_PROGRESS_LOGGING_ENABLED, false);
1255+
this.addRmdToBatchPushForHybridStores =
1256+
serverProperties.getBoolean(SERVER_ADD_RMD_TO_BATCH_PUSH_FOR_HYBRID_STORES, false);
1257+
this.activeKeyCountForAllBatchPushEnabled =
1258+
serverProperties.getBoolean(SERVER_ACTIVE_KEY_COUNT_FOR_ALL_BATCH_PUSH_ENABLED, false);
1259+
this.activeKeyCountForHybridStoreEnabled =
1260+
serverProperties.getBoolean(SERVER_ACTIVE_KEY_COUNT_FOR_HYBRID_STORE_ENABLED, false);
12491261
this.partialUpdateLargeResultLogThresholdBytes =
12501262
serverProperties.getInt(PARTIAL_UPDATE_LARGE_RESULT_LOG_THRESHOLD_BYTES, 100 * 1024);
12511263
this.partialUpdateAmplificationReportIntervalMs =
@@ -2261,6 +2273,18 @@ public boolean isIngestionProgressLoggingEnabled() {
22612273
return ingestionProgressLoggingEnabled;
22622274
}
22632275

2276+
public boolean isAddRmdToBatchPushForHybridStoresEnabled() {
2277+
return addRmdToBatchPushForHybridStores;
2278+
}
2279+
2280+
public boolean isActiveKeyCountForAllBatchPushEnabled() {
2281+
return activeKeyCountForAllBatchPushEnabled;
2282+
}
2283+
2284+
public boolean isActiveKeyCountForHybridStoreEnabled() {
2285+
return activeKeyCountForHybridStoreEnabled;
2286+
}
2287+
22642288
public int getPartialUpdateLargeResultLogThresholdBytes() {
22652289
return partialUpdateLargeResultLogThresholdBytes;
22662290
}

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

Lines changed: 280 additions & 6 deletions
Large diffs are not rendered by default.

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ public class MergeConflictResultWrapper {
2121
private final RmdWithValueSchemaId oldRmdWithValueSchemaId;
2222
private final ChunkedValueManifestContainer oldValueManifestContainer;
2323

24+
/** Whether a live value existed before this write's conflict resolution (for key count signal). */
25+
private final boolean oldValueAlive;
26+
/** Snapshot of activeKeyCount before wasOldValueAlive ran. Used to detect mid-record invalidation. */
27+
private final long activeKeyCountBeforeAliveCheck;
28+
2429
// Serialized and potentially compressed updated value bytes
2530
private final ByteBuffer updatedValueBytes;
2631
private final ByteBuffer updatedRmdBytes;
@@ -35,6 +40,8 @@ public MergeConflictResultWrapper(
3540
MergeConflictResult mergeConflictResult,
3641
Lazy<ByteBufferValueRecord<ByteBuffer>> oldValueProvider,
3742
Lazy<ByteBuffer> oldValueByteBufferProvider,
43+
boolean oldValueAlive,
44+
long activeKeyCountBeforeAliveCheck,
3845
RmdWithValueSchemaId oldRmdWithValueSchemaId,
3946
ChunkedValueManifestContainer oldValueManifestContainer,
4047
ByteBuffer updatedValueBytes,
@@ -43,6 +50,8 @@ public MergeConflictResultWrapper(
4350
this.mergeConflictResult = mergeConflictResult;
4451
this.oldValueProvider = oldValueProvider;
4552
this.oldValueByteBufferProvider = oldValueByteBufferProvider;
53+
this.oldValueAlive = oldValueAlive;
54+
this.activeKeyCountBeforeAliveCheck = activeKeyCountBeforeAliveCheck;
4655
this.oldRmdWithValueSchemaId = oldRmdWithValueSchemaId;
4756
this.oldValueManifestContainer = oldValueManifestContainer;
4857
this.updatedValueBytes = updatedValueBytes;
@@ -77,6 +86,16 @@ public Lazy<ByteBuffer> getOldValueByteBufferProvider() {
7786
return oldValueByteBufferProvider;
7887
}
7988

89+
/** Captured before the transient record cache is updated, so it reflects the true pre-DCR state. */
90+
public boolean wasOldValueAlive() {
91+
return oldValueAlive;
92+
}
93+
94+
/** Returns the activeKeyCount snapshot taken before wasOldValueAlive ran (for invalidation detection). */
95+
public long getActiveKeyCountBeforeAliveCheck() {
96+
return activeKeyCountBeforeAliveCheck;
97+
}
98+
8099
public RmdWithValueSchemaId getOldRmdWithValueSchemaId() {
81100
return oldRmdWithValueSchemaId;
82101
}

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.linkedin.venice.pubsub.api.PubSubTopicPartition;
2222
import com.linkedin.venice.serialization.avro.AvroProtocolDefinition;
2323
import com.linkedin.venice.storage.protocol.ChunkedValueManifest;
24+
import com.linkedin.venice.utils.ArrayUtils;
2425
import com.linkedin.venice.utils.LatencyUtils;
2526
import com.linkedin.venice.utils.concurrent.VeniceConcurrentHashMap;
2627
import com.linkedin.venice.utils.lazy.Lazy;
@@ -34,6 +35,7 @@
3435
import java.util.concurrent.CompletableFuture;
3536
import java.util.concurrent.Future;
3637
import java.util.concurrent.TimeUnit;
38+
import java.util.concurrent.atomic.AtomicLong;
3739
import java.util.concurrent.atomic.AtomicReference;
3840
import java.util.function.BooleanSupplier;
3941
import org.apache.avro.generic.GenericRecord;
@@ -348,6 +350,16 @@ enum LatchStatus {
348350
*/
349351
private final Map<String, HeartbeatKey> cachedHeartbeatKeys;
350352

353+
/**
354+
* Active logical key count. Batch phase: incremented per logical PUT. RT phase (hybrid A/A):
355+
* adjusted by +1/-1 signals from conflict resolution. {@link OffsetRecord#ACTIVE_KEY_COUNT_NOT_TRACKED} = not tracked
356+
* (no batch baseline); RT signals are skipped when not tracked. AtomicLong for AA/WC parallel processing.
357+
*/
358+
private final AtomicLong activeKeyCount = new AtomicLong(OffsetRecord.ACTIVE_KEY_COUNT_NOT_TRACKED);
359+
/** Last PUT key for batch dedup. Not persisted; used by {@link #incrementActiveKeyCountForBatchRecord}.
360+
* Single-threaded: only accessed from the drainer thread during batch (pre-EOP). */
361+
private byte[] lastBatchKeyForDedup;
362+
351363
/** Lazily allocated per-partition detector for partial-update amplification. */
352364
private volatile PartialUpdateAmplificationDetector partialUpdateAmplificationDetector;
353365

@@ -415,6 +427,7 @@ public PartitionConsumptionState(
415427
this.lastLeaderCompleteStateUpdateInMs = 0;
416428
this.pendingReportIncPushVersionList = offsetRecord.getPendingReportIncPushVersionList();
417429
this.hasResubscribedAfterBootstrapAsCurrentVersion = false;
430+
this.activeKeyCount.set(offsetRecord.getActiveKeyCount());
418431
}
419432

420433
/** Create a fresh HLL sketch with {@link #HLL_DEFAULT_LOG_K}. */
@@ -450,6 +463,73 @@ public void restoreUniqueKeyCountHll(int lgK) {
450463
LOGGER.warn("Partition {} has no HLL checkpoint data to restore.", getPartition());
451464
return;
452465
}
466+
restoreUniqueKeyCountHllFromCheckpoint(lgK, hllBytes);
467+
}
468+
469+
public long getActiveKeyCount() {
470+
return activeKeyCount.get();
471+
}
472+
473+
public void setActiveKeyCount(long count) {
474+
activeKeyCount.set(count);
475+
}
476+
477+
public void incrementActiveKeyCount() {
478+
activeKeyCount.incrementAndGet();
479+
}
480+
481+
/**
482+
* Called at SOP to mark this partition as actively counting. Without this, a mid-batch
483+
* restart with a newly enabled config would start counting partway through, producing
484+
* an inaccurate partial count. On restart, SOP is not re-processed (already past the
485+
* checkpoint), so the count stays at {@link OffsetRecord#ACTIVE_KEY_COUNT_NOT_TRACKED} and batch
486+
* records are skipped by {@link #incrementActiveKeyCountForBatchRecord}.
487+
*/
488+
public void initializeActiveKeyCount() {
489+
activeKeyCount.compareAndSet(OffsetRecord.ACTIVE_KEY_COUNT_NOT_TRACKED, 0);
490+
}
491+
492+
/**
493+
* Decrements activeKeyCount. If the count is already at 0, this indicates drift
494+
* (more deletes than creates), so we invalidate to {@link OffsetRecord#ACTIVE_KEY_COUNT_NOT_TRACKED}
495+
* rather than continue tracking with a wrong baseline. If already not tracked, stays not tracked.
496+
*
497+
* @return true if the count was successfully decremented, false if invalidated or already invalid
498+
*/
499+
public boolean decrementActiveKeyCount() {
500+
long prev = activeKeyCount.getAndUpdate(v -> v > 0 ? v - 1 : OffsetRecord.ACTIVE_KEY_COUNT_NOT_TRACKED);
501+
return prev > 0;
502+
}
503+
504+
/**
505+
* Increments activeKeyCount for a batch PUT, skipping speculative execution duplicates
506+
* (key &lt;= last key). Only PUTs call this — DELETEs are tombstones and excluded.
507+
* Requires prior {@link #initializeActiveKeyCount()} at SOP; skips if not initialized
508+
* (mid-batch config enablement). Checkpoint-safe: partial counts are persisted.
509+
*/
510+
public void incrementActiveKeyCountForBatchRecord(byte[] keyBytes) {
511+
if (activeKeyCount.get() < 0) {
512+
return; // Not initialized at SOP — mid-batch config enablement, skip
513+
}
514+
if (lastBatchKeyForDedup != null && ArrayUtils.compareUnsigned(keyBytes, lastBatchKeyForDedup) <= 0) {
515+
return; // Speculative execution duplicate — key order went backwards
516+
}
517+
lastBatchKeyForDedup = keyBytes;
518+
activeKeyCount.incrementAndGet();
519+
}
520+
521+
/**
522+
* Called at EOP. Releases dedup state. Empty partitions are already at 0 from
523+
* {@link #initializeActiveKeyCount()} at SOP. If SOP was missed (mid-batch config enablement),
524+
* the count stays at {@link OffsetRecord#ACTIVE_KEY_COUNT_NOT_TRACKED} — no partial baseline is created.
525+
*/
526+
public void cleanupBatchKeyCountState() {
527+
lastBatchKeyForDedup = null; // Release reference; dedup is only needed during batch
528+
}
529+
530+
// --- HLL unique ingested key count methods ---
531+
532+
private void restoreUniqueKeyCountHllFromCheckpoint(int lgK, ByteBuffer hllBytes) {
453533
byte[] bytes = new byte[hllBytes.remaining()];
454534
hllBytes.duplicate().get(bytes);
455535
this.uniqueIngestedKeyCountHll = HllSketch.heapify(Memory.wrap(bytes));

0 commit comments

Comments
 (0)