|
270 | 270 | import java.util.concurrent.ExecutorService; |
271 | 271 | import java.util.concurrent.Executors; |
272 | 272 | import java.util.concurrent.Future; |
| 273 | +import java.util.concurrent.PriorityBlockingQueue; |
273 | 274 | import java.util.concurrent.ThreadPoolExecutor; |
274 | 275 | import java.util.concurrent.TimeUnit; |
275 | 276 | import java.util.concurrent.TimeoutException; |
@@ -4720,6 +4721,108 @@ public void testResubscribeForCompletedCurrentVersionPartition() throws Interrup |
4720 | 4721 |
|
4721 | 4722 | } |
4722 | 4723 |
|
| 4724 | + /** |
| 4725 | + * Verifies that {@link StoreIngestionTask#resubscribeForAllPartitions()} skips partitions whose |
| 4726 | + * blob transfer is still in flight. Without this guard, a version-role change (e.g. FUTURE->CURRENT) |
| 4727 | + * would unsubscribe + resubscribe the partition, causing RocksDB to reopen on the final partition |
| 4728 | + * directory while the blob transfer was still streaming into the temp directory -- which breaks |
| 4729 | + * the post-transfer rename and drives the replica to Helix ERROR. |
| 4730 | + */ |
| 4731 | + @Test |
| 4732 | + public void testResubscribeForAllPartitionsSkipsBlobTransferInProgress() throws Exception { |
| 4733 | + StoreIngestionTask storeIngestionTask = mock(StoreIngestionTask.class); |
| 4734 | + doCallRealMethod().when(storeIngestionTask).resubscribeForAllPartitions(); |
| 4735 | + |
| 4736 | + // pcs0: blob transfer in progress -- MUST be skipped. |
| 4737 | + PartitionConsumptionState pcsBlobInProgress = mock(PartitionConsumptionState.class); |
| 4738 | + doReturn(true).when(pcsBlobInProgress).isBlobTransferInProgress(); |
| 4739 | + doReturn("store_v1-0").when(pcsBlobInProgress).getReplicaId(); |
| 4740 | + |
| 4741 | + // pcs1: no blob transfer, not complete -- should be resubscribed. |
| 4742 | + PartitionConsumptionState pcsNotComplete = mock(PartitionConsumptionState.class); |
| 4743 | + doReturn(false).when(pcsNotComplete).isBlobTransferInProgress(); |
| 4744 | + doReturn(false).when(pcsNotComplete).isComplete(); |
| 4745 | + |
| 4746 | + // pcs2: no blob transfer, complete, not yet flipped -- should be resubscribed AND flipped. |
| 4747 | + PartitionConsumptionState pcsCompleteNotFlipped = mock(PartitionConsumptionState.class); |
| 4748 | + doReturn(false).when(pcsCompleteNotFlipped).isBlobTransferInProgress(); |
| 4749 | + doReturn(true).when(pcsCompleteNotFlipped).isComplete(); |
| 4750 | + doReturn(false).when(pcsCompleteNotFlipped).hasResubscribedAfterBootstrapAsCurrentVersion(); |
| 4751 | + |
| 4752 | + VeniceConcurrentHashMap<Integer, PartitionConsumptionState> pcsMap = new VeniceConcurrentHashMap<>(); |
| 4753 | + pcsMap.put(0, pcsBlobInProgress); |
| 4754 | + pcsMap.put(1, pcsNotComplete); |
| 4755 | + pcsMap.put(2, pcsCompleteNotFlipped); |
| 4756 | + |
| 4757 | + // Inject into the protected final partitionConsumptionStateMap field on the mock. |
| 4758 | + Field pcsMapField = StoreIngestionTask.class.getDeclaredField("partitionConsumptionStateMap"); |
| 4759 | + pcsMapField.setAccessible(true); |
| 4760 | + pcsMapField.set(storeIngestionTask, pcsMap); |
| 4761 | + |
| 4762 | + doReturn(true).when(storeIngestionTask).isCurrentVersion(); |
| 4763 | + |
| 4764 | + storeIngestionTask.resubscribeForAllPartitions(); |
| 4765 | + |
| 4766 | + // Blob-transfer-in-progress partition is skipped entirely. |
| 4767 | + verify(storeIngestionTask, never()).resubscribe(pcsBlobInProgress); |
| 4768 | + verify(pcsBlobInProgress, never()).setHasResubscribedAfterBootstrapAsCurrentVersion(anyBoolean()); |
| 4769 | + |
| 4770 | + // Other partitions are resubscribed as usual. |
| 4771 | + verify(storeIngestionTask, times(1)).resubscribe(pcsNotComplete); |
| 4772 | + verify(storeIngestionTask, times(1)).resubscribe(pcsCompleteNotFlipped); |
| 4773 | + verify(pcsCompleteNotFlipped, times(1)).setHasResubscribedAfterBootstrapAsCurrentVersion(eq(true)); |
| 4774 | + } |
| 4775 | + |
| 4776 | + /** |
| 4777 | + * Verifies that {@link StoreIngestionTask#maybeProcessResubscribeRequest()} (the lag-based |
| 4778 | + * auto-resubscribe path fed by {@code HeartbeatMonitoringService}) skips partitions whose blob |
| 4779 | + * transfer is still in flight. Same race as the version-role-change path: resubscribing would |
| 4780 | + * reopen RocksDB on the final partition directory mid-transfer and break the post-transfer rename. |
| 4781 | + */ |
| 4782 | + @Test |
| 4783 | + public void testMaybeProcessResubscribeRequestSkipsBlobTransferInProgress() throws Exception { |
| 4784 | + StoreIngestionTask storeIngestionTask = mock(StoreIngestionTask.class); |
| 4785 | + doCallRealMethod().when(storeIngestionTask).maybeProcessResubscribeRequest(); |
| 4786 | + |
| 4787 | + VeniceServerConfig serverConfig = mock(VeniceServerConfig.class); |
| 4788 | + doReturn(serverConfig).when(storeIngestionTask).getServerConfig(); |
| 4789 | + doReturn(10).when(serverConfig).getLagBasedReplicaAutoResubscribeMaxReplicaCount(); |
| 4790 | + doReturn(60).when(serverConfig).getLagBasedReplicaAutoResubscribeIntervalInSeconds(); |
| 4791 | + |
| 4792 | + // pcs0: blob transfer in progress -- MUST be skipped. |
| 4793 | + PartitionConsumptionState pcsBlobInProgress = mock(PartitionConsumptionState.class); |
| 4794 | + doReturn(true).when(pcsBlobInProgress).isBlobTransferInProgress(); |
| 4795 | + doReturn("store_v1-0").when(pcsBlobInProgress).getReplicaId(); |
| 4796 | + |
| 4797 | + // pcs1: no blob transfer -- should be resubscribed. |
| 4798 | + PartitionConsumptionState pcsReady = mock(PartitionConsumptionState.class); |
| 4799 | + doReturn(false).when(pcsReady).isBlobTransferInProgress(); |
| 4800 | + doReturn(false).when(pcsReady).isErrorReported(); |
| 4801 | + doReturn("store_v1-1").when(pcsReady).getReplicaId(); |
| 4802 | + |
| 4803 | + VeniceConcurrentHashMap<Integer, PartitionConsumptionState> pcsMap = new VeniceConcurrentHashMap<>(); |
| 4804 | + pcsMap.put(0, pcsBlobInProgress); |
| 4805 | + pcsMap.put(1, pcsReady); |
| 4806 | + doReturn(pcsMap).when(storeIngestionTask).getPartitionConsumptionStateMap(); |
| 4807 | + |
| 4808 | + PriorityBlockingQueue<Integer> resubscribeQueue = new PriorityBlockingQueue<>(); |
| 4809 | + resubscribeQueue.add(0); |
| 4810 | + resubscribeQueue.add(1); |
| 4811 | + doReturn(resubscribeQueue).when(storeIngestionTask).getResubscribeRequestQueue(); |
| 4812 | + |
| 4813 | + // Empty previous-resubscribe-time map means no rate-limiting will trigger. |
| 4814 | + doReturn(new VeniceConcurrentHashMap<Integer, Long>()).when(storeIngestionTask) |
| 4815 | + .getPartitionToPreviousResubscribeTimeMap(); |
| 4816 | + |
| 4817 | + storeIngestionTask.maybeProcessResubscribeRequest(); |
| 4818 | + |
| 4819 | + // Blob-transfer-in-progress partition is skipped entirely. |
| 4820 | + verify(storeIngestionTask, never()).resubscribe(pcsBlobInProgress); |
| 4821 | + |
| 4822 | + // Ready partition is resubscribed as usual. |
| 4823 | + verify(storeIngestionTask, times(1)).resubscribe(pcsReady); |
| 4824 | + } |
| 4825 | + |
4723 | 4826 | @Test(dataProvider = "aaConfigProvider") |
4724 | 4827 | public void testWrappedInterruptExceptionDuringGracefulShutdown(AAConfig aaConfig) throws Exception { |
4725 | 4828 | hybridStoreConfig = Optional.of( |
|
0 commit comments