Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
Expand Down Expand Up @@ -141,9 +140,9 @@ public List<DynamoDbCommandWithDistance> getEvaluatedDynamoDbCommands() {
return Collections.emptyList();
}

Map<String, List<Map<String, Object>>> itemsByTable = new HashMap<>();
DynamoDbTableItemsCache itemsCache = new DynamoDbTableItemsCache(tableDataAccessor, dynamoDbClient);
for (DynamoDbCommand command : commands) {
evaluateCommand(command, itemsByTable);
evaluateCommand(command, itemsCache);
}
commands.clear();
return new ArrayList<>(evaluatedCommands);
Expand All @@ -159,16 +158,14 @@ public DynamoDbExecutionsDto getExecutionDto() {
}

/**
* Evaluates one successfully executed command and reuses table scans within the current batch.
* Evaluates one command and reuses table scans within the current batch.
* Failed conditional writes are evaluated because their predicates provide the
* distance needed to guide subsequent candidates toward a successful write.
*
* @param command command to evaluate
* @param itemsByTable cached table contents
* @param itemsCache cached table contents
*/
private void evaluateCommand(DynamoDbCommand command, Map<String, List<Map<String, Object>>> itemsByTable) {
if (!command.isSuccessfullyExecuted()) {
return;
}

private void evaluateCommand(DynamoDbCommand command, DynamoDbTableItemsCache itemsCache) {
Map<String, ParsedDynamoDbRequest> parsedByTable;
try {
parsedByTable = requestParser.parseByTable(command.getDdbRequest(), command.getOperationName());
Expand All @@ -182,14 +179,13 @@ private void evaluateCommand(DynamoDbCommand command, Map<String, List<Map<Strin
if (parsed == null || (parsed.getKeyCondition() == null && parsed.getFilterExpression() == null)) {
continue;
}
if (!command.isSuccessfullyExecuted() && parsed.getFilterExpression() == null) {
continue;
}

String tableName = entry.getKey();
try {
List<Map<String, Object>> items = itemsByTable.get(tableName);
if (items == null) {
items = tableDataAccessor.getItems(dynamoDbClient, tableName);
itemsByTable.put(tableName, items);
}
List<Map<String, Object>> items = itemsCache.getItems(tableName);
double distance = calculator.computeDistance(
parsed.getKeyCondition(), parsed.getFilterExpression(), items);
DynamoDbDistanceWithMetrics metrics = new DynamoDbDistanceWithMetrics(distance, items.size(), false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.evomaster.client.java.controller.internal.db.dynamodb;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
* Caches normalized DynamoDB table items loaded during one command evaluation batch.
* <p>
* The cache maintains an unmodifiable list of item maps for each table accessed in the batch.
* Note that while the outer list and the per-item attribute maps are unmodifiable, the underlying
* {@link Object} attribute values (for instance, nested collections, maps, or byte arrays) are not
* guaranteed to be deeply immutable.
*/
final class DynamoDbTableItemsCache {

private final DynamoDbTableDataAccessor tableDataAccessor;
private final Object dynamoDbClient;

/**
* Map from DynamoDB table name ({@link String}) to the list of items scanned from that table.
* Each item is represented as a {@link Map} where the key is the attribute name ({@link String})
* and the value is the attribute value ({@link Object}).
*/
private final Map<String, List<Map<String, Object>>> itemsByTable = new HashMap<>();
Comment thread
aschenzle marked this conversation as resolved.

/**
* Creates a cache backed by the given table accessor and DynamoDB client.
*
* @param tableDataAccessor accessor used to scan tables on cache misses
* @param dynamoDbClient SDK v2 synchronous or asynchronous DynamoDB client
*/
DynamoDbTableItemsCache(DynamoDbTableDataAccessor tableDataAccessor, Object dynamoDbClient) {
this.tableDataAccessor = Objects.requireNonNull(tableDataAccessor,
"DynamoDB table data accessor cannot be null");
this.dynamoDbClient = dynamoDbClient;
}

/**
* Returns the cached items for a table, scanning it on the first request in this batch.
*
* @param tableName table to read
* @return unmodifiable list of normalized items for the table, where each item is represented
* as an unmodifiable map of attribute name ({@link String}) to attribute value ({@link Object}).
* Note that the individual attribute value {@link Object}s are not guaranteed to be
* deeply immutable.
*/
List<Map<String, Object>> getItems(String tableName) {
String validTableName = validateTableName(tableName);
List<Map<String, Object>> items = itemsByTable.get(validTableName);
if (items == null) {
items = validateItems(tableDataAccessor.getItems(dynamoDbClient, validTableName));
itemsByTable.put(validTableName, items);
}
return items;
}

/**
* Checks that a table name can be used as a stable cache key and returns its trimmed representation.
*
* @param tableName candidate table name
* @return validated and trimmed table name
*/
private String validateTableName(String tableName) {
Objects.requireNonNull(tableName, "DynamoDB table name cannot be null");
String trimmed = tableName.trim();
if (trimmed.isEmpty()) {
throw new IllegalArgumentException("DynamoDB table name cannot be blank");
}
return trimmed;
}

/**
* Copies scanned items into unmodifiable structures and checks the cache invariants.
*
* @param items scanned table items, where each item is a {@link Map} of attribute name ({@link String})
* to attribute value ({@link Object})
* @return unmodifiable list of unmodifiable item maps. Note that individual attribute value
* {@link Object} instances within each map are not guaranteed to be deeply immutable.
*/
private List<Map<String, Object>> validateItems(List<Map<String, Object>> items) {
Objects.requireNonNull(items, "DynamoDB table items cannot be null");
List<Map<String, Object>> copy = new ArrayList<>(items.size());
for (Map<String, Object> item : items) {
Objects.requireNonNull(item, "DynamoDB table item cannot be null");
for (String attributeName : item.keySet()) {
Objects.requireNonNull(attributeName, "DynamoDB attribute name cannot be null");
}
copy.add(Collections.unmodifiableMap(new LinkedHashMap<>(item)));
}
return Collections.unmodifiableList(copy);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import software.amazon.awssdk.services.dynamodb.model.QueryRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;
import software.amazon.awssdk.services.dynamodb.model.PutItemRequest;

import java.util.ArrayList;
import java.util.Collections;
Expand Down Expand Up @@ -63,6 +64,28 @@ public void testAsyncClient() {
assertEquals(1, evaluated.get(0).getDistanceWithMetrics().getNumberOfEvaluatedItems());
}

@Test
public void testFailedConditionalPutItemIsEvaluated() {
SyncDynamoDbClient client = new SyncDynamoDbClient();
DynamoDbHandler handler = enabledHandler(client);
Map<String, AttributeValue> values = new HashMap<>();
values.put(":player", AttributeValue.builder().s("Lionel Scaloni").build());
PutItemRequest request = PutItemRequest.builder()
.tableName(TABLE)
.item(item("Argentina", "Lionel Messi"))
.conditionExpression("playerName = :player")
.expressionAttributeValues(values)
.build();
handler.handle(new DynamoDbCommand(Collections.singletonList(TABLE), DynamoDbOperationNames.PUT_ITEM,
request, false, 1L));

List<DynamoDbCommandWithDistance> evaluated = handler.getEvaluatedDynamoDbCommands();

assertEquals(1, evaluated.size());
assertTrue(evaluated.get(0).getDistanceWithMetrics().getDistance() > 0.0d);
assertTrue(evaluated.get(0).getDistanceWithMetrics().getDistance() < 1.0d);
}

@Test
public void testPrivateClientImplementation() {
DynamoDbHandler handler = enabledHandler(new PrivateAsyncDynamoDbClient());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.evomaster.client.java.controller.internal.db.dynamodb;

import org.junit.jupiter.api.Test;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
import software.amazon.awssdk.services.dynamodb.model.ScanRequest;
import software.amazon.awssdk.services.dynamodb.model.ScanResponse;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
* Tests DynamoDB table item caching and validation invariants.
*/
public class DynamoDbTableItemsCacheTest {

private static final String TABLE = "WorldCupPlayers";

private final DynamoDbTableDataAccessor accessor = new DynamoDbTableDataAccessor();

/**
* Verifies that items for a table are loaded on the first call and cached for subsequent calls.
*/
@Test
public void testCachesTableItemsAcrossMultipleRequests() {
DynamoDbClient client = mock(DynamoDbClient.class);
ScanResponse response = ScanResponse.builder()
.items(Collections.singletonList(player("Argentina", "Lionel Messi", 10)))
.build();
when(client.scan(any(ScanRequest.class))).thenReturn(response);

DynamoDbTableItemsCache cache = new DynamoDbTableItemsCache(accessor, client);

List<Map<String, Object>> first = cache.getItems(TABLE);
List<Map<String, Object>> second = cache.getItems(TABLE);

assertEquals(1, first.size());
assertEquals("Lionel Messi", first.get(0).get("playerName"));
assertEquals(first, second);
verify(client, times(1)).scan(any(ScanRequest.class));
}

/**
* Verifies that whitespace-padded table names are trimmed and resolve to the same cache entry.
*/
@Test
public void testTrimsTableNameAndReusesCachedItems() {
DynamoDbClient client = mock(DynamoDbClient.class);
ScanResponse response = ScanResponse.builder()
.items(Collections.singletonList(player("Argentina", "Diego Maradona", 10)))
.build();
when(client.scan(any(ScanRequest.class))).thenReturn(response);

DynamoDbTableItemsCache cache = new DynamoDbTableItemsCache(accessor, client);

List<Map<String, Object>> direct = cache.getItems(TABLE);
List<Map<String, Object>> padded = cache.getItems(" " + TABLE + " ");

assertEquals(direct, padded);
verify(client, times(1)).scan(any(ScanRequest.class));
}

/**
* Verifies that null and blank table names are rejected.
*/
@Test
public void testRejectsNullOrBlankTableName() {
DynamoDbTableItemsCache cache = new DynamoDbTableItemsCache(accessor, mock(DynamoDbClient.class));

NullPointerException nullTable = assertThrows(NullPointerException.class, () -> cache.getItems(null));
assertEquals("DynamoDB table name cannot be null", nullTable.getMessage());

IllegalArgumentException emptyTable = assertThrows(IllegalArgumentException.class, () -> cache.getItems(""));
assertEquals("DynamoDB table name cannot be blank", emptyTable.getMessage());

IllegalArgumentException blankTable = assertThrows(IllegalArgumentException.class, () -> cache.getItems(" "));
assertEquals("DynamoDB table name cannot be blank", blankTable.getMessage());
}

/**
* Verifies that null accessor arguments in constructor are rejected.
*/
@Test
public void testRejectsNullConstructorArguments() {
NullPointerException exception = assertThrows(NullPointerException.class,
() -> new DynamoDbTableItemsCache(null, new Object()));
assertEquals("DynamoDB table data accessor cannot be null", exception.getMessage());
}

/**
* Verifies that cached item lists and maps cannot be mutated by callers.
*/
@Test
public void testReturnsUnmodifiableListAndMaps() {
DynamoDbClient client = mock(DynamoDbClient.class);
ScanResponse response = ScanResponse.builder()
.items(Collections.singletonList(player("Croatia", "Luka Modric", 10)))
.build();
when(client.scan(any(ScanRequest.class))).thenReturn(response);

DynamoDbTableItemsCache cache = new DynamoDbTableItemsCache(accessor, client);
List<Map<String, Object>> items = cache.getItems(TABLE);

Map<String, Object> newItem = new HashMap<>();
newItem.put("country", "France");
newItem.put("playerName", "Kylian Mbappe");
assertThrows(UnsupportedOperationException.class, () -> items.add(newItem));
assertThrows(UnsupportedOperationException.class, () -> items.get(0).put("goals", 5));
}

/**
* Creates one deterministic World Cup player item representation.
*
* @param country country name
* @param playerName player name
* @param squadNumber player squad number
* @return map representing player attributes as AttributeValues
*/
private Map<String, AttributeValue> player(String country, String playerName, int squadNumber) {
Map<String, AttributeValue> item = new HashMap<>();
item.put("country", AttributeValue.builder().s(country).build());
item.put("playerName", AttributeValue.builder().s(playerName).build());
item.put("squadNumber", AttributeValue.builder().n(Integer.toString(squadNumber)).build());
return item;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,34 @@ protected static Object handle(Object client, String id, Object ddbRequest, Dyna
try {
Method method = getOriginal(Sync.singleton, id, client);
Object result = method.invoke(client, ddbRequest);

long end = System.currentTimeMillis();
List<String> tableNames = extractTableNames(ddbRequest);
long executionTime = end - start;
DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, ddbRequest, true, executionTime);
ExecutionTracer.addDynamoDbInfo(info);
return result;
recordDynamoDbTraceInfo(ddbRequest, start, operationName, true);
return result;
}
catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
recordDynamoDbTraceInfo(ddbRequest, start, operationName, false);
throw new RuntimeException(e.getCause());
}
}

/**
* Records trace information for a DynamoDB operation, including execution time, operation details,
* and whether the operation was successfully executed.
*
* @param ddbRequest the DynamoDB request object containing operation-specific information
* @param start the start time of the operation in milliseconds, used to calculate execution duration
* @param operationName the name of the DynamoDB operation being performed (e.g., "GetItem", "PutItem")
* @param successfullyExecuted a boolean indicating if the operation was executed successfully
*/
private static void recordDynamoDbTraceInfo(Object ddbRequest, long start, DynamoDbOperationNames operationName, boolean successfullyExecuted) {
long end = System.currentTimeMillis();
List<String> tableNames = extractTableNames(ddbRequest);
long executionTime = end - start;
DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, ddbRequest, successfullyExecuted, executionTime);
ExecutionTracer.addDynamoDbInfo(info);
}

/**
* Invoke the original asynchronous client method and trace completion status.
*
Expand All @@ -180,12 +193,7 @@ protected static Object handleAsync(Object client, String id, Object ddbRequest,

CompletableFuture<?> future = (CompletableFuture<?>) result;
return future.handle((res, ex) -> {
long end = System.currentTimeMillis();
List<String> tableNames = extractTableNames(ddbRequest);
boolean successful = ex == null;
long executionTime = end - start;
DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, ddbRequest, successful, executionTime);
ExecutionTracer.addDynamoDbInfo(info);
recordDynamoDbTraceInfo(ddbRequest, start, operationName, ex == null);
if (ex != null) {
if (ex instanceof RuntimeException) throw (RuntimeException) ex;
throw new RuntimeException(ex);
Expand All @@ -195,6 +203,7 @@ protected static Object handleAsync(Object client, String id, Object ddbRequest,
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
recordDynamoDbTraceInfo(ddbRequest, start, operationName, false);
throw new RuntimeException(e.getCause());
}
}
Expand Down
Loading
Loading