Skip to content

Commit a7cceaf

Browse files
KaiSernLimclaude
andauthored
[da-vinci][server] Global RT DIV: Metadata Partition (linkedin#2498)
Global RT DIV is stored in the metadata partition to avoid conflict with the user data. Added multiple new end-to-end integration tests. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c3a718a commit a7cceaf

11 files changed

Lines changed: 759 additions & 84 deletions

File tree

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

Lines changed: 62 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@
3030
import com.linkedin.davinci.stats.ingestion.heartbeat.HeartbeatMonitoringService;
3131
import com.linkedin.davinci.storage.StorageService;
3232
import com.linkedin.davinci.storage.chunking.ChunkedValueManifestContainer;
33-
import com.linkedin.davinci.storage.chunking.GenericChunkingAdapter;
33+
import com.linkedin.davinci.storage.chunking.ChunkingUtils;
3434
import com.linkedin.davinci.storage.chunking.GenericRecordChunkingAdapter;
35+
import com.linkedin.davinci.storage.chunking.RawBytesChunkingAdapter;
3536
import com.linkedin.davinci.store.StorageEngine;
3637
import com.linkedin.davinci.store.StoragePartitionAdjustmentTrigger;
3738
import com.linkedin.davinci.store.cache.backend.ObjectCacheBackend;
@@ -112,6 +113,7 @@
112113
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
113114
import java.io.IOException;
114115
import java.nio.ByteBuffer;
116+
import java.nio.charset.StandardCharsets;
115117
import java.util.ArrayList;
116118
import java.util.Arrays;
117119
import java.util.Collections;
@@ -177,7 +179,6 @@
177179
*/
178180
public class LeaderFollowerStoreIngestionTask extends StoreIngestionTask {
179181
private static final Logger LOGGER = LogManager.getLogger(LeaderFollowerStoreIngestionTask.class);
180-
public static final String GLOBAL_RT_DIV_KEY_PREFIX = "GLOBAL_RT_DIV_KEY.";
181182
static final long VIEW_WRITER_CLOSE_TIMEOUT_IN_MS = 60000; // 60s
182183

183184
/**
@@ -3827,14 +3828,17 @@ private void produceToLocalKafkaHelper(
38273828
}
38283829

38293830
/**
3830-
* The Global RT DIV is produced on a per-broker basis, so the name includes the broker URL for differentiation.
3831+
* The Global RT DIV key includes the partition ID to prevent collisions when a server hosts multiple partitions,
3832+
* and the broker URL to distinguish between different upstream brokers.
38313833
*/
3832-
public static String getGlobalRtDivKeyName(String brokerUrl) {
3833-
return GLOBAL_RT_DIV_KEY_PREFIX + brokerUrl;
3834+
public static String getGlobalRtDivKeyName(int partitionId, String brokerUrl) {
3835+
return GLOBAL_RT_DIV_KEY_PREFIX + partitionId + "." + brokerUrl;
38343836
}
38353837

3836-
public byte[] getGlobalRtDivKeyBytes(String brokerUrl) {
3837-
return globalRtDivKeyBytesCache.computeIfAbsent(brokerUrl, url -> getGlobalRtDivKeyName(url).getBytes());
3838+
public byte[] getGlobalRtDivKeyBytes(int partitionId, String brokerUrl) {
3839+
return globalRtDivKeyBytesCache.computeIfAbsent(
3840+
partitionId + "." + brokerUrl,
3841+
k -> getGlobalRtDivKeyName(partitionId, brokerUrl).getBytes(StandardCharsets.UTF_8));
38383842
}
38393843

38403844
/**
@@ -3852,7 +3856,7 @@ void sendGlobalRtDivMessage(
38523856
long beforeProcessingRecordTimestampNs,
38533857
LeaderMetadataWrapper leaderMetadataWrapper,
38543858
LeaderProducedRecordContext context) {
3855-
final byte[] keyBytes = getGlobalRtDivKeyBytes(brokerUrl);
3859+
final byte[] keyBytes = getGlobalRtDivKeyBytes(partition, brokerUrl);
38563860
final PubSubTopicPartition topicPartition = previousMessage.getTopicPartition();
38573861
TopicType realTimeTopicType = TopicType.of(REALTIME_TOPIC_TYPE, brokerUrl);
38583862

@@ -3878,10 +3882,17 @@ void sendGlobalRtDivMessage(
38783882
topicPartition,
38793883
vtDiv);
38803884

3881-
// Get the old value manifest which contains the list of old chunks, so they can be deleted
3882-
final int schemaId = AvroProtocolDefinition.GLOBAL_RT_DIV_STATE.getCurrentProtocolVersion();
3885+
// Read the old manifest (if any) so VeniceWriter can delete orphaned old chunks in Kafka.
38833886
ChunkedValueManifestContainer valueManifestContainer = new ChunkedValueManifestContainer();
3884-
readGlobalRtDivState(keyBytes, schemaId, topicPartition, valueManifestContainer);
3887+
byte[] manifestKey = ChunkingUtils.KEY_WITH_CHUNKING_SUFFIX_SERIALIZER.serializeNonChunkedKey(keyBytes);
3888+
byte[] oldRaw = storageEngine.getGlobalRtDivMetadata(manifestKey);
3889+
if (oldRaw != null && oldRaw.length >= ValueRecord.SCHEMA_HEADER_LENGTH) {
3890+
int schemaId = ValueRecord.parseSchemaId(oldRaw);
3891+
if (schemaId == AvroProtocolDefinition.CHUNKED_VALUE_MANIFEST.getCurrentProtocolVersion()) {
3892+
valueManifestContainer
3893+
.setManifest(ChunkingUtils.CHUNKED_VALUE_MANIFEST_SERIALIZER.deserialize(oldRaw, schemaId));
3894+
}
3895+
}
38853896

38863897
// TODO: remove. this is a temporary log for debugging while the feature is in its infancy
38873898
LOGGER.info(
@@ -3986,61 +3997,74 @@ private LeaderProducerCallback createGlobalRtDivCallback(
39863997
}
39873998

39883999
/**
3989-
* Reads a GlobalRtDivState value from the storage engine. Returns null if the value does not exist.
4000+
* Reads a GlobalRtDivState value from the metadata storage partition.
4001+
* Returns null if the key does not carry the expected prefix or the value does not exist.
4002+
* For chunked states, chunk assembly is performed at read time via {@link RawBytesChunkingAdapter}.
39904003
*
3991-
* @param keyBytes the serialized key for the value
3992-
* @param readerValueSchemaID the schemaId to use for deserialization
4004+
* @param keyBytes the serialized key for the value (without any chunking suffix)
4005+
* @param readerValueSchemaID the schemaId to use when deserializing the assembled value
39934006
* @param topicPartition the topic/partition for the value
3994-
* @param manifestContainer a container to store any manifest information retrieved from the storage engine
4007+
* @param manifestContainer populated with the {@link ChunkedValueManifest} if the value was chunked
39954008
* @return the deserialized GlobalRtDivState object, or null if the value does not exist
39964009
*/
39974010
GlobalRtDivState readGlobalRtDivState(
39984011
byte[] keyBytes,
39994012
int readerValueSchemaID,
40004013
PubSubTopicPartition topicPartition,
40014014
ChunkedValueManifestContainer manifestContainer) {
4002-
ByteBuffer valueBytes;
4015+
final String key = new String(keyBytes, StandardCharsets.UTF_8);
4016+
if (!key.startsWith(GLOBAL_RT_DIV_KEY_PREFIX)) {
4017+
return null;
4018+
}
4019+
final int partitionId = topicPartition.getPartitionNumber();
4020+
// Extract brokerUrl for logging (key format: GLOBAL_RT_DIV_KEY.{partitionId}.{brokerUrl})
4021+
final String afterPrefix = key.substring(GLOBAL_RT_DIV_KEY_PREFIX.length());
4022+
final int dotIdx = afterPrefix.indexOf('.');
4023+
final String brokerUrl = dotIdx >= 0 ? afterPrefix.substring(dotIdx + 1) : afterPrefix;
4024+
4025+
final BiFunction<Integer, ByteBuffer, byte[]> metadataGetter =
4026+
(part, keyBuf) -> storageEngine.getGlobalRtDivMetadata(ByteUtils.extractByteArray(keyBuf));
4027+
ByteBuffer assembledBytes;
40034028
try {
4004-
valueBytes = (ByteBuffer) GenericChunkingAdapter.INSTANCE.get(
4005-
storageEngine,
4006-
topicPartition.getPartitionNumber(),
4029+
assembledBytes = RawBytesChunkingAdapter.INSTANCE.get(
4030+
metadataGetter,
4031+
storageEngine.getStoreVersionName(),
4032+
partitionId,
40074033
ByteBuffer.wrap(keyBytes),
4008-
isChunked,
4034+
true, // must be true because Global RT DIV could always be large and require chunking
40094035
null,
40104036
null,
40114037
NoOpReadResponseStats.SINGLETON,
40124038
readerValueSchemaID,
40134039
RawBytesStoreDeserializerCache.getInstance(),
40144040
compressor.get(),
40154041
manifestContainer);
4016-
} catch (Exception e) {
4017-
// TODO: evaluate whether these logs can be set to debug
4042+
} catch (VeniceException e) {
40184043
LOGGER.error(
4019-
"Unable to retrieve the stored value bytes for key: {}, topic-partition: {}",
4020-
new String(keyBytes),
4044+
"Unable to read Global RT DIV state from metadata storage for topic-partition: {}, brokerUrl: {}",
40214045
topicPartition,
4046+
brokerUrl,
40224047
e);
40234048
return null;
40244049
}
4025-
4026-
if (valueBytes == null) {
4027-
// TODO: evaluate whether these logs can be set to debug
4028-
LOGGER.warn(
4029-
"No value found in the storage engine for key: {}, topic-partition: {}",
4030-
new String(keyBytes),
4031-
topicPartition);
4050+
if (assembledBytes == null) {
40324051
return null;
40334052
}
4053+
return deserializeGlobalRtDivState(ByteUtils.extractByteArray(assembledBytes), keyBytes, topicPartition);
4054+
}
40344055

4056+
private GlobalRtDivState deserializeGlobalRtDivState(
4057+
byte[] serializedValueBytes,
4058+
byte[] keyBytes,
4059+
PubSubTopicPartition topicPartition) {
40354060
try {
4036-
return globalRtDivStateSerializer.deserialize(
4037-
ByteUtils.extractByteArray(valueBytes),
4038-
AvroProtocolDefinition.GLOBAL_RT_DIV_STATE.getCurrentProtocolVersion());
4061+
return globalRtDivStateSerializer
4062+
.deserialize(serializedValueBytes, AvroProtocolDefinition.GLOBAL_RT_DIV_STATE.getCurrentProtocolVersion());
40394063
} catch (Exception e) {
40404064
// TODO: evaluate whether these logs can be set to debug
40414065
LOGGER.error(
40424066
"Unable to deserialize stored value bytes for key: {}, topic-partition: {}",
4043-
new String(keyBytes),
4067+
new String(keyBytes, StandardCharsets.UTF_8),
40444068
topicPartition,
40454069
e);
40464070
return null;
@@ -4147,14 +4171,13 @@ void loadGlobalRtDiv(int partition, String brokerUrl) {
41474171
final PubSubTopic topic = pcs.getOffsetRecord().getLeaderTopic(getPubSubTopicRepository());
41484172
final PubSubTopicPartition topicPartition = new PubSubTopicPartitionImpl(topic, pcs.getPartition());
41494173

4150-
String globalRtDivKey = getGlobalRtDivKeyName(brokerUrl);
4151-
byte[] keyBytes = globalRtDivKey.getBytes();
4152-
final ChunkedValueManifestContainer valueManifestContainer = new ChunkedValueManifestContainer();
4174+
byte[] keyBytes = getGlobalRtDivKeyBytes(partition, brokerUrl);
41534175
GlobalRtDivState globalRtDivState = readGlobalRtDivState(
41544176
keyBytes,
41554177
AvroProtocolDefinition.GLOBAL_RT_DIV_STATE.getCurrentProtocolVersion(),
41564178
topicPartition,
4157-
valueManifestContainer);
4179+
new ChunkedValueManifestContainer());
4180+
41584181
if (globalRtDivState == null) {
41594182
// If the GlobalRtDivState is not present, it could be acceptable if this could be the first leader to be elected
41604183
// Object not existing could be problematic if this isn't the first leader (detected via nonzero leaderPosition)

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

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ public abstract class StoreIngestionTask implements Runnable, Closeable {
223223
protected static final int CHUNK_SCHEMA_ID = AvroProtocolDefinition.CHUNK.getCurrentProtocolVersion();
224224
private static final int CHUNK_MANIFEST_SCHEMA_ID =
225225
AvroProtocolDefinition.CHUNKED_VALUE_MANIFEST.getCurrentProtocolVersion();
226+
public static final String GLOBAL_RT_DIV_KEY_PREFIX = "GLOBAL_RT_DIV_KEY.";
226227

227228
protected static final RedundantExceptionFilter REDUNDANT_LOGGING_FILTER =
228229
RedundantExceptionFilter.getRedundantExceptionFilter();
@@ -4424,6 +4425,12 @@ protected void putInStorageEngine(int partition, byte[] keyBytes, Put put) {
44244425
executeStorageEngineRunnable(partition, () -> storageEngine.put(partition, keyBytes, put.putValue));
44254426
}
44264427

4428+
protected void putGlobalRtDivStateInMetadata(int partition, byte[] keyBytes, Put put) {
4429+
storageEngine.putGlobalRtDivMetadata(
4430+
keyBytes,
4431+
ByteUtils.prependIntHeaderToByteBuffer(put.putValue, put.schemaId, false).array());
4432+
}
4433+
44274434
protected void removeFromStorageEngine(int partition, byte[] keyBytes, Delete delete) {
44284435
executeStorageEngineRunnable(partition, () -> storageEngine.delete(partition, keyBytes));
44294436
}
@@ -4626,7 +4633,9 @@ private int processKafkaDataMessage(
46264633

46274634
int writerSchemaId = put.getSchemaId();
46284635

4629-
if (recordTransformer != null && messageType == MessageType.PUT) {
4636+
if (kafkaKey.isGlobalRtDiv()) {
4637+
putGlobalRtDivStateInMetadata(producedPartition, keyBytes, put);
4638+
} else if (recordTransformer != null && messageType == MessageType.PUT) {
46304639
long recordTransformerStartTime = System.nanoTime();
46314640
ByteBufferValueRecord<ByteBuffer> assembledRecord = chunkAssembler.bufferAndAssembleRecord(
46324641
consumerRecord.getTopicPartition(),
@@ -4792,7 +4801,11 @@ private int processKafkaDataMessage(
47924801
}
47934802

47944803
keyLen = keyBytes.length;
4795-
deleteFromStorageEngine(producedPartition, keyBytes, delete);
4804+
if (kafkaKey.isGlobalRtDiv()) {
4805+
storageEngine.deleteGlobalRtDivMetadata(keyBytes);
4806+
} else {
4807+
deleteFromStorageEngine(producedPartition, keyBytes, delete);
4808+
}
47964809
if (recordMetrics) {
47974810
double deleteLatency = LatencyUtils.getElapsedTimeFromNSToMS(startTimeNs);
47984811
versionedIngestionStats.recordStorageEngineDeleteTime(storeName, versionNumber, deleteLatency);

clients/da-vinci-client/src/main/java/com/linkedin/davinci/storage/chunking/AbstractAvroChunkingAdapter.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.io.IOException;
1717
import java.io.InputStream;
1818
import java.nio.ByteBuffer;
19+
import java.util.function.BiFunction;
1920
import org.apache.avro.generic.GenericRecord;
2021
import org.apache.avro.io.BinaryDecoder;
2122

@@ -135,6 +136,42 @@ public T get(
135136
manifestContainer);
136137
}
137138

139+
/**
140+
* Variant of {@link #get} that accepts a custom storage getter function instead of a {@link StorageEngine}.
141+
* Use this when the data lives outside a regular data partition (e.g., in a metadata partition) and a
142+
* custom lookup is needed to locate both the manifest and the individual chunks.
143+
*/
144+
public T get(
145+
BiFunction<Integer, ByteBuffer, byte[]> storageGetter,
146+
String storeVersionName,
147+
int partition,
148+
ByteBuffer key,
149+
boolean isChunked,
150+
T reusedValue,
151+
BinaryDecoder reusedDecoder,
152+
ReadResponseStats responseStats,
153+
int readerSchemaId,
154+
StoreDeserializerCache<T> storeDeserializerCache,
155+
VeniceCompressor compressor,
156+
ChunkedValueManifestContainer manifestContainer) {
157+
if (isChunked) {
158+
key = ChunkingUtils.KEY_WITH_CHUNKING_SUFFIX_SERIALIZER.serializeNonChunkedKey(key);
159+
}
160+
return ChunkingUtils.getFromStorage(
161+
this,
162+
(p, k) -> storageGetter.apply(p, k),
163+
storeVersionName,
164+
partition,
165+
key,
166+
responseStats,
167+
reusedValue,
168+
reusedDecoder,
169+
readerSchemaId,
170+
storeDeserializerCache,
171+
compressor,
172+
manifestContainer);
173+
}
174+
138175
public ByteBufferValueRecord<T> getWithSchemaId(
139176
StorageEngine store,
140177
int partition,

clients/da-vinci-client/src/main/java/com/linkedin/davinci/storage/chunking/ChunkingUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
* a chunked value.
6262
*/
6363
public class ChunkingUtils {
64-
static final ChunkedValueManifestSerializer CHUNKED_VALUE_MANIFEST_SERIALIZER =
64+
public static final ChunkedValueManifestSerializer CHUNKED_VALUE_MANIFEST_SERIALIZER =
6565
new ChunkedValueManifestSerializer(false);
6666
public static final KeyWithChunkingSuffixSerializer KEY_WITH_CHUNKING_SUFFIX_SERIALIZER =
6767
new KeyWithChunkingSuffixSerializer();

clients/da-vinci-client/src/main/java/com/linkedin/davinci/store/AbstractStorageEngine.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,30 @@ public synchronized void clearStoreVersionState() {
669669
metadataPartition.delete(VERSION_METADATA_KEY);
670670
}
671671

672+
@Override
673+
public synchronized void putGlobalRtDivMetadata(byte[] keyBytes, byte[] valueWithHeader) {
674+
if (!metadataPartitionCreated()) {
675+
throw new StorageInitializationException("Metadata partition not created!");
676+
}
677+
metadataPartition.put(keyBytes, valueWithHeader);
678+
}
679+
680+
@Override
681+
public synchronized byte[] getGlobalRtDivMetadata(byte[] keyBytes) {
682+
if (!metadataPartitionCreated()) {
683+
return null;
684+
}
685+
return metadataPartition.get(keyBytes);
686+
}
687+
688+
@Override
689+
public synchronized void deleteGlobalRtDivMetadata(byte[] keyBytes) {
690+
if (!metadataPartitionCreated()) {
691+
return;
692+
}
693+
metadataPartition.delete(keyBytes);
694+
}
695+
672696
/**
673697
* Return true or false based on whether a given partition exists within this storage engine
674698
*

clients/da-vinci-client/src/main/java/com/linkedin/davinci/store/DelegatingStorageEngine.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,21 @@ public void clearStoreVersionState() {
279279
this.delegate.clearStoreVersionState();
280280
}
281281

282+
@Override
283+
public void putGlobalRtDivMetadata(byte[] keyBytes, byte[] valueWithHeader) {
284+
this.delegate.putGlobalRtDivMetadata(keyBytes, valueWithHeader);
285+
}
286+
287+
@Override
288+
public byte[] getGlobalRtDivMetadata(byte[] keyBytes) {
289+
return this.delegate.getGlobalRtDivMetadata(keyBytes);
290+
}
291+
292+
@Override
293+
public void deleteGlobalRtDivMetadata(byte[] keyBytes) {
294+
this.delegate.deleteGlobalRtDivMetadata(keyBytes);
295+
}
296+
282297
@Override
283298
public boolean containsPartition(int partitionId) {
284299
return this.delegate.containsPartition(partitionId);

clients/da-vinci-client/src/main/java/com/linkedin/davinci/store/StorageEngine.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Optional;
1313
import java.util.Set;
1414
import java.util.function.Supplier;
15+
import javax.annotation.Nullable;
1516

1617

1718
public interface StorageEngine<Partition extends AbstractStoragePartition> extends Closeable {
@@ -154,6 +155,27 @@ void putWithReplicationMetadata(int partitionId, byte[] key, ByteBuffer value, b
154155
*/
155156
void clearStoreVersionState();
156157

158+
/**
159+
* Store a raw GlobalRtDiv entry (chunk, manifest, or full value — all with schema header prepended)
160+
* directly in the metadata partition at the given raw key bytes.
161+
* The key must already contain the VeniceWriter chunking suffix so that
162+
* {@link com.linkedin.davinci.storage.chunking.GenericChunkingAdapter} can assemble it at read time.
163+
*/
164+
void putGlobalRtDivMetadata(byte[] keyBytes, byte[] valueWithHeader);
165+
166+
/**
167+
* Retrieve a raw GlobalRtDiv entry from the metadata partition at the given raw key bytes.
168+
*
169+
* @return the stored bytes (schema header prepended), or {@code null} if not present
170+
*/
171+
@Nullable
172+
byte[] getGlobalRtDivMetadata(byte[] keyBytes);
173+
174+
/**
175+
* Delete a raw GlobalRtDiv entry from the metadata partition at the given raw key bytes.
176+
*/
177+
void deleteGlobalRtDivMetadata(byte[] keyBytes);
178+
157179
/**
158180
* Return true or false based on whether a given partition exists within this storage engine
159181
*

0 commit comments

Comments
 (0)