/** * Copyright 2016 LinkedIn Corp. All rights reserved. * * Licensed 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. */ package com.github.ambry.router; import com.github.ambry.account.Account; import com.github.ambry.account.Container; import com.github.ambry.account.InMemAccountService; import com.github.ambry.clustermap.DataNodeId; import com.github.ambry.clustermap.MockClusterMap; import com.github.ambry.clustermap.MockDataNodeId; import com.github.ambry.clustermap.ReplicaId; import com.github.ambry.commons.BlobId; import com.github.ambry.commons.ByteBufferReadableStreamChannel; import com.github.ambry.commons.LoggingNotificationSystem; import com.github.ambry.commons.ResponseHandler; import com.github.ambry.config.CryptoServiceConfig; import com.github.ambry.config.KMSConfig; import com.github.ambry.config.RouterConfig; import com.github.ambry.config.VerifiableProperties; import com.github.ambry.messageformat.BlobProperties; import com.github.ambry.messageformat.BlobType; import com.github.ambry.messageformat.MessageFormatRecord; import com.github.ambry.messageformat.MetadataContentSerDe; import com.github.ambry.network.NetworkClient; import com.github.ambry.network.NetworkClientErrorCode; import com.github.ambry.network.NetworkClientFactory; import com.github.ambry.network.RequestInfo; import com.github.ambry.network.ResponseInfo; import com.github.ambry.network.SocketNetworkClient; import com.github.ambry.notification.NotificationSystem; import com.github.ambry.protocol.DeleteRequest; import com.github.ambry.protocol.GetOption; import com.github.ambry.protocol.PutRequest; import com.github.ambry.protocol.RequestOrResponseType; import com.github.ambry.protocol.UndeleteRequest; import com.github.ambry.server.ServerErrorCode; import com.github.ambry.store.StoreKey; import com.github.ambry.utils.MockTime; import com.github.ambry.utils.NettyByteBufLeakHelper; import com.github.ambry.utils.Pair; import com.github.ambry.utils.SystemTime; import com.github.ambry.utils.TestUtils; import com.github.ambry.utils.Utils; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.PrimitiveIterator; import java.util.Properties; import java.util.Random; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.LongStream; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.github.ambry.router.RouterTestHelpers.*; import static com.github.ambry.utils.TestUtils.*; import static org.junit.Assert.*; import static org.junit.Assume.*; import static org.mockito.Mockito.*; /** * Class to test the {@link NonBlockingRouter} */ @RunWith(Parameterized.class) public class NonBlockingRouterTest { protected static final int MAX_PORTS_PLAIN_TEXT = 3; protected static final int MAX_PORTS_SSL = 3; protected static final int CHECKOUT_TIMEOUT_MS = 1000; private static final int REQUEST_TIMEOUT_MS = 1000; private static final int PUT_REQUEST_PARALLELISM = 3; private static final int PUT_SUCCESS_TARGET = 2; private static final int GET_REQUEST_PARALLELISM = 2; private static final int GET_SUCCESS_TARGET = 1; private static final int DELETE_REQUEST_PARALLELISM = 3; private static final int DELETE_SUCCESS_TARGET = 2; private static final int PUT_CONTENT_SIZE = 1000; private static final int USER_METADATA_SIZE = 10; private int maxPutChunkSize = PUT_CONTENT_SIZE; private final Random random = new Random(); protected NonBlockingRouter router; protected NonBlockingRouterMetrics routerMetrics; private PutManager putManager; private GetManager getManager; private DeleteManager deleteManager; protected final AtomicReference<MockSelectorState> mockSelectorState = new AtomicReference<>(MockSelectorState.Good); protected final MockTime mockTime; protected final KeyManagementService kms; protected final String singleKeyForKMS; protected final CryptoService cryptoService; protected final MockClusterMap mockClusterMap; protected final MockServerLayout mockServerLayout; protected RouterConfig routerConfig; protected final boolean testEncryption; protected final int metadataContentVersion; protected final boolean includeCloudDc; protected final InMemAccountService accountService; protected CryptoJobHandler cryptoJobHandler; private static final Logger logger = LoggerFactory.getLogger(NonBlockingRouterTest.class); // Request params; BlobProperties putBlobProperties; byte[] putUserMetadata; byte[] putContent; ReadableStreamChannel putChannel; private NettyByteBufLeakHelper nettyByteBufLeakHelper = new NettyByteBufLeakHelper(); /** * Running for both regular and encrypted blobs, and versions 2 and 3 of MetadataContent * @return an array with all four different choices */ @Parameterized.Parameters public static List<Object[]> data() { return Arrays.asList(new Object[][]{{false, MessageFormatRecord.Metadata_Content_Version_V2}, {false, MessageFormatRecord.Metadata_Content_Version_V3}, {true, MessageFormatRecord.Metadata_Content_Version_V2}, {true, MessageFormatRecord.Metadata_Content_Version_V3}}); } /** * Initialize parameters common to all tests. * @param testEncryption {@code true} to test with encryption enabled. {@code false} otherwise * @param metadataContentVersion the metadata content version to test with. * @throws Exception if initialization fails */ public NonBlockingRouterTest(boolean testEncryption, int metadataContentVersion) throws Exception { this(testEncryption, metadataContentVersion, false); } /** * Initialize parameters common to all tests. This constructor is exposed for use by {@link CloudRouterTest}. * @param testEncryption {@code true} to test with encryption enabled. {@code false} otherwise * @param metadataContentVersion the metadata content version to test with. * @param includeCloudDc {@code true} to make the local datacenter a cloud DC. * @throws Exception if initialization fails */ protected NonBlockingRouterTest(boolean testEncryption, int metadataContentVersion, boolean includeCloudDc) throws Exception { this.testEncryption = testEncryption; this.metadataContentVersion = metadataContentVersion; this.includeCloudDc = includeCloudDc; mockTime = new MockTime(); mockClusterMap = new MockClusterMap(false, true, 9, 3, 3, false, includeCloudDc); mockServerLayout = new MockServerLayout(mockClusterMap); NonBlockingRouter.currentOperationsCount.set(0); VerifiableProperties vProps = new VerifiableProperties(new Properties()); singleKeyForKMS = TestUtils.getRandomKey(SingleKeyManagementServiceTest.DEFAULT_KEY_SIZE_CHARS); kms = new SingleKeyManagementService(new KMSConfig(vProps), singleKeyForKMS); cryptoService = new GCMCryptoService(new CryptoServiceConfig(vProps)); cryptoJobHandler = new CryptoJobHandler(CryptoJobHandlerTest.DEFAULT_THREAD_COUNT); accountService = new InMemAccountService(false, true); } @Before public void before() { nettyByteBufLeakHelper.beforeTest(); } @After public void after() { Assert.assertEquals("Current operations count should be 0", 0, NonBlockingRouter.currentOperationsCount.get()); nettyByteBufLeakHelper.afterTest(); nettyByteBufLeakHelper.setDisabled(false); } /** * Constructs and returns a VerifiableProperties instance with the defaults required for instantiating * the {@link NonBlockingRouter}. * @return the created VerifiableProperties instance. */ protected Properties getNonBlockingRouterProperties(String routerDataCenter) { return getNonBlockingRouterProperties(routerDataCenter, PUT_REQUEST_PARALLELISM, DELETE_REQUEST_PARALLELISM); } /** * Constructs and returns a VerifiableProperties instance with the defaults required for instantiating * the {@link NonBlockingRouter}. * @param routerDataCenter the router's datacenter name * @param putParallelism put request parallelism * @param deleteParallelism delete request parallelism * @return the created VerifiableProperties instance. */ protected Properties getNonBlockingRouterProperties(String routerDataCenter, int putParallelism, int deleteParallelism) { Properties properties = new Properties(); properties.setProperty("router.hostname", "localhost"); properties.setProperty("router.datacenter.name", routerDataCenter); properties.setProperty("router.put.request.parallelism", Integer.toString(putParallelism)); properties.setProperty("router.put.success.target", Integer.toString(PUT_SUCCESS_TARGET)); properties.setProperty("router.max.put.chunk.size.bytes", Integer.toString(maxPutChunkSize)); properties.setProperty("router.get.request.parallelism", Integer.toString(GET_REQUEST_PARALLELISM)); properties.setProperty("router.get.success.target", Integer.toString(GET_SUCCESS_TARGET)); properties.setProperty("router.delete.request.parallelism", Integer.toString(deleteParallelism)); properties.setProperty("router.delete.success.target", Integer.toString(DELETE_SUCCESS_TARGET)); properties.setProperty("router.connection.checkout.timeout.ms", Integer.toString(CHECKOUT_TIMEOUT_MS)); properties.setProperty("router.request.timeout.ms", Integer.toString(REQUEST_TIMEOUT_MS)); properties.setProperty("router.connections.local.dc.warm.up.percentage", Integer.toString(67)); properties.setProperty("router.connections.remote.dc.warm.up.percentage", Integer.toString(34)); properties.setProperty("clustermap.cluster.name", "test"); properties.setProperty("clustermap.datacenter.name", "dc1"); properties.setProperty("clustermap.host.name", "localhost"); properties.setProperty("kms.default.container.key", TestUtils.getRandomKey(128)); properties.setProperty("router.metadata.content.version", String.valueOf(metadataContentVersion)); return properties; } /** * Construct {@link Properties} and {@link MockServerLayout} and initialize and set the * router with them. */ protected void setRouter() throws Exception { setRouter(getNonBlockingRouterProperties("DC1"), mockServerLayout, new LoggingNotificationSystem()); } /** * Initialize and set the router with the given {@link Properties} and {@link MockServerLayout} * @param props the {@link Properties} * @param serverLayout the {@link MockServerLayout}. * @param notificationSystem the {@link NotificationSystem} to use. */ protected void setRouter(Properties props, MockServerLayout serverLayout, NotificationSystem notificationSystem) throws Exception { VerifiableProperties verifiableProperties = new VerifiableProperties((props)); routerConfig = new RouterConfig(verifiableProperties); routerMetrics = new NonBlockingRouterMetrics(mockClusterMap, routerConfig); router = new NonBlockingRouter(routerConfig, routerMetrics, new MockNetworkClientFactory(verifiableProperties, mockSelectorState, MAX_PORTS_PLAIN_TEXT, MAX_PORTS_SSL, CHECKOUT_TIMEOUT_MS, serverLayout, mockTime), notificationSystem, mockClusterMap, kms, cryptoService, cryptoJobHandler, accountService, mockTime, MockClusterMap.DEFAULT_PARTITION_CLASS); } /** * Setup test suite to perform a {@link Router#putBlob} call using the constant {@link #PUT_CONTENT_SIZE} */ protected void setOperationParams() { setOperationParams(PUT_CONTENT_SIZE, TTL_SECS); } /** * Setup test suite to perform a {@link Router#putBlob} call. * @param putContentSize the size of the content to put * @param ttlSecs the TTL in seconds for the blob. */ protected void setOperationParams(int putContentSize, long ttlSecs) { putBlobProperties = new BlobProperties(-1, "serviceId", "memberId", "contentType", false, ttlSecs, Utils.getRandomShort(TestUtils.RANDOM), Utils.getRandomShort(TestUtils.RANDOM), testEncryption, null); putUserMetadata = new byte[USER_METADATA_SIZE]; random.nextBytes(putUserMetadata); putContent = new byte[putContentSize]; random.nextBytes(putContent); putChannel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(putContent)); } /** * Test the {@link NonBlockingRouterFactory} */ @Test public void testNonBlockingRouterFactory() throws Exception { Properties props = getNonBlockingRouterProperties("NotInClusterMap"); VerifiableProperties verifiableProperties = new VerifiableProperties((props)); try { router = (NonBlockingRouter) new NonBlockingRouterFactory(verifiableProperties, mockClusterMap, new LoggingNotificationSystem(), null, accountService).getRouter(); Assert.fail("NonBlockingRouterFactory instantiation should have failed because the router datacenter is not in " + "the cluster map"); } catch (IllegalStateException e) { } props = getNonBlockingRouterProperties("DC1"); verifiableProperties = new VerifiableProperties((props)); router = (NonBlockingRouter) new NonBlockingRouterFactory(verifiableProperties, mockClusterMap, new LoggingNotificationSystem(), null, accountService).getRouter(); assertExpectedThreadCounts(2, 1); router.close(); assertExpectedThreadCounts(0, 0); } /** * Test Router with a single scaling unit. */ @Test public void testRouterBasic() throws Exception { setRouter(); assertExpectedThreadCounts(2, 1); // More extensive test for puts present elsewhere - these statements are here just to exercise the flow within the // NonBlockingRouter class, and to ensure that operations submitted to a router eventually completes. List<String> blobIds = new ArrayList<>(); for (int i = 0; i < 2; i++) { setOperationParams(); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, PutBlobOptions.DEFAULT).get(); logger.debug("Put blob {}", blobId); blobIds.add(blobId); } setOperationParams(); String stitchedBlobId = router.stitchBlob(putBlobProperties, putUserMetadata, blobIds.stream() .map(blobId -> new ChunkInfo(blobId, PUT_CONTENT_SIZE, Utils.Infinite_Time)) .collect(Collectors.toList())).get(); blobIds.add(stitchedBlobId); for (String blobId : blobIds) { router.getBlob(blobId, new GetBlobOptionsBuilder().build()).get(); router.updateBlobTtl(blobId, null, Utils.Infinite_Time); router.getBlob(blobId, new GetBlobOptionsBuilder().build()).get(); router.getBlob(blobId, new GetBlobOptionsBuilder().operationType(GetBlobOptions.OperationType.BlobInfo).build()) .get(); router.deleteBlob(blobId, null).get(); try { router.getBlob(blobId, new GetBlobOptionsBuilder().build()).get(); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("BlobDeleted error is expected", RouterErrorCode.BlobDeleted, r.getErrorCode()); } router.getBlob(blobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_Deleted_Blobs).build()).get(); router.getBlob(blobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_All).build()).get(); } router.close(); assertExpectedThreadCounts(0, 0); //submission after closing should return a future that is already done. assertClosed(); } /** * Test undelete with Router with a single scaling unit. * @throws Exception */ @Test public void testUndeleteBasic() throws Exception { assumeTrue(!testEncryption && !includeCloudDc); // Setting put and delete parallelism to 2, same as the put and delete success target. // If put or delete requests' parallelism is 3 (default value), when we ensure all the servers has the correspoding // requests, it might have override by the dangling request. // For example, when deleting a blob, there are three delete requests being sent to the mock server. But only two of // them required to be acknowledged. There is a chance that when we undelete this blob, this third unacknowledged // delete request would override the undelete state. setRouter(getNonBlockingRouterProperties("DC1", 3, 2), mockServerLayout, new LoggingNotificationSystem()); assertExpectedThreadCounts(2, 1); // 1. Test undelete a composite blob List<String> blobIds = new ArrayList<>(); for (int i = 0; i < 2; i++) { setOperationParams(); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, PutBlobOptions.DEFAULT).get(); ensurePutInAllServers(blobId, mockServerLayout); logger.debug("Put blob {}", blobId); blobIds.add(blobId); } setOperationParams(); List<ChunkInfo> chunksToStitch = blobIds.stream() .map(blobId -> new ChunkInfo(blobId, PUT_CONTENT_SIZE, Utils.Infinite_Time)) .collect(Collectors.toList()); String stitchedBlobId = router.stitchBlob(putBlobProperties, putUserMetadata, chunksToStitch).get(); ensureStitchInAllServers(stitchedBlobId, mockServerLayout, chunksToStitch, PUT_CONTENT_SIZE); blobIds.add(stitchedBlobId); for (String blobId : blobIds) { router.getBlob(blobId, new GetBlobOptionsBuilder().build()).get(); router.deleteBlob(blobId, null).get(); ensureDeleteInAllServers(blobId, mockServerLayout); try { router.getBlob(blobId, new GetBlobOptionsBuilder().build()).get(); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("BlobDeleted error is expected", RouterErrorCode.BlobDeleted, r.getErrorCode()); } router.getBlob(blobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_Deleted_Blobs).build()).get(); router.getBlob(blobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_All).build()).get(); } // StitchedBlob is a composite blob router.undeleteBlob(stitchedBlobId, "undelete_server_id").get(); for (String blobId : blobIds) { ensureUndeleteInAllServers(blobId, mockServerLayout); } // Now we should be able to fetch all the blobs for (String blobId : blobIds) { router.getBlob(blobId, new GetBlobOptionsBuilder().build()).get(); } // 2. Test undelete a simple blob setOperationParams(); String simpleBlobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, PutBlobOptions.DEFAULT).get(); ensurePutInAllServers(simpleBlobId, mockServerLayout); router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().build()).get(); router.deleteBlob(simpleBlobId, null).get(); ensureDeleteInAllServers(simpleBlobId, mockServerLayout); try { router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().build()).get(); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("BlobDeleted error is expected", RouterErrorCode.BlobDeleted, r.getErrorCode()); } router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_Deleted_Blobs).build()).get(); router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_All).build()).get(); router.undeleteBlob(simpleBlobId, "undelete_server_id").get(); ensureUndeleteInAllServers(simpleBlobId, mockServerLayout); router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().build()).get(); // 3. Test delete after undelete router.deleteBlob(simpleBlobId, null).get(); ensureDeleteInAllServers(simpleBlobId, mockServerLayout); try { router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().build()).get(); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("BlobDeleted error is expected", RouterErrorCode.BlobDeleted, r.getErrorCode()); } router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_Deleted_Blobs).build()).get(); router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().getOption(GetOption.Include_All).build()).get(); // 4. Test undelete more than once router.undeleteBlob(simpleBlobId, "undelete_server_id").get(); ensureUndeleteInAllServers(simpleBlobId, mockServerLayout); router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().build()).get(); // 5. Undelete the same blob again router.undeleteBlob(simpleBlobId, "undelete_server_id").get(); // 6. Test ttl update after undelete router.updateBlobTtl(simpleBlobId, null, Utils.Infinite_Time); router.getBlob(simpleBlobId, new GetBlobOptionsBuilder().build()).get(); router.close(); assertExpectedThreadCounts(0, 0); //submission after closing should return a future that is already done. assertClosed(); } /** * Test undelete notification system when successfully undelete a blob. * @throws Exception */ @Test public void testUndeleteWithNotificationSystem() throws Exception { assumeTrue(!includeCloudDc); final CountDownLatch undeletesDoneLatch = new CountDownLatch(2); final Set<String> blobsThatAreUndeleted = new HashSet<>(); LoggingNotificationSystem undeleteTrackingNotificationSystem = new LoggingNotificationSystem() { @Override public void onBlobUndeleted(String blobId, String serviceId, Account account, Container container) { blobsThatAreUndeleted.add(blobId); undeletesDoneLatch.countDown(); } }; setRouter(getNonBlockingRouterProperties("DC1"), mockServerLayout, undeleteTrackingNotificationSystem); List<String> blobIds = new ArrayList<>(); for (int i = 0; i < 4; i++) { setOperationParams(); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, PutBlobOptions.DEFAULT).get(); ensurePutInAllServers(blobId, mockServerLayout); blobIds.add(blobId); } setOperationParams(); List<ChunkInfo> chunksToStitch = blobIds.stream() .map(blobId -> new ChunkInfo(blobId, PUT_CONTENT_SIZE, Utils.Infinite_Time)) .collect(Collectors.toList()); String blobId = router.stitchBlob(putBlobProperties, putUserMetadata, chunksToStitch).get(); ensureStitchInAllServers(blobId, mockServerLayout, chunksToStitch, PUT_CONTENT_SIZE); blobIds.add(blobId); Set<String> blobsToBeUndeleted = getBlobsInServers(mockServerLayout); router.getBlob(blobId, new GetBlobOptionsBuilder().build()).get(); router.deleteBlob(blobId, null).get(); for (String chunkBlobId : blobIds) { ensureDeleteInAllServers(chunkBlobId, mockServerLayout); } router.undeleteBlob(blobId, "undelete_server_id").get(); Assert.assertTrue("Undelete should not take longer than " + AWAIT_TIMEOUT_MS, undeletesDoneLatch.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); Assert.assertTrue("All blobs in server are deleted", blobsThatAreUndeleted.containsAll(blobsToBeUndeleted)); Assert.assertTrue("Only blobs in server are undeleted", blobsToBeUndeleted.containsAll(blobsThatAreUndeleted)); router.close(); assertClosed(); } /** * Test failure cases of undelete. * @throws Exception */ @Test public void testUndeleteFailure() throws Exception { assumeTrue(!testEncryption && !includeCloudDc); setRouter(); assertExpectedThreadCounts(2, 1); // 1. Test undelete a non-exist blob setOperationParams(); String nonExistBlobId = new BlobId(routerConfig.routerBlobidCurrentVersion, BlobId.BlobIdType.NATIVE, mockClusterMap.getLocalDatacenterId(), putBlobProperties.getAccountId(), putBlobProperties.getContainerId(), mockClusterMap.getWritablePartitionIds(MockClusterMap.DEFAULT_PARTITION_CLASS).get(0), false, BlobId.BlobDataType.DATACHUNK).getID(); try { router.getBlob(nonExistBlobId, new GetBlobOptionsBuilder().build()).get(); fail("Should fail because of non-existed id"); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("BlobDoesNotExist error is expected", RouterErrorCode.BlobDoesNotExist, r.getErrorCode()); } try { router.undeleteBlob(nonExistBlobId, "undelete_server_id").get(); fail("Should fail because of non-existed id"); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("BlobDoesNotExist error is expected", RouterErrorCode.BlobDoesNotExist, r.getErrorCode()); } // 2. Test not-deleted blob setOperationParams(); String notDeletedBlobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, PutBlobOptions.DEFAULT).get(); ensurePutInAllServers(notDeletedBlobId, mockServerLayout); router.getBlob(notDeletedBlobId, new GetBlobOptionsBuilder().build()).get(); try { router.undeleteBlob(notDeletedBlobId, "undelete_server_id").get(); fail("Should fail because of not-deleted id"); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("BlobNotDeleted error is expected", RouterErrorCode.BlobNotDeleted, r.getErrorCode()); } // 3. Test lifeVersion conflict blob setOperationParams(); String conflictBlobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, PutBlobOptions.DEFAULT).get(); ensurePutInAllServers(conflictBlobId, mockServerLayout); router.getBlob(conflictBlobId, new GetBlobOptionsBuilder().build()).get(); router.deleteBlob(conflictBlobId, null).get(); ensureDeleteInAllServers(conflictBlobId, mockServerLayout); // All lifeVersion should be 0 int count = 0; for (MockServer server : mockServerLayout.getMockServers()) { server.getBlobs().get(conflictBlobId).lifeVersion = 3; count++; if (count == 4) { // Only change 4 servers, since they are 3 datacenters and 9 servers. If we chang less than 4 servers, eg 3, then // this 3 changes might be distributed to 3 datacenters and undelete can still reach global quorum. break; } } try { router.undeleteBlob(conflictBlobId, "undelete_server_id").get(); fail("Should fail because of lifeVersion conflict"); } catch (ExecutionException e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("LifeVersionConflict error is expected", RouterErrorCode.LifeVersionConflict, r.getErrorCode()); } router.close(); assertExpectedThreadCounts(0, 0); //submission after closing should return a future that is already done. assertClosed(); } /** * Test behavior with various null inputs to router methods. * @throws Exception */ @Test public void testNullArguments() throws Exception { setRouter(); assertExpectedThreadCounts(2, 1); setOperationParams(); try { router.getBlob(null, new GetBlobOptionsBuilder().build()); Assert.fail("null blobId should have resulted in IllegalArgumentException"); } catch (IllegalArgumentException expected) { } try { router.getBlob("", null); Assert.fail("null options should have resulted in IllegalArgumentException"); } catch (IllegalArgumentException expected) { } try { router.putBlob(putBlobProperties, putUserMetadata, null, new PutBlobOptionsBuilder().build()); Assert.fail("null channel should have resulted in IllegalArgumentException"); } catch (IllegalArgumentException expected) { } try { router.putBlob(null, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()); Assert.fail("null blobProperties should have resulted in IllegalArgumentException"); } catch (IllegalArgumentException expected) { } try { router.deleteBlob(null, null); Assert.fail("null blobId should have resulted in IllegalArgumentException"); } catch (IllegalArgumentException expected) { } try { router.updateBlobTtl(null, null, Utils.Infinite_Time); Assert.fail("null blobId should have resulted in IllegalArgumentException"); } catch (IllegalArgumentException expected) { } try { router.undeleteBlob(null, null); Assert.fail("null blobId should have resulted in IllegalArgumentException"); } catch (IllegalArgumentException expected) { } // null user metadata should work. router.putBlob(putBlobProperties, null, putChannel, new PutBlobOptionsBuilder().build()).get(); router.close(); assertExpectedThreadCounts(0, 0); //submission after closing should return a future that is already done. assertClosed(); } /** * Test router put operation in a scenario where there are no partitions available. */ @Test public void testRouterPartitionsUnavailable() throws Exception { setRouter(); setOperationParams(); mockClusterMap.markAllPartitionsUnavailable(); try { router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); Assert.fail("Put should have failed if there are no partitions"); } catch (Exception e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals("Should have received AmbryUnavailable error", RouterErrorCode.AmbryUnavailable, r.getErrorCode()); } router.close(); assertExpectedThreadCounts(0, 0); assertClosed(); } /** * Test router put operation in a scenario where there are partitions, but none in the local DC. * This should not ideally happen unless there is a bad config, but the router should be resilient and * just error out these operations. */ @Test public void testRouterNoPartitionInLocalDC() throws Exception { // set the local DC to invalid, so that for puts, no partitions are available locally. Properties props = getNonBlockingRouterProperties("invalidDC"); setRouter(props, new MockServerLayout(mockClusterMap), new LoggingNotificationSystem()); setOperationParams(); try { router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); Assert.fail("Put should have failed if there are no partitions"); } catch (Exception e) { RouterException r = (RouterException) e.getCause(); Assert.assertEquals(RouterErrorCode.UnexpectedInternalError, r.getErrorCode()); } router.close(); assertExpectedThreadCounts(0, 0); assertClosed(); } /** * Test RequestResponseHandler thread exit flow. If the RequestResponseHandlerThread exits on its own (due to a * Throwable), then the router gets closed immediately along with the completion of all the operations. */ @Test public void testRequestResponseHandlerThreadExitFlow() throws Exception { nettyByteBufLeakHelper.setDisabled(true); Properties props = getNonBlockingRouterProperties("DC1"); VerifiableProperties verifiableProperties = new VerifiableProperties((props)); RouterConfig routerConfig = new RouterConfig(verifiableProperties); MockClusterMap mockClusterMap = new MockClusterMap(); MockTime mockTime = new MockTime(); router = new NonBlockingRouter(routerConfig, new NonBlockingRouterMetrics(mockClusterMap, routerConfig), new MockNetworkClientFactory(verifiableProperties, mockSelectorState, MAX_PORTS_PLAIN_TEXT, MAX_PORTS_SSL, CHECKOUT_TIMEOUT_MS, new MockServerLayout(mockClusterMap), mockTime), new LoggingNotificationSystem(), mockClusterMap, kms, cryptoService, cryptoJobHandler, accountService, mockTime, MockClusterMap.DEFAULT_PARTITION_CLASS); assertExpectedThreadCounts(2, 1); setOperationParams(); mockSelectorState.set(MockSelectorState.ThrowExceptionOnAllPoll); Future future = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()); try { while (!future.isDone()) { mockTime.sleep(1000); Thread.yield(); } future.get(); Assert.fail("The operation should have failed"); } catch (ExecutionException e) { Assert.assertEquals(RouterErrorCode.OperationTimedOut, ((RouterException) e.getCause()).getErrorCode()); } setOperationParams(); mockSelectorState.set(MockSelectorState.ThrowThrowableOnSend); future = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()); Thread requestResponseHandlerThreadRegular = TestUtils.getThreadByThisName("RequestResponseHandlerThread-0"); Thread requestResponseHandlerThreadBackground = TestUtils.getThreadByThisName("RequestResponseHandlerThread-backgroundDeleter"); if (requestResponseHandlerThreadRegular != null) { requestResponseHandlerThreadRegular.join(NonBlockingRouter.SHUTDOWN_WAIT_MS); } if (requestResponseHandlerThreadBackground != null) { requestResponseHandlerThreadBackground.join(NonBlockingRouter.SHUTDOWN_WAIT_MS); } try { future.get(); Assert.fail("The operation should have failed"); } catch (ExecutionException e) { Assert.assertEquals(RouterErrorCode.RouterClosed, ((RouterException) e.getCause()).getErrorCode()); } assertClosed(); // Ensure that both operations failed and with the right exceptions. Assert.assertEquals("No ChunkFiller Thread should be running after the router is closed", 0, TestUtils.numThreadsByThisName("ChunkFillerThread")); Assert.assertEquals("No RequestResponseHandler should be running after the router is closed", 0, TestUtils.numThreadsByThisName("RequestResponseHandlerThread")); Assert.assertEquals("All operations should have completed", 0, router.getOperationsCount()); } /** * Test that if a composite blob put fails, the successfully put data chunks are deleted. */ @Test public void testUnsuccessfulPutDataChunkDelete() throws Exception { // Ensure there are 4 chunks. maxPutChunkSize = PUT_CONTENT_SIZE / 4; Properties props = getNonBlockingRouterProperties("DC1"); VerifiableProperties verifiableProperties = new VerifiableProperties((props)); RouterConfig routerConfig = new RouterConfig(verifiableProperties); MockClusterMap mockClusterMap = new MockClusterMap(); MockTime mockTime = new MockTime(); MockServerLayout mockServerLayout = new MockServerLayout(mockClusterMap); // Since this test wants to ensure that successfully put data chunks are deleted when the overall put operation // fails, it uses a notification system to track the deletions. final CountDownLatch deletesDoneLatch = new CountDownLatch(2); final Map<String, String> blobsThatAreDeleted = new HashMap<>(); LoggingNotificationSystem deleteTrackingNotificationSystem = new LoggingNotificationSystem() { @Override public void onBlobDeleted(String blobId, String serviceId, Account account, Container container) { blobsThatAreDeleted.put(blobId, serviceId); deletesDoneLatch.countDown(); } }; router = new NonBlockingRouter(routerConfig, new NonBlockingRouterMetrics(mockClusterMap, routerConfig), new MockNetworkClientFactory(verifiableProperties, mockSelectorState, MAX_PORTS_PLAIN_TEXT, MAX_PORTS_SSL, CHECKOUT_TIMEOUT_MS, mockServerLayout, mockTime), deleteTrackingNotificationSystem, mockClusterMap, kms, cryptoService, cryptoJobHandler, accountService, mockTime, MockClusterMap.DEFAULT_PARTITION_CLASS); setOperationParams(); List<DataNodeId> dataNodeIds = mockClusterMap.getDataNodeIds(); List<ServerErrorCode> serverErrorList = new ArrayList<>(); // There are 4 chunks for this blob. // All put operations make one request to each local server as there are 3 servers overall in the local DC. // Set the state of the mock servers so that they return success for the first 2 requests in order to succeed // the first two chunks. serverErrorList.add(ServerErrorCode.No_Error); serverErrorList.add(ServerErrorCode.No_Error); // fail requests for third and fourth data chunks including the slipped put attempts: serverErrorList.add(ServerErrorCode.Unknown_Error); serverErrorList.add(ServerErrorCode.Unknown_Error); serverErrorList.add(ServerErrorCode.Unknown_Error); serverErrorList.add(ServerErrorCode.Unknown_Error); // all subsequent requests (no more puts, but there will be deletes) will succeed. for (DataNodeId dataNodeId : dataNodeIds) { MockServer server = mockServerLayout.getMockServer(dataNodeId.getHostname(), dataNodeId.getPort()); server.setServerErrors(serverErrorList); } // Submit the put operation and wait for it to fail. try { router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); } catch (ExecutionException e) { Assert.assertEquals(RouterErrorCode.AmbryUnavailable, ((RouterException) e.getCause()).getErrorCode()); } // Now, wait until the deletes of the successfully put blobs are complete. Assert.assertTrue("Deletes should not take longer than " + AWAIT_TIMEOUT_MS, deletesDoneLatch.await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); for (Map.Entry<String, String> blobIdAndServiceId : blobsThatAreDeleted.entrySet()) { Assert.assertEquals("Unexpected service ID for deleted blob", BackgroundDeleteRequest.SERVICE_ID_PREFIX + putBlobProperties.getServiceId(), blobIdAndServiceId.getValue()); } router.close(); assertClosed(); Assert.assertEquals("All operations should have completed", 0, router.getOperationsCount()); } /** * Test that if a composite blob is deleted, the data chunks are eventually deleted. Also check the service IDs used * for delete operations. */ @Test public void testCompositeBlobDataChunksDelete() throws Exception { // Ensure there are 4 chunks. maxPutChunkSize = PUT_CONTENT_SIZE / 4; Properties props = getNonBlockingRouterProperties("DC1"); VerifiableProperties verifiableProperties = new VerifiableProperties((props)); RouterConfig routerConfig = new RouterConfig(verifiableProperties); MockClusterMap mockClusterMap = new MockClusterMap(); MockTime mockTime = new MockTime(); MockServerLayout mockServerLayout = new MockServerLayout(mockClusterMap); // metadata blob + data chunks. final AtomicReference<CountDownLatch> deletesDoneLatch = new AtomicReference<>(); final Map<String, String> blobsThatAreDeleted = new HashMap<>(); LoggingNotificationSystem deleteTrackingNotificationSystem = new LoggingNotificationSystem() { @Override public void onBlobDeleted(String blobId, String serviceId, Account account, Container container) { blobsThatAreDeleted.put(blobId, serviceId); deletesDoneLatch.get().countDown(); } }; NonBlockingRouterMetrics localMetrics = new NonBlockingRouterMetrics(mockClusterMap, routerConfig); router = new NonBlockingRouter(routerConfig, localMetrics, new MockNetworkClientFactory(verifiableProperties, mockSelectorState, MAX_PORTS_PLAIN_TEXT, MAX_PORTS_SSL, CHECKOUT_TIMEOUT_MS, mockServerLayout, mockTime), deleteTrackingNotificationSystem, mockClusterMap, kms, cryptoService, cryptoJobHandler, accountService, mockTime, MockClusterMap.DEFAULT_PARTITION_CLASS); setOperationParams(); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); String deleteServiceId = "delete-service"; Set<String> blobsToBeDeleted = getBlobsInServers(mockServerLayout); int getRequestCount = mockServerLayout.getCount(RequestOrResponseType.GetRequest); // The second iteration is to test the case where the blob was already deleted. // The third iteration is to test the case where the blob has expired. for (int i = 0; i < 3; i++) { if (i == 2) { // Create a clean cluster and put another blob that immediate expires. setOperationParams(); putBlobProperties = new BlobProperties(-1, "serviceId", "memberId", "contentType", false, 0, Utils.getRandomShort(TestUtils.RANDOM), Utils.getRandomShort(TestUtils.RANDOM), false, null); blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); Set<String> allBlobsInServer = getBlobsInServers(mockServerLayout); allBlobsInServer.removeAll(blobsToBeDeleted); blobsToBeDeleted = allBlobsInServer; } blobsThatAreDeleted.clear(); deletesDoneLatch.set(new CountDownLatch(5)); router.deleteBlob(blobId, deleteServiceId, null).get(); Assert.assertTrue("Deletes should not take longer than " + AWAIT_TIMEOUT_MS, deletesDoneLatch.get().await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); Assert.assertTrue("All blobs in server are deleted", blobsThatAreDeleted.keySet().containsAll(blobsToBeDeleted)); Assert.assertTrue("Only blobs in server are deleted", blobsToBeDeleted.containsAll(blobsThatAreDeleted.keySet())); for (Map.Entry<String, String> blobIdAndServiceId : blobsThatAreDeleted.entrySet()) { String expectedServiceId = blobIdAndServiceId.getKey().equals(blobId) ? deleteServiceId : BackgroundDeleteRequest.SERVICE_ID_PREFIX + deleteServiceId; Assert.assertEquals("Unexpected service ID for deleted blob", expectedServiceId, blobIdAndServiceId.getValue()); } // For 1 chunk deletion attempt, 1 background operation for Get is initiated which results in 2 Get Requests at // the servers. getRequestCount += 2; Assert.assertEquals("Only one attempt of chunk deletion should have been done", getRequestCount, mockServerLayout.getCount(RequestOrResponseType.GetRequest)); } deletesDoneLatch.set(new CountDownLatch(5)); router.deleteBlob(blobId, null, null).get(); Assert.assertTrue("Deletes should not take longer than " + AWAIT_TIMEOUT_MS, deletesDoneLatch.get().await(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)); Assert.assertEquals("Get should NOT have been skipped", 0, localMetrics.skippedGetBlobCount.getCount()); router.close(); assertClosed(); Assert.assertEquals("All operations should have completed", 0, router.getOperationsCount()); } /** * Return the blob ids of all the blobs in the servers in the cluster. * @param mockServerLayout the {@link MockServerLayout} representing the cluster. * @return a Set of blob id strings of the blobs in the servers in the cluster. */ private Set<String> getBlobsInServers(MockServerLayout mockServerLayout) { Set<String> blobsInServers = new HashSet<>(); for (MockServer mockServer : mockServerLayout.getMockServers()) { blobsInServers.addAll(mockServer.getBlobs().keySet()); } return blobsInServers; } /** * Test to ensure that for simple blob deletions, no additional background delete operations * are initiated. */ @Test public void testSimpleBlobDelete() throws Exception { // Ensure there are 4 chunks. maxPutChunkSize = PUT_CONTENT_SIZE; String deleteServiceId = "delete-service"; // metadata blob + data chunks. final AtomicInteger deletesInitiated = new AtomicInteger(); final AtomicReference<String> receivedDeleteServiceId = new AtomicReference<>(); LoggingNotificationSystem deleteTrackingNotificationSystem = new LoggingNotificationSystem() { @Override public void onBlobDeleted(String blobId, String serviceId, Account account, Container container) { deletesInitiated.incrementAndGet(); receivedDeleteServiceId.set(serviceId); } }; Properties props = getNonBlockingRouterProperties("DC1"); setRouter(props, new MockServerLayout(mockClusterMap), deleteTrackingNotificationSystem); setOperationParams(); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); router.deleteBlob(blobId, deleteServiceId, null).get(); long waitStart = SystemTime.getInstance().milliseconds(); while (router.getBackgroundOperationsCount() != 0 && SystemTime.getInstance().milliseconds() < waitStart + AWAIT_TIMEOUT_MS) { Thread.sleep(1000); } Assert.assertEquals("All background operations should be complete ", 0, router.getBackgroundOperationsCount()); Assert.assertEquals("Only the original blob deletion should have been initiated", 1, deletesInitiated.get()); Assert.assertEquals("The delete service ID should match the expected value", deleteServiceId, receivedDeleteServiceId.get()); Assert.assertEquals("Get should have been skipped", 1, routerMetrics.skippedGetBlobCount.getCount()); router.close(); assertClosed(); Assert.assertEquals("All operations should have completed", 0, router.getOperationsCount()); } /** * Tests basic TTL update for simple (one chunk) blobs * @throws Exception */ @Test public void testSimpleBlobTtlUpdate() throws Exception { doTtlUpdateTest(1); } /** * Tests basic TTL update for composite (multiple chunk) blobs * @throws Exception */ @Test public void testCompositeBlobTtlUpdate() throws Exception { doTtlUpdateTest(4); } /** * Test that stitched blobs are usable by the other router methods. * @throws Exception */ @Test public void testStitchGetUpdateDelete() throws Exception { AtomicReference<CountDownLatch> deletesDoneLatch = new AtomicReference<>(); Set<String> deletedBlobs = ConcurrentHashMap.newKeySet(); LoggingNotificationSystem deleteTrackingNotificationSystem = new LoggingNotificationSystem() { @Override public void onBlobDeleted(String blobId, String serviceId, Account account, Container container) { deletedBlobs.add(blobId); deletesDoneLatch.get().countDown(); } }; setRouter(getNonBlockingRouterProperties("DC1"), new MockServerLayout(mockClusterMap), deleteTrackingNotificationSystem); for (int intermediateChunkSize : new int[]{maxPutChunkSize, maxPutChunkSize / 2}) { for (LongStream chunkSizeStream : new LongStream[]{ RouterTestHelpers.buildValidChunkSizeStream(3 * intermediateChunkSize, intermediateChunkSize), RouterTestHelpers.buildValidChunkSizeStream( 3 * intermediateChunkSize + random.nextInt(intermediateChunkSize - 1) + 1, intermediateChunkSize)}) { // Upload data chunks ByteArrayOutputStream stitchedContentStream = new ByteArrayOutputStream(); List<ChunkInfo> chunksToStitch = new ArrayList<>(); PrimitiveIterator.OfLong chunkSizeIter = chunkSizeStream.iterator(); while (chunkSizeIter.hasNext()) { long chunkSize = chunkSizeIter.nextLong(); setOperationParams((int) chunkSize, TTL_SECS); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().chunkUpload(true).maxUploadSize(PUT_CONTENT_SIZE).build()) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); long expirationTime = Utils.addSecondsToEpochTime(putBlobProperties.getCreationTimeInMs(), putBlobProperties.getTimeToLiveInSeconds()); chunksToStitch.add(new ChunkInfo(blobId, chunkSize, expirationTime)); stitchedContentStream.write(putContent); } byte[] expectedContent = stitchedContentStream.toByteArray(); // Stitch the chunks together setOperationParams(0, TTL_SECS / 2); String stitchedBlobId = router.stitchBlob(putBlobProperties, putUserMetadata, chunksToStitch) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); // Fetch the stitched blob GetBlobResult getBlobResult = router.getBlob(stitchedBlobId, new GetBlobOptionsBuilder().build()) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); assertTrue("Blob properties must be the same", RouterTestHelpers.arePersistedFieldsEquivalent(putBlobProperties, getBlobResult.getBlobInfo().getBlobProperties())); assertEquals("Unexpected blob size", expectedContent.length, getBlobResult.getBlobInfo().getBlobProperties().getBlobSize()); assertArrayEquals("User metadata must be the same", putUserMetadata, getBlobResult.getBlobInfo().getUserMetadata()); RouterTestHelpers.compareContent(expectedContent, null, getBlobResult.getBlobDataChannel()); // TtlUpdate the blob. router.updateBlobTtl(stitchedBlobId, "update-service", Utils.Infinite_Time) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); // Ensure that TTL was updated on the metadata blob and all data chunks Set<String> allBlobIds = chunksToStitch.stream().map(ChunkInfo::getBlobId).collect(Collectors.toSet()); allBlobIds.add(stitchedBlobId); assertTtl(router, allBlobIds, Utils.Infinite_Time); // Delete and ensure that all stitched chunks are deleted deletedBlobs.clear(); deletesDoneLatch.set(new CountDownLatch(chunksToStitch.size() + 1)); router.deleteBlob(stitchedBlobId, "delete-service").get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); TestUtils.awaitLatchOrTimeout(deletesDoneLatch.get(), AWAIT_TIMEOUT_MS); assertEquals("Metadata chunk and all data chunks should be deleted", allBlobIds, deletedBlobs); } } router.close(); assertExpectedThreadCounts(0, 0); } /** * Test for most error cases involving TTL updates * @throws Exception */ @Test public void testBlobTtlUpdateErrors() throws Exception { String updateServiceId = "update-service"; MockServerLayout layout = new MockServerLayout(mockClusterMap); setRouter(getNonBlockingRouterProperties("DC1"), layout, new LoggingNotificationSystem()); setOperationParams(); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); Map<ServerErrorCode, RouterErrorCode> testsAndExpected = new HashMap<>(); testsAndExpected.put(ServerErrorCode.Blob_Not_Found, RouterErrorCode.BlobDoesNotExist); testsAndExpected.put(ServerErrorCode.Blob_Deleted, RouterErrorCode.BlobDeleted); testsAndExpected.put(ServerErrorCode.Blob_Expired, RouterErrorCode.BlobExpired); testsAndExpected.put(ServerErrorCode.Disk_Unavailable, RouterErrorCode.BlobDoesNotExist); testsAndExpected.put(ServerErrorCode.Replica_Unavailable, RouterErrorCode.AmbryUnavailable); testsAndExpected.put(ServerErrorCode.Unknown_Error, RouterErrorCode.UnexpectedInternalError); // note that this test makes all nodes return same server error code. For Disk_Unavailable error, the router will // return BlobDoesNotExist because all disks are down (which should be extremely rare) and blob is gone. for (Map.Entry<ServerErrorCode, RouterErrorCode> testAndExpected : testsAndExpected.entrySet()) { layout.getMockServers().forEach(mockServer -> mockServer.setServerErrorForAllRequests(testAndExpected.getKey())); TestCallback<Void> testCallback = new TestCallback<>(); Future<Void> future = router.updateBlobTtl(blobId, updateServiceId, Utils.Infinite_Time, testCallback); assertFailureAndCheckErrorCode(future, testCallback, testAndExpected.getValue()); } layout.getMockServers().forEach(mockServer -> mockServer.setServerErrorForAllRequests(null)); // bad blob id TestCallback<Void> testCallback = new TestCallback<>(); Future<Void> future = router.updateBlobTtl("bad-blob-id", updateServiceId, Utils.Infinite_Time, testCallback); assertFailureAndCheckErrorCode(future, testCallback, RouterErrorCode.InvalidBlobId); router.close(); } /** * Test that a bad user defined callback will not crash the router or the manager. * @throws Exception */ @Test public void testBadCallbackForUpdateTtl() throws Exception { MockServerLayout serverLayout = new MockServerLayout(mockClusterMap); setRouter(getNonBlockingRouterProperties("DC1"), serverLayout, new LoggingNotificationSystem()); setOperationParams(); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); putChannel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(putContent)); String blobIdCheck = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); testWithErrorCodes(Collections.singletonMap(ServerErrorCode.No_Error, 9), serverLayout, null, expectedError -> { final CountDownLatch callbackCalled = new CountDownLatch(1); router.updateBlobTtl(blobId, null, Utils.Infinite_Time, (result, exception) -> { callbackCalled.countDown(); throw new RuntimeException("Throwing an exception in the user callback"); }).get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); assertTrue("Callback not called.", callbackCalled.await(10, TimeUnit.MILLISECONDS)); assertEquals("All operations should be finished.", 0, router.getOperationsCount()); assertTrue("Router should not be closed", router.isOpen()); assertTtl(router, Collections.singleton(blobId), Utils.Infinite_Time); //Test that TtlUpdateManager is still functional router.updateBlobTtl(blobIdCheck, null, Utils.Infinite_Time).get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); assertTtl(router, Collections.singleton(blobIdCheck), Utils.Infinite_Time); }); router.close(); } /** * Test that multiple scaling units can be instantiated, exercised and closed. */ @Test public void testMultipleScalingUnit() throws Exception { final int SCALING_UNITS = 3; Properties props = getNonBlockingRouterProperties("DC1"); props.setProperty("router.scaling.unit.count", Integer.toString(SCALING_UNITS)); setRouter(props, new MockServerLayout(mockClusterMap), new LoggingNotificationSystem()); assertExpectedThreadCounts(SCALING_UNITS + 1, SCALING_UNITS); // Submit a few jobs so that all the scaling units get exercised. for (int i = 0; i < SCALING_UNITS * 10; i++) { setOperationParams(); router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); } router.close(); assertExpectedThreadCounts(0, 0); //submission after closing should return a future that is already done. setOperationParams(); assertClosed(); } /** * Test that Response Handler correctly handles disconnected connections after warming up. */ @Test public void testWarmUpConnectionFailureHandling() throws Exception { Properties props = getNonBlockingRouterProperties("DC3"); MockServerLayout mockServerLayout = new MockServerLayout(mockClusterMap); mockSelectorState.set(MockSelectorState.FailConnectionInitiationOnPoll); setRouter(props, mockServerLayout, new LoggingNotificationSystem()); for (DataNodeId node : mockClusterMap.getDataNodes()) { assertTrue("Node should be marked as timed out by ResponseHandler.", ((MockDataNodeId) node).isTimedOut()); } router.close(); mockSelectorState.set(MockSelectorState.Good); } /** * Test the case where request is timed out in the pending queue and network client returns response with null requestInfo * to mark node down via response handler. * @throws Exception */ @Test public void testResponseWithNullRequestInfo() throws Exception { Properties props = getNonBlockingRouterProperties("DC1"); VerifiableProperties verifiableProperties = new VerifiableProperties((props)); RouterConfig routerConfig = new RouterConfig(verifiableProperties); routerMetrics = new NonBlockingRouterMetrics(mockClusterMap, routerConfig); NetworkClient mockNetworkClient = Mockito.mock(NetworkClient.class); Mockito.when(mockNetworkClient.warmUpConnections(anyList(), anyInt(), anyLong(), anyList())).thenReturn(1); doNothing().when(mockNetworkClient).close(); List<ResponseInfo> responseInfoList = new ArrayList<>(); MockDataNodeId testDataNode = (MockDataNodeId) mockClusterMap.getDataNodeIds().get(0); responseInfoList.add(new ResponseInfo(null, NetworkClientErrorCode.NetworkError, null, testDataNode)); // By default, there are 1 operation controller and 1 background deleter thread. We set CountDownLatch to 3 so that // at least one thread has completed calling onResponse() and test node's state has been updated in ResponseHandler CountDownLatch invocationLatch = new CountDownLatch(3); doAnswer(invocation -> { invocationLatch.countDown(); return responseInfoList; }).when(mockNetworkClient).sendAndPoll(anyList(), anySet(), anyInt()); NetworkClientFactory networkClientFactory = Mockito.mock(NetworkClientFactory.class); Mockito.when(networkClientFactory.getNetworkClient()).thenReturn(mockNetworkClient); NonBlockingRouter testRouter = new NonBlockingRouter(routerConfig, routerMetrics, networkClientFactory, new LoggingNotificationSystem(), mockClusterMap, kms, cryptoService, cryptoJobHandler, accountService, mockTime, MockClusterMap.DEFAULT_PARTITION_CLASS); assertTrue("Invocation latch didn't count to 0 within 10 seconds", invocationLatch.await(10, TimeUnit.SECONDS)); // verify the test node is considered timeout assertTrue("The node should be considered timeout", testDataNode.isTimedOut()); testRouter.close(); } /** * Response handling related tests for all operation managers. */ @Test public void testResponseHandling() throws Exception { Properties props = getNonBlockingRouterProperties("DC1"); VerifiableProperties verifiableProperties = new VerifiableProperties((props)); setOperationParams(); final List<ReplicaId> failedReplicaIds = new ArrayList<>(); final AtomicInteger successfulResponseCount = new AtomicInteger(0); final AtomicBoolean invalidResponse = new AtomicBoolean(false); ResponseHandler mockResponseHandler = new ResponseHandler(mockClusterMap) { @Override public void onEvent(ReplicaId replicaId, Object e) { if (e instanceof ServerErrorCode) { if (e == ServerErrorCode.No_Error) { successfulResponseCount.incrementAndGet(); } else { invalidResponse.set(true); } } else { failedReplicaIds.add(replicaId); } } }; // Instantiate a router just to put a blob successfully. MockServerLayout mockServerLayout = new MockServerLayout(mockClusterMap); setRouter(props, mockServerLayout, new LoggingNotificationSystem()); setOperationParams(); // More extensive test for puts present elsewhere - these statements are here just to exercise the flow within the // NonBlockingRouter class, and to ensure that operations submitted to a router eventually completes. String blobIdStr = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()).get(); BlobId blobId = RouterUtils.getBlobIdFromString(blobIdStr, mockClusterMap); router.close(); for (MockServer mockServer : mockServerLayout.getMockServers()) { mockServer.setServerErrorForAllRequests(ServerErrorCode.No_Error); } SocketNetworkClient networkClient = new MockNetworkClientFactory(verifiableProperties, mockSelectorState, MAX_PORTS_PLAIN_TEXT, MAX_PORTS_SSL, CHECKOUT_TIMEOUT_MS, mockServerLayout, mockTime).getNetworkClient(); cryptoJobHandler = new CryptoJobHandler(CryptoJobHandlerTest.DEFAULT_THREAD_COUNT); KeyManagementService localKMS = new MockKeyManagementService(new KMSConfig(verifiableProperties), singleKeyForKMS); putManager = new PutManager(mockClusterMap, mockResponseHandler, new LoggingNotificationSystem(), new RouterConfig(verifiableProperties), new NonBlockingRouterMetrics(mockClusterMap, null), new RouterCallback(networkClient, new ArrayList<>()), "0", localKMS, cryptoService, cryptoJobHandler, accountService, mockTime, MockClusterMap.DEFAULT_PARTITION_CLASS); OperationHelper opHelper = new OperationHelper(OperationType.PUT); testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, null, successfulResponseCount, invalidResponse, -1); // Test that if a failed response comes before the operation is completed, failure detector is notified. testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, null, successfulResponseCount, invalidResponse, 0); // Test that if a failed response comes after the operation is completed, failure detector is notified. testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, null, successfulResponseCount, invalidResponse, PUT_REQUEST_PARALLELISM - 1); testNoResponseNoNotification(opHelper, failedReplicaIds, null, successfulResponseCount, invalidResponse); testResponseDeserializationError(opHelper, networkClient, null); opHelper = new OperationHelper(OperationType.GET); getManager = new GetManager(mockClusterMap, mockResponseHandler, new RouterConfig(verifiableProperties), new NonBlockingRouterMetrics(mockClusterMap, null), new RouterCallback(networkClient, new ArrayList<BackgroundDeleteRequest>()), localKMS, cryptoService, cryptoJobHandler, mockTime); testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, blobId, successfulResponseCount, invalidResponse, -1); // Test that if a failed response comes before the operation is completed, failure detector is notified. testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, blobId, successfulResponseCount, invalidResponse, 0); // Test that if a failed response comes after the operation is completed, failure detector is notified. testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, blobId, successfulResponseCount, invalidResponse, GET_REQUEST_PARALLELISM - 1); testNoResponseNoNotification(opHelper, failedReplicaIds, blobId, successfulResponseCount, invalidResponse); testResponseDeserializationError(opHelper, networkClient, blobId); opHelper = new OperationHelper(OperationType.DELETE); deleteManager = new DeleteManager(mockClusterMap, mockResponseHandler, accountService, new LoggingNotificationSystem(), new RouterConfig(verifiableProperties), new NonBlockingRouterMetrics(mockClusterMap, null), new RouterCallback(null, new ArrayList<BackgroundDeleteRequest>()), mockTime); testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, blobId, successfulResponseCount, invalidResponse, -1); // Test that if a failed response comes before the operation is completed, failure detector is notified. testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, blobId, successfulResponseCount, invalidResponse, 0); // Test that if a failed response comes after the operation is completed, failure detector is notified. testFailureDetectorNotification(opHelper, networkClient, failedReplicaIds, blobId, successfulResponseCount, invalidResponse, DELETE_REQUEST_PARALLELISM - 1); testNoResponseNoNotification(opHelper, failedReplicaIds, blobId, successfulResponseCount, invalidResponse); testResponseDeserializationError(opHelper, networkClient, blobId); putManager.close(); getManager.close(); deleteManager.close(); } /** * Ensure that Put request for given blob id reaches to all the mock servers in the {@link MockServerLayout}. * @param blobId The blob id of which Put request will be created. * @param serverLayout The mock server layout. * @throws IOException */ private void ensurePutInAllServers(String blobId, MockServerLayout serverLayout) throws IOException { // Make sure all the mock servers have this put BlobId id = new BlobId(blobId, mockClusterMap); for (MockServer server : serverLayout.getMockServers()) { if (!server.getBlobs().containsKey(blobId)) { server.send( new PutRequest(NonBlockingRouter.correlationIdGenerator.incrementAndGet(), routerConfig.routerHostname, id, putBlobProperties, ByteBuffer.wrap(putUserMetadata), Unpooled.wrappedBuffer(putContent), putContent.length, BlobType.DataBlob, null)).release(); } } } /** * Ensure that Stitch requests for given blob id reaches to all the mock servees in the {@link MockServerLayout}. * @param blobId The blob id of which stitch request will be created. * @param serverLayout The mock server layout. * @param chunksToStitch The list of {@link ChunkInfo} to stitch. * @param singleBlobSize The size of each chunk * @throws IOException */ private void ensureStitchInAllServers(String blobId, MockServerLayout serverLayout, List<ChunkInfo> chunksToStitch, int singleBlobSize) throws IOException { TreeMap<Integer, Pair<StoreKey, Long>> indexToChunkIdsAndChunkSizes = new TreeMap<>(); int i = 0; for (ChunkInfo chunkInfo : chunksToStitch) { indexToChunkIdsAndChunkSizes.put(i, new Pair<>(new BlobId(chunkInfo.getBlobId(), mockClusterMap), chunkInfo.getChunkSizeInBytes())); i++; } ByteBuffer serializedContent; int totalSize = singleBlobSize * chunksToStitch.size(); if (routerConfig.routerMetadataContentVersion == MessageFormatRecord.Metadata_Content_Version_V2) { serializedContent = MetadataContentSerDe.serializeMetadataContentV2(singleBlobSize, totalSize, indexToChunkIdsAndChunkSizes.values().stream().map(Pair::getFirst).collect(Collectors.toList())); } else { List<Pair<StoreKey, Long>> orderedChunkIdList = new ArrayList<>(indexToChunkIdsAndChunkSizes.values()); serializedContent = MetadataContentSerDe.serializeMetadataContentV3(totalSize, orderedChunkIdList); } BlobId id = new BlobId(blobId, mockClusterMap); for (MockServer server : serverLayout.getMockServers()) { if (!server.getBlobs().containsKey(blobId)) { server.send( new PutRequest(NonBlockingRouter.correlationIdGenerator.incrementAndGet(), routerConfig.routerHostname, id, putBlobProperties, ByteBuffer.wrap(putUserMetadata), Unpooled.wrappedBuffer(serializedContent), serializedContent.remaining(), BlobType.MetadataBlob, null)).release(); } } } /** * Ensure that Delete request for given blob is reaches to all the mock servers in the {@link MockServerLayout}. * @param blobId The blob id of which Delete request will be created. * @param serverLayout The mock server layout. * @throws IOException */ private void ensureDeleteInAllServers(String blobId, MockServerLayout serverLayout) throws IOException { BlobId id = new BlobId(blobId, mockClusterMap); for (MockServer server : serverLayout.getMockServers()) { if (!server.getBlobs().get(blobId).isDeleted()) { server.send( new DeleteRequest(NonBlockingRouter.correlationIdGenerator.incrementAndGet(), routerConfig.routerHostname, id, mockTime.milliseconds())).release(); } } } /** * Ensure that Undelete request for given blob is reaches to all the mock servers in the {@link MockServerLayout}. * @param blobId The blob id of which Undelete request will be created. * @param serverLayout The mock server layout. * @throws IOException */ private void ensureUndeleteInAllServers(String blobId, MockServerLayout serverLayout) throws IOException { BlobId id = new BlobId(blobId, mockClusterMap); for (MockServer server : serverLayout.getMockServers()) { if (!server.getBlobs().get(blobId).isUndeleted()) { server.send( new UndeleteRequest(NonBlockingRouter.correlationIdGenerator.incrementAndGet(), routerConfig.routerHostname, id, mockTime.milliseconds())).release(); } } } /** * Test that failure detector is correctly notified for all responses regardless of the order in which successful * and failed responses arrive. * @param opHelper the {@link OperationHelper} * @param networkClient the {@link SocketNetworkClient} * @param failedReplicaIds the list that will contain all the replicas for which failure was notified. * @param blobId the id of the blob to get/delete. For puts, this will be null. * @param successfulResponseCount the AtomicInteger that will contain the count of replicas for which success was * notified. * @param invalidResponse the AtomicBoolean that will contain whether an unexpected failure was notified. * @param indexToFail if greater than 0, the index representing which response for which failure is to be simulated. * For example, if index is 0, then the first response will be failed. * If the index is -1, no responses will be failed, and successful responses will be returned to * the operation managers. */ private void testFailureDetectorNotification(OperationHelper opHelper, SocketNetworkClient networkClient, List<ReplicaId> failedReplicaIds, BlobId blobId, AtomicInteger successfulResponseCount, AtomicBoolean invalidResponse, int indexToFail) throws Exception { failedReplicaIds.clear(); successfulResponseCount.set(0); invalidResponse.set(false); mockSelectorState.set(MockSelectorState.Good); FutureResult futureResult = opHelper.submitOperation(blobId); int requestParallelism = opHelper.requestParallelism; List<RequestInfo> allRequests = new ArrayList<>(); Set<Integer> allDropped = new HashSet<>(); long loopStartTimeMs = SystemTime.getInstance().milliseconds(); while (allRequests.size() < requestParallelism) { if (loopStartTimeMs + AWAIT_TIMEOUT_MS < SystemTime.getInstance().milliseconds()) { Assert.fail("Waited too long for requests."); } opHelper.pollOpManager(allRequests, allDropped); } ReplicaId replicaIdToFail = indexToFail == -1 ? null : allRequests.get(indexToFail).getReplicaId(); for (RequestInfo requestInfo : allRequests) { ResponseInfo responseInfo; if (replicaIdToFail != null && replicaIdToFail.equals(requestInfo.getReplicaId())) { responseInfo = new ResponseInfo(requestInfo, NetworkClientErrorCode.NetworkError, null); requestInfo.getRequest().release(); } else { List<RequestInfo> requestInfoListToSend = new ArrayList<>(); requestInfoListToSend.add(requestInfo); List<ResponseInfo> responseInfoList; loopStartTimeMs = SystemTime.getInstance().milliseconds(); do { if (loopStartTimeMs + AWAIT_TIMEOUT_MS < SystemTime.getInstance().milliseconds()) { Assert.fail("Waited too long for the response."); } responseInfoList = networkClient.sendAndPoll(requestInfoListToSend, Collections.emptySet(), 10); requestInfoListToSend.clear(); } while (responseInfoList.size() == 0); responseInfo = responseInfoList.get(0); } opHelper.handleResponse(responseInfo); responseInfo.release(); } // Poll once again so that the operation gets a chance to complete. allRequests.clear(); if (testEncryption) { opHelper.awaitOpCompletionOrTimeOut(futureResult); } else { opHelper.pollOpManager(allRequests, allDropped); } futureResult.get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); Assert.assertEquals(0, allDropped.size()); if (indexToFail == -1) { Assert.assertEquals("Successful notification should have arrived for replicas that were up", opHelper.requestParallelism, successfulResponseCount.get()); Assert.assertEquals("Failure detector should not have been notified", 0, failedReplicaIds.size()); Assert.assertFalse("There should be no notifications of any other kind", invalidResponse.get()); } else { Assert.assertEquals("Failure detector should have been notified", 1, failedReplicaIds.size()); Assert.assertEquals("Failed notification should have arrived for the failed replica", replicaIdToFail, failedReplicaIds.get(0)); Assert.assertEquals("Successful notification should have arrived for replicas that were up", opHelper.requestParallelism - 1, successfulResponseCount.get()); Assert.assertFalse("There should be no notifications of any other kind", invalidResponse.get()); } } /** * Test that failure detector is not notified when the router times out requests. * @param opHelper the {@link OperationHelper} * @param failedReplicaIds the list that will contain all the replicas for which failure was notified. * @param blobId the id of the blob to get/delete. For puts, this will be null. * @param successfulResponseCount the AtomicInteger that will contain the count of replicas for which success was * notified. * @param invalidResponse the AtomicBoolean that will contain whether an unexpected failure was notified. */ private void testNoResponseNoNotification(OperationHelper opHelper, List<ReplicaId> failedReplicaIds, BlobId blobId, AtomicInteger successfulResponseCount, AtomicBoolean invalidResponse) throws Exception { failedReplicaIds.clear(); successfulResponseCount.set(0); invalidResponse.set(false); FutureResult futureResult = opHelper.submitOperation(blobId); List<RequestInfo> allRequests = new ArrayList<>(); Set<Integer> allDropped = new HashSet<>(); long loopStartTimeMs = SystemTime.getInstance().milliseconds(); while (!futureResult.isDone()) { if (loopStartTimeMs + AWAIT_TIMEOUT_MS < SystemTime.getInstance().milliseconds()) { Assert.fail("Waited too long for requests."); } opHelper.pollOpManager(allRequests, allDropped); mockTime.sleep(REQUEST_TIMEOUT_MS + 1); } System.out.println(allDropped); Assert.assertEquals("Successful notification should not have arrived for replicas that were up", 0, successfulResponseCount.get()); Assert.assertEquals("Failure detector should not have been notified", 0, failedReplicaIds.size()); Assert.assertFalse("There should be no notifications of any other kind", invalidResponse.get()); Set<Integer> allCorrelationIds = allRequests.stream() .map(requestInfo -> requestInfo.getRequest().getCorrelationId()) .collect(Collectors.toSet()); Assert.assertEquals("Timed out requests should be dropped", allCorrelationIds, new HashSet<>(allDropped)); allRequests.forEach(r -> r.getRequest().release()); } /** * Test that operations succeed even in the presence of responses that are corrupt and fail to deserialize. * @param opHelper the {@link OperationHelper} * @param networkClient the {@link SocketNetworkClient} * @param blobId the id of the blob to get/delete. For puts, this will be null. * @throws Exception */ private void testResponseDeserializationError(OperationHelper opHelper, SocketNetworkClient networkClient, BlobId blobId) throws Exception { mockSelectorState.set(MockSelectorState.Good); FutureResult futureResult = opHelper.submitOperation(blobId); int requestParallelism = opHelper.requestParallelism; List<RequestInfo> allRequests = new ArrayList<>(); Set<Integer> allDropped = new HashSet<>(); long loopStartTimeMs = SystemTime.getInstance().milliseconds(); while (allRequests.size() < requestParallelism) { if (loopStartTimeMs + AWAIT_TIMEOUT_MS < SystemTime.getInstance().milliseconds()) { Assert.fail("Waited too long for requests."); } opHelper.pollOpManager(allRequests, allDropped); } List<ResponseInfo> responseInfoList = new ArrayList<>(); loopStartTimeMs = SystemTime.getInstance().milliseconds(); do { if (loopStartTimeMs + AWAIT_TIMEOUT_MS < SystemTime.getInstance().milliseconds()) { Assert.fail("Waited too long for the response."); } responseInfoList.addAll(networkClient.sendAndPoll(allRequests, allDropped, 10)); allRequests.clear(); } while (responseInfoList.size() < requestParallelism); // corrupt the first response. ByteBuf response = responseInfoList.get(0).content(); byte b = response.getByte(response.writerIndex() - 1); response.setByte(response.writerIndex() - 1, (byte) ~b); for (ResponseInfo responseInfo : responseInfoList) { opHelper.handleResponse(responseInfo); } responseInfoList.forEach(ResponseInfo::release); allRequests.clear(); if (testEncryption) { opHelper.awaitOpCompletionOrTimeOut(futureResult); } else { opHelper.pollOpManager(allRequests, allDropped); } try { futureResult.get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (ExecutionException e) { Assert.fail("Operation should have succeeded with one corrupt response"); } } /** * Assert that the number of ChunkFiller and RequestResponseHandler threads running are as expected. * @param expectedRequestResponseHandlerCount the expected number of ChunkFiller and RequestResponseHandler threads. * @param expectedChunkFillerCount the expected number of ChunkFiller threads. */ protected void assertExpectedThreadCounts(int expectedRequestResponseHandlerCount, int expectedChunkFillerCount) { Assert.assertEquals("Number of RequestResponseHandler threads running should be as expected", expectedRequestResponseHandlerCount, TestUtils.numThreadsByThisName("RequestResponseHandlerThread")); Assert.assertEquals("Number of chunkFiller threads running should be as expected", expectedChunkFillerCount, TestUtils.numThreadsByThisName("ChunkFillerThread")); if (expectedRequestResponseHandlerCount == 0) { Assert.assertFalse("Router should be closed if there are no worker threads running", router.isOpen()); Assert.assertEquals("All operations should have completed if the router is closed", 0, router.getOperationsCount()); } } /** * Assert that submission after closing the router returns a future that is already done and an appropriate * exception. */ protected void assertClosed() { Future<String> future = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()); Assert.assertTrue(future.isDone()); RouterException e = (RouterException) ((FutureResult<String>) future).error(); Assert.assertEquals(e.getErrorCode(), RouterErrorCode.RouterClosed); } /** * Does the TTL update test by putting a blob, checking its TTL, updating TTL and then rechecking the TTL again. * @param numChunks the number of chunks required when the blob is put. Has to divide {@link #PUT_CONTENT_SIZE} * perfectly for test to work. * @throws Exception */ protected void doTtlUpdateTest(int numChunks) throws Exception { Assert.assertEquals("This test works only if the number of chunks is a perfect divisor of PUT_CONTENT_SIZE", 0, PUT_CONTENT_SIZE % numChunks); maxPutChunkSize = PUT_CONTENT_SIZE / numChunks; String updateServiceId = "update-service"; TtlUpdateNotificationSystem notificationSystem = new TtlUpdateNotificationSystem(); setRouter(getNonBlockingRouterProperties("DC1"), new MockServerLayout(mockClusterMap), notificationSystem); setOperationParams(); Assert.assertFalse("The original ttl should not be infinite for this test to work", putBlobProperties.getTimeToLiveInSeconds() == Utils.Infinite_Time); String blobId = router.putBlob(putBlobProperties, putUserMetadata, putChannel, new PutBlobOptionsBuilder().build()) .get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); assertTtl(router, Collections.singleton(blobId), TTL_SECS); router.updateBlobTtl(blobId, updateServiceId, Utils.Infinite_Time).get(AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS); // if more than one chunk is created, also account for metadata blob notificationSystem.checkNotifications(numChunks == 1 ? 1 : numChunks + 1, updateServiceId, Utils.Infinite_Time); assertTtl(router, Collections.singleton(blobId), Utils.Infinite_Time); Assert.assertEquals("All operations should have completed", 0, router.getOperationsCount()); if (numChunks == 1) { Assert.assertEquals("Get should have been skipped", 1, routerMetrics.skippedGetBlobCount.getCount()); } else { Assert.assertEquals("Get should NOT have been skipped", 0, routerMetrics.skippedGetBlobCount.getCount()); } router.close(); // check that ttl update won't work after router close Future<Void> future = router.updateBlobTtl(blobId, updateServiceId, Utils.Infinite_Time); Assert.assertTrue(future.isDone()); RouterException e = (RouterException) ((FutureResult<Void>) future).error(); Assert.assertEquals(e.getErrorCode(), RouterErrorCode.RouterClosed); } /** * Enum for the three operation types. */ private enum OperationType { PUT, GET, DELETE, } /** * A helper class to abstract away the details about specific operation manager. */ private class OperationHelper { final OperationType opType; int requestParallelism = 0; /** * Construct an OperationHelper object with the associated type. * @param opType the type of operation. */ OperationHelper(OperationType opType) { this.opType = opType; switch (opType) { case PUT: requestParallelism = PUT_REQUEST_PARALLELISM; break; case GET: requestParallelism = GET_REQUEST_PARALLELISM; break; case DELETE: requestParallelism = DELETE_REQUEST_PARALLELISM; break; } } /** * Submit a put, get or delete operation based on the associated {@link OperationType} of this object. * @param blobId the blobId to get or delete. For puts, this is ignored. * @return the {@link FutureResult} associated with the submitted operation. * @throws RouterException if the blobIdStr is invalid. */ FutureResult submitOperation(BlobId blobId) throws RouterException { FutureResult futureResult = null; switch (opType) { case PUT: futureResult = new FutureResult<String>(); ReadableStreamChannel putChannel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(putContent)); putManager.submitPutBlobOperation(putBlobProperties, putUserMetadata, putChannel, PutBlobOptions.DEFAULT, futureResult, null); break; case GET: final FutureResult<GetBlobResultInternal> getFutureResult = new FutureResult<>(); getManager.submitGetBlobOperation(blobId.getID(), new GetBlobOptionsInternal( new GetBlobOptionsBuilder().operationType(GetBlobOptions.OperationType.BlobInfo).build(), false, routerMetrics.ageAtGet), getFutureResult::done); futureResult = getFutureResult; break; case DELETE: futureResult = new FutureResult<Void>(); deleteManager.submitDeleteBlobOperation(blobId.getID(), null, futureResult, null); break; } NonBlockingRouter.currentOperationsCount.incrementAndGet(); return futureResult; } /** * Poll the associated operation manager. * @param requestsToSend the list of {@link RequestInfo} to send to pass into the poll call. * @param requestsToDrop the list of correlation IDs to drop to pass into the poll call. */ void pollOpManager(List<RequestInfo> requestsToSend, Set<Integer> requestsToDrop) { switch (opType) { case PUT: putManager.poll(requestsToSend, requestsToDrop); break; case GET: getManager.poll(requestsToSend, requestsToDrop); break; case DELETE: deleteManager.poll(requestsToSend, requestsToDrop); break; } } /** * Polls all managers at regular intervals until the operation is complete or timeout is reached * @param futureResult {@link FutureResult} that needs to be tested for completion * @throws InterruptedException */ private void awaitOpCompletionOrTimeOut(FutureResult futureResult) throws InterruptedException { int timer = 0; List<RequestInfo> allRequests = new ArrayList<>(); Set<Integer> allDropped = new HashSet<>(); while (timer < AWAIT_TIMEOUT_MS / 2 && !futureResult.completed()) { pollOpManager(allRequests, allDropped); Thread.sleep(50); timer += 50; allRequests.clear(); allDropped.clear(); } } /** * Hand over a responseInfo to the operation manager. * @param responseInfo the {@link ResponseInfo} to hand over. */ void handleResponse(ResponseInfo responseInfo) { switch (opType) { case PUT: putManager.handleResponse(responseInfo); break; case GET: getManager.handleResponse(responseInfo); break; case DELETE: deleteManager.handleResponse(responseInfo); break; } } } }