Skip to content

Commit ba78edd

Browse files
authored
[server] Config-gate collection-merge element replacement for order:ignore fields (linkedin#2846)
Add config-gated element replacement for A/A collection merge During Active/Active write-compute collection merge, SET_UNION on an array field only advanced the element RMD timestamp when the incoming element compared equal to an existing one. For records with order:ignore fields, this silently dropped incoming content in ignored fields while still reporting the write as partially updated. Add element replacement in SortBasedCollectionFieldOpHandler behind the new server config: server.aa.collection.field.element.replacement.enabled, default false. When enabled, newer timestamps replace the stored element with the incoming one. Equal timestamps use a full element comparison, including order:ignore fields, to deterministically break ties and keep A/A colos converged. Thread the config through the A/A ingestion and write-compute path, and add unit and integration tests for array elements with order:ignore fields.
1 parent 108555d commit ba78edd

11 files changed

Lines changed: 449 additions & 10 deletions

File tree

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import static com.linkedin.venice.ConfigKeys.PUBSUB_TOPIC_MANAGER_METADATA_FETCHER_CONSUMER_POOL_SIZE;
6767
import static com.linkedin.venice.ConfigKeys.PUBSUB_TOPIC_MANAGER_METADATA_FETCHER_THREAD_POOL_SIZE;
6868
import static com.linkedin.venice.ConfigKeys.ROUTER_PRINCIPAL_NAME;
69+
import static com.linkedin.venice.ConfigKeys.SERVER_AA_COLLECTION_FIELD_ELEMENT_REPLACEMENT_ENABLED;
6970
import static com.linkedin.venice.ConfigKeys.SERVER_AA_WC_INGESTION_STORAGE_LOOKUP_THREAD_POOL_SIZE;
7071
import static com.linkedin.venice.ConfigKeys.SERVER_AA_WC_WORKLOAD_PARALLEL_PROCESSING_ENABLED;
7172
import static com.linkedin.venice.ConfigKeys.SERVER_AA_WC_WORKLOAD_PARALLEL_PROCESSING_THREAD_POOL_SIZE;
@@ -481,6 +482,12 @@ public class VeniceServerConfig extends VeniceClusterConfig {
481482

482483
private final boolean computeFastAvroEnabled;
483484

485+
/**
486+
* Whether to replace an existing collection-merge array element with the incoming element on a conflict, instead of
487+
* only advancing its replication-metadata timestamp. See SERVER_AA_COLLECTION_FIELD_ELEMENT_REPLACEMENT_ENABLED.
488+
*/
489+
private final boolean activeActiveCollectionFieldElementReplacementEnabled;
490+
484491
private final long participantMessageConsumptionDelayMs;
485492

486493
/**
@@ -911,6 +918,8 @@ public VeniceServerConfig(VeniceProperties serverProperties, Map<String, Map<Str
911918
storeVersionMetadataWaitDuringStateTransitionTimeMs =
912919
serverProperties.getLong(SERVER_STORE_VERSION_METADATA_WAIT_DURING_STATE_TRANSITION_TIME_MS, 300_000);
913920
computeFastAvroEnabled = serverProperties.getBoolean(SERVER_COMPUTE_FAST_AVRO_ENABLED, true);
921+
activeActiveCollectionFieldElementReplacementEnabled =
922+
serverProperties.getBoolean(SERVER_AA_COLLECTION_FIELD_ELEMENT_REPLACEMENT_ENABLED, false);
914923
participantMessageConsumptionDelayMs = serverProperties.getLong(PARTICIPANT_MESSAGE_CONSUMPTION_DELAY_MS, 60000);
915924
serverPromotionToLeaderReplicaDelayMs =
916925
TimeUnit.SECONDS.toMillis(serverProperties.getLong(SERVER_PROMOTION_TO_LEADER_REPLICA_DELAY_SECONDS, 300));
@@ -1565,6 +1574,10 @@ public boolean isComputeFastAvroEnabled() {
15651574
return computeFastAvroEnabled;
15661575
}
15671576

1577+
public boolean isActiveActiveCollectionFieldElementReplacementEnabled() {
1578+
return activeActiveCollectionFieldElementReplacementEnabled;
1579+
}
1580+
15681581
public long getParticipantMessageConsumptionDelayMs() {
15691582
return participantMessageConsumptionDelayMs;
15701583
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ public ActiveActiveStoreIngestionTask(
175175
rmdSerDe,
176176
getStoreName(),
177177
isWriteComputationEnabled,
178-
getServerConfig().isComputeFastAvroEnabled());
178+
getServerConfig().isComputeFastAvroEnabled(),
179+
getServerConfig().isActiveActiveCollectionFieldElementReplacementEnabled());
179180
this.remoteIngestionRepairService = builder.getRemoteIngestionRepairService();
180181
this.reusableObjectsSupplier = Objects.requireNonNull(builder.getReusableObjectsSupplier());
181182

clients/da-vinci-client/src/main/java/com/linkedin/davinci/replication/merge/MergeConflictResolverFactory.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,30 @@ public MergeConflictResolver createMergeConflictResolver(
2323
String storeName,
2424
boolean rmdUseFieldLevelTs,
2525
boolean fastAvroEnabled) {
26+
return createMergeConflictResolver(
27+
annotatedReadOnlySchemaRepository,
28+
rmdSerDe,
29+
storeName,
30+
rmdUseFieldLevelTs,
31+
fastAvroEnabled,
32+
false);
33+
}
34+
35+
public MergeConflictResolver createMergeConflictResolver(
36+
StringAnnotatedStoreSchemaCache annotatedReadOnlySchemaRepository,
37+
RmdSerDe rmdSerDe,
38+
String storeName,
39+
boolean rmdUseFieldLevelTs,
40+
boolean fastAvroEnabled,
41+
boolean collectionFieldElementReplacementEnabled) {
2642
MergeRecordHelper mergeRecordHelper = new CollectionTimestampMergeRecordHelper();
2743
return new MergeConflictResolver(
2844
annotatedReadOnlySchemaRepository,
2945
storeName,
3046
valueSchemaID -> new GenericData.Record(rmdSerDe.getRmdSchema(valueSchemaID)),
31-
new MergeGenericRecord(new WriteComputeProcessor(mergeRecordHelper), mergeRecordHelper),
47+
new MergeGenericRecord(
48+
new WriteComputeProcessor(mergeRecordHelper, collectionFieldElementReplacementEnabled),
49+
mergeRecordHelper),
3250
new MergeByteBuffer(),
3351
new MergeResultValueSchemaResolverImpl(annotatedReadOnlySchemaRepository, storeName),
3452
rmdSerDe,

clients/da-vinci-client/src/main/java/com/linkedin/davinci/schema/merge/AvroCollectionElementComparator.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,22 @@
1717
*/
1818
@ThreadSafe
1919
public class AvroCollectionElementComparator {
20-
public final static AvroCollectionElementComparator INSTANCE = new AvroCollectionElementComparator();
20+
public final static AvroCollectionElementComparator INSTANCE = new AvroCollectionElementComparator(false);
2121

22-
private AvroCollectionElementComparator() {
23-
// Singleton class.
22+
/**
23+
* Unlike {@link #INSTANCE}, this comparator also compares record fields marked with {@code order: ignore}. It yields a
24+
* total order over elements that {@link #INSTANCE} would consider equal because they only differ in ignored fields,
25+
* which is required to deterministically break ties between two same-timestamp collection elements during
26+
* Active/Active conflict resolution.
27+
*/
28+
public final static AvroCollectionElementComparator FULL_COMPARISON_INSTANCE =
29+
new AvroCollectionElementComparator(true);
30+
31+
private final boolean compareOrderIgnoredFields;
32+
33+
private AvroCollectionElementComparator(boolean compareOrderIgnoredFields) {
34+
// Private constructor: instances are exposed only through the shared INSTANCE and FULL_COMPARISON_INSTANCE fields.
35+
this.compareOrderIgnoredFields = compareOrderIgnoredFields;
2436
}
2537

2638
/**
@@ -55,7 +67,7 @@ public int compare(Object o1, Object o2, Schema schema) {
5567

5668
private int compareRecords(GenericRecord r1, GenericRecord r2, Schema schema) {
5769
for (Schema.Field field: schema.getFields()) {
58-
if (field.order() == Schema.Field.Order.IGNORE) {
70+
if (!compareOrderIgnoredFields && field.order() == Schema.Field.Order.IGNORE) {
5971
continue;
6072
}
6173
int pos = field.pos();

clients/da-vinci-client/src/main/java/com/linkedin/davinci/schema/merge/SortBasedCollectionFieldOpHandler.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,22 @@
2727

2828
@ThreadSafe
2929
public class SortBasedCollectionFieldOpHandler extends CollectionFieldOperationHandler {
30+
/**
31+
* When true, a collection-merge (SET_UNION) element that wins a conflict against an existing, comparison-equal element
32+
* replaces the stored element (so content in {@code order: ignore} fields is propagated) rather than only advancing the
33+
* existing element's replication-metadata timestamp. See ConfigKeys#SERVER_AA_COLLECTION_FIELD_ELEMENT_REPLACEMENT_ENABLED.
34+
*/
35+
private final boolean elementReplacementEnabled;
36+
3037
public SortBasedCollectionFieldOpHandler(AvroCollectionElementComparator elementComparator) {
38+
this(elementComparator, false);
39+
}
40+
41+
public SortBasedCollectionFieldOpHandler(
42+
AvroCollectionElementComparator elementComparator,
43+
boolean elementReplacementEnabled) {
3144
super(elementComparator);
45+
this.elementReplacementEnabled = elementReplacementEnabled;
3246
}
3347

3448
@Override
@@ -589,8 +603,29 @@ private UpdateResultStatus handleModifyCollectionMergeList(
589603
activeElementToTsMap.put(toAddElement, modifyTimestamp);
590604
updated = true;
591605
} else if (activeTimestamp < modifyTimestamp) {
606+
if (elementReplacementEnabled) {
607+
// The incoming element wins on a strictly newer timestamp. Drop the stored element first so the incoming one
608+
// (which may carry different content in order:ignore fields) replaces it, rather than IndexedHashMap#put
609+
// keeping the existing key and only updating its timestamp.
610+
activeElementToTsMap.remove(toAddElement);
611+
}
592612
activeElementToTsMap.put(toAddElement, modifyTimestamp);
593613
updated = true;
614+
} else if (elementReplacementEnabled && activeTimestamp == modifyTimestamp) {
615+
// Same timestamp: break the tie deterministically using full element content (including order:ignore fields),
616+
// so all Active/Active colos converge on the same element. A negative index means the element was removed above
617+
// as part of the put-only part, in which case the legacy behavior is preserved.
618+
int existingElementIndex = activeElementToTsMap.indexOf(toAddElement);
619+
if (existingElementIndex >= 0) {
620+
Object existingElement = activeElementToTsMap.getByIndex(existingElementIndex).getKey();
621+
Schema elementSchema = getArraySchema(currValueRecordField.schema()).getElementType();
622+
if (AvroCollectionElementComparator.FULL_COMPARISON_INSTANCE
623+
.compare(toAddElement, existingElement, elementSchema) > 0) {
624+
activeElementToTsMap.remove(toAddElement);
625+
activeElementToTsMap.put(toAddElement, modifyTimestamp);
626+
updated = true;
627+
}
628+
}
594629
}
595630
}
596631

clients/da-vinci-client/src/main/java/com/linkedin/davinci/schema/writecompute/WriteComputeHandlerV2.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,15 @@ public class WriteComputeHandlerV2 extends WriteComputeHandlerV1 {
3232
private final CollectionFieldOperationHandler collectionFieldOperationHandler;
3333

3434
WriteComputeHandlerV2(MergeRecordHelper mergeRecordHelper) {
35+
this(mergeRecordHelper, false);
36+
}
37+
38+
WriteComputeHandlerV2(MergeRecordHelper mergeRecordHelper, boolean collectionFieldElementReplacementEnabled) {
3539
Validate.notNull(mergeRecordHelper);
3640
this.mergeRecordHelper = mergeRecordHelper;
37-
// TODO: get this variable as a argument passed to this constructor.
38-
this.collectionFieldOperationHandler =
39-
new SortBasedCollectionFieldOpHandler(AvroCollectionElementComparator.INSTANCE);
41+
this.collectionFieldOperationHandler = new SortBasedCollectionFieldOpHandler(
42+
AvroCollectionElementComparator.INSTANCE,
43+
collectionFieldElementReplacementEnabled);
4044
}
4145

4246
/**

clients/da-vinci-client/src/main/java/com/linkedin/davinci/schema/writecompute/WriteComputeProcessor.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ public class WriteComputeProcessor {
4141
private final WriteComputeHandlerV2 writeComputeHandlerV2;
4242

4343
public WriteComputeProcessor(MergeRecordHelper mergeRecordHelper) {
44-
this.writeComputeHandlerV2 = new WriteComputeHandlerV2(mergeRecordHelper);
44+
this(mergeRecordHelper, false);
45+
}
46+
47+
public WriteComputeProcessor(MergeRecordHelper mergeRecordHelper, boolean collectionFieldElementReplacementEnabled) {
48+
this.writeComputeHandlerV2 = new WriteComputeHandlerV2(mergeRecordHelper, collectionFieldElementReplacementEnabled);
4549
}
4650

4751
/**
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package com.linkedin.davinci.schema.merge;
2+
3+
import com.linkedin.davinci.schema.SchemaUtils;
4+
import com.linkedin.venice.schema.AvroSchemaParseUtils;
5+
import com.linkedin.venice.schema.rmd.RmdConstants;
6+
import com.linkedin.venice.schema.rmd.RmdSchemaGenerator;
7+
import com.linkedin.venice.schema.rmd.v1.CollectionRmdTimestamp;
8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.List;
11+
import org.apache.avro.Schema;
12+
import org.apache.avro.generic.GenericData;
13+
import org.apache.avro.generic.GenericRecord;
14+
import org.testng.Assert;
15+
import org.testng.annotations.Test;
16+
17+
18+
/**
19+
* Verifies the collection-merge (SET_UNION) element-replacement behavior gated by
20+
* {@code server.aa.collection.field.element.replacement.enabled}.
21+
*
22+
* The list elements are records with a field marked {@code order: ignore}. Two elements that differ only in that ignored
23+
* field are equal under Avro comparison (and {@link Object#equals}/{@link Object#hashCode}), so a union-add of such an
24+
* element collides with the stored one. The legacy behavior keeps the stored element and only advances its timestamp,
25+
* dropping the incoming element's content in the ignored field; the new behavior (flag on) applies the winning element.
26+
*/
27+
public class SortBasedCollectionFieldElementReplacementTest {
28+
private static final String LIST_FIELD = "RecordListField";
29+
private static final String ID_FIELD = "id";
30+
private static final String IGNORED_FIELD = "metadata";
31+
32+
private static final Schema VALUE_SCHEMA = AvroSchemaParseUtils.parseSchemaFromJSONStrictValidation(
33+
"{\n" + " \"type\": \"record\",\n" + " \"name\": \"TestValueWithIgnoredField\",\n"
34+
+ " \"namespace\": \"com.linkedin.davinci.schema.merge\",\n" + " \"fields\": [\n" + " {\n"
35+
+ " \"name\": \"" + LIST_FIELD + "\",\n" + " \"type\": {\n" + " \"type\": \"array\",\n"
36+
+ " \"items\": {\n" + " \"type\": \"record\",\n"
37+
+ " \"name\": \"ElementWithIgnoredField\",\n" + " \"fields\": [\n"
38+
+ " {\"name\": \"" + ID_FIELD + "\", \"type\": \"string\"},\n" + " {\"name\": \""
39+
+ IGNORED_FIELD + "\", \"type\": \"string\", \"order\": \"ignore\", \"default\": \"\"}\n" + " ]\n"
40+
+ " }\n" + " },\n" + " \"default\": []\n" + " }\n" + " ]\n" + "}");
41+
42+
private static final Schema ELEMENT_SCHEMA = VALUE_SCHEMA.getField(LIST_FIELD).schema().getElementType();
43+
44+
private static final Schema RMD_SCHEMA =
45+
SchemaUtils.annotateRmdSchema(RmdSchemaGenerator.generateMetadataSchema(VALUE_SCHEMA));
46+
private static final Schema RMD_TIMESTAMP_SCHEMA =
47+
RMD_SCHEMA.getField(RmdConstants.TIMESTAMP_FIELD_NAME).schema().getTypes().get(1);
48+
49+
@Test
50+
public void testNewerTimestampKeepsStoredElementWhenDisabled() {
51+
SortBasedCollectionFieldOpHandler handler =
52+
new SortBasedCollectionFieldOpHandler(AvroCollectionElementComparator.INSTANCE);
53+
GenericRecord value = valueWithElement(element("a", "old"));
54+
CollectionRmdTimestamp<Object> rmd = collectionMetadata(1L, Collections.singletonList(5L));
55+
56+
UpdateResultStatus status = handler.handleModifyList(
57+
10L,
58+
rmd,
59+
value,
60+
value.getSchema().getField(LIST_FIELD),
61+
Collections.singletonList(element("a", "new")),
62+
Collections.emptyList());
63+
64+
Assert.assertEquals(status, UpdateResultStatus.PARTIALLY_UPDATED);
65+
List<GenericRecord> result = currentList(value);
66+
Assert.assertEquals(result.size(), 1);
67+
// Legacy behavior: the ignored-field content of the incoming element is dropped, only the timestamp is advanced.
68+
Assert.assertEquals(result.get(0).get(IGNORED_FIELD).toString(), "old");
69+
Assert.assertEquals(rmd.getActiveElementTimestamps().get(0).longValue(), 10L);
70+
}
71+
72+
@Test
73+
public void testNewerTimestampReplacesStoredElementWhenEnabled() {
74+
SortBasedCollectionFieldOpHandler handler =
75+
new SortBasedCollectionFieldOpHandler(AvroCollectionElementComparator.INSTANCE, true);
76+
GenericRecord value = valueWithElement(element("a", "old"));
77+
CollectionRmdTimestamp<Object> rmd = collectionMetadata(1L, Collections.singletonList(5L));
78+
79+
UpdateResultStatus status = handler.handleModifyList(
80+
10L,
81+
rmd,
82+
value,
83+
value.getSchema().getField(LIST_FIELD),
84+
Collections.singletonList(element("a", "new")),
85+
Collections.emptyList());
86+
87+
Assert.assertEquals(status, UpdateResultStatus.PARTIALLY_UPDATED);
88+
List<GenericRecord> result = currentList(value);
89+
Assert.assertEquals(result.size(), 1);
90+
// New behavior: the incoming element (newer timestamp) replaces the stored one, propagating the ignored field.
91+
Assert.assertEquals(result.get(0).get(IGNORED_FIELD).toString(), "new");
92+
Assert.assertEquals(rmd.getActiveElementTimestamps().get(0).longValue(), 10L);
93+
}
94+
95+
@Test
96+
public void testEqualTimestampReplacesWhenIncomingWinsTieBreak() {
97+
SortBasedCollectionFieldOpHandler handler =
98+
new SortBasedCollectionFieldOpHandler(AvroCollectionElementComparator.INSTANCE, true);
99+
GenericRecord value = valueWithElement(element("a", "aaa"));
100+
CollectionRmdTimestamp<Object> rmd = collectionMetadata(1L, Collections.singletonList(5L));
101+
102+
// Same timestamp as the stored element; "zzz" > "aaa" by full-content comparison, so the incoming element wins.
103+
handler.handleModifyList(
104+
5L,
105+
rmd,
106+
value,
107+
value.getSchema().getField(LIST_FIELD),
108+
Collections.singletonList(element("a", "zzz")),
109+
Collections.emptyList());
110+
111+
List<GenericRecord> result = currentList(value);
112+
Assert.assertEquals(result.size(), 1);
113+
Assert.assertEquals(result.get(0).get(IGNORED_FIELD).toString(), "zzz");
114+
Assert.assertEquals(rmd.getActiveElementTimestamps().get(0).longValue(), 5L);
115+
}
116+
117+
@Test
118+
public void testEqualTimestampKeepsExistingWhenIncomingLosesTieBreak() {
119+
SortBasedCollectionFieldOpHandler handler =
120+
new SortBasedCollectionFieldOpHandler(AvroCollectionElementComparator.INSTANCE, true);
121+
GenericRecord value = valueWithElement(element("a", "zzz"));
122+
CollectionRmdTimestamp<Object> rmd = collectionMetadata(1L, Collections.singletonList(5L));
123+
124+
// Same timestamp; "aaa" < "zzz" by full-content comparison, so the stored element is kept (deterministic
125+
// tie-break).
126+
handler.handleModifyList(
127+
5L,
128+
rmd,
129+
value,
130+
value.getSchema().getField(LIST_FIELD),
131+
Collections.singletonList(element("a", "aaa")),
132+
Collections.emptyList());
133+
134+
List<GenericRecord> result = currentList(value);
135+
Assert.assertEquals(result.size(), 1);
136+
Assert.assertEquals(result.get(0).get(IGNORED_FIELD).toString(), "zzz");
137+
Assert.assertEquals(rmd.getActiveElementTimestamps().get(0).longValue(), 5L);
138+
}
139+
140+
@Test
141+
public void testEqualTimestampNoOpWhenContentIsIdentical() {
142+
SortBasedCollectionFieldOpHandler handler =
143+
new SortBasedCollectionFieldOpHandler(AvroCollectionElementComparator.INSTANCE, true);
144+
GenericRecord value = valueWithElement(element("a", "same"));
145+
CollectionRmdTimestamp<Object> rmd = collectionMetadata(1L, Collections.singletonList(5L));
146+
147+
// Same timestamp and identical content (full-content comparison == 0): the deterministic tie-break replaces
148+
// nothing, so the element is untouched and no update is reported (no Active/Active churn on a redundant re-add).
149+
UpdateResultStatus status = handler.handleModifyList(
150+
5L,
151+
rmd,
152+
value,
153+
value.getSchema().getField(LIST_FIELD),
154+
Collections.singletonList(element("a", "same")),
155+
Collections.emptyList());
156+
157+
Assert.assertEquals(status, UpdateResultStatus.NOT_UPDATED_AT_ALL);
158+
List<GenericRecord> result = currentList(value);
159+
Assert.assertEquals(result.size(), 1);
160+
Assert.assertEquals(result.get(0).get(IGNORED_FIELD).toString(), "same");
161+
Assert.assertEquals(rmd.getActiveElementTimestamps().get(0).longValue(), 5L);
162+
}
163+
164+
private GenericRecord element(String id, String ignoredFieldValue) {
165+
GenericRecord record = new GenericData.Record(ELEMENT_SCHEMA);
166+
record.put(ID_FIELD, id);
167+
record.put(IGNORED_FIELD, ignoredFieldValue);
168+
return record;
169+
}
170+
171+
private GenericRecord valueWithElement(GenericRecord element) {
172+
GenericRecord value = new GenericData.Record(VALUE_SCHEMA);
173+
value.put(LIST_FIELD, new ArrayList<>(Collections.singletonList(element)));
174+
return value;
175+
}
176+
177+
@SuppressWarnings("unchecked")
178+
private List<GenericRecord> currentList(GenericRecord value) {
179+
return (List<GenericRecord>) value.get(LIST_FIELD);
180+
}
181+
182+
private CollectionRmdTimestamp<Object> collectionMetadata(
183+
long topLevelTimestamp,
184+
List<Long> activeElementTimestamps) {
185+
CollectionTimestampBuilder builder = new CollectionTimestampBuilder(ELEMENT_SCHEMA);
186+
builder.setTopLevelTimestamps(topLevelTimestamp);
187+
builder.setTopLevelColoID(0);
188+
builder.setPutOnlyPartLength(0);
189+
builder.setActiveElementsTimestamps(activeElementTimestamps);
190+
builder.setDeletedElementTimestamps(Collections.<Long>emptyList());
191+
builder.setDeletedElements(ELEMENT_SCHEMA, Collections.emptyList());
192+
builder.setCollectionTimestampSchema(RMD_TIMESTAMP_SCHEMA.getField(LIST_FIELD).schema());
193+
return new CollectionRmdTimestamp<>(builder.build());
194+
}
195+
}

0 commit comments

Comments
 (0)