xiangfu0 commented on code in PR #19477:
URL: https://github.com/apache/pinot/pull/19477#discussion_r4091516952
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImpl.java:
##########
@@ -174,6 +216,117 @@ public ImmutableSegmentImpl(
this(segmentDirectory, segmentMetadata, columnIndexContainerMap,
starTreeIndexContainer, null);
}
+ /// Creates a segment that materializes its physical columns lazily through
`columnMaterializer`.
+ ///
+ /// `materializedIndexContainers` holds the containers created at load
(built-in virtual columns and star-tree
+ /// dimensions) and becomes the registry of every container created
afterwards, so that [#destroy()] closes exactly
+ /// the materialized ones. The columns already in it get their data source
now, as in the eager mode.
+ ImmutableSegmentImpl(SegmentDirectory segmentDirectory, SegmentMetadataImpl
segmentMetadata,
+ ColumnMaterializer columnMaterializer, ConcurrentMap<String,
ColumnIndexContainer> materializedIndexContainers,
+ @Nullable StarTreeIndexContainer starTreeIndexContainer,
+ @Nullable MultiColumnLuceneTextIndexReader multiColumnTextIndex) {
+ _segmentDirectory = segmentDirectory;
+ _segmentMetadata = segmentMetadata;
+ _indexContainerMap = materializedIndexContainers;
+ _starTreeIndexContainer = starTreeIndexContainer;
+ _multiColumnTextIndex = multiColumnTextIndex;
+ _columnMaterializer = columnMaterializer;
+ _openStructChildren = groupOpenStructChildren(segmentMetadata);
+ _materializationLock = new ReentrantReadWriteLock();
+ _dataSources = new ConcurrentHashMap<>();
+ for (String column : materializedIndexContainers.keySet()) {
+ materializeDataSource(column);
+ }
+ }
+
+ /// Groups the materialized OPEN_STRUCT child columns under their parent,
keeping only the parents the segment schema
+ /// declares as complex (the same rule the eager constructor applies).
+ @Nullable
+ private static Map<String, List<String>>
groupOpenStructChildren(SegmentMetadataImpl segmentMetadata) {
+ Map<String, List<String>> children = null;
+ for (Map.Entry<String, ColumnMetadata> entry :
segmentMetadata.getColumnMetadataMap().entrySet()) {
+ if (entry.getValue() instanceof ColumnMetadataImpl impl &&
impl.isMaterializedChild()) {
+ if (children == null) {
+ children = new HashMap<>();
+ }
+ children.computeIfAbsent(impl.getParentColumn(), k -> new
ArrayList<>()).add(entry.getKey());
+ }
+ }
+ if (children == null) {
+ return null;
+ }
+ Schema schema = segmentMetadata.getSchema();
+ children.keySet()
+ .removeIf(parent -> !(schema != null && schema.getFieldSpecFor(parent)
instanceof ComplexFieldSpec));
+ return children.isEmpty() ? null : children;
+ }
+
+ /// Lazy mode: returns the data source of the column, creating it on first
access, or `null` when the segment has no
+ /// such column. OPEN_STRUCT child columns are reachable only through their
parent, as in the eager mode.
+ @Nullable
+ private DataSource materializeDataSource(String column) {
+ ColumnMetadata columnMetadata =
_segmentMetadata.getColumnMetadataMap().get(column);
+ boolean openStructParent = _openStructChildren != null &&
_openStructChildren.containsKey(column);
+ if (!openStructParent && (columnMetadata == null ||
isMaterializedChild(columnMetadata))) {
+ return null;
+ }
+ Lock lock = _materializationLock.readLock();
+ lock.lock();
+ try {
+ checkNotDestroyed(column);
+ // Single flight per column: the mapping function runs at most once per
column and leaves no mapping when it
+ // fails. It never reads this map again (creating the children of an
OPEN_STRUCT parent goes through
+ // _indexContainerMap only), which computeIfAbsent forbids.
+ return _dataSources.computeIfAbsent(column,
+ k -> openStructParent ? createOpenStructDataSource(k) :
createDataSource(k, columnMetadata));
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private DataSource createDataSource(String column, ColumnMetadata
columnMetadata) {
+ ColumnIndexContainer container = materializedIndexContainer(column,
columnMetadata);
+ return columnMetadata.getFieldSpec().getDataType() ==
FieldSpec.DataType.MAP
+ ? new ImmutableMapDataSource(columnMetadata, container) : new
ImmutableDataSource(columnMetadata, container);
+ }
+
+ private DataSource createOpenStructDataSource(String parent) {
+ Map<String, ColumnMetadata> columnMetadataMap =
_segmentMetadata.getColumnMetadataMap();
+ Map<String, DataSource> denseChildren = new HashMap<>();
+ DataSource sparseChild = null;
+ for (String child : _openStructChildren.get(parent)) {
+ ColumnMetadata childMetadata = columnMetadataMap.get(child);
+ DataSource childDataSource =
+ new ImmutableDataSource(childMetadata,
materializedIndexContainer(child, childMetadata));
+ if (OpenStructNaming.isSparseColumn(child)) {
+ sparseChild = childDataSource;
+ } else {
+ denseChildren.put(OpenStructNaming.parseKey(child), childDataSource);
+ }
+ }
+ ComplexFieldSpec fieldSpec = (ComplexFieldSpec)
_segmentMetadata.getSchema().getFieldSpecFor(parent);
+ List<String> sparseKeys =
+ columnMetadataMap.get(parent) instanceof ColumnMetadataImpl impl ?
impl.getSparseKeys() : null;
+ return new ImmutableOpenStructDataSource(fieldSpec, denseChildren,
sparseChild, _segmentMetadata.getTotalDocs(),
+ sparseKeys);
+ }
+
+ /// Lazy mode: returns the index container of the column, creating and
registering it on first access. The mapping
+ /// function opens the column's index readers while it holds the map's bin
lock, so a slow open (e.g. an on-heap
+ /// dictionary) can briefly stall the first access to an unrelated column in
the same bin.
+ private ColumnIndexContainer materializedIndexContainer(String column,
ColumnMetadata columnMetadata) {
+ return _indexContainerMap.computeIfAbsent(column, k ->
_columnMaterializer.createIndexContainer(columnMetadata));
Review Comment:
**MAJOR [BUG-RACE]:** this moves `SegmentDirectory.Reader#getIndexFor` from
the single loader thread onto query threads, and distinct columns land in
distinct bins, so two query threads run `PhysicalColumnIndexContainer`
constructors for different columns of one segment concurrently.
For **v1/v2 segments** `SegmentLocalFSDirectory.loadData()` picks
`FilePerIndexDirectory`, whose `_indexBuffers` is a plain `new HashMap<>()`
(`FilePerIndexDirectory.java:52`) and whose `getReadBufferFor` does
`containsKey` / `mapForReads` / `put` with no synchronization (`:144-159`); the
`getIndexForColumn` path in `SegmentLocalFSDirectory` (`:302-306`) takes no
lock either. Nothing calls `getBuffer` concurrently on one directory today
(eager load is single-threaded, preprocess handlers are sequential), so this PR
introduces the first concurrent use. Concurrent first accesses to two columns
can lose an entry (that `PinotDataBuffer` is then never released by `close()` →
mmap leak on every destroy/reload) or leave the table inconsistent. v3 is safe:
`SingleFileIndexDirectory.checkAndGetIndexBuffer` only reads `_columnEntries`
populated at construction.
Not CRITICAL because it needs the opt-in flag plus a non-default segment
format, and the bytes each thread reads are still correct.
Fix, either: (a) make `FilePerIndexDirectory.getReadBufferFor` /
`getWriteBufferFor` safe for concurrent callers
(`ConcurrentHashMap.computeIfAbsent` wrapping the `IOException`, or
`synchronized`); or (b) fall back to eager in
`ImmutableSegmentLoader.load(SegmentDirectory, …)` when
`segmentMetadata.getVersion() != v3` and document it. Either way, state on the
`SegmentDirectory.Reader` SPI (`SegmentDirectory.java:163-176`, which has no
thread-safety contract today) that lazy mode requires `getIndexFor` /
`hasIndexFor` to be safe under concurrent calls on distinct columns — external
tiered readers must meet that too.
##########
pinot-segment-local/src/test/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentImplTest.java:
##########
@@ -86,4 +125,287 @@ private static ImmutableSegmentImpl
createSegment(SegmentDirectory segmentDirect
when(segmentMetadata.getColumnMetadataMap()).thenReturn(new TreeMap<>());
return new ImmutableSegmentImpl(segmentDirectory, segmentMetadata,
Map.of(), null);
}
+
+ /// The eager (flag off) mode is untouched: every data source exists from
construction over the given containers.
+ @Test
+ public void testEagerModeCreatesDataSourcesAtConstruction() {
+ ColumnMetadataImpl a = columnMetadata(intColumn("a"), null);
+ ColumnIndexContainer containerA = mock(ColumnIndexContainer.class);
+ ImmutableSegmentImpl segment =
+ new ImmutableSegmentImpl(mock(SegmentDirectory.class),
segmentMetadata(schema(a), a), Map.of("a", containerA),
+ null);
+
+ assertSame(segment.getDataSourceNullable("a").getIndexContainer(),
containerA);
+ assertNull(segment.getDataSourceNullable("unknown"));
+ }
+
+ @Test
+ public void testLazyModeCreatesNothingAtConstruction()
+ throws Exception {
+ ColumnMetadataImpl a = columnMetadata(intColumn("a"), null);
+ ColumnMetadataImpl b = columnMetadata(intColumn("b"), null);
+ ColumnMaterializer materializer = mock(ColumnMaterializer.class);
+ SegmentDirectory segmentDirectory = mock(SegmentDirectory.class);
+ ImmutableSegmentImpl segment = lazySegment(segmentDirectory, schema(a, b),
materializer, a, b);
+
+ verifyNoInteractions(materializer);
+ // Column listings come from the metadata schema and never materialize
anything
+ assertEquals(segment.getColumnNames(), Set.of("a", "b"));
+ assertEquals(segment.getPhysicalColumnNames(), Set.of("a", "b"));
+ // Neither does asking for a column the segment does not have
+ assertNull(segment.getDataSourceNullable("unknown"));
+ assertThrows(NullPointerException.class, () -> segment.getIndex("unknown",
StandardIndexes.forward()));
+ verifyNoInteractions(materializer);
+
+ segment.destroy();
+ verify(segmentDirectory).close();
+ }
+
+ @Test
+ public void testLazyModeMaterializesEachColumnOnceUnderConcurrentAccess()
+ throws Exception {
+ ColumnMetadataImpl a = columnMetadata(intColumn("a"), null);
+ ColumnIndexContainer containerA = mock(ColumnIndexContainer.class);
+ ColumnMaterializer materializer = mock(ColumnMaterializer.class);
+ AtomicInteger creations = new AtomicInteger();
+ when(materializer.createIndexContainer(a)).thenAnswer(invocation -> {
+ creations.incrementAndGet();
+ // Widen the window in which every other caller must wait for this
creation instead of starting its own
+ Thread.sleep(50);
+ return containerA;
+ });
+ ImmutableSegmentImpl segment = lazySegment(mock(SegmentDirectory.class),
schema(a), materializer, a);
+
+ int numCallers = 16;
+ ExecutorService executor = Executors.newFixedThreadPool(numCallers);
+ DataSource first;
+ try {
+ CountDownLatch start = new CountDownLatch(1);
+ List<Future<DataSource>> futures = new ArrayList<>(numCallers);
+ for (int i = 0; i < numCallers; i++) {
+ futures.add(executor.submit(() -> {
+ start.await();
+ return segment.getDataSourceNullable("a");
+ }));
+ }
+ start.countDown();
+ first = futures.get(0).get();
+ for (Future<DataSource> future : futures) {
+ assertSame(future.get(), first);
+ }
+ } finally {
+ executor.shutdownNow();
+ }
+
+ assertNotNull(first);
+ assertSame(first.getIndexContainer(), containerA);
+ assertEquals(creations.get(), 1);
+ verify(materializer, times(1)).createIndexContainer(a);
+ }
+
+ @Test
+ public void
testDestroyClosesOnlyMaterializedContainersAndRefusesLaterMaterialization()
Review Comment:
**MAJOR [testing]:** the only reason `_materializationLock` exists is the
destroy-vs-in-flight-materialization interleaving, and this test never
exercises it: column `a` is materialized to completion before `destroy()`, so
the write-lock wait never runs. Dropping the lock, or flipping `_destroyed`
before the container is registered, would still pass; the concurrency test
above covers single-flight only. I verified the ordering by reading (read lock
across `checkNotDestroyed` + `computeIfAbsent`; write lock excludes all
readers), but this is exactly the property that needs a guard.
Suggested shape: block `createIndexContainer(a)` on a latch inside the
mocked `ColumnMaterializer`, call `destroy()` from another thread, assert it
has not returned while blocked, release the latch, then assert the container
was closed exactly once and `getDataSourceNullable("b")` throws
`IllegalStateException`.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ImmutableSegmentLoader.java:
##########
@@ -282,6 +277,58 @@ public static ImmutableSegment load(SegmentDirectory
segmentDirectory, IndexLoad
return segment;
}
+ /// Lazy counterpart of the load above (see [ImmutableSegmentImpl]): no
per-column container is created here. The
+ /// built-in virtual columns keep their eager containers, the star-tree
dimensions are materialized now because the
+ /// star-tree shares their dictionaries, and every other physical column
waits for its first access. The
+ /// [ColumnMaterializer] snapshots the per-column index configs before the
virtual columns are added to the metadata,
+ /// so it covers exactly the physical columns.
+ private static ImmutableSegmentImpl loadWithLazyColumns(SegmentDirectory
segmentDirectory,
+ SegmentDirectory.Reader segmentReader, SegmentMetadataImpl
segmentMetadata,
+ IndexLoadingConfig indexLoadingConfig)
+ throws IOException {
+ Map<String, ColumnMetadata> columnMetadataMap =
segmentMetadata.getColumnMetadataMap();
+ MultiColumnLuceneTextIndexReader mcTextReader = null;
+ Set<String> mcTextColumns = Set.of();
+ if (segmentReader.hasMultiColumnTextIndex()) {
+ mcTextReader = new MultiColumnLuceneTextIndexReader(segmentMetadata);
Review Comment:
**MINOR (resource leak on failure):** the lazy path creates
`MultiColumnLuceneTextIndexReader` before the star-tree container, while the
eager path does the reverse (`:257-270`). If `new StarTreeIndexContainer(...)`
throws here, the Lucene file handles leak: `segmentDirectory.close()` in the
file-load catch (`:160-165`) does not close `mcTextReader`. Create the
star-tree first (matching eager) or close `mcTextReader` on failure.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/segment/index/loader/IndexLoadingConfig.java:
##########
@@ -45,200 +44,65 @@
import org.apache.pinot.spi.config.table.OpenStructIndexConfig;
import org.apache.pinot.spi.config.table.StarTreeIndexConfig;
import org.apache.pinot.spi.config.table.TableConfig;
-import org.apache.pinot.spi.data.ComplexFieldSpec;
import org.apache.pinot.spi.data.DimensionFieldSpec;
import org.apache.pinot.spi.data.FieldSpec;
-import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.data.OpenStructNaming;
import org.apache.pinot.spi.data.Schema;
import org.apache.pinot.spi.utils.ReadMode;
import org.apache.pinot.spi.utils.TimestampIndexUtils;
-/// Index loading config with shared table-level state and segment-local
mutable overrides.
+/// Table level index loading config.
public class IndexLoadingConfig {
private static final int DEFAULT_REALTIME_AVG_MULTI_VALUE_COUNT = 2;
public static final String READ_MODE_KEY = "readMode";
- private final ImmutableState _immutableState;
+ private final InstanceDataManagerConfig _instanceDataManagerConfig;
+ private final TableConfig _tableConfig;
+ private final Schema _schema;
- // Mutable config and segment-specific overrides.
- @Nullable
- private ReadMode _readModeOverride;
- @Nullable
- private SegmentVersion _segmentVersionOverride;
+ // These fields can be modified after initialization
+ // TODO: Revisit them
+ private ReadMode _readMode = ReadMode.DEFAULT_MODE;
+ private SegmentVersion _segmentVersion;
private String _segmentTier;
private Set<String> _knownColumns;
private String _tableDataDir;
private boolean _errorOnColumnBuildFailure;
private boolean _forwardIndexOnly;
- private ResolvedIndexState _resolvedIndexState;
-
- /// Immutable table-level state shared by derived segment configs.
- private static final class ImmutableState {
- @Nullable
- private final InstanceDataManagerConfig _instanceDataManagerConfig;
- @Nullable
- private final TableConfig _tableConfig;
- @Nullable
- private final Schema _schema;
- private final ReadMode _readMode;
- @Nullable
- private final SegmentVersion _segmentVersion;
- @Nullable
- private final String _instanceId;
- private final boolean _isRealtimeOffHeapAllocation;
- private final boolean _isDirectRealtimeOffHeapAllocation;
- private final int _realtimeAvgMultiValueCount;
- @Nullable
- private final String _segmentStoreURI;
- @Nullable
- private final String _segmentDirectoryLoader;
- @Nullable
- private final Map<String, Map<String, String>> _instanceTierConfigs;
- private final List<String> _sortedColumns;
- private final ColumnMinMaxValueGeneratorMode
_columnMinMaxValueGeneratorMode;
- private final boolean _hasOpenStructColumns;
-
- private ImmutableState(@Nullable InstanceDataManagerConfig
instanceDataManagerConfig,
- @Nullable TableConfig tableConfig, @Nullable Schema schema) {
- _instanceDataManagerConfig = instanceDataManagerConfig;
- _tableConfig = tableConfig;
- _schema = schema;
-
- String instanceId = null;
- boolean isRealtimeOffHeapAllocation = false;
- boolean isDirectRealtimeOffHeapAllocation = false;
- int realtimeAvgMultiValueCount = DEFAULT_REALTIME_AVG_MULTI_VALUE_COUNT;
- ReadMode readMode = ReadMode.DEFAULT_MODE;
- SegmentVersion segmentVersion = null;
- String segmentStoreURI = null;
- String segmentDirectoryLoader = null;
- Map<String, Map<String, String>> instanceTierConfigs = null;
- if (instanceDataManagerConfig != null) {
- ReadMode instanceReadMode = instanceDataManagerConfig.getReadMode();
- if (instanceReadMode != null) {
- readMode = instanceReadMode;
- }
- String instanceSegmentVersion =
instanceDataManagerConfig.getSegmentFormatVersion();
- if (instanceSegmentVersion != null) {
- segmentVersion =
SegmentVersion.valueOf(instanceSegmentVersion.toLowerCase());
- }
- instanceId = instanceDataManagerConfig.getInstanceId();
- isRealtimeOffHeapAllocation =
instanceDataManagerConfig.isRealtimeOffHeapAllocation();
- isDirectRealtimeOffHeapAllocation =
instanceDataManagerConfig.isDirectRealtimeOffHeapAllocation();
- String avgMultiValueCount =
instanceDataManagerConfig.getAvgMultiValueCount();
- if (avgMultiValueCount != null) {
- realtimeAvgMultiValueCount = Integer.parseInt(avgMultiValueCount);
- }
- segmentStoreURI = instanceDataManagerConfig.getSegmentStoreUri();
- segmentDirectoryLoader =
instanceDataManagerConfig.getSegmentDirectoryLoader();
- Map<String, Map<String, String>> tierConfigs =
instanceDataManagerConfig.getTierConfigs();
- instanceTierConfigs = tierConfigs != null ? tierConfigs : Map.of();
- }
-
- List<String> sortedColumns = List.of();
- ColumnMinMaxValueGeneratorMode columnMinMaxValueGeneratorMode =
ColumnMinMaxValueGeneratorMode.DEFAULT_MODE;
- boolean hasOpenStructColumns = false;
- if (tableConfig != null) {
- if (schema != null) {
- TimestampIndexUtils.applyTimestampIndex(tableConfig, schema);
- for (ComplexFieldSpec fieldSpec : schema.getComplexFieldSpecs()) {
- if (fieldSpec.getDataType() == DataType.OPEN_STRUCT) {
- hasOpenStructColumns = true;
- break;
- }
- }
- }
- IndexingConfig indexingConfig = tableConfig.getIndexingConfig();
- String tableReadMode = indexingConfig.getLoadMode();
- if (tableReadMode != null) {
- readMode = ReadMode.getEnum(tableReadMode);
- }
- String tableSegmentVersion = indexingConfig.getSegmentFormatVersion();
- if (tableSegmentVersion != null) {
- segmentVersion =
SegmentVersion.valueOf(tableSegmentVersion.toLowerCase());
- }
- List<String> tableSortedColumns = indexingConfig.getSortedColumn();
- if (tableSortedColumns != null) {
- sortedColumns = tableSortedColumns;
- }
- String generatorMode =
indexingConfig.getColumnMinMaxValueGeneratorMode();
- if (generatorMode != null) {
- columnMinMaxValueGeneratorMode =
ColumnMinMaxValueGeneratorMode.valueOf(generatorMode.toUpperCase());
- }
- }
-
- _instanceId = instanceId;
- _readMode = readMode;
- _segmentVersion = segmentVersion;
- _isRealtimeOffHeapAllocation = isRealtimeOffHeapAllocation;
- _isDirectRealtimeOffHeapAllocation = isDirectRealtimeOffHeapAllocation;
- _realtimeAvgMultiValueCount = realtimeAvgMultiValueCount;
- _segmentStoreURI = segmentStoreURI;
- _segmentDirectoryLoader = segmentDirectoryLoader;
- _instanceTierConfigs = instanceTierConfigs;
- _sortedColumns = sortedColumns;
- _columnMinMaxValueGeneratorMode = columnMinMaxValueGeneratorMode;
- _hasOpenStructColumns = hasOpenStructColumns;
- }
- }
-
- /// Index settings resolved from the table config, segment tier, schema, and
known segment columns.
- private static final class ResolvedIndexState {
- private static final ResolvedIndexState EMPTY =
- new ResolvedIndexState(false, null, false, Map.of(), false, null);
-
- private final boolean _enableDynamicStarTreeCreation;
- @Nullable
- private final List<StarTreeIndexConfig> _starTreeIndexConfigs;
- private final boolean _enableDefaultStarTree;
- private final Map<String, FieldIndexConfigs> _indexConfigsByColName;
- private final boolean _skipSegmentPreprocess;
- @Nullable
- private final MultiColumnTextIndexConfig _multiColTextIndexConfig;
-
- private ResolvedIndexState(boolean enableDynamicStarTreeCreation,
- @Nullable List<StarTreeIndexConfig> starTreeIndexConfigs, boolean
enableDefaultStarTree,
- Map<String, FieldIndexConfigs> indexConfigsByColName, boolean
skipSegmentPreprocess,
- @Nullable MultiColumnTextIndexConfig multiColTextIndexConfig) {
- _enableDynamicStarTreeCreation = enableDynamicStarTreeCreation;
- _starTreeIndexConfigs = starTreeIndexConfigs;
- _enableDefaultStarTree = enableDefaultStarTree;
- _indexConfigsByColName = indexConfigsByColName;
- _skipSegmentPreprocess = skipSegmentPreprocess;
- _multiColTextIndexConfig = multiColTextIndexConfig;
- }
- private ResolvedIndexState withIndexConfigsByColName(Map<String,
FieldIndexConfigs> indexConfigsByColName) {
- return new ResolvedIndexState(_enableDynamicStarTreeCreation,
_starTreeIndexConfigs, _enableDefaultStarTree,
- indexConfigsByColName, _skipSegmentPreprocess,
_multiColTextIndexConfig);
- }
- }
+ // Initialized by instance data manager config
+ private String _instanceId;
+ private boolean _isRealtimeOffHeapAllocation;
+ private boolean _isDirectRealtimeOffHeapAllocation;
+ private int _realtimeAvgMultiValueCount =
DEFAULT_REALTIME_AVG_MULTI_VALUE_COUNT;
+ private String _segmentStoreURI;
+ private String _segmentDirectoryLoader;
+ private Map<String, Map<String, String>> _instanceTierConfigs;
+ private boolean _lazyColumnMaterialization;
Review Comment:
**MINOR (restack):** after rebasing onto #19571, `IndexLoadingConfig` is
`ImmutableState` + `ResolvedIndexState` with `copyWithSegmentTier`. This flag
must move into `ImmutableState` (sourced from the instance config at
construction) and be carried by `copyWithSegmentTier`, or a tier copy silently
loads eagerly. The mutable field + `setLazyColumnMaterialization` setter
pattern here will not survive the merge as-is.
##########
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/indexsegment/immutable/ColumnMaterializer.java:
##########
@@ -0,0 +1,125 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.segment.local.indexsegment.immutable;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import javax.annotation.Nullable;
+import
org.apache.pinot.segment.local.segment.index.column.PhysicalColumnIndexContainer;
+import
org.apache.pinot.segment.local.segment.index.readers.text.MultiColumnLuceneTextIndexReader;
+import org.apache.pinot.segment.spi.ColumnMetadata;
+import org.apache.pinot.segment.spi.index.FieldIndexConfigs;
+import org.apache.pinot.segment.spi.index.column.ColumnIndexContainer;
+import org.apache.pinot.segment.spi.store.SegmentDirectory;
+
+
+/// Creates the [ColumnIndexContainer] of a physical column of an
[ImmutableSegmentImpl] on demand.
+///
+/// [ImmutableSegmentLoader] builds one per segment when lazy column
materialization is on, instead of a
+/// [PhysicalColumnIndexContainer] per column at load. It retains only what
creating a container later needs: the
+/// segment reader (held for the segment's lifetime anyway), the
forward-index-only flag, the shared multi-column text
+/// index reader and, per column, the [FieldIndexConfigs] that were in effect
at load.
+///
+/// The per-column configs are snapshotted at construction because the loading
config they come from is mutable and
+/// shared: its map is replaced whenever the config is refreshed and mutated
in place while OPEN_STRUCT child configs
+/// are resolved. The snapshot is compacted so that a wide segment retains
close to nothing per column, which is the
+/// point of materializing lazily: configs that are equal by value collapse to
one instance, the most common one
+/// becomes the implicit default, and only the columns that differ from it
keep an entry (keyed by the column-name
+/// strings the segment metadata already holds). A column absent from the
loading config maps to
+/// [FieldIndexConfigs#EMPTY], exactly what the eager path hands to the
container. Collapsing relies on the value
+/// equality of the index configs; a config type that inherits the
enabled/disabled-only equality of `IndexConfig`
Review Comment:
**MINOR:** `OpenStructIndexConfig` is not the only one — `FstIndexConfig`
also has no `equals`/`hashCode` (those two are the only `extends IndexConfig`
types in `src/main` without overrides). Safe today only because
`FstIndexType.ReaderFactory` ignores the config (`FstIndexType.java:159-176`),
so collapse-by-value rests on an unguarded invariant. Suggest adding
`equals`/`hashCode` to both, plus a test that every config class reachable from
`IndexService.getAllIndexes()` overrides `equals`.
##########
pinot-server/src/main/java/org/apache/pinot/server/starter/helix/HelixInstanceDataManagerConfig.java:
##########
@@ -107,6 +107,11 @@ public class HelixInstanceDataManagerConfig implements
InstanceDataManagerConfig
public static final String DISABLE_DIMENSION_TABLE_PRELOAD =
"disable.dimension.table.preload";
private static final boolean DEFAULT_DISABLE_DIMENSION_TABLE_PRELOAD = false;
+ // Whether to create the index container and data source of a physical
column on first access instead of for every
+ // column at segment load. Off by default. See
InstanceDataManagerConfig#isLazyColumnMaterialization().
+ public static final String LAZY_COLUMN_MATERIALIZATION =
"segment.lazy.column.materialization";
Review Comment:
**MINOR (docs):** new public server config key
`pinot.server.instance.segment.lazy.column.materialization` (default off) needs
a pinot-docs entry, including the trade-off already stated in the description
(index errors on never-queried columns surface at first access, on the query
thread) and, once fixed, the v1/v2 caveat from the race finding.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]