-
Notifications
You must be signed in to change notification settings - Fork 110
Introducing DynamoDB interception via SDK client class replacement #1495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 6 commits
9e50d26
bc8ce66
430eb48
c5c6b49
7f4db21
20b0837
db62b9d
5f29f56
9016275
198cb0c
3de28df
94c5656
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| package org.evomaster.client.java.instrumentation; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Info related to DynamoDB command execution. | ||
| */ | ||
| public class DynamoDbCommand implements Serializable { | ||
|
|
||
|
|
||
| /** | ||
| * Names of the tables the operation was applied to (with Batch operations, more than one table is possible) | ||
| */ | ||
| private final List<String> tableNames; | ||
| /** | ||
| * Name of the operation that was executed | ||
| */ | ||
| private final String operationName; | ||
| /** | ||
| * Actual executed operation | ||
| */ | ||
| private final Object request; | ||
| /** | ||
| * If the operation was successfully executed | ||
| */ | ||
| private final boolean successfullyExecuted; | ||
| /** | ||
| * Elapsed execution time | ||
| */ | ||
| private final long executionTime; | ||
|
|
||
| public DynamoDbCommand(List<String> tableNames, String operationName, Object request, boolean successfullyExecuted, long executionTime) { | ||
| this.tableNames = tableNames == null | ||
| ? Collections.emptyList() | ||
| : Collections.unmodifiableList(new ArrayList<>(tableNames)); | ||
| this.operationName = operationName; | ||
| this.request = request; | ||
| this.successfullyExecuted = successfullyExecuted; | ||
| this.executionTime = executionTime; | ||
| } | ||
|
|
||
| public List<String> getTableNames() { | ||
| return tableNames; | ||
| } | ||
|
|
||
| public String getOperationName() { | ||
| return operationName; | ||
| } | ||
|
|
||
| public Object getRequest() { | ||
| return request; | ||
| } | ||
|
|
||
| public boolean isSuccessfullyExecuted() { | ||
| return successfullyExecuted; | ||
| } | ||
|
|
||
| public long getExecutionTime() { | ||
| return executionTime; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,201 @@ | ||
| package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses; | ||
|
|
||
| import org.evomaster.client.java.instrumentation.DynamoDbCommand; | ||
| import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement; | ||
| import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast; | ||
| import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass; | ||
| import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter; | ||
| import org.evomaster.client.java.instrumentation.shared.ReplacementCategory; | ||
| import org.evomaster.client.java.instrumentation.shared.ReplacementType; | ||
| import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.lang.reflect.InvocationTargetException; | ||
| import java.lang.reflect.Method; | ||
| import java.util.concurrent.CompletableFuture; | ||
|
|
||
| import static org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass.getOriginal; | ||
|
|
||
| /** | ||
| * Made the decision to add both Sync and Async replacements in one class. They are mostly the same but I couldn't find a better way with static and annotations | ||
| */ | ||
| public class DynamoDbClassReplacement { | ||
|
|
||
| public static class Sync extends ThirdPartyMethodReplacementClass { | ||
| public static final String DDB_GET_ITEM = "ddbGetItem"; | ||
| public static final String DDB_BATCH_GET_ITEM = "ddbBatchGetItem"; | ||
| public static final String DDB_PUT_ITEM = "ddbPutItem"; | ||
| public static final String DDB_UPDATE_ITEM = "ddbUpdateItem"; | ||
| public static final String DDB_DELETE_ITEM = "ddbDeleteItem"; | ||
| public static final String DDB_QUERY = "ddbQuery"; | ||
| public static final String DDB_SCAN = "ddbScan"; | ||
| private static final Sync singleton = new Sync(); | ||
|
|
||
| @Override | ||
| protected String getNameOfThirdPartyTargetClass() { | ||
| return "software.amazon.awssdk.services.dynamodb.DynamoDbClient"; | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.GetItemResponse") | ||
| public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_GET_ITEM, request, "GetItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_BATCH_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemResponse") | ||
| public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_BATCH_GET_ITEM, request, "BatchGetItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_PUT_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.PutItemResponse") | ||
| public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_PUT_ITEM, request, "PutItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_UPDATE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.UpdateItemResponse") | ||
| public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_UPDATE_ITEM, request, "UpdateItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_DELETE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.DeleteItemResponse") | ||
| public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_DELETE_ITEM, request, "DeleteItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_QUERY, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.QueryResponse") | ||
| public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object request) { | ||
| return handle(singleton, client, DDB_QUERY, request, "Query"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_SCAN, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "software.amazon.awssdk.services.dynamodb.model.ScanResponse") | ||
| public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object request) { | ||
| return handle(singleton, client, DDB_SCAN, request, "Scan"); | ||
| } | ||
| } | ||
|
|
||
| public static class Async extends ThirdPartyMethodReplacementClass { | ||
| public static final String DDB_ASYNC_GET_ITEM = "ddbAsyncGetItem"; | ||
| public static final String DDB_ASYNC_BATCH_GET_ITEM = "ddbAsyncBatchGetItem"; | ||
| public static final String DDB_ASYNC_PUT_ITEM = "ddbAsyncPutItem"; | ||
| public static final String DDB_ASYNC_UPDATE_ITEM = "ddbAsyncUpdateItem"; | ||
| public static final String DDB_ASYNC_DELETE_ITEM = "ddbAsyncDeleteItem"; | ||
| public static final String DDB_ASYNC_QUERY = "ddbAsyncQuery"; | ||
| public static final String DDB_ASYNC_SCAN = "ddbAsyncScan"; | ||
| private static final Async singleton = new Async(); | ||
|
|
||
| @Override | ||
| protected String getNameOfThirdPartyTargetClass() { | ||
| return "software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient"; | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") | ||
| public static Object getItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.GetItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_ASYNC_GET_ITEM, request, "GetItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_BATCH_GET_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") | ||
| public static Object batchGetItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.BatchGetItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_ASYNC_BATCH_GET_ITEM, request, "BatchGetItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_PUT_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") | ||
| public static Object putItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.PutItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_ASYNC_PUT_ITEM, request, "PutItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_UPDATE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") | ||
| public static Object updateItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_ASYNC_UPDATE_ITEM, request, "UpdateItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_DELETE_ITEM, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") | ||
| public static Object deleteItem(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest") Object request) { | ||
| return handle(singleton, client, DDB_ASYNC_DELETE_ITEM, request, "DeleteItem"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_QUERY, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") | ||
| public static Object query(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.QueryRequest") Object request) { | ||
| return handle(singleton, client, DDB_ASYNC_QUERY, request, "Query"); | ||
| } | ||
|
|
||
| @Replacement(type = ReplacementType.TRACKER, id = DDB_ASYNC_SCAN, usageFilter = UsageFilter.ANY, category = ReplacementCategory.DYNAMODB, castTo = "java.util.concurrent.CompletableFuture") | ||
| public static Object scan(Object client, @ThirdPartyCast(actualType = "software.amazon.awssdk.services.dynamodb.model.ScanRequest") Object request) { | ||
| return handle(singleton, client, DDB_ASYNC_SCAN, request, "Scan"); | ||
| } | ||
| } | ||
|
|
||
| protected static Object handle(ThirdPartyMethodReplacementClass singleton, Object client, String id, Object request, String operationName) { | ||
| long start = System.currentTimeMillis(); | ||
| boolean isAsync = client.getClass().getName().contains("Async"); | ||
| try { | ||
| Method method = getOriginal(singleton, id, client); | ||
| Object result = method.invoke(client, request); | ||
|
|
||
| if (isAsync) { | ||
| CompletableFuture<?> future = (CompletableFuture<?>) result; | ||
| return future.handle((res, ex) -> { | ||
| long end = System.currentTimeMillis(); | ||
| List<String> tableNames = extractTableNames(request); | ||
| boolean successful = ex == null; | ||
| long executionTime = end - start; | ||
| DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, request, successful, executionTime); | ||
| ExecutionTracer.addDynamoDbInfo(info); | ||
| if (ex != null) { | ||
| if (ex instanceof RuntimeException) throw (RuntimeException) ex; | ||
| throw new RuntimeException(ex); | ||
| } | ||
| return res; | ||
| }); | ||
| } else { | ||
| long end = System.currentTimeMillis(); | ||
| List<String> tableNames = extractTableNames(request); | ||
| long executionTime = end - start; | ||
| DynamoDbCommand info = new DynamoDbCommand(tableNames, operationName, request, true, executionTime); | ||
| ExecutionTracer.addDynamoDbInfo(info); | ||
| return result; | ||
| } | ||
| } catch (IllegalAccessException e) { | ||
| throw new RuntimeException(e); | ||
| } catch (InvocationTargetException e) { | ||
| throw new RuntimeException(e.getCause()); | ||
| } | ||
| } | ||
|
|
||
| private static List<String> extractTableNames(Object request) { | ||
| if (request == null) return Collections.emptyList(); | ||
|
|
||
| try { | ||
| Method getTableNameMethod = request.getClass().getMethod("tableName"); | ||
| String tableName = (String) getTableNameMethod.invoke(request); | ||
| if (tableName != null) { | ||
| return Collections.singletonList(tableName); | ||
| } | ||
| } catch (NoSuchMethodException ignored) { | ||
| // no-op | ||
|
aschenzle marked this conversation as resolved.
Outdated
|
||
| } catch (IllegalAccessException | InvocationTargetException e) { | ||
| return Collections.emptyList(); | ||
| } | ||
|
|
||
| try { | ||
| Method getRequestItemsMethod = request.getClass().getMethod("requestItems"); | ||
| Object requestItems = getRequestItemsMethod.invoke(request); | ||
| if (!(requestItems instanceof Map)) { | ||
| return Collections.emptyList(); | ||
| } | ||
|
|
||
| List<String> tableNames = new ArrayList<>(); | ||
| for (Object key : ((Map<?, ?>) requestItems).keySet()) { | ||
| if (key instanceof String) { | ||
| tableNames.add((String) key); | ||
| } | ||
| } | ||
|
|
||
| Collections.sort(tableNames); | ||
| return tableNames; | ||
| } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { | ||
| return Collections.emptyList(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is is defaulted to empty?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seemed like the best option, as we don't want the interception to throw exceptions and retuning null requires handling it. We are just getting less metadata about the interception for this particular request if for some weird reason it fails (Only case I can think this failing would be a dependency problem with another version of the SDK breaking API compatibility, not expected from AWS).
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't you encapsulate this and throw a RuntimeException?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just committed a refactored version. |
||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.