From f7d2c884e4b5de61b0edc642fed20fde40a3cc31 Mon Sep 17 00:00:00 2001 From: Arun Sai Bhima Date: Thu, 30 Jan 2025 11:54:25 -0800 Subject: [PATCH] Add parameter to GET /accounts API to ignore containers information (#2998) --- .../ambry/account/AccountCollectionSerde.java | 9 ++++--- .../java/com/github/ambry/rest/RestUtils.java | 5 ++++ .../frontend/FrontendIntegrationTestBase.java | 2 +- .../ambry/frontend/GetAccountsHandler.java | 12 +++++---- .../FrontendRestRequestServiceTest.java | 3 ++- .../frontend/GetAccountsHandlerTest.java | 27 +++++++++++++++++++ .../frontend/PostAccountsHandlerTest.java | 6 ++--- 7 files changed, 51 insertions(+), 13 deletions(-) diff --git a/ambry-api/src/main/java/com/github/ambry/account/AccountCollectionSerde.java b/ambry-api/src/main/java/com/github/ambry/account/AccountCollectionSerde.java index 3f6c466959..8a3c6feafb 100644 --- a/ambry-api/src/main/java/com/github/ambry/account/AccountCollectionSerde.java +++ b/ambry-api/src/main/java/com/github/ambry/account/AccountCollectionSerde.java @@ -44,13 +44,16 @@ abstract class AccountMixIn { /** * Serialize a collection of accounts to json bytes that can be used in requests/responses. - * @param accounts the {@link Account}s to serialize. + * @param accounts the {@link Account}s to serialize. + * @param ignoreContainers {@code true} to ignore containers in the serialization, {@code false} otherwise. * @return the serialized bytes in json format. */ - public static byte[] serializeAccountsInJson(Collection accounts) throws IOException { + public static byte[] serializeAccountsInJson(Collection accounts, boolean ignoreContainers) + throws IOException { Map> resultObj = new HashMap<>(); resultObj.put(ACCOUNTS_KEY, accounts); - return objectMapper.writeValueAsBytes(resultObj); + return ignoreContainers ? objectMapperWithoutContainer.writeValueAsBytes(resultObj) + : objectMapper.writeValueAsBytes(resultObj); } /** diff --git a/ambry-api/src/main/java/com/github/ambry/rest/RestUtils.java b/ambry-api/src/main/java/com/github/ambry/rest/RestUtils.java index 8868aa5822..8b4a1ca9d7 100644 --- a/ambry-api/src/main/java/com/github/ambry/rest/RestUtils.java +++ b/ambry-api/src/main/java/com/github/ambry/rest/RestUtils.java @@ -365,6 +365,11 @@ public static final class Headers { * Request header to carry hostname (with port); */ public final static String HOSTNAME = "x-ambry-hostname"; + + /** + * Boolean field set to "true" for ignoring containers when fetching accounts information via GET /accounts API. + */ + public static final String IGNORE_CONTAINERS = "x-ambry-ignore-containers"; } public static final class TrackingHeaders { diff --git a/ambry-frontend/src/integration-test/java/com/github/ambry/frontend/FrontendIntegrationTestBase.java b/ambry-frontend/src/integration-test/java/com/github/ambry/frontend/FrontendIntegrationTestBase.java index 87d8631e0e..a2340f308c 100644 --- a/ambry-frontend/src/integration-test/java/com/github/ambry/frontend/FrontendIntegrationTestBase.java +++ b/ambry-frontend/src/integration-test/java/com/github/ambry/frontend/FrontendIntegrationTestBase.java @@ -1825,7 +1825,7 @@ private void verifyCommonRequestCostHeaders(HttpResponse response, double expect * @param accounts the accounts to replace or add using the {@code POST /accounts} call. */ void updateAccountsAndVerify(AccountService accountService, Account... accounts) throws Exception { - byte[] accountUpdateJson = AccountCollectionSerde.serializeAccountsInJson(Arrays.asList(accounts)); + byte[] accountUpdateJson = AccountCollectionSerde.serializeAccountsInJson(Arrays.asList(accounts), false); FullHttpRequest request = buildRequest(HttpMethod.POST, Operations.ACCOUNTS, null, ByteBuffer.wrap(accountUpdateJson)); NettyClient.ResponseParts responseParts = nettyClient.sendRequest(request, null, null).get(); diff --git a/ambry-frontend/src/main/java/com/github/ambry/frontend/GetAccountsHandler.java b/ambry-frontend/src/main/java/com/github/ambry/frontend/GetAccountsHandler.java index b6d53cf6a6..4d83aa142a 100644 --- a/ambry-frontend/src/main/java/com/github/ambry/frontend/GetAccountsHandler.java +++ b/ambry-frontend/src/main/java/com/github/ambry/frontend/GetAccountsHandler.java @@ -19,7 +19,6 @@ import com.github.ambry.account.AccountService; import com.github.ambry.account.AccountServiceException; import com.github.ambry.account.Container; -import com.github.ambry.account.Dataset; import com.github.ambry.commons.ByteBufferReadableStreamChannel; import com.github.ambry.commons.Callback; import com.github.ambry.rest.RestRequest; @@ -121,16 +120,19 @@ private Callback securityProcessRequestCallback() { */ private Callback securityPostProcessRequestCallback() { return buildCallback(frontendMetrics.getAccountsSecurityPostProcessRequestMetrics, securityCheckResult -> { - byte[] serialized; + byte[] serializedAccountsOrContainers; if (RestUtils.getRequestPath(restRequest).matchesOperation(ACCOUNTS_CONTAINERS)) { LOGGER.debug("Received request for getting single container with arguments: {}", restRequest.getArgs()); Container container = getContainer(); - serialized = AccountCollectionSerde.serializeContainersInJson(Collections.singletonList(container)); + serializedAccountsOrContainers = AccountCollectionSerde.serializeContainersInJson(Collections.singletonList(container)); restResponseChannel.setHeader(RestUtils.Headers.TARGET_ACCOUNT_ID, container.getParentAccountId()); } else { - serialized = AccountCollectionSerde.serializeAccountsInJson(getAccounts()); + boolean ignoreContainers = + RestUtils.getBooleanHeader(restRequest.getArgs(), RestUtils.Headers.IGNORE_CONTAINERS, false); + serializedAccountsOrContainers = AccountCollectionSerde.serializeAccountsInJson(getAccounts(), ignoreContainers); } - ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap(serialized)); + ReadableStreamChannel channel = new ByteBufferReadableStreamChannel(ByteBuffer.wrap( + serializedAccountsOrContainers)); restResponseChannel.setHeader(RestUtils.Headers.DATE, new GregorianCalendar().getTime()); restResponseChannel.setHeader(RestUtils.Headers.CONTENT_TYPE, RestUtils.JSON_CONTENT_TYPE); restResponseChannel.setHeader(RestUtils.Headers.CONTENT_LENGTH, channel.getSize()); diff --git a/ambry-frontend/src/test/java/com/github/ambry/frontend/FrontendRestRequestServiceTest.java b/ambry-frontend/src/test/java/com/github/ambry/frontend/FrontendRestRequestServiceTest.java index 02c10a869b..31925b302c 100644 --- a/ambry-frontend/src/test/java/com/github/ambry/frontend/FrontendRestRequestServiceTest.java +++ b/ambry-frontend/src/test/java/com/github/ambry/frontend/FrontendRestRequestServiceTest.java @@ -2349,7 +2349,8 @@ public void addGetAndDeleteDatasetTest() throws Exception { public void postAccountsTest() throws Exception { Account accountToAdd = accountService.generateRandomAccount(); List body = new LinkedList<>(); - body.add(ByteBuffer.wrap(AccountCollectionSerde.serializeAccountsInJson(Collections.singleton(accountToAdd)))); + body.add( + ByteBuffer.wrap(AccountCollectionSerde.serializeAccountsInJson(Collections.singleton(accountToAdd), false))); body.add(null); RestRequest restRequest = createRestRequest(RestMethod.POST, Operations.ACCOUNTS, null, body); MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(); diff --git a/ambry-frontend/src/test/java/com/github/ambry/frontend/GetAccountsHandlerTest.java b/ambry-frontend/src/test/java/com/github/ambry/frontend/GetAccountsHandlerTest.java index 8dcaf99120..53e4ebb973 100644 --- a/ambry-frontend/src/test/java/com/github/ambry/frontend/GetAccountsHandlerTest.java +++ b/ambry-frontend/src/test/java/com/github/ambry/frontend/GetAccountsHandlerTest.java @@ -170,6 +170,33 @@ public void getSingleContainerSuccessTest() throws Exception { existingContainer); } + @Test + public void getAccountsWithoutContainersTest() throws Exception { + Account account = accountService.createAndAddRandomAccount(); + ThrowingConsumer testAction = (request) -> { + // Set the header to ignore containers + request.setArg(RestUtils.Headers.IGNORE_CONTAINERS, true); + RestResponseChannel restResponseChannel = new MockRestResponseChannel(); + ReadableStreamChannel channel = sendRequestGetResponse(request, restResponseChannel); + assertNotNull("There should be a response", channel); + assertNotNull("Date has not been set", restResponseChannel.getHeader(RestUtils.Headers.DATE)); + assertEquals("Content-type is not as expected", RestUtils.JSON_CONTENT_TYPE, + restResponseChannel.getHeader(RestUtils.Headers.CONTENT_TYPE)); + assertEquals("Content-length is not as expected", channel.getSize(), + Integer.parseInt((String) restResponseChannel.getHeader(RestUtils.Headers.CONTENT_LENGTH))); + RetainingAsyncWritableChannel asyncWritableChannel = new RetainingAsyncWritableChannel((int) channel.getSize()); + channel.readInto(asyncWritableChannel, null).get(); + Account receivedAccount = + AccountCollectionSerde.accountsFromInputStreamInJson(asyncWritableChannel.consumeContentAsInputStream()) + .iterator() + .next(); + assertTrue("Accounts do not match", account.equalsWithoutContainers(receivedAccount)); + assertTrue("Containers should not be present", receivedAccount.getAllContainers().isEmpty()); + }; + testAction.accept(createRestRequest(account.getName(), null, null, Operations.ACCOUNTS)); + testAction.accept(createRestRequest(null, Short.toString(account.getId()), null, Operations.ACCOUNTS)); + } + /** * Test failure case of getting single container. * @throws Exception diff --git a/ambry-frontend/src/test/java/com/github/ambry/frontend/PostAccountsHandlerTest.java b/ambry-frontend/src/test/java/com/github/ambry/frontend/PostAccountsHandlerTest.java index 47d35c8e52..91aeb1b66b 100644 --- a/ambry-frontend/src/test/java/com/github/ambry/frontend/PostAccountsHandlerTest.java +++ b/ambry-frontend/src/test/java/com/github/ambry/frontend/PostAccountsHandlerTest.java @@ -73,7 +73,7 @@ public PostAccountsHandlerTest() { @Test public void validRequestsTest() throws Exception { ThrowingConsumer> testAction = accountsToUpdate -> { - String requestBody = new String(AccountCollectionSerde.serializeAccountsInJson(accountsToUpdate)); + String requestBody = new String(AccountCollectionSerde.serializeAccountsInJson(accountsToUpdate, false)); RestResponseChannel restResponseChannel = new MockRestResponseChannel(); sendRequestGetResponse(requestBody, restResponseChannel); assertNotNull("Date has not been set", restResponseChannel.getHeader(RestUtils.Headers.DATE)); @@ -111,7 +111,7 @@ public void badRequestsTest() throws Exception { testAction.accept(new JSONObject().append("accounts", "ABC").toString(), RestServiceErrorCode.BadRequest); // AccountService update failure accountService.setShouldUpdateSucceed(false); - testAction.accept(new String(AccountCollectionSerde.serializeAccountsInJson(Collections.emptyList())), + testAction.accept(new String(AccountCollectionSerde.serializeAccountsInJson(Collections.emptyList(), false)), RestServiceErrorCode.InternalServerError); } @@ -123,7 +123,7 @@ public void badRequestsTest() throws Exception { public void securityServiceDenialTest() throws Exception { IllegalStateException injectedException = new IllegalStateException("@@expected"); TestUtils.ThrowingRunnable testAction = () -> sendRequestGetResponse( - new String(AccountCollectionSerde.serializeAccountsInJson(Collections.emptyList())), + new String(AccountCollectionSerde.serializeAccountsInJson(Collections.emptyList(), false)), new MockRestResponseChannel()); ThrowingConsumer errorChecker = e -> assertEquals("Wrong exception", injectedException, e); securityServiceFactory.exceptionToReturn = injectedException;