Skip to content

Commit 8f8863e

Browse files
author
Xin Gao
committed
Auto reset offset during ingestion lag
1 parent 78dabce commit 8f8863e

7 files changed

Lines changed: 217 additions & 13 deletions

File tree

pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManager.java

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -796,9 +796,10 @@ private String createNewSegmentMetadata(TableConfig tableConfig, IdealState idea
796796
// segment. For tables with pauseless mode enabled, this size is unavailable at this step because the
797797
// segment has not yet been built.
798798

799-
createNewSegmentZKMetadata(tableConfig, streamConfigs.get(0), newLLCSegment, newSegmentCreationTimeMs,
799+
createNewSegmentZKMetadataWithOffsetAutoReset(tableConfig, streamConfigs.get(0), newLLCSegment,
800+
newSegmentCreationTimeMs,
800801
committingSegmentDescriptor, committingSegmentZKMetadata, instancePartitions, partitionIds.size(),
801-
numReplicas);
802+
numReplicas, false);
802803
newConsumingSegmentName = newLLCSegment.getSegmentName();
803804
LOGGER.info("Created new segment metadata for segment: {} with status: {}.", newConsumingSegmentName,
804805
Status.IN_PROGRESS);
@@ -928,10 +929,9 @@ private boolean isPeerURL(String segmentLocation) {
928929
private void createNewSegmentZKMetadata(TableConfig tableConfig, StreamConfig streamConfig,
929930
LLCSegmentName newLLCSegmentName, long creationTimeMs, CommittingSegmentDescriptor committingSegmentDescriptor,
930931
@Nullable SegmentZKMetadata committingSegmentZKMetadata, InstancePartitions instancePartitions, int numPartitions,
931-
int numReplicas) {
932+
int numReplicas, String startOffset) {
932933
String realtimeTableName = tableConfig.getTableName();
933934
String segmentName = newLLCSegmentName.getSegmentName();
934-
String startOffset = committingSegmentDescriptor.getNextOffset();
935935

936936
LOGGER.info(
937937
"Creating segment ZK metadata for new CONSUMING segment: {} with start offset: {} and creation time: {}",
@@ -962,6 +962,72 @@ private void createNewSegmentZKMetadata(TableConfig tableConfig, StreamConfig st
962962
persistSegmentZKMetadata(realtimeTableName, newSegmentZKMetadata, -1);
963963
}
964964

965+
/**
966+
* Creates and persists segment ZK metadata for the new CONSUMING segment.
967+
*/
968+
private void createNewSegmentZKMetadataWithOffsetAutoReset(TableConfig tableConfig, StreamConfig streamConfig,
969+
LLCSegmentName newLLCSegmentName, long creationTimeMs, CommittingSegmentDescriptor committingSegmentDescriptor,
970+
@Nullable SegmentZKMetadata committingSegmentZKMetadata, InstancePartitions instancePartitions, int numPartitions,
971+
int numReplicas, boolean skipAutoReset) {
972+
String oldStartOffset = committingSegmentDescriptor.getNextOffset();
973+
String startOffset = skipAutoReset ? oldStartOffset : computeStartOffset(
974+
oldStartOffset, streamConfig, newLLCSegmentName.getPartitionGroupId());
975+
createNewSegmentZKMetadata(tableConfig, streamConfig, newLLCSegmentName, creationTimeMs,
976+
committingSegmentDescriptor, committingSegmentZKMetadata, instancePartitions, numPartitions, numReplicas,
977+
startOffset);
978+
}
979+
980+
private String computeStartOffset(
981+
String nextOffset, StreamConfig streamConfig, int partitionId) {
982+
long timeThreshold = streamConfig.getOffsetAutoResetTimeSecThreshold();
983+
int offsetThreshold = streamConfig.getOffsetAutoResetOffsetThreshold();
984+
if (timeThreshold <= 0 && offsetThreshold <= 0) {
985+
return nextOffset;
986+
}
987+
String clientId =
988+
PinotLLCRealtimeSegmentManager.class.getSimpleName() + "-" + streamConfig.getTableNameWithType() + "-"
989+
+ streamConfig.getTopicName();
990+
StreamConsumerFactory consumerFactory = StreamConsumerFactoryProvider.create(streamConfig);
991+
StreamPartitionMsgOffset offsetAtSLA;
992+
StreamPartitionMsgOffset latestOffset;
993+
try (StreamMetadataProvider metadataProvider = consumerFactory.createPartitionMetadataProvider(
994+
clientId, partitionId)) {
995+
// Fetching timestamp from an offset is an expensive operation which requires reading the data,
996+
// while fetching offset from timestamp is lightweight and only needs to read metadata.
997+
// Hence, instead of checking if latestOffset's time - nextOffset's time < SLA, we would check
998+
// (CurrentTime - SLA)'s offset > nextOffset.
999+
// TODO: it is relying on System.currentTimeMillis() which might be affected by time drift. If we are able to
1000+
// get nextOffset's time, we should instead check (nextOffset's time + SLA)'s offset < latestOffset
1001+
latestOffset = metadataProvider.fetchStreamPartitionOffset(OffsetCriteria.LARGEST_OFFSET_CRITERIA, 5000);
1002+
LOGGER.info("Latest offset of topic {} and partition {} is {}",
1003+
streamConfig.getTopicName(), partitionId, latestOffset);
1004+
offsetAtSLA = metadataProvider.getOffsetAtTimestamp(
1005+
partitionId, System.currentTimeMillis() - timeThreshold * 1000);
1006+
LOGGER.info("Offset at SLA of topic {} and partition {} is {}",
1007+
streamConfig.getTopicName(), partitionId, offsetAtSLA);
1008+
} catch (Exception e) {
1009+
LOGGER.warn("Not able to fetch the offset metadata, skip auto resetting offsets", e);
1010+
return nextOffset;
1011+
}
1012+
try {
1013+
if (timeThreshold > 0 && offsetAtSLA != null
1014+
&& Long.valueOf(offsetAtSLA.toString()) > Long.valueOf(nextOffset)) {
1015+
LOGGER.info("Auto reset offset from {} to {} on partition {} because time threshold reached",
1016+
nextOffset, latestOffset, partitionId);
1017+
return latestOffset.toString();
1018+
}
1019+
if (offsetThreshold > 0
1020+
&& Long.valueOf(latestOffset.toString()) - Long.valueOf(nextOffset) > offsetThreshold) {
1021+
LOGGER.info("Auto reset offset from {} to {} on partition {} because number of offsets threshold reached",
1022+
nextOffset, latestOffset, partitionId);
1023+
return latestOffset.toString();
1024+
}
1025+
} catch (Exception e) {
1026+
LOGGER.warn("Not able to convert the offset to LONG type, skip auto resetting offsets", e);
1027+
}
1028+
return nextOffset;
1029+
}
1030+
9651031
@Nullable
9661032
private SegmentPartitionMetadata getPartitionMetadataFromTableConfig(TableConfig tableConfig, int partitionId,
9671033
int numPartitionGroups) {
@@ -1585,8 +1651,10 @@ IdealState ensureAllPartitionsConsuming(TableConfig tableConfig, List<StreamConf
15851651
CommittingSegmentDescriptor committingSegmentDescriptor =
15861652
new CommittingSegmentDescriptor(latestSegmentName,
15871653
(offsetFactory.create(latestSegmentZKMetadata.getEndOffset()).toString()), 0);
1588-
createNewSegmentZKMetadata(tableConfig, streamConfigs.get(0), newLLCSegmentName, currentTimeMs,
1589-
committingSegmentDescriptor, latestSegmentZKMetadata, instancePartitions, numPartitions, numReplicas);
1654+
createNewSegmentZKMetadataWithOffsetAutoReset(tableConfig, streamConfigs.get(0), newLLCSegmentName,
1655+
currentTimeMs,
1656+
committingSegmentDescriptor, latestSegmentZKMetadata, instancePartitions, numPartitions, numReplicas,
1657+
false);
15901658
updateInstanceStatesForNewConsumingSegment(instanceStatesMap, latestSegmentName, newSegmentName,
15911659
segmentAssignment, instancePartitionsMap);
15921660
} else { // partition group reached end of life
@@ -1711,8 +1779,8 @@ private void createNewConsumingSegment(TableConfig tableConfig, StreamConfig str
17111779
LLCSegmentName newLLCSegmentName = getNextLLCSegmentName(latestLLCSegmentName, currentTimeMs);
17121780
CommittingSegmentDescriptor committingSegmentDescriptor =
17131781
new CommittingSegmentDescriptor(latestSegmentZKMetadata.getSegmentName(), startOffset.toString(), 0);
1714-
createNewSegmentZKMetadata(tableConfig, streamConfig, newLLCSegmentName, currentTimeMs, committingSegmentDescriptor,
1715-
latestSegmentZKMetadata, instancePartitions, numPartitions, numReplicas);
1782+
createNewSegmentZKMetadataWithOffsetAutoReset(tableConfig, streamConfig, newLLCSegmentName, currentTimeMs,
1783+
committingSegmentDescriptor, latestSegmentZKMetadata, instancePartitions, numPartitions, numReplicas, false);
17161784
String newSegmentName = newLLCSegmentName.getSegmentName();
17171785
updateInstanceStatesForNewConsumingSegment(instanceStatesMap, null, newSegmentName, segmentAssignment,
17181786
instancePartitionsMap);
@@ -1790,8 +1858,8 @@ private String setupNewPartitionGroup(TableConfig tableConfig, StreamConfig stre
17901858
String newSegmentName = newLLCSegmentName.getSegmentName();
17911859

17921860
CommittingSegmentDescriptor committingSegmentDescriptor = new CommittingSegmentDescriptor(null, startOffset, 0);
1793-
createNewSegmentZKMetadata(tableConfig, streamConfig, newLLCSegmentName, creationTimeMs,
1794-
committingSegmentDescriptor, null, instancePartitions, numPartitions, numReplicas);
1861+
createNewSegmentZKMetadataWithOffsetAutoReset(tableConfig, streamConfig, newLLCSegmentName, creationTimeMs,
1862+
committingSegmentDescriptor, null, instancePartitions, numPartitions, numReplicas, true);
17951863

17961864
return newSegmentName;
17971865
}

pinot-controller/src/test/java/org/apache/pinot/controller/helix/core/realtime/PinotLLCRealtimeSegmentManagerTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,14 @@
7777
import org.apache.pinot.spi.env.PinotConfiguration;
7878
import org.apache.pinot.spi.filesystem.PinotFSFactory;
7979
import org.apache.pinot.spi.stream.LongMsgOffset;
80+
import org.apache.pinot.spi.stream.OffsetCriteria;
8081
import org.apache.pinot.spi.stream.PartitionGroupConsumptionStatus;
8182
import org.apache.pinot.spi.stream.PartitionGroupMetadata;
8283
import org.apache.pinot.spi.stream.StreamConfig;
84+
import org.apache.pinot.spi.stream.StreamConfigProperties;
85+
import org.apache.pinot.spi.stream.StreamConsumerFactory;
86+
import org.apache.pinot.spi.stream.StreamConsumerFactoryProvider;
87+
import org.apache.pinot.spi.stream.StreamMetadataProvider;
8388
import org.apache.pinot.spi.utils.CommonConstants;
8489
import org.apache.pinot.spi.utils.CommonConstants.Helix;
8590
import org.apache.pinot.spi.utils.CommonConstants.Helix.Instance;
@@ -91,6 +96,7 @@
9196
import org.apache.pinot.util.TestUtils;
9297
import org.apache.zookeeper.data.Stat;
9398
import org.joda.time.Interval;
99+
import org.mockito.MockedStatic;
94100
import org.testng.Assert;
95101
import org.testng.annotations.AfterClass;
96102
import org.testng.annotations.Test;
@@ -122,6 +128,7 @@ public class PinotLLCRealtimeSegmentManagerTest {
122128
static final String CRC = Long.toString(RANDOM.nextLong() & 0xFFFFFFFFL);
123129
static final SegmentVersion SEGMENT_VERSION = RANDOM.nextBoolean() ? SegmentVersion.v1 : SegmentVersion.v3;
124130
static final int NUM_DOCS = RANDOM.nextInt(Integer.MAX_VALUE) + 1;
131+
static final long LATEST_OFFSET = PARTITION_OFFSET.getOffset() * 2 + NUM_DOCS;
125132
static final int SEGMENT_SIZE_IN_BYTES = 100000000;
126133
@AfterClass
127134
public void tearDown()
@@ -325,6 +332,73 @@ public void testCommitSegment() {
325332
assertNull(consumingSegmentZKMetadata);
326333
}
327334

335+
@Test
336+
public void testCommitSegmentWithOffsetAutoReset() throws Exception {
337+
// Set up a new table with 2 replicas, 5 instances, 4 partition
338+
PinotHelixResourceManager mockHelixResourceManager = mock(PinotHelixResourceManager.class);
339+
FakePinotLLCRealtimeSegmentManager segmentManager =
340+
new FakePinotLLCRealtimeSegmentManager(mockHelixResourceManager);
341+
setUpNewTable(segmentManager, 2, 5, 4);
342+
Map<String, Map<String, String>> instanceStatesMap = segmentManager._idealState.getRecord().getMapFields();
343+
Map<String, String> streamConfigMap = IngestionConfigUtils.getStreamConfigMaps(segmentManager._tableConfig).get(0);
344+
streamConfigMap.put(
345+
StreamConfigProperties.OFFSET_AUTO_RESET_OFFSET_THRESHOLD_KEY, "100"
346+
);
347+
segmentManager.makeTableConfig(streamConfigMap);
348+
349+
StreamConsumerFactory mockConsumerFactory = mock(StreamConsumerFactory.class);
350+
StreamMetadataProvider mockMetadataProvider = mock(StreamMetadataProvider.class);
351+
when(mockConsumerFactory.createPartitionMetadataProvider(anyString(), anyInt()))
352+
.thenReturn(mockMetadataProvider);
353+
when(mockMetadataProvider.fetchStreamPartitionOffset(eq(OffsetCriteria.LARGEST_OFFSET_CRITERIA), anyLong()))
354+
.thenReturn(new LongMsgOffset(LATEST_OFFSET));
355+
when(mockMetadataProvider.getOffsetAtTimestamp(eq(0), anyLong()))
356+
.thenReturn(PARTITION_OFFSET);
357+
358+
try (MockedStatic<StreamConsumerFactoryProvider> mockedStaticProvider = mockStatic(
359+
StreamConsumerFactoryProvider.class)) {
360+
361+
mockedStaticProvider.when(() -> StreamConsumerFactoryProvider.create(segmentManager._streamConfigs.get(0)))
362+
.thenReturn(mockConsumerFactory);
363+
364+
// Commit a segment for partition group 0
365+
String committingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 0, CURRENT_TIME_MS).getSegmentName();
366+
String endOffset = new LongMsgOffset(PARTITION_OFFSET.getOffset() + NUM_DOCS).toString();
367+
CommittingSegmentDescriptor committingSegmentDescriptor =
368+
new CommittingSegmentDescriptor(committingSegment, endOffset, SEGMENT_SIZE_IN_BYTES);
369+
committingSegmentDescriptor.setSegmentMetadata(mockSegmentMetadata());
370+
segmentManager.commitSegmentMetadata(REALTIME_TABLE_NAME, committingSegmentDescriptor);
371+
372+
// Verify instance states for committed segment and new consuming segment
373+
Map<String, String> committedSegmentInstanceStateMap = instanceStatesMap.get(committingSegment);
374+
assertNotNull(committedSegmentInstanceStateMap);
375+
assertEquals(new HashSet<>(committedSegmentInstanceStateMap.values()),
376+
Collections.singleton(SegmentStateModel.ONLINE));
377+
378+
String consumingSegment = new LLCSegmentName(RAW_TABLE_NAME, 0, 1, CURRENT_TIME_MS).getSegmentName();
379+
Map<String, String> consumingSegmentInstanceStateMap = instanceStatesMap.get(consumingSegment);
380+
assertNotNull(consumingSegmentInstanceStateMap);
381+
assertEquals(new HashSet<>(consumingSegmentInstanceStateMap.values()),
382+
Collections.singleton(SegmentStateModel.CONSUMING));
383+
384+
// Verify segment ZK metadata for committed segment and new consuming segment
385+
SegmentZKMetadata committedSegmentZKMetadata = segmentManager._segmentZKMetadataMap.get(committingSegment);
386+
assertEquals(committedSegmentZKMetadata.getStatus(), Status.DONE);
387+
assertEquals(committedSegmentZKMetadata.getStartOffset(), PARTITION_OFFSET.toString());
388+
assertEquals(committedSegmentZKMetadata.getEndOffset(), endOffset);
389+
assertEquals(committedSegmentZKMetadata.getCreationTime(), CURRENT_TIME_MS);
390+
assertEquals(committedSegmentZKMetadata.getCrc(), Long.parseLong(CRC));
391+
assertEquals(committedSegmentZKMetadata.getIndexVersion(), SEGMENT_VERSION.name());
392+
assertEquals(committedSegmentZKMetadata.getTotalDocs(), NUM_DOCS);
393+
assertEquals(committedSegmentZKMetadata.getSizeInBytes(), SEGMENT_SIZE_IN_BYTES);
394+
395+
SegmentZKMetadata consumingSegmentZKMetadata = segmentManager._segmentZKMetadataMap.get(consumingSegment);
396+
assertEquals(consumingSegmentZKMetadata.getStatus(), Status.IN_PROGRESS);
397+
assertEquals(consumingSegmentZKMetadata.getStartOffset(), String.valueOf(LATEST_OFFSET));
398+
assertEquals(committedSegmentZKMetadata.getCreationTime(), CURRENT_TIME_MS);
399+
}
400+
}
401+
328402
/**
329403
* Test cases for the scenario where stream partitions increase, and the validation manager is attempting to create
330404
* segments for new partitions. This test assumes that all other factors remain the same (no error conditions or
@@ -1725,6 +1799,13 @@ void makeTableConfig() {
17251799
_streamConfigs = IngestionConfigUtils.getStreamConfigs(_tableConfig);
17261800
}
17271801

1802+
void makeTableConfig(Map<String, String> streamConfigMap) {
1803+
_tableConfig =
1804+
new TableConfigBuilder(TableType.REALTIME).setTableName(RAW_TABLE_NAME).setNumReplicas(_numReplicas)
1805+
.setStreamConfigs(streamConfigMap).build();
1806+
_streamConfigs = IngestionConfigUtils.getStreamConfigs(_tableConfig);
1807+
}
1808+
17281809
void makeConsumingInstancePartitions() {
17291810
List<String> instances = new ArrayList<>(_numInstances);
17301811
for (int i = 0; i < _numInstances; i++) {

pinot-plugins/pinot-stream-ingestion/pinot-kafka-2.0/src/main/java/org/apache/pinot/plugin/stream/kafka20/KafkaStreamMetadataProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ public List<TopicMetadata> getTopics() {
187187
}
188188
}
189189

190+
@Override
191+
public StreamPartitionMsgOffset getOffsetAtTimestamp(int partitionId, long timestampMillis) {
192+
return new LongMsgOffset(
193+
_consumer.offsetsForTimes(Map.of(_topicPartition, timestampMillis)).get(_topicPartition).offset());
194+
}
195+
190196
public static class KafkaTopicMetadata implements TopicMetadata {
191197
private String _name;
192198

pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/main/java/org/apache/pinot/plugin/stream/kafka30/KafkaStreamMetadataProvider.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ public KafkaTopicMetadata setName(String name) {
199199
return this;
200200
}
201201
}
202+
203+
@Override
204+
public StreamPartitionMsgOffset getOffsetAtTimestamp(int partitionId, long timestampMillis) {
205+
return new LongMsgOffset(
206+
_consumer.offsetsForTimes(Map.of(_topicPartition, timestampMillis)).get(_topicPartition).offset());
207+
}
208+
202209
@Override
203210
public void close()
204211
throws IOException {

0 commit comments

Comments
 (0)