Skip to content

Commit 31fedf6

Browse files
sixpluszeroclaude
andcommitted
[da-vinci][server] Move transient record cache flag to version level and add
host-level kill switch - Move transientRecordCacheEnabled from Store interface to Version interface so the cache can be toggled per version without affecting other versions. - Add SERVER_INGESTION_TRANSIENT_RECORD_CACHE_ENABLED server config (default true) as a host-level kill switch to disable the cache on a node without a new push. - Fix hot cache staleness: invalidate hot cache entry when a key is overwritten with a value below the size threshold or a null-value update. - Add tests for cache invalidation on small-value and null-value overwrites. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e495f4b commit 31fedf6

13 files changed

Lines changed: 115 additions & 56 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
import static com.linkedin.venice.ConfigKeys.SERVER_INGESTION_OTEL_STATS_ENABLED;
133133
import static com.linkedin.venice.ConfigKeys.SERVER_INGESTION_TASK_MAX_IDLE_COUNT;
134134
import static com.linkedin.venice.ConfigKeys.SERVER_INGESTION_TASK_REUSABLE_OBJECTS_STRATEGY;
135+
import static com.linkedin.venice.ConfigKeys.SERVER_INGESTION_TRANSIENT_RECORD_CACHE_ENABLED;
135136
import static com.linkedin.venice.ConfigKeys.SERVER_INGESTION_TRANSIENT_RECORD_CACHE_MAX_WEIGHT;
136137
import static com.linkedin.venice.ConfigKeys.SERVER_INGESTION_TRANSIENT_RECORD_CACHE_MIN_VALUE_SIZE;
137138
import static com.linkedin.venice.ConfigKeys.SERVER_KAFKA_CONSUMER_OFFSET_COLLECTION_ENABLED;
@@ -713,6 +714,7 @@ public class VeniceServerConfig extends VeniceClusterConfig {
713714
private final int lagMonitorCleanupCycle;
714715
private final boolean readQuotaInitializationFallbackEnabled;
715716
private final boolean ingestionProgressLoggingEnabled;
717+
private final boolean transientRecordCacheEnabled;
716718
private final long transientRecordCacheMaxWeight;
717719
private final int transientRecordCacheMinValueSize;
718720

@@ -1231,6 +1233,8 @@ public VeniceServerConfig(VeniceProperties serverProperties, Map<String, Map<Str
12311233
this.readQuotaInitializationFallbackEnabled =
12321234
serverProperties.getBoolean(SERVER_READ_QUOTA_INITIALIZATION_FALLBACK_ENABLED, true);
12331235
this.ingestionProgressLoggingEnabled = serverProperties.getBoolean(POSITIONAL_PROGRESS_LOGGING_ENABLED, false);
1236+
this.transientRecordCacheEnabled =
1237+
serverProperties.getBoolean(SERVER_INGESTION_TRANSIENT_RECORD_CACHE_ENABLED, true);
12341238
this.transientRecordCacheMaxWeight =
12351239
serverProperties.getLong(SERVER_INGESTION_TRANSIENT_RECORD_CACHE_MAX_WEIGHT, 32 * 1024 * 1024L);
12361240
this.transientRecordCacheMinValueSize =
@@ -2234,6 +2238,10 @@ public boolean isIngestionProgressLoggingEnabled() {
22342238
return ingestionProgressLoggingEnabled;
22352239
}
22362240

2241+
public boolean isTransientRecordCacheEnabled() {
2242+
return transientRecordCacheEnabled;
2243+
}
2244+
22372245
public long getTransientRecordCacheMaxWeight() {
22382246
return transientRecordCacheMaxWeight;
22392247
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,8 +789,14 @@ public void setTransientRecord(
789789

790790
transientRecordMap.put(ByteArrayKey.wrap(key), transientRecord);
791791

792-
if (hotRecordCache != null && valueLen >= minValueSizeForHotCache) {
793-
hotRecordCache.put(buildHotCacheKey(key), transientRecord);
792+
if (hotRecordCache != null) {
793+
ByteArrayKey hotKey = buildHotCacheKey(key);
794+
if (valueLen >= minValueSizeForHotCache) {
795+
hotRecordCache.put(hotKey, transientRecord);
796+
} else {
797+
// Invalidate any stale entry when the new record no longer qualifies
798+
hotRecordCache.invalidate(hotKey);
799+
}
794800
}
795801
}
796802

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,7 @@ public StoreIngestionTask(
689689
this.localKafkaServerSingletonSet = Collections.singleton(localKafkaServer);
690690
this.isActiveActiveReplicationEnabled = version.isActiveActiveReplicationEnabled();
691691

692-
if (store.isTransientRecordCacheEnabled()
692+
if (serverConfig.isTransientRecordCacheEnabled() && version.isTransientRecordCacheEnabled()
693693
&& (this.isActiveActiveReplicationEnabled || this.isWriteComputationEnabled)) {
694694
long maxWeight = serverConfig.getTransientRecordCacheMaxWeight();
695695
this.hotRecordCache = Caffeine.newBuilder()

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,79 @@ public void testHotRecordCacheAdmissionThreshold() {
348348
assertEquals(pcs.getHotRecordCacheHitCount(), 0);
349349
}
350350

351+
@Test
352+
public void testHotCacheInvalidatedWhenReplacedWithSmallValue() {
353+
Cache<ByteArrayKey, PartitionConsumptionState.TransientRecord> hotCache = Caffeine.newBuilder()
354+
.maximumWeight(1024 * 1024)
355+
.weigher((ByteArrayKey k, PartitionConsumptionState.TransientRecord v) -> {
356+
return k.getContent().length + (v.getValue() != null ? v.getValueLen() : 0);
357+
})
358+
.build();
359+
360+
int minValueSize = 50;
361+
PartitionConsumptionState pcs = new PartitionConsumptionState(
362+
TOPIC_PARTITION,
363+
mock(OffsetRecord.class),
364+
pubSubContext,
365+
false,
366+
Schema.create(Schema.Type.STRING),
367+
hotCache,
368+
minValueSize);
369+
370+
PubSubPosition pos1 = mock(PubSubPosition.class);
371+
PubSubPosition pos2 = mock(PubSubPosition.class);
372+
byte[] key = new byte[] { 1, 2, 3 };
373+
byte[] largeValue = new byte[100]; // >= minValueSize, admitted to hot cache
374+
byte[] smallValue = new byte[10]; // < minValueSize
375+
376+
// First write: large value gets into hot cache
377+
pcs.setTransientRecord(-1, pos1, key, largeValue, 0, largeValue.length, 1, null);
378+
pcs.mayRemoveTransientRecord(-1, pos1, key);
379+
// Verify hot cache serves it
380+
assertNotNull(pcs.getTransientRecord(key));
381+
382+
// Second write: small value replaces it — hot cache entry must be invalidated
383+
pcs.setTransientRecord(-1, pos2, key, smallValue, 0, smallValue.length, 2, null);
384+
pcs.mayRemoveTransientRecord(-1, pos2, key);
385+
// Stale large-value entry should NOT be returned
386+
assertNull(pcs.getTransientRecord(key));
387+
}
388+
389+
@Test
390+
public void testHotCacheInvalidatedWhenReplacedWithNullValue() {
391+
Cache<ByteArrayKey, PartitionConsumptionState.TransientRecord> hotCache = Caffeine.newBuilder()
392+
.maximumWeight(1024 * 1024)
393+
.weigher((ByteArrayKey k, PartitionConsumptionState.TransientRecord v) -> {
394+
return k.getContent().length + (v.getValue() != null ? v.getValueLen() : 0);
395+
})
396+
.build();
397+
398+
int minValueSize = 50;
399+
PartitionConsumptionState pcs = new PartitionConsumptionState(
400+
TOPIC_PARTITION,
401+
mock(OffsetRecord.class),
402+
pubSubContext,
403+
false,
404+
Schema.create(Schema.Type.STRING),
405+
hotCache,
406+
minValueSize);
407+
408+
PubSubPosition pos1 = mock(PubSubPosition.class);
409+
PubSubPosition pos2 = mock(PubSubPosition.class);
410+
byte[] key = new byte[] { 1, 2, 3 };
411+
byte[] largeValue = new byte[100];
412+
413+
// First write: large value gets into hot cache
414+
pcs.setTransientRecord(-1, pos1, key, largeValue, 0, largeValue.length, 1, null);
415+
pcs.mayRemoveTransientRecord(-1, pos1, key);
416+
assertNotNull(pcs.getTransientRecord(key));
417+
418+
// Second write: null-value overload (valueLen = -1) — must invalidate hot cache entry
419+
pcs.setTransientRecord(-1, pos2, key, 2, null);
420+
pcs.mayRemoveTransientRecord(-1, pos2, key);
421+
assertNull(pcs.getTransientRecord(key));
422+
}
423+
351424
@Test
352425
public void testNullHotCacheBehavesIdentically() {
353426
// Without hot cache (null), behavior should be identical to previous implementation

internal/venice-common/src/main/java/com/linkedin/venice/ConfigKeys.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3351,4 +3351,12 @@ private ConfigKeys() {
33513351
*/
33523352
public static final String SERVER_INGESTION_TRANSIENT_RECORD_CACHE_MIN_VALUE_SIZE =
33533353
"server.ingestion.transient.record.cache.min.value.size";
3354+
3355+
/**
3356+
* Host-level kill switch for the transient record cache. When set to false, the cache is disabled
3357+
* on this node regardless of the version-level setting. This allows operators to quickly disable
3358+
* the cache without requiring a new push. Default is true (enabled, defers to version-level config).
3359+
*/
3360+
public static final String SERVER_INGESTION_TRANSIENT_RECORD_CACHE_ENABLED =
3361+
"server.ingestion.transient.record.cache.enabled";
33543362
}

internal/venice-common/src/main/java/com/linkedin/venice/meta/ReadOnlyStore.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,6 @@ public StoreProperties cloneStoreProperties() {
10671067
storeProperties.setKeyUrnCompressionEnabled(isKeyUrnCompressionEnabled());
10681068
storeProperties.setKeyUrnFields(getKeyUrnFields().stream().map(String::toString).collect(Collectors.toList()));
10691069
storeProperties.setPreviousCurrentVersion(getPreviousCurrentVersion());
1070-
storeProperties.setTransientRecordCacheEnabled(isTransientRecordCacheEnabled());
10711070
// Set blobDbEnabled to default value - field exists in schema but not yet exposed via Store interface
10721071
storeProperties.setBlobDbEnabled("NOT_SPECIFIED");
10731072

@@ -1810,16 +1809,6 @@ public void setPreviousCurrentVersion(int previousCurrentVersion) {
18101809
throw new UnsupportedOperationException();
18111810
}
18121811

1813-
@Override
1814-
public boolean isTransientRecordCacheEnabled() {
1815-
return this.delegate.isTransientRecordCacheEnabled();
1816-
}
1817-
1818-
@Override
1819-
public void setTransientRecordCacheEnabled(boolean transientRecordCacheEnabled) {
1820-
throw new UnsupportedOperationException();
1821-
}
1822-
18231812
@Override
18241813
public String toString() {
18251814
return this.delegate.toString();

internal/venice-common/src/main/java/com/linkedin/venice/meta/Store.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,4 @@ default IntSet getVersionNumbers() {
393393
int getPreviousCurrentVersion();
394394

395395
void setPreviousCurrentVersion(int previousCurrentVersion);
396-
397-
boolean isTransientRecordCacheEnabled();
398-
399-
void setTransientRecordCacheEnabled(boolean transientRecordCacheEnabled);
400396
}

internal/venice-common/src/main/java/com/linkedin/venice/meta/StoreInfo.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ public static StoreInfo fromStore(Store store) {
7272
storeInfo.setReplicationMetadataVersionId(store.getRmdVersion());
7373
storeInfo.setViewConfigs(store.getViewConfigs());
7474
storeInfo.setStorageNodeReadQuotaEnabled(store.isStorageNodeReadQuotaEnabled());
75-
storeInfo.setTransientRecordCacheEnabled(store.isTransientRecordCacheEnabled());
7675
storeInfo.setCompactionEnabled(store.isCompactionEnabled());
7776
storeInfo.setCompactionThreshold(store.getCompactionThresholdMilliseconds());
7877
storeInfo.setMinCompactionLagSeconds(store.getMinCompactionLagSeconds());
@@ -339,11 +338,6 @@ public static StoreInfo fromStore(Store store) {
339338
*/
340339
private boolean storageNodeReadQuotaEnabled;
341340

342-
/**
343-
* Whether the bounded hot transient record cache is enabled for this store.
344-
*/
345-
private boolean transientRecordCacheEnabled = false;
346-
347341
/**
348342
* Reasons for why or why not the store is dead
349343
*/
@@ -840,14 +834,6 @@ public void setStorageNodeReadQuotaEnabled(boolean storageNodeReadQuotaEnabled)
840834
this.storageNodeReadQuotaEnabled = storageNodeReadQuotaEnabled;
841835
}
842836

843-
public boolean isTransientRecordCacheEnabled() {
844-
return transientRecordCacheEnabled;
845-
}
846-
847-
public void setTransientRecordCacheEnabled(boolean transientRecordCacheEnabled) {
848-
this.transientRecordCacheEnabled = transientRecordCacheEnabled;
849-
}
850-
851837
public boolean isCompactionEnabled() {
852838
return this.compactionEnabled;
853839
}

internal/venice-common/src/main/java/com/linkedin/venice/meta/SystemStore.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -857,16 +857,6 @@ public void setPreviousCurrentVersion(int previousCurrentVersion) {
857857
throwUnsupportedOperationException("setPreviousCurrentVersion");
858858
}
859859

860-
@Override
861-
public boolean isTransientRecordCacheEnabled() {
862-
return zkSharedStore.isTransientRecordCacheEnabled();
863-
}
864-
865-
@Override
866-
public void setTransientRecordCacheEnabled(boolean transientRecordCacheEnabled) {
867-
throwUnsupportedOperationException("setTransientRecordCacheEnabled");
868-
}
869-
870860
@Override
871861
public boolean isGlobalRtDivEnabled() {
872862
return zkSharedStore.isGlobalRtDivEnabled();

internal/venice-common/src/main/java/com/linkedin/venice/meta/Version.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,10 @@ default void setTimestampMetadataVersionId(int replicationMetadataVersionId) {
329329

330330
void setPreviousCurrentVersion(int previousCurrentVersion);
331331

332+
boolean isTransientRecordCacheEnabled();
333+
334+
void setTransientRecordCacheEnabled(boolean transientRecordCacheEnabled);
335+
332336
/**
333337
* Kafka topic name is composed by store name and version.
334338
* <p>

0 commit comments

Comments
 (0)