This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new bb448114ab2 Add segmentAgeField option to time-based tier selector
(endTime | startTime | creationTime) (#19415)
bb448114ab2 is described below
commit bb448114ab2b93ddc2e27d53ec0ef1b4dee73d9d
Author: Chaitanya Deepthi <[email protected]>
AuthorDate: Tue Sep 1 23:06:49 2026 -0700
Add segmentAgeField option to time-based tier selector (endTime | startTime
| creationTime) (#19415)
---
.../org/apache/pinot/common/tier/TierFactory.java | 4 +-
.../common/tier/TimeBasedTierSegmentSelector.java | 81 +++++++++++++--
.../pinot/common/tier/TierSegmentSelectorTest.java | 109 +++++++++++++++++++++
.../apache/pinot/spi/config/table/TierConfig.java | 19 ++++
4 files changed, 205 insertions(+), 8 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/tier/TierFactory.java
b/pinot-common/src/main/java/org/apache/pinot/common/tier/TierFactory.java
index a0062f969b9..2ef0170cc89 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/tier/TierFactory.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/tier/TierFactory.java
@@ -51,7 +51,9 @@ public final class TierFactory {
LOGGER.debug("Provided segments: {} for tier: {}",
providedSegmentsForTier, tierConfig.getName());
segmentSelector = new FixedTierSegmentSelector(providedSegmentsForTier);
} else if
(segmentSelectorType.equalsIgnoreCase(TierFactory.TIME_SEGMENT_SELECTOR_TYPE)) {
- segmentSelector = new
TimeBasedTierSegmentSelector(tierConfig.getSegmentAge());
+ TimeBasedTierSegmentSelector.AgeField ageField =
+
TimeBasedTierSegmentSelector.AgeField.fromConfig(tierConfig.getSegmentAgeField());
+ segmentSelector = new
TimeBasedTierSegmentSelector(tierConfig.getSegmentAge(), ageField);
} else if
(segmentSelectorType.equalsIgnoreCase(TierFactory.FIXED_SEGMENT_SELECTOR_TYPE))
{
List<String> segments = tierConfig.getSegmentList();
segmentSelector =
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/tier/TimeBasedTierSegmentSelector.java
b/pinot-common/src/main/java/org/apache/pinot/common/tier/TimeBasedTierSegmentSelector.java
index f182e6a17ee..d9e46679e46 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/tier/TimeBasedTierSegmentSelector.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/tier/TimeBasedTierSegmentSelector.java
@@ -19,16 +19,58 @@
package org.apache.pinot.common.tier;
import com.google.common.base.Preconditions;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
import org.apache.pinot.common.metadata.segment.SegmentZKMetadata;
import org.apache.pinot.spi.utils.TimeUtils;
-/// A [TierSegmentSelector] strategy which selects segments for a tier based
on the age of the segment
+/// A [TierSegmentSelector] strategy which selects segments for a tier based
on the age of the segment.
+///
+/// The age reference is controlled by the tier's `segmentAgeField`:
+/// - `endTime` (default, backward-compatible): uses
`SegmentZKMetadata#getEndTimeMs()`, the segment's
+/// max data timestamp. Suitable when segment age tracks data recency,
e.g. streaming ingest where
+/// endTime is close to wall-clock now.
+/// - `startTime`: uses `SegmentZKMetadata#getStartTimeMs()`, the segment's
min data timestamp.
+/// Suitable when segment age should track the oldest data in the segment.
+/// - `creationTime`: uses `SegmentZKMetadata#getCreationTime()`, when the
segment file was built.
+/// Suitable when segment age should track ingestion recency, e.g. batch
ingest of historical data
+/// where endTime lies far in the past regardless of when the segment was
created.
public class TimeBasedTierSegmentSelector implements TierSegmentSelector {
+
+ /// Which timestamp field on [SegmentZKMetadata] is compared against the age
threshold.
+ public enum AgeField {
+ END_TIME, START_TIME, CREATION_TIME;
+
+ public static AgeField fromConfig(@Nullable String value) {
+ if (StringUtils.isEmpty(value)) {
+ return END_TIME;
+ }
+ String normalized = value.trim();
+ if ("endtime".equalsIgnoreCase(normalized) ||
"end_time".equalsIgnoreCase(normalized)) {
+ return END_TIME;
+ }
+ if ("starttime".equalsIgnoreCase(normalized) ||
"start_time".equalsIgnoreCase(normalized)) {
+ return START_TIME;
+ }
+ if ("creationtime".equalsIgnoreCase(normalized) ||
"creation_time".equalsIgnoreCase(normalized)) {
+ return CREATION_TIME;
+ }
+ throw new IllegalArgumentException(
+ "Unsupported segmentAgeField: '" + value + "'. Expected 'endTime',
'startTime' or 'creationTime'.");
+ }
+ }
+
private final long _segmentAgeMillis;
+ private final AgeField _ageField;
public TimeBasedTierSegmentSelector(String segmentAge) {
+ this(segmentAge, AgeField.END_TIME);
+ }
+
+ public TimeBasedTierSegmentSelector(String segmentAge, AgeField ageField) {
_segmentAgeMillis = TimeUtils.convertPeriodToMillis(segmentAge);
+ _ageField = ageField != null ? ageField : AgeField.END_TIME;
}
@Override
@@ -43,11 +85,30 @@ public class TimeBasedTierSegmentSelector implements
TierSegmentSelector {
return false;
}
- // get segment end time to decide if segment gets selected
- long endTimeMs = segmentZKMetadata.getEndTimeMs();
- Preconditions.checkState(endTimeMs > 0, "Invalid endTimeMs: %s for
segment: %s of table: %s", endTimeMs,
- segmentZKMetadata.getSegmentName(), tableNameWithType);
- return (System.currentTimeMillis() - endTimeMs) > _segmentAgeMillis;
+ long referenceMs;
+ switch (_ageField) {
+ case CREATION_TIME:
+ referenceMs = segmentZKMetadata.getCreationTime();
+ // Segments predating the creationTime field return a non-positive
value; treat as aged so they
+ // qualify for the tier rather than failing evaluation.
+ if (referenceMs <= 0) {
+ return true;
+ }
+ break;
+ case END_TIME:
+ referenceMs = segmentZKMetadata.getEndTimeMs();
+ Preconditions.checkState(referenceMs > 0, "Invalid endTimeMs: %s for
segment: %s of table: %s", referenceMs,
+ segmentZKMetadata.getSegmentName(), tableNameWithType);
+ break;
+ case START_TIME:
+ referenceMs = segmentZKMetadata.getStartTimeMs();
+ Preconditions.checkState(referenceMs > 0, "Invalid startTimeMs: %s for
segment: %s of table: %s", referenceMs,
+ segmentZKMetadata.getSegmentName(), tableNameWithType);
+ break;
+ default:
+ throw new IllegalStateException("Unhandled segmentAgeField: " +
_ageField);
+ }
+ return (System.currentTimeMillis() - referenceMs) > _segmentAgeMillis;
}
/// Gets the age cutoff for segments accepted by this strategy
@@ -55,8 +116,14 @@ public class TimeBasedTierSegmentSelector implements
TierSegmentSelector {
return _segmentAgeMillis;
}
+ /// The [SegmentZKMetadata] field this selector compares against the age
threshold.
+ public AgeField getAgeField() {
+ return _ageField;
+ }
+
@Override
public String toString() {
- return "TimeBasedTierSegmentSelector{_segmentAgeMillis=" +
_segmentAgeMillis + "}";
+ return "TimeBasedTierSegmentSelector{_segmentAgeMillis=" +
_segmentAgeMillis
+ + ", _ageField=" + _ageField + "}";
}
}
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/tier/TierSegmentSelectorTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/tier/TierSegmentSelectorTest.java
index 6b3166c8e6f..bb9ef3a0b13 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/tier/TierSegmentSelectorTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/tier/TierSegmentSelectorTest.java
@@ -85,6 +85,115 @@ public class TierSegmentSelectorTest {
Assert.assertFalse(segmentSelector.selectSegment(tableNameWithType,
realtimeSegmentZKMetadata));
}
+ @Test
+ public void testTimeBasedSegmentSelectorWithCreationTimeAgeField() {
+ long now = System.currentTimeMillis();
+ String segmentName = "segment_1";
+ String tableNameWithType = "myTable_OFFLINE";
+
+ // A segment whose *data* is 2 years old (endTime long in the past) but
was created 5 minutes ago —
+ // e.g. batch ingest of historical Iceberg data. The endTime-based default
would match any tier
+ // with any age threshold, defeating tier lifecycle intent. The
creationTime-based selector should
+ // treat this as a 5-minute-old segment.
+ SegmentZKMetadata zk = new SegmentZKMetadata(segmentName);
+ zk.setStartTime(now - TimeUnit.DAYS.toMillis(730));
+ zk.setEndTime(now - TimeUnit.DAYS.toMillis(729));
+ zk.setTimeUnit(TimeUnit.MILLISECONDS);
+ zk.setCreationTime(now - TimeUnit.MINUTES.toMillis(5));
+ zk.setStatus(Status.DONE);
+
+ // Default (END_TIME): historical data → matches ANY sane threshold.
+ TimeBasedTierSegmentSelector byEndTime = new
TimeBasedTierSegmentSelector("30m");
+ Assert.assertTrue(byEndTime.selectSegment(tableNameWithType, zk));
+
+ // creationTime with 30m threshold: segment created 5m ago → NOT matched.
+ TimeBasedTierSegmentSelector byCreation30m = new
TimeBasedTierSegmentSelector("30m",
+ TimeBasedTierSegmentSelector.AgeField.CREATION_TIME);
+ Assert.assertEquals(byCreation30m.getAgeField(),
TimeBasedTierSegmentSelector.AgeField.CREATION_TIME);
+ Assert.assertFalse(byCreation30m.selectSegment(tableNameWithType, zk));
+
+ // creationTime with 1m threshold: created 5m ago > 1m → matched.
+ TimeBasedTierSegmentSelector byCreation1m = new
TimeBasedTierSegmentSelector("1m",
+ TimeBasedTierSegmentSelector.AgeField.CREATION_TIME);
+ Assert.assertTrue(byCreation1m.selectSegment(tableNameWithType, zk));
+
+ // Segments predating the creationTime field (value <= 0) are treated as
aged and qualify for the tier.
+ SegmentZKMetadata legacy = new SegmentZKMetadata("legacy_segment");
+ legacy.setStartTime(now - TimeUnit.DAYS.toMillis(30));
+ legacy.setEndTime(now - TimeUnit.DAYS.toMillis(29));
+ legacy.setTimeUnit(TimeUnit.MILLISECONDS);
+ legacy.setStatus(Status.DONE);
+ // creationTime not set — defaults to -1
+ Assert.assertTrue(byCreation1m.selectSegment(tableNameWithType, legacy));
+
+ // Consuming segments never match regardless of reference field.
+ SegmentZKMetadata consuming = new SegmentZKMetadata("myTable__0__1__" +
now);
+ consuming.setStatus(Status.IN_PROGRESS);
+ consuming.setCreationTime(now - TimeUnit.HOURS.toMillis(1));
+ Assert.assertFalse(byCreation1m.selectSegment("myTable_REALTIME",
consuming));
+ }
+
+ @Test
+ public void testTimeBasedSegmentSelectorWithStartTimeAgeField() {
+ long now = System.currentTimeMillis();
+ String tableNameWithType = "myTable_OFFLINE";
+
+ // Segment covers a 1-day range 8 days ago: startTime 8d ago, endTime 7d
ago.
+ SegmentZKMetadata zk = new SegmentZKMetadata("segment_start");
+ zk.setStartTime(now - TimeUnit.DAYS.toMillis(8));
+ zk.setEndTime(now - TimeUnit.DAYS.toMillis(7));
+ zk.setTimeUnit(TimeUnit.MILLISECONDS);
+ zk.setStatus(Status.DONE);
+
+ // startTime with 7d threshold: startTime is 8d ago > 7d → matched.
+ TimeBasedTierSegmentSelector byStart7d = new
TimeBasedTierSegmentSelector("7d",
+ TimeBasedTierSegmentSelector.AgeField.START_TIME);
+ Assert.assertEquals(byStart7d.getAgeField(),
TimeBasedTierSegmentSelector.AgeField.START_TIME);
+ Assert.assertTrue(byStart7d.selectSegment(tableNameWithType, zk));
+
+ // startTime with 10d threshold: startTime is 8d ago < 10d → NOT matched.
+ TimeBasedTierSegmentSelector byStart10d = new
TimeBasedTierSegmentSelector("10d",
+ TimeBasedTierSegmentSelector.AgeField.START_TIME);
+ Assert.assertFalse(byStart10d.selectSegment(tableNameWithType, zk));
+ }
+
+ @Test
+ public void testAgeFieldParsing() {
+ // Default / null / empty → END_TIME (backward compatible).
+ Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig(null),
+ TimeBasedTierSegmentSelector.AgeField.END_TIME);
+ Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig(""),
+ TimeBasedTierSegmentSelector.AgeField.END_TIME);
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("endTime"),
+ TimeBasedTierSegmentSelector.AgeField.END_TIME);
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("END_TIME"),
+ TimeBasedTierSegmentSelector.AgeField.END_TIME);
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("end_time"),
+ TimeBasedTierSegmentSelector.AgeField.END_TIME);
+
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("startTime"),
+ TimeBasedTierSegmentSelector.AgeField.START_TIME);
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("START_TIME"),
+ TimeBasedTierSegmentSelector.AgeField.START_TIME);
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("start_time"),
+ TimeBasedTierSegmentSelector.AgeField.START_TIME);
+
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("creationTime"),
+ TimeBasedTierSegmentSelector.AgeField.CREATION_TIME);
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("CREATION_TIME"),
+ TimeBasedTierSegmentSelector.AgeField.CREATION_TIME);
+
Assert.assertEquals(TimeBasedTierSegmentSelector.AgeField.fromConfig("creation_time"),
+ TimeBasedTierSegmentSelector.AgeField.CREATION_TIME);
+
+ // Unknown value → clear error message so operators spot the typo.
+ try {
+ TimeBasedTierSegmentSelector.AgeField.fromConfig("pushTime");
+ Assert.fail("Expected IllegalArgumentException for unsupported value");
+ } catch (IllegalArgumentException expected) {
+ Assert.assertTrue(expected.getMessage().contains("pushTime"));
+ }
+ }
+
@Test
public void testRealTimeConsumingSegmentShouldNotBeRelocated() {
diff --git
a/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/TierConfig.java
b/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/TierConfig.java
index ba39abc5ded..522485e650f 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/TierConfig.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/config/table/TierConfig.java
@@ -38,6 +38,10 @@ public class TierConfig extends BaseJsonConfig {
@JsonPropertyDescription("For 'TIME' segment selector, the period after
which to select segments for this tier")
private final String _segmentAge;
+ @JsonPropertyDescription("For 'TIME' segment selector, the segment ZK
metadata field to compare against for age. "
+ + "Accepts 'endTime' (default, backward-compatible) or 'creationTime'.")
+ private final String _segmentAgeField;
+
@JsonPropertyDescription("For 'FIXED' segment selector, the list of segments
to select for this tier")
private final List<String> _segmentList;
@@ -60,6 +64,7 @@ public class TierConfig extends BaseJsonConfig {
public TierConfig(@JsonProperty(value = "name", required = true) String name,
@JsonProperty(value = "segmentSelectorType", required = true) String
segmentSelectorType,
@JsonProperty("segmentAge") @Nullable String segmentAge,
+ @JsonProperty("segmentAgeField") @Nullable String segmentAgeField,
@JsonProperty("segmentList") @Nullable List<String> segmentList,
@JsonProperty(value = "storageType", required = true) String storageType,
@JsonProperty("serverTag") @Nullable String serverTag,
@@ -72,6 +77,7 @@ public class TierConfig extends BaseJsonConfig {
_name = name;
_segmentSelectorType = segmentSelectorType;
_segmentAge = segmentAge;
+ _segmentAgeField = segmentAgeField;
_segmentList = segmentList;
_storageType = storageType;
_serverTag = serverTag;
@@ -79,6 +85,14 @@ public class TierConfig extends BaseJsonConfig {
_tierBackendProperties = tierBackendProperties;
}
+ /// Backward-compatible constructor without segmentAgeField (defaults to
endTime).
+ public TierConfig(String name, String segmentSelectorType, @Nullable String
segmentAge,
+ @Nullable List<String> segmentList, String storageType, @Nullable String
serverTag,
+ @Nullable String tierBackend, @Nullable Map<String, String>
tierBackendProperties) {
+ this(name, segmentSelectorType, segmentAge, null, segmentList,
storageType, serverTag, tierBackend,
+ tierBackendProperties);
+ }
+
public String getName() {
return _name;
}
@@ -92,6 +106,11 @@ public class TierConfig extends BaseJsonConfig {
return _segmentAge;
}
+ @Nullable
+ public String getSegmentAgeField() {
+ return _segmentAgeField;
+ }
+
@Nullable
public List<String> getSegmentList() {
return _segmentList;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]