ibessonov commented on code in PR #1800: URL: https://github.com/apache/ignite-3/pull/1800#discussion_r1145763549
########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { Review Comment: Is this method called after the busyLock is blocked or before? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { + shutdownAndAwaitTermination(buildIndexExecutor, 10, TimeUnit.SECONDS); + } + + /** + * Initializes the build of the index. + */ + void startIndexBuild(TableIndexView tableIndexView, TableImpl table) { + for (int partitionId = 0; partitionId < table.internalTable().partitions(); partitionId++) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, true)); + } + } + + /** + * Task of building a table index for a partition. + * + * <p>Only the leader of the raft group will manage the building of the index. Leader sends batches of row IDs via + * {@link BuildIndexCommand}, the next batch will only be send after the previous batch has been processed. + * + * <p>Index building itself occurs locally on each node of the raft group when processing {@link BuildIndexCommand}. This ensures that + * the index build process in the raft group is consistent and that the index build process is restored after restarting the raft group + * (not from the beginning). + */ + private class BuildIndexTask implements Runnable { + private final TableImpl table; + + private final TableIndexView tableIndexView; + + private final int partitionId; + + private final boolean firstBatch; + + private BuildIndexTask(TableImpl table, TableIndexView tableIndexView, int partitionId, boolean firstBatch) { + this.table = table; + this.tableIndexView = tableIndexView; + this.partitionId = partitionId; + this.firstBatch = firstBatch; + } + + @Override + public void run() { + if (!busyLock.enterBusy()) { + return; + } + + try { + InternalTable internalTable = table.internalTable(); + + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + if (!isLocalNodeLeader(raftGroupService)) { + // TODO: IGNITE-19053 Must handle the change of leader + return; + } + + RowId lastBuildRowId = internalTable.storage().getOrCreateIndex(partitionId, tableIndexView.id()).getLastBuildRowId(); Review Comment: I don't like the usage of "getOrCreateIndex", it should already be created at this point, right? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { + shutdownAndAwaitTermination(buildIndexExecutor, 10, TimeUnit.SECONDS); + } + + /** + * Initializes the build of the index. + */ + void startIndexBuild(TableIndexView tableIndexView, TableImpl table) { + for (int partitionId = 0; partitionId < table.internalTable().partitions(); partitionId++) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, true)); + } + } + + /** + * Task of building a table index for a partition. + * + * <p>Only the leader of the raft group will manage the building of the index. Leader sends batches of row IDs via + * {@link BuildIndexCommand}, the next batch will only be send after the previous batch has been processed. + * + * <p>Index building itself occurs locally on each node of the raft group when processing {@link BuildIndexCommand}. This ensures that + * the index build process in the raft group is consistent and that the index build process is restored after restarting the raft group + * (not from the beginning). + */ + private class BuildIndexTask implements Runnable { + private final TableImpl table; + + private final TableIndexView tableIndexView; + + private final int partitionId; + + private final boolean firstBatch; + + private BuildIndexTask(TableImpl table, TableIndexView tableIndexView, int partitionId, boolean firstBatch) { + this.table = table; + this.tableIndexView = tableIndexView; + this.partitionId = partitionId; + this.firstBatch = firstBatch; + } + + @Override + public void run() { + if (!busyLock.enterBusy()) { + return; + } + + try { + InternalTable internalTable = table.internalTable(); + + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + if (!isLocalNodeLeader(raftGroupService)) { + // TODO: IGNITE-19053 Must handle the change of leader + return; + } + + RowId lastBuildRowId = internalTable.storage().getOrCreateIndex(partitionId, tableIndexView.id()).getLastBuildRowId(); + + if (lastBuildRowId == null) { + // Index has already been built. + return; + } + + if (firstBatch) { Review Comment: What exactly do you mean by first batch? First batch after restart? Or first batch for the index in general? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { + shutdownAndAwaitTermination(buildIndexExecutor, 10, TimeUnit.SECONDS); + } + + /** + * Initializes the build of the index. + */ + void startIndexBuild(TableIndexView tableIndexView, TableImpl table) { + for (int partitionId = 0; partitionId < table.internalTable().partitions(); partitionId++) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, true)); + } + } + + /** + * Task of building a table index for a partition. + * + * <p>Only the leader of the raft group will manage the building of the index. Leader sends batches of row IDs via + * {@link BuildIndexCommand}, the next batch will only be send after the previous batch has been processed. + * + * <p>Index building itself occurs locally on each node of the raft group when processing {@link BuildIndexCommand}. This ensures that + * the index build process in the raft group is consistent and that the index build process is restored after restarting the raft group + * (not from the beginning). + */ + private class BuildIndexTask implements Runnable { + private final TableImpl table; + + private final TableIndexView tableIndexView; + + private final int partitionId; + + private final boolean firstBatch; + + private BuildIndexTask(TableImpl table, TableIndexView tableIndexView, int partitionId, boolean firstBatch) { + this.table = table; + this.tableIndexView = tableIndexView; + this.partitionId = partitionId; + this.firstBatch = firstBatch; + } + + @Override + public void run() { + if (!busyLock.enterBusy()) { + return; + } + + try { + InternalTable internalTable = table.internalTable(); + + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + if (!isLocalNodeLeader(raftGroupService)) { + // TODO: IGNITE-19053 Must handle the change of leader + return; + } + + RowId lastBuildRowId = internalTable.storage().getOrCreateIndex(partitionId, tableIndexView.id()).getLastBuildRowId(); + + if (lastBuildRowId == null) { + // Index has already been built. + return; + } + + if (firstBatch) { + LOG.info("Start building the index: [{}]", createCommonTableIndexInfo()); + } + + List<RowId> batchRowIds = createBatchRowIds(lastBuildRowId, BUILD_INDEX_ROW_ID_BATCH_SIZE); + + boolean finish = batchRowIds.size() < BUILD_INDEX_ROW_ID_BATCH_SIZE; Review Comment: With such check, there's a chance of replicating empty list of row ids. I don't thing that this is a good solution, can you please fix it? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { + shutdownAndAwaitTermination(buildIndexExecutor, 10, TimeUnit.SECONDS); + } + + /** + * Initializes the build of the index. + */ + void startIndexBuild(TableIndexView tableIndexView, TableImpl table) { + for (int partitionId = 0; partitionId < table.internalTable().partitions(); partitionId++) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, true)); + } + } + + /** + * Task of building a table index for a partition. + * + * <p>Only the leader of the raft group will manage the building of the index. Leader sends batches of row IDs via + * {@link BuildIndexCommand}, the next batch will only be send after the previous batch has been processed. + * + * <p>Index building itself occurs locally on each node of the raft group when processing {@link BuildIndexCommand}. This ensures that + * the index build process in the raft group is consistent and that the index build process is restored after restarting the raft group + * (not from the beginning). + */ + private class BuildIndexTask implements Runnable { + private final TableImpl table; + + private final TableIndexView tableIndexView; + + private final int partitionId; + + private final boolean firstBatch; + + private BuildIndexTask(TableImpl table, TableIndexView tableIndexView, int partitionId, boolean firstBatch) { + this.table = table; + this.tableIndexView = tableIndexView; + this.partitionId = partitionId; + this.firstBatch = firstBatch; + } + + @Override + public void run() { + if (!busyLock.enterBusy()) { + return; + } + + try { + InternalTable internalTable = table.internalTable(); + + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + if (!isLocalNodeLeader(raftGroupService)) { + // TODO: IGNITE-19053 Must handle the change of leader + return; + } + + RowId lastBuildRowId = internalTable.storage().getOrCreateIndex(partitionId, tableIndexView.id()).getLastBuildRowId(); + + if (lastBuildRowId == null) { + // Index has already been built. + return; + } + + if (firstBatch) { + LOG.info("Start building the index: [{}]", createCommonTableIndexInfo()); + } + + List<RowId> batchRowIds = createBatchRowIds(lastBuildRowId, BUILD_INDEX_ROW_ID_BATCH_SIZE); + + boolean finish = batchRowIds.size() < BUILD_INDEX_ROW_ID_BATCH_SIZE; + + raftGroupService.run(createBuildIndexCommand(batchRowIds, finish)) + .thenRun(() -> { + if (!finish) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, false)); + } + }); + } catch (Throwable t) { + LOG.error("Index build error: [{}]", t, createCommonTableIndexInfo()); + } finally { + busyLock.leaveBusy(); + } + } + + private boolean isLocalNodeLeader(RaftGroupService raftGroupService) { + Peer leader = raftGroupService.leader(); + + assert leader != null : "tableId=" + table.tableId() + ", partitionId=" + partitionId; + + return localNodeConsistentId().equals(leader.consistentId()); + } + + private List<RowId> createBatchRowIds(RowId lastBuildRowId, int batchSize) { Review Comment: By the way, when you say "last build", you mean "last buil**t**" or something else? Maybe it would make sense to rename it, for clarity ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; Review Comment: Should we make it configurable? I think that it's a big problem, that we don't discuss configuration and just hard-code such parameters. ########## modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItBuildIndexTest.java: ########## @@ -0,0 +1,144 @@ +/* + * 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.ignite.internal.sql.engine; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScan; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import org.apache.ignite.Ignite; +import org.apache.ignite.internal.app.IgniteImpl; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.TablesConfiguration; +import org.apache.ignite.internal.storage.index.IndexStorage; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.lang.IgniteStringFormatter; +import org.apache.ignite.table.Table; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Integration test of index building. + */ +public class ItBuildIndexTest extends ClusterPerClassIntegrationTest { + private static final String TABLE_NAME = "test_table"; + + private static final String INDEX_NAME = "test_index"; + + @AfterEach + void tearDown() { + sql("DROP TABLE IF EXISTS " + TABLE_NAME); + } + + @ParameterizedTest + @MethodSource("replicas") + @Disabled("https://issues.apache.org/jira/browse/IGNITE-19085") + void testBuildIndexOnStableTopology(int replicas) throws Exception { + sql(IgniteStringFormatter.format( + "CREATE TABLE {} (i0 INTEGER PRIMARY KEY, i1 INTEGER) WITH replicas={}, partitions={}", + TABLE_NAME, replicas, 2 + )); + + sql(IgniteStringFormatter.format( + "INSERT INTO {} VALUES {}", + TABLE_NAME, toValuesString(List.of(1, 1), List.of(2, 2), List.of(3, 3), List.of(4, 4), List.of(5, 5)) + )); + + sql(IgniteStringFormatter.format("CREATE INDEX {} ON {} (i1)", INDEX_NAME, TABLE_NAME)); + + // FIXME: IGNITE-18733 + waitForIndex(INDEX_NAME); + + waitForIndexBuild(TABLE_NAME, INDEX_NAME); + + assertQuery(IgniteStringFormatter.format("SELECT * FROM {} WHERE i1 > 0", TABLE_NAME)) Review Comment: Wow, that's a nice method, didn't know about its existence ########## modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItBuildIndexTest.java: ########## @@ -0,0 +1,144 @@ +/* + * 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.ignite.internal.sql.engine; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScan; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import org.apache.ignite.Ignite; +import org.apache.ignite.internal.app.IgniteImpl; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.TablesConfiguration; +import org.apache.ignite.internal.storage.index.IndexStorage; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.lang.IgniteStringFormatter; +import org.apache.ignite.table.Table; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Integration test of index building. + */ +public class ItBuildIndexTest extends ClusterPerClassIntegrationTest { + private static final String TABLE_NAME = "test_table"; + + private static final String INDEX_NAME = "test_index"; + + @AfterEach + void tearDown() { + sql("DROP TABLE IF EXISTS " + TABLE_NAME); + } + + @ParameterizedTest + @MethodSource("replicas") + @Disabled("https://issues.apache.org/jira/browse/IGNITE-19085") + void testBuildIndexOnStableTopology(int replicas) throws Exception { + sql(IgniteStringFormatter.format( + "CREATE TABLE {} (i0 INTEGER PRIMARY KEY, i1 INTEGER) WITH replicas={}, partitions={}", + TABLE_NAME, replicas, 2 + )); + + sql(IgniteStringFormatter.format( + "INSERT INTO {} VALUES {}", + TABLE_NAME, toValuesString(List.of(1, 1), List.of(2, 2), List.of(3, 3), List.of(4, 4), List.of(5, 5)) + )); + + sql(IgniteStringFormatter.format("CREATE INDEX {} ON {} (i1)", INDEX_NAME, TABLE_NAME)); + + // FIXME: IGNITE-18733 + waitForIndex(INDEX_NAME); + + waitForIndexBuild(TABLE_NAME, INDEX_NAME); + + assertQuery(IgniteStringFormatter.format("SELECT * FROM {} WHERE i1 > 0", TABLE_NAME)) + .matches(containsIndexScan("PUBLIC", TABLE_NAME.toUpperCase(), INDEX_NAME.toUpperCase())) + .returns(1, 1) + .returns(2, 2) + .returns(3, 3) + .returns(4, 4) + .returns(5, 5) + .check(); + } + + private static int[] replicas() { + // FIXME: IGNITE-19086 Fix NullPointerException on insertALl + // return new int[]{1, 2, 3}; + return new int[]{1}; + } + + private static String toValuesString(List<Object>... values) { + return Stream.of(values) + .peek(Assertions::assertNotNull) + .map(objects -> objects.stream().map(Object::toString).collect(joining(", ", "(", ")"))) + .collect(joining(", ")); + } + + private void waitForIndexBuild(String tableName, String indexName) throws Exception { + for (Ignite clusterNode : CLUSTER_NODES) { + CompletableFuture<Table> tableFuture = clusterNode.tables().tableAsync(tableName); + + assertThat(tableFuture, willCompleteSuccessfully()); + + TableImpl tableImpl = (TableImpl) tableFuture.join(); + + InternalTable internalTable = tableImpl.internalTable(); + + UUID indexId = ((IgniteImpl) clusterNode).clusterConfiguration() + .getConfiguration(TablesConfiguration.KEY) + .indexes() + .get(INDEX_NAME.toUpperCase()) + .id() + .value(); + + assertNotNull(indexId, "table=" + tableName + ", index=" + indexName); + + for (int partitionId = 0; partitionId < internalTable.partitions(); partitionId++) { + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + Set<String> allPeers = Stream.concat( + Stream.of(raftGroupService.leader()), + raftGroupService.peers().stream() + ).map(Peer::consistentId).collect(toSet()); + + if (!allPeers.contains(clusterNode.name())) { + continue; + } Review Comment: Oh no, please, never collect anything into a collection just to check it for "contains". Why don't you use `anyMatch`? ########## modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/IndexStorage.java: ########## @@ -54,4 +55,22 @@ public interface IndexStorage { * @throws StorageException If failed to remove data. */ void remove(IndexRow row) throws StorageException; + + /** + * Returns the last row ID that has been processed by an ongoing index build process or {@code null} if the process has finished. Review Comment: Please document, what happens if the process is not started. ########## modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItBuildIndexTest.java: ########## @@ -0,0 +1,144 @@ +/* + * 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.ignite.internal.sql.engine; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScan; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import org.apache.ignite.Ignite; +import org.apache.ignite.internal.app.IgniteImpl; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.TablesConfiguration; +import org.apache.ignite.internal.storage.index.IndexStorage; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.lang.IgniteStringFormatter; +import org.apache.ignite.table.Table; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Integration test of index building. + */ +public class ItBuildIndexTest extends ClusterPerClassIntegrationTest { + private static final String TABLE_NAME = "test_table"; + + private static final String INDEX_NAME = "test_index"; + + @AfterEach + void tearDown() { + sql("DROP TABLE IF EXISTS " + TABLE_NAME); + } + + @ParameterizedTest + @MethodSource("replicas") + @Disabled("https://issues.apache.org/jira/browse/IGNITE-19085") + void testBuildIndexOnStableTopology(int replicas) throws Exception { + sql(IgniteStringFormatter.format( + "CREATE TABLE {} (i0 INTEGER PRIMARY KEY, i1 INTEGER) WITH replicas={}, partitions={}", + TABLE_NAME, replicas, 2 + )); + + sql(IgniteStringFormatter.format( + "INSERT INTO {} VALUES {}", + TABLE_NAME, toValuesString(List.of(1, 1), List.of(2, 2), List.of(3, 3), List.of(4, 4), List.of(5, 5)) + )); + + sql(IgniteStringFormatter.format("CREATE INDEX {} ON {} (i1)", INDEX_NAME, TABLE_NAME)); + + // FIXME: IGNITE-18733 + waitForIndex(INDEX_NAME); + + waitForIndexBuild(TABLE_NAME, INDEX_NAME); + + assertQuery(IgniteStringFormatter.format("SELECT * FROM {} WHERE i1 > 0", TABLE_NAME)) + .matches(containsIndexScan("PUBLIC", TABLE_NAME.toUpperCase(), INDEX_NAME.toUpperCase())) + .returns(1, 1) + .returns(2, 2) + .returns(3, 3) + .returns(4, 4) + .returns(5, 5) + .check(); + } + + private static int[] replicas() { + // FIXME: IGNITE-19086 Fix NullPointerException on insertALl + // return new int[]{1, 2, 3}; + return new int[]{1}; + } + + private static String toValuesString(List<Object>... values) { + return Stream.of(values) + .peek(Assertions::assertNotNull) + .map(objects -> objects.stream().map(Object::toString).collect(joining(", ", "(", ")"))) + .collect(joining(", ")); + } + + private void waitForIndexBuild(String tableName, String indexName) throws Exception { + for (Ignite clusterNode : CLUSTER_NODES) { + CompletableFuture<Table> tableFuture = clusterNode.tables().tableAsync(tableName); + + assertThat(tableFuture, willCompleteSuccessfully()); + + TableImpl tableImpl = (TableImpl) tableFuture.join(); + + InternalTable internalTable = tableImpl.internalTable(); + + UUID indexId = ((IgniteImpl) clusterNode).clusterConfiguration() + .getConfiguration(TablesConfiguration.KEY) + .indexes() + .get(INDEX_NAME.toUpperCase()) + .id() + .value(); + + assertNotNull(indexId, "table=" + tableName + ", index=" + indexName); + + for (int partitionId = 0; partitionId < internalTable.partitions(); partitionId++) { + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + Set<String> allPeers = Stream.concat( + Stream.of(raftGroupService.leader()), + raftGroupService.peers().stream() + ).map(Peer::consistentId).collect(toSet()); + + if (!allPeers.contains(clusterNode.name())) { + continue; + } + + IndexStorage index = internalTable.storage().getOrCreateIndex(partitionId, indexId); Review Comment: Do we even have a `getIndex` method? ########## modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/hash/PageMemoryHashIndexStorage.java: ########## @@ -329,4 +340,34 @@ public void startCleanup() { public void finishCleanup() { state.compareAndSet(StorageState.CLEANUP, StorageState.RUNNABLE); } + + @Override + public @Nullable RowId getLastBuildRowId() { + return busy(() -> { + throwExceptionIfStorageInProgressOfRebalance(state.get(), this::createStorageInfo); + + try { + UUID lastBuildRowIdUuid = indexMetaTree.findOne(new IndexMetaKey(indexDescriptor().id())).lastBuildRowIdUuid(); Review Comment: I don't like that we always read it from the tree, access to cached version would be so much faster. The only time we really must read it is the start ########## modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/meta/UpdateLastBuildRowIdInvokeClosure.java: ########## @@ -0,0 +1,59 @@ +/* + * 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.ignite.internal.storage.pagememory.index.meta; + +import org.apache.ignite.internal.pagememory.tree.IgniteTree.InvokeClosure; +import org.apache.ignite.internal.pagememory.tree.IgniteTree.OperationType; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.lang.IgniteInternalCheckedException; +import org.jetbrains.annotations.Nullable; + +/** + * Closure for updating the last row ID for which the index was built. + */ +public class UpdateLastBuildRowIdInvokeClosure implements InvokeClosure<IndexMeta> { + private final @Nullable RowId newRowId; + + private IndexMeta newRow; + + /** + * Constructor. + * + * @param newRowId New last row ID for which the index was built, {@code null} means index building is finished. + */ + public UpdateLastBuildRowIdInvokeClosure(@Nullable RowId newRowId) { + this.newRowId = newRowId; + } + + @Override + public void call(@Nullable IndexMeta oldRow) throws IgniteInternalCheckedException { + assert oldRow != null; + + newRow = new IndexMeta(oldRow.indexId(), oldRow.metaPageId(), newRowId == null ? null : newRowId.uuid()); Review Comment: This is not the best place to write such comment, probably, but can new value be smaller then the old one? On leader reelection, for example? ########## modules/storage-api/src/main/java/org/apache/ignite/internal/storage/index/IndexStorage.java: ########## @@ -54,4 +55,22 @@ public interface IndexStorage { * @throws StorageException If failed to remove data. */ void remove(IndexRow row) throws StorageException; + + /** + * Returns the last row ID that has been processed by an ongoing index build process or {@code null} if the process has finished. + * + * @throws StorageException If failed to get the last row ID. + */ + @Nullable RowId getLastBuildRowId(); + + /** + * Sets last row ID for which the index was built, {@code null} means index building is finished. + * + * @apiNote This method <b>must</b> always be called inside the corresponding partition's + * {@link org.apache.ignite.internal.storage.MvPartitionStorage#runConsistently} closure. + * + * @param rowId Row ID. + * @throws StorageException If failed to set the last row ID. + */ + void setLastBuildRowId(@Nullable RowId rowId); Review Comment: ```suggestion void setLastBuildRowId(@Nullable RowId rowId) throws StorageExeption; ``` ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { + shutdownAndAwaitTermination(buildIndexExecutor, 10, TimeUnit.SECONDS); + } + + /** + * Initializes the build of the index. + */ + void startIndexBuild(TableIndexView tableIndexView, TableImpl table) { + for (int partitionId = 0; partitionId < table.internalTable().partitions(); partitionId++) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, true)); + } + } + + /** + * Task of building a table index for a partition. + * + * <p>Only the leader of the raft group will manage the building of the index. Leader sends batches of row IDs via + * {@link BuildIndexCommand}, the next batch will only be send after the previous batch has been processed. + * + * <p>Index building itself occurs locally on each node of the raft group when processing {@link BuildIndexCommand}. This ensures that + * the index build process in the raft group is consistent and that the index build process is restored after restarting the raft group + * (not from the beginning). + */ + private class BuildIndexTask implements Runnable { + private final TableImpl table; + + private final TableIndexView tableIndexView; + + private final int partitionId; + + private final boolean firstBatch; + + private BuildIndexTask(TableImpl table, TableIndexView tableIndexView, int partitionId, boolean firstBatch) { + this.table = table; + this.tableIndexView = tableIndexView; + this.partitionId = partitionId; + this.firstBatch = firstBatch; + } + + @Override + public void run() { + if (!busyLock.enterBusy()) { + return; + } + + try { + InternalTable internalTable = table.internalTable(); + + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + if (!isLocalNodeLeader(raftGroupService)) { + // TODO: IGNITE-19053 Must handle the change of leader + return; + } + + RowId lastBuildRowId = internalTable.storage().getOrCreateIndex(partitionId, tableIndexView.id()).getLastBuildRowId(); + + if (lastBuildRowId == null) { Review Comment: It's weird that you rely on the value in the storage itself, not the last row id that you replicated. What's the reason of such solution? Can you please document, why the value that you're reading is already replicated and processed? Or maybe use locally preserved last row id instead? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { + shutdownAndAwaitTermination(buildIndexExecutor, 10, TimeUnit.SECONDS); + } + + /** + * Initializes the build of the index. + */ + void startIndexBuild(TableIndexView tableIndexView, TableImpl table) { + for (int partitionId = 0; partitionId < table.internalTable().partitions(); partitionId++) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, true)); + } + } + + /** + * Task of building a table index for a partition. + * + * <p>Only the leader of the raft group will manage the building of the index. Leader sends batches of row IDs via + * {@link BuildIndexCommand}, the next batch will only be send after the previous batch has been processed. + * + * <p>Index building itself occurs locally on each node of the raft group when processing {@link BuildIndexCommand}. This ensures that + * the index build process in the raft group is consistent and that the index build process is restored after restarting the raft group + * (not from the beginning). + */ + private class BuildIndexTask implements Runnable { + private final TableImpl table; + + private final TableIndexView tableIndexView; + + private final int partitionId; + + private final boolean firstBatch; + + private BuildIndexTask(TableImpl table, TableIndexView tableIndexView, int partitionId, boolean firstBatch) { + this.table = table; + this.tableIndexView = tableIndexView; + this.partitionId = partitionId; + this.firstBatch = firstBatch; + } + + @Override + public void run() { + if (!busyLock.enterBusy()) { + return; + } + + try { + InternalTable internalTable = table.internalTable(); + + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); Review Comment: If current node is not an affinity node, will this method return null? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexBuilder.java: ########## @@ -0,0 +1,220 @@ +/* + * 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.ignite.internal.index; + +import static java.util.stream.Collectors.toList; +import static org.apache.ignite.internal.util.IgniteUtils.shutdownAndAwaitTermination; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.apache.ignite.internal.logger.IgniteLogger; +import org.apache.ignite.internal.logger.Loggers; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.index.TableIndexView; +import org.apache.ignite.internal.storage.MvPartitionStorage; +import org.apache.ignite.internal.storage.RowId; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.internal.table.distributed.TableMessagesFactory; +import org.apache.ignite.internal.table.distributed.command.BuildIndexCommand; +import org.apache.ignite.internal.thread.NamedThreadFactory; +import org.apache.ignite.internal.util.IgniteSpinBusyLock; +import org.apache.ignite.network.ClusterService; + +/** + * Class for managing the index building process. + */ +class IndexBuilder { + private static final IgniteLogger LOG = Loggers.forClass(IndexBuilder.class); + + /** Batch size of row IDs to build the index. */ + private static final int BUILD_INDEX_ROW_ID_BATCH_SIZE = 100; + + /** Message factory to create messages - RAFT commands. */ + private static final TableMessagesFactory TABLE_MESSAGES_FACTORY = new TableMessagesFactory(); + + /** Busy lock to stop synchronously. */ + private final IgniteSpinBusyLock busyLock; + + /** Cluster service. */ + private final ClusterService clusterService; + + /** Index building executor. */ + private final ExecutorService buildIndexExecutor; + + IndexBuilder(String nodeName, IgniteSpinBusyLock busyLock, ClusterService clusterService) { + this.busyLock = busyLock; + this.clusterService = clusterService; + + int cpus = Runtime.getRuntime().availableProcessors(); + + buildIndexExecutor = new ThreadPoolExecutor( + cpus, + cpus, + 30, + TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), + NamedThreadFactory.create(nodeName, "build-index", LOG) + ); + } + + /** + * Stops the index builder. + */ + void stop() { + shutdownAndAwaitTermination(buildIndexExecutor, 10, TimeUnit.SECONDS); + } + + /** + * Initializes the build of the index. + */ + void startIndexBuild(TableIndexView tableIndexView, TableImpl table) { + for (int partitionId = 0; partitionId < table.internalTable().partitions(); partitionId++) { + buildIndexExecutor.submit(new BuildIndexTask(table, tableIndexView, partitionId, true)); + } + } + + /** + * Task of building a table index for a partition. + * + * <p>Only the leader of the raft group will manage the building of the index. Leader sends batches of row IDs via + * {@link BuildIndexCommand}, the next batch will only be send after the previous batch has been processed. + * + * <p>Index building itself occurs locally on each node of the raft group when processing {@link BuildIndexCommand}. This ensures that + * the index build process in the raft group is consistent and that the index build process is restored after restarting the raft group + * (not from the beginning). + */ + private class BuildIndexTask implements Runnable { + private final TableImpl table; + + private final TableIndexView tableIndexView; + + private final int partitionId; + + private final boolean firstBatch; + + private BuildIndexTask(TableImpl table, TableIndexView tableIndexView, int partitionId, boolean firstBatch) { + this.table = table; + this.tableIndexView = tableIndexView; + this.partitionId = partitionId; + this.firstBatch = firstBatch; + } + + @Override + public void run() { + if (!busyLock.enterBusy()) { + return; + } + + try { + InternalTable internalTable = table.internalTable(); + + RaftGroupService raftGroupService = internalTable.partitionRaftGroupService(partitionId); + + if (!isLocalNodeLeader(raftGroupService)) { + // TODO: IGNITE-19053 Must handle the change of leader + return; + } + + RowId lastBuildRowId = internalTable.storage().getOrCreateIndex(partitionId, tableIndexView.id()).getLastBuildRowId(); + + if (lastBuildRowId == null) { Review Comment: What's the initial value for the "lastBuildRowId" if build hasn't started yet? Minimal possible value? ########## modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/index/RocksDbHashIndexStorage.java: ########## @@ -369,4 +373,26 @@ public void finishCleanup() { busyLock.unblock(); } } + + @Override + public @Nullable RowId getLastBuildRowId() { + return busy(() -> { + throwExceptionIfStorageInProgressOfRebalance(state.get(), this::createStorageInfo); Review Comment: Same comment, it's much more convenient to read local field, rather than the entry from the storage ########## modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableManager.java: ########## @@ -1163,7 +1164,7 @@ private CompletableFuture<?> createTableLocally(long causalityToken, String name partitions, clusterNodeResolver, txManager, tableStorage, txStateStorage, replicaSvc, clock); // TODO: IGNITE-16288 directIndexIds should use async configuration API - var table = new TableImpl(internalTable, lockMgr, () -> CompletableFuture.supplyAsync(() -> directIndexIds())); + var table = new TableImpl(internalTable, lockMgr, () -> supplyAsync(this::directIndexIds, ioExecutor)); Review Comment: Nice, thank you! ########## modules/table/src/main/java/org/apache/ignite/internal/table/distributed/StorageUpdateHandler.java: ########## @@ -416,4 +416,39 @@ public void addToIndexes(@Nullable BinaryRow binaryRow, RowId rowId) { public void waitIndexes() { indexes.get(); } + + /** + * Builds an index for all versions of a row. + * + * <p>Index is expected to exist, skips the tombstones. + * + * @param indexId Index ID. + * @param rowUuids Row uuids. + * @param finish Index build completion flag. + */ + public void buildIndex(UUID indexId, List<UUID> rowUuids, boolean finish) { + TableSchemaAwareIndexStorage index = indexes.get().get(indexId); + + assert index != null : "indexId=" + indexId + ", partitionId=" + partitionId; + + RowId lastRowId = null; + + for (UUID rowUuid : rowUuids) { + lastRowId = new RowId(partitionId, rowUuid); + + try (Cursor<ReadResult> cursor = storage.scanVersions(lastRowId)) { + while (cursor.hasNext()) { + ReadResult next = cursor.next(); + + if (!next.isEmpty()) { + index.put(next.binaryRow(), lastRowId); + } + } + } + } + + assert lastRowId != null || finish : "indexId=" + indexId + ", partitionId=" + partitionId; Review Comment: Oh, so you explicitly assert that last batch can be empty. Why? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexManager.java: ########## @@ -91,33 +92,44 @@ public class IndexManager extends Producer<IndexEvent, IndexEventParameters> imp /** Prevents double stopping of the component. */ private final AtomicBoolean stopGuard = new AtomicBoolean(); + /** Index builder. */ + private IndexBuilder indexBuilder; + /** * Constructor. * - * @param tablesCfg Tables and indexes configuration. + * @param nodeName Node name. + * @param tablesConfig Tables and indexes configuration. * @param schemaManager Schema manager. * @param tableManager Table manager. + * @param clusterService Cluster service. */ - public IndexManager(TablesConfiguration tablesCfg, SchemaManager schemaManager, TableManager tableManager) { - this.tablesCfg = Objects.requireNonNull(tablesCfg, "tablesCfg"); + public IndexManager( + String nodeName, + TablesConfiguration tablesConfig, + SchemaManager schemaManager, + TableManager tableManager, + ClusterService clusterService + ) { + this.tablesConfig = Objects.requireNonNull(tablesConfig, "tablesCfg"); Review Comment: You renamed the field, please change the name in the check as well ########## modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItBuildIndexTest.java: ########## @@ -0,0 +1,144 @@ +/* + * 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.ignite.internal.sql.engine; + +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; +import static org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScan; +import static org.apache.ignite.internal.testframework.IgniteTestUtils.waitForCondition; +import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; +import org.apache.ignite.Ignite; +import org.apache.ignite.internal.app.IgniteImpl; +import org.apache.ignite.internal.raft.Peer; +import org.apache.ignite.internal.raft.service.RaftGroupService; +import org.apache.ignite.internal.schema.configuration.TablesConfiguration; +import org.apache.ignite.internal.storage.index.IndexStorage; +import org.apache.ignite.internal.table.InternalTable; +import org.apache.ignite.internal.table.TableImpl; +import org.apache.ignite.lang.IgniteStringFormatter; +import org.apache.ignite.table.Table; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Integration test of index building. + */ +public class ItBuildIndexTest extends ClusterPerClassIntegrationTest { + private static final String TABLE_NAME = "test_table"; + + private static final String INDEX_NAME = "test_index"; + + @AfterEach + void tearDown() { + sql("DROP TABLE IF EXISTS " + TABLE_NAME); + } + + @ParameterizedTest + @MethodSource("replicas") + @Disabled("https://issues.apache.org/jira/browse/IGNITE-19085") + void testBuildIndexOnStableTopology(int replicas) throws Exception { + sql(IgniteStringFormatter.format( + "CREATE TABLE {} (i0 INTEGER PRIMARY KEY, i1 INTEGER) WITH replicas={}, partitions={}", + TABLE_NAME, replicas, 2 + )); + + sql(IgniteStringFormatter.format( + "INSERT INTO {} VALUES {}", + TABLE_NAME, toValuesString(List.of(1, 1), List.of(2, 2), List.of(3, 3), List.of(4, 4), List.of(5, 5)) + )); + + sql(IgniteStringFormatter.format("CREATE INDEX {} ON {} (i1)", INDEX_NAME, TABLE_NAME)); + + // FIXME: IGNITE-18733 + waitForIndex(INDEX_NAME); + + waitForIndexBuild(TABLE_NAME, INDEX_NAME); + + assertQuery(IgniteStringFormatter.format("SELECT * FROM {} WHERE i1 > 0", TABLE_NAME)) + .matches(containsIndexScan("PUBLIC", TABLE_NAME.toUpperCase(), INDEX_NAME.toUpperCase())) + .returns(1, 1) + .returns(2, 2) + .returns(3, 3) + .returns(4, 4) + .returns(5, 5) + .check(); + } + + private static int[] replicas() { + // FIXME: IGNITE-19086 Fix NullPointerException on insertALl Review Comment: ```suggestion // FIXME: IGNITE-19086 Fix NullPointerException on insertAll ``` ########## modules/table/src/main/java/org/apache/ignite/internal/table/distributed/command/BuildIndexCommand.java: ########## @@ -0,0 +1,50 @@ +/* + * 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.ignite.internal.table.distributed.command; + +import java.util.List; +import java.util.UUID; +import org.apache.ignite.internal.raft.WriteCommand; +import org.apache.ignite.internal.table.distributed.TableMessageGroup; +import org.apache.ignite.network.annotations.Transferable; + +/** + * State machine command to build a table index. + */ +@Transferable(TableMessageGroup.Commands.BUILD_INDEX) +public interface BuildIndexCommand extends WriteCommand { + /** + * Return ID of table partition. + */ + TablePartitionIdMessage tablePartitionId(); + + /** + * Returns index ID. + */ + UUID indexId(); + + /** + * Returns row IDs for which to build indexes. + */ + List<UUID> rowIds(); + + /** + * Returns {@code true} if index building for the partition has completed. Review Comment: I'd rephrase that, not "completed", but that this batch is the last ########## modules/table/src/main/java/org/apache/ignite/internal/table/distributed/raft/PartitionListener.java: ########## @@ -437,4 +439,33 @@ public void onShutdown() { public MvPartitionStorage getMvStorage() { return storage.getStorage(); } + + /** + * Handler for the {@link BuildIndexCommand}. + * + * @param cmd Command. + * @param commandIndex RAFT index of the command. + * @param commandTerm RAFT term of the command. + */ + void handleBuildIndexCommand(BuildIndexCommand cmd, long commandIndex, long commandTerm) { + // Skips the write command because the storage has already executed it. Review Comment: I wonder, why do we have this check in every single method, instead of making it once in "onWrite". Any ideas? ########## modules/table/src/main/java/org/apache/ignite/internal/table/distributed/TableMessageGroup.java: ########## @@ -154,5 +155,8 @@ interface Commands { /** Message type for {@link TablePartitionIdMessage}. */ short TABLE_PARTITION_ID = 61; + + /** Message type for {@link BuildIndexCommand}. */ + short BUILD_INDEX = 62; Review Comment: I guess you didn't notice the pattern. Here commands are 40 and higher. 60+ is for random stuff, like TablePartitionId pojos. Please make it 44 and move it higher. ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexManager.java: ########## @@ -385,6 +399,8 @@ private CompletableFuture<?> createIndexLocally(long causalityToken, UUID tableI index.descriptor().columns().toArray(STRING_EMPTY_ARRAY) ); + indexBuilder.startIndexBuild(tableIndexView, table); Review Comment: Why do you call this method before methods like "table.registerHashIndex"? Are these operations independent? ########## modules/index/src/main/java/org/apache/ignite/internal/index/IndexManager.java: ########## @@ -385,6 +399,8 @@ private CompletableFuture<?> createIndexLocally(long causalityToken, UUID tableI index.descriptor().columns().toArray(STRING_EMPTY_ARRAY) ); + indexBuilder.startIndexBuild(tableIndexView, table); Review Comment: Maybe you should start this operation somewhere else, but I don't know where exactly. There's a `fireEvent(IndexEvent.CREATE...` later in the code, but you start build even before that firing. Seems dangerous ########## modules/storage-api/src/testFixtures/java/org/apache/ignite/internal/storage/AbstractMvTableStorageTest.java: ########## @@ -729,6 +729,82 @@ void testDestroyStartedRebalance() { assertThat(tableStorage.destroyPartition(PARTITION_ID), willCompleteSuccessfully()); } + @Test + void testIndexLastBuildRowId() { + MvPartitionStorage mvPartitionStorage = getOrCreateMvPartition(PARTITION_ID); + + IndexStorage sortedIndexStorage = tableStorage.getOrCreateIndex(PARTITION_ID, sortedIdx.id()); Review Comment: I agree with Alexander, why writing the same code twice? ########## modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/HashIndex.java: ########## @@ -39,9 +39,12 @@ class HashIndex { private final ConcurrentMap<Integer, RocksDbHashIndexStorage> storages = new ConcurrentHashMap<>(); - HashIndex(ColumnFamily indexCf, HashIndexDescriptor descriptor) { + private final RocksDbMetaStorage metaStorage; + + HashIndex(ColumnFamily indexCf, HashIndexDescriptor descriptor, RocksDbMetaStorage metaStorage) { this.indexCf = indexCf; this.descriptor = descriptor; + this.metaStorage = metaStorage; Review Comment: I would prefer different name, this one looks too similar to the name of other, completely unrelated, component ########## modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/PartitionDataHelper.java: ########## @@ -138,14 +142,24 @@ void putRowId(ByteBuffer keyBuffer, RowId rowId) { assert rowId.partitionId() == partitionId : rowId; assert keyBuffer.order() == KEY_BYTE_ORDER; - keyBuffer.putLong(normalize(rowId.mostSignificantBits())); - keyBuffer.putLong(normalize(rowId.leastSignificantBits())); + putUuid(keyBuffer, rowId.uuid()); + } + + static void putUuid(ByteBuffer buffer, UUID uuid) { Review Comment: Maybe a "rowIdUuid", because there's also "normalize", which may or may not be necessary for general-purpose UUIDs ########## modules/storage-rocksdb/src/main/java/org/apache/ignite/internal/storage/rocksdb/RocksDbMetaStorage.java: ########## @@ -103,13 +112,85 @@ void putPartitionId(int partitionId) { } } + /** + * Puts last row ID for which the index was built, {@code null} means index building is finished. + * + * @param partitionId Partition ID. + * @param indexId Index ID. + * @param rowId Row ID. + */ + public void putIndexLastBuildRowId(int partitionId, UUID indexId, @Nullable RowId rowId) { + try { + metaColumnFamily.put(indexMetaKey(partitionId, indexId), indexLastBuildRowId(rowId)); Review Comment: Batch is encapsulated somewhere inside, right? ########## modules/storage-page-memory/src/main/java/org/apache/ignite/internal/storage/pagememory/index/sorted/PageMemorySortedIndexStorage.java: ########## @@ -497,4 +511,34 @@ public void startCleanup() { public void finishCleanup() { state.compareAndSet(StorageState.CLEANUP, StorageState.RUNNABLE); } + + @Override + public @Nullable RowId getLastBuildRowId() { + return busy(() -> { + throwExceptionIfStorageInProgressOfRebalance(state.get(), this::createStorageInfo); + + try { + UUID lastBuildRowIdUuid = indexMetaTree.findOne(new IndexMetaKey(indexDescriptor().id())).lastBuildRowIdUuid(); Review Comment: Looks copy-pasted, maybe we should have a common super-class for sorted and hash indexes? -- 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]
