|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one |
| 3 | + * or more contributor license agreements. See the NOTICE file |
| 4 | + * distributed with this work for additional information |
| 5 | + * regarding copyright ownership. The ASF licenses this file |
| 6 | + * to you under the Apache License, Version 2.0 (the |
| 7 | + * "License"); you may not use this file except in compliance |
| 8 | + * with the License. You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | + |
| 20 | +package org.apache.iotdb.db.subscription.broker.consensus; |
| 21 | + |
| 22 | +import org.apache.iotdb.commons.conf.CommonConfig; |
| 23 | +import org.apache.iotdb.commons.conf.CommonDescriptor; |
| 24 | +import org.apache.iotdb.commons.consensus.DataRegionId; |
| 25 | +import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest; |
| 26 | +import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl; |
| 27 | +import org.apache.iotdb.consensus.iot.SubscriptionWalRetentionPolicy; |
| 28 | +import org.apache.iotdb.consensus.iot.WriterSafeFrontierTracker; |
| 29 | +import org.apache.iotdb.db.conf.IoTDBDescriptor; |
| 30 | +import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode; |
| 31 | +import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils; |
| 32 | +import org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode; |
| 33 | +import org.apache.iotdb.db.subscription.event.SubscriptionEvent; |
| 34 | +import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager; |
| 35 | +import org.apache.iotdb.db.subscription.task.execution.ConsensusSubscriptionPrefetchExecutor; |
| 36 | +import org.apache.iotdb.db.subscription.task.execution.ConsensusSubscriptionPrefetchExecutorManager; |
| 37 | +import org.apache.iotdb.rpc.subscription.config.TopicConstant; |
| 38 | +import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress; |
| 39 | +import org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseType; |
| 40 | +import org.apache.iotdb.rpc.subscription.payload.poll.TabletsPayload; |
| 41 | + |
| 42 | +import org.apache.tsfile.enums.ColumnCategory; |
| 43 | +import org.apache.tsfile.enums.TSDataType; |
| 44 | +import org.apache.tsfile.write.record.Tablet; |
| 45 | +import org.junit.Rule; |
| 46 | +import org.junit.Test; |
| 47 | +import org.junit.rules.TemporaryFolder; |
| 48 | +import org.junit.runner.RunWith; |
| 49 | +import org.powermock.api.mockito.PowerMockito; |
| 50 | +import org.powermock.core.classloader.annotations.PowerMockIgnore; |
| 51 | +import org.powermock.core.classloader.annotations.PrepareForTest; |
| 52 | +import org.powermock.modules.junit4.PowerMockRunner; |
| 53 | + |
| 54 | +import java.io.File; |
| 55 | +import java.util.ArrayList; |
| 56 | +import java.util.Arrays; |
| 57 | +import java.util.Collections; |
| 58 | +import java.util.Iterator; |
| 59 | +import java.util.List; |
| 60 | +import java.util.concurrent.TimeUnit; |
| 61 | + |
| 62 | +import static org.junit.Assert.assertEquals; |
| 63 | +import static org.mockito.ArgumentMatchers.any; |
| 64 | +import static org.mockito.Mockito.mock; |
| 65 | +import static org.mockito.Mockito.times; |
| 66 | +import static org.mockito.Mockito.verify; |
| 67 | +import static org.mockito.Mockito.when; |
| 68 | + |
| 69 | +@RunWith(PowerMockRunner.class) |
| 70 | +@PrepareForTest(ConsensusSubscriptionPrefetchExecutorManager.class) |
| 71 | +@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "javax.management.*"}) |
| 72 | +public class ConsensusPrefetchingQueueSeekTest { |
| 73 | + |
| 74 | + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); |
| 75 | + |
| 76 | + @Test(timeout = 10_000L) |
| 77 | + public void testSeekToBeginningReplaysFirstLocalEntryWithoutWalGap() throws Exception { |
| 78 | + assertBeginningReplay(Collections.singletonList(createRequest(1L, 1L, 7)), 2L, 0L); |
| 79 | + } |
| 80 | + |
| 81 | + @Test(timeout = 10_000L) |
| 82 | + public void testSeekToBeginningPreservesFollowerEntryBeforeFirstLocalEntry() throws Exception { |
| 83 | + assertBeginningReplay( |
| 84 | + Arrays.asList(createRequest(-1L, 10L, 8), createRequest(1L, 1L, 7)), 2L, 0L); |
| 85 | + } |
| 86 | + |
| 87 | + @Test(timeout = 10_000L) |
| 88 | + public void testSeekToBeginningCountsOnlyMissingValidLocalIndexes() throws Exception { |
| 89 | + // Local indexes 1 and 2 are absent. Index 0 is the empty-WAL sentinel, not a missing entry. |
| 90 | + assertBeginningReplay( |
| 91 | + Arrays.asList(createRequest(3L, 3L, 7), createRequest(4L, 4L, 7)), 5L, 2L); |
| 92 | + } |
| 93 | + |
| 94 | + private void assertBeginningReplay( |
| 95 | + final List<IndexedConsensusRequest> requests, |
| 96 | + final long expectedNextIndex, |
| 97 | + final long expectedSkippedEntries) |
| 98 | + throws Exception { |
| 99 | + final CommonConfig config = CommonDescriptor.getInstance().getConfig(); |
| 100 | + final int originalBatchMaxDelay = config.getSubscriptionConsensusBatchMaxDelayInMs(); |
| 101 | + final int originalPrefetchThreads = |
| 102 | + config.getSubscriptionConsensusPrefetchExecutorMaxThreadNum(); |
| 103 | + final String originalSystemDir = IoTDBDescriptor.getInstance().getConfig().getSystemDir(); |
| 104 | + final File systemDir = temporaryFolder.newFolder(); |
| 105 | + ConsensusSubscriptionPrefetchExecutor executor = null; |
| 106 | + ConsensusPrefetchingQueue queue = null; |
| 107 | + try { |
| 108 | + config.setSubscriptionConsensusBatchMaxDelayInMs(0); |
| 109 | + config.setSubscriptionConsensusPrefetchExecutorMaxThreadNum(1); |
| 110 | + IoTDBDescriptor.getInstance().getConfig().setSystemDir(systemDir.getAbsolutePath()); |
| 111 | + |
| 112 | + // Supply an isolated runtime even in builds where subscriptions are disabled. Seek reset |
| 113 | + // and replay still run through the real serial prefetch worker and the public queue API. |
| 114 | + executor = new ConsensusSubscriptionPrefetchExecutor(); |
| 115 | + final ConsensusSubscriptionPrefetchExecutorManager manager = |
| 116 | + mock(ConsensusSubscriptionPrefetchExecutorManager.class); |
| 117 | + PowerMockito.mockStatic(ConsensusSubscriptionPrefetchExecutorManager.class); |
| 118 | + PowerMockito.when(ConsensusSubscriptionPrefetchExecutorManager.getInstance()) |
| 119 | + .thenReturn(manager); |
| 120 | + when(manager.getExecutor()).thenReturn(executor); |
| 121 | + |
| 122 | + final WALNode walNode = mock(WALNode.class); |
| 123 | + when(walNode.getCurrentSearchIndex()).thenReturn(expectedNextIndex - 1L); |
| 124 | + when(walNode.getLogDirectory()).thenReturn(systemDir); |
| 125 | + final IoTConsensusServerImpl server = mock(IoTConsensusServerImpl.class); |
| 126 | + when(server.getConsensusReqReader()).thenReturn(walNode); |
| 127 | + when(server.getWriterSafeFrontierTracker()).thenReturn(new WriterSafeFrontierTracker()); |
| 128 | + final ConsensusLogToTabletConverter converter = mock(ConsensusLogToTabletConverter.class); |
| 129 | + when(converter.getDatabaseName()).thenReturn("db"); |
| 130 | + when(converter.isTableModel()).thenReturn(true); |
| 131 | + when(converter.convert(any())) |
| 132 | + .thenAnswer( |
| 133 | + invocation -> |
| 134 | + Collections.singletonList( |
| 135 | + createTablet(((InsertRowNode) invocation.getArgument(0)).getTime()))); |
| 136 | + |
| 137 | + queue = |
| 138 | + new ConsensusPrefetchingQueue( |
| 139 | + "consumerGroup", |
| 140 | + "topic", |
| 141 | + TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE, |
| 142 | + new DataRegionId(1), |
| 143 | + server, |
| 144 | + new SubscriptionWalRetentionPolicy( |
| 145 | + "topic", |
| 146 | + SubscriptionWalRetentionPolicy.UNBOUNDED, |
| 147 | + SubscriptionWalRetentionPolicy.UNBOUNDED), |
| 148 | + converter, |
| 149 | + new ConsensusSubscriptionCommitManager( |
| 150 | + (consumerGroupId, topicName, regionId) -> |
| 151 | + ConsensusSubscriptionCommitManager.ConfigNodeProgressQueryResult.absent()), |
| 152 | + new RegionProgress(Collections.emptyMap()), |
| 153 | + expectedNextIndex, |
| 154 | + 1L, |
| 155 | + true) { |
| 156 | + @Override |
| 157 | + protected ProgressWALIterator createSubscriptionWALIterator( |
| 158 | + final long startSearchIndex) { |
| 159 | + final Iterator<IndexedConsensusRequest> retainedEntries = |
| 160 | + requests.stream() |
| 161 | + .filter( |
| 162 | + request -> |
| 163 | + request.getSearchIndex() < 0 |
| 164 | + || request.getSearchIndex() >= startSearchIndex) |
| 165 | + .iterator(); |
| 166 | + final ProgressWALIterator iterator = mock(ProgressWALIterator.class); |
| 167 | + when(iterator.hasNext()).thenAnswer(ignored -> retainedEntries.hasNext()); |
| 168 | + when(iterator.next()).thenAnswer(ignored -> retainedEntries.next()); |
| 169 | + return iterator; |
| 170 | + } |
| 171 | + }; |
| 172 | + queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 1024 * 1024)); |
| 173 | + |
| 174 | + queue.seekToBeginning(); |
| 175 | + |
| 176 | + final List<Long> actualTimestamps = new ArrayList<>(); |
| 177 | + final long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L); |
| 178 | + while (actualTimestamps.size() < requests.size() && System.nanoTime() < deadline) { |
| 179 | + final SubscriptionEvent event = queue.poll("consumer"); |
| 180 | + if (event == null) { |
| 181 | + TimeUnit.MILLISECONDS.sleep(10L); |
| 182 | + continue; |
| 183 | + } |
| 184 | + assertEquals( |
| 185 | + SubscriptionPollResponseType.TABLETS.getType(), |
| 186 | + event.getCurrentResponse().getResponseType()); |
| 187 | + for (final Tablet tablet : |
| 188 | + ((TabletsPayload) event.getCurrentResponse().getPayload()).getTablets()) { |
| 189 | + assertEquals(1, tablet.getRowSize()); |
| 190 | + actualTimestamps.add(tablet.getTimestamps()[0]); |
| 191 | + } |
| 192 | + } |
| 193 | + final List<Long> expectedTimestamps = new ArrayList<>(); |
| 194 | + for (final IndexedConsensusRequest request : requests) { |
| 195 | + expectedTimestamps.add(((InsertRowNode) request.getRequests().get(0)).getTime()); |
| 196 | + } |
| 197 | + Collections.sort(expectedTimestamps); |
| 198 | + Collections.sort(actualTimestamps); |
| 199 | + assertEquals(expectedTimestamps, actualTimestamps); |
| 200 | + assertEquals(expectedNextIndex, queue.getCurrentReadSearchIndex()); |
| 201 | + assertEquals(expectedSkippedEntries, queue.getWalGapSkippedEntries()); |
| 202 | + verify(converter, times(requests.size())).convert(any()); |
| 203 | + } finally { |
| 204 | + if (queue != null) { |
| 205 | + queue.close(); |
| 206 | + } |
| 207 | + if (executor != null) { |
| 208 | + executor.shutdown(); |
| 209 | + } |
| 210 | + config.setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay); |
| 211 | + config.setSubscriptionConsensusPrefetchExecutorMaxThreadNum(originalPrefetchThreads); |
| 212 | + IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + private static IndexedConsensusRequest createRequest( |
| 217 | + final long searchIndex, final long localSeq, final int writerNodeId) { |
| 218 | + return new IndexedConsensusRequest( |
| 219 | + searchIndex, |
| 220 | + localSeq, |
| 221 | + Collections.singletonList( |
| 222 | + StatementTestUtils.genInsertRowNode(Math.toIntExact(localSeq)))) |
| 223 | + .setPhysicalTime(1000L + localSeq) |
| 224 | + .setNodeId(writerNodeId); |
| 225 | + } |
| 226 | + |
| 227 | + private static Tablet createTablet(final long timestamp) { |
| 228 | + final Tablet tablet = |
| 229 | + new Tablet( |
| 230 | + "sensors", |
| 231 | + Arrays.asList("device", "temperature"), |
| 232 | + Arrays.asList(TSDataType.STRING, TSDataType.DOUBLE), |
| 233 | + Arrays.asList(ColumnCategory.TAG, ColumnCategory.FIELD), |
| 234 | + 1); |
| 235 | + tablet.addTimestamp(0, timestamp); |
| 236 | + tablet.addValue(0, 0, "d1"); |
| 237 | + tablet.addValue(0, 1, 36.5); |
| 238 | + tablet.setRowSize(1); |
| 239 | + return tablet; |
| 240 | + } |
| 241 | +} |
0 commit comments