diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java index 97c54bea9..30be17f6c 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/AgentInitializer.java @@ -22,6 +22,7 @@ public static void initialize(Instrumentation inst, File agentFile, String agent File[] extensionFiles = getExtensionJarFiles(agentFile); classLoader = createAgentClassLoader(agentFile, extensionFiles); InstrumentationHolder.setAgentClassLoader(classLoader); + InstrumentationHolder.setInstrumentation(inst); AgentInstaller installer = createAgentInstaller(inst, agentFile, agentArgs); addJarToLoaderSearch(agentFile, extensionFiles); installer.install(); diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java index 3aedb58c8..4e9df7a12 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/internal/WeakCache.java @@ -5,15 +5,22 @@ import java.lang.ref.WeakReference; import java.util.concurrent.*; -class WeakCache extends ReferenceQueue implements Cache { +public class WeakCache extends ReferenceQueue implements Cache { final ConcurrentMap, V> target; + final CleanUpTask cleanUpTask; + public WeakCache() { - this(new ConcurrentHashMap<>()); + this(new ConcurrentHashMap<>(), null); + } + + public WeakCache(CleanUpTask cleanUpTask) { + this(new ConcurrentHashMap<>(), cleanUpTask); } - public WeakCache(ConcurrentMap, V> target) { + public WeakCache(ConcurrentMap, V> target, CleanUpTask cleanUpTask) { this.target = target; + this.cleanUpTask = cleanUpTask; } public V get(K key) { @@ -38,20 +45,31 @@ public void clear() { target.clear(); } + public boolean containsKey(K key) { + check(); + return target.containsKey(new WeakReferenceKey<>(key)); + } + void check() { Reference reference; while ((reference = poll()) != null) { - target.remove(reference); + final V value = target.remove(reference); + if (cleanUpTask != null && value != null) { + cleanUpTask.cleanUp(value); + } } } static final class WeakReferenceKey extends WeakReference { private final int hashCode; + WeakReferenceKey(K key) { + this(key, null); + } + WeakReferenceKey(K key, ReferenceQueue queue) { super(key, queue); - - hashCode = System.identityHashCode(key); + hashCode = (key == null) ? 0 : System.identityHashCode(key); } @Override @@ -68,5 +86,12 @@ public boolean equals(Object other) { return other != null && other.equals(this); } } + + public interface CleanUpTask { + /** + * @param object object to cleanup + */ + void cleanUp(T object); + } } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java index c1a25e61c..8fc902750 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/ArexMocker.java @@ -15,6 +15,7 @@ public class ArexMocker implements Mocker { private long creationTime; private Mocker.Target targetRequest; private Mocker.Target targetResponse; + private boolean needMerge; private String operationName; public ArexMocker() { @@ -116,4 +117,12 @@ public void setTargetResponse(Mocker.Target targetResponse) { public void setOperationName(String operationName) { this.operationName = operationName; } + + public boolean isNeedMerge() { + return needMerge; + } + + public void setNeedMerge(boolean needMerge) { + this.needMerge = needMerge; + } } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java index 77a252a10..2e14963f4 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/MockCategoryType.java @@ -38,7 +38,7 @@ public static MockCategoryType createDependency(String name) { public static MockCategoryType create(String name, boolean entryPoint, boolean skipComparison) { return CATEGORY_TYPE_MAP.computeIfAbsent(name, - key -> new MockCategoryType(name, entryPoint, skipComparison)); + key -> new MockCategoryType(name, entryPoint, skipComparison)); } public static Collection values() { @@ -78,6 +78,9 @@ private MockCategoryType(String name, boolean entryPoint, boolean skipComparison this.skipComparison = skipComparison; } + public static MockCategoryType of(String name) { + return CATEGORY_TYPE_MAP.get(name); + } @Override public String toString() { diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java index 7162d5447..12f660d35 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/model/Mocker.java @@ -116,4 +116,8 @@ default String recordLogTitle() { default String replayLogTitle() { return "replay." + getCategoryType().getName(); } + + public boolean isNeedMerge(); + + public void setNeedMerge(boolean needMerge); } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java index a206c1ba6..153ff23ad 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/CollectionUtil.java @@ -1,9 +1,6 @@ package io.arex.agent.bootstrap.util; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; public class CollectionUtil { private static final List EMPTY_LIST = newArrayList(); @@ -41,4 +38,42 @@ public static List newArrayList(E... elements) { Collections.addAll(list, elements); return list; } + + /** + * split to multiple list by split count + */ + public static List> split(List originalList, int splitCount) { + if (isEmpty(originalList)) { + return emptyList(); + } + int originalSize = originalList.size(); + List> splitList = new ArrayList<>(); + if (originalSize < splitCount || splitCount == 0) { + splitList.add(originalList); + return splitList; + } + for (int i = 0; i < splitCount; i++) { + List list = new ArrayList<>(); + splitList.add(list); + } + int index = 0; + for (V value : originalList) { + splitList.get(index).add(value); + index = (index + 1) % splitCount; + } + return splitList; + } + + public static List filterNull(List originalList) { + if (isEmpty(originalList)) { + return emptyList(); + } + List filterList = new ArrayList<>(originalList.size()); + for (V element : originalList) { + if (element != null) { + filterList.add(element); + } + } + return filterList; + } } diff --git a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java index 8b6716239..26231ea70 100644 --- a/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java +++ b/arex-agent-bootstrap/src/main/java/io/arex/agent/bootstrap/util/MapUtils.java @@ -39,4 +39,26 @@ static int capacity(int expectedSize) { // any large value return Integer.MAX_VALUE; } + + public static boolean getBoolean(final Map map, final K key) { + return getBoolean(map, key, false); + } + + public static boolean getBoolean(final Map map, final K key, boolean defaultValue) { + if (isEmpty(map) || map.get(key) == null) { + return defaultValue; + } + final Object answer = map.get(key); + if (answer instanceof Boolean) { + return (Boolean) answer; + } + if (answer instanceof String) { + return Boolean.parseBoolean((String) answer); + } + if (answer instanceof Number) { + final Number num = (Number) answer; + return num.intValue() != 0; + } + return defaultValue; + } } diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java index a011ab84b..e45e4ce43 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/CollectionUtilTest.java @@ -1,11 +1,17 @@ package io.arex.agent.bootstrap.util; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import java.util.*; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class CollectionUtilTest { @@ -33,4 +39,33 @@ void newArrayList() { actualResult = CollectionUtil.newArrayList("test"); assertInstanceOf(ArrayList.class, actualResult); } + + @ParameterizedTest + @MethodSource("splitCase") + void split(List originalList, int splitCount, Predicate>> predicate) { + assertTrue(predicate.test(CollectionUtil.split(originalList, splitCount))); + } + + static Stream splitCase() { + Supplier> lessSplitCountList = () -> CollectionUtil.newArrayList("mock"); + Supplier> normalSplitCountList = () -> CollectionUtil.newArrayList("mock1", "mock2"); + + Predicate>> empty = CollectionUtil::isEmpty; + Predicate>> notEmpty = CollectionUtil::isNotEmpty; + + return Stream.of( + arguments(null, 1, empty), + arguments(lessSplitCountList.get(), 2, notEmpty), + arguments(normalSplitCountList.get(), 2, notEmpty) + ); + } + + @Test + void filterNull() { + List actualResult = CollectionUtil.filterNull(null); + assertEquals(0, actualResult.size()); + + actualResult = CollectionUtil.filterNull(CollectionUtil.newArrayList("mock")); + assertEquals(1, actualResult.size()); + } } diff --git a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java index 95938ed52..499bba7ee 100644 --- a/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java +++ b/arex-agent-bootstrap/src/test/java/io/arex/agent/bootstrap/util/MapUtilsTest.java @@ -1,12 +1,17 @@ package io.arex.agent.bootstrap.util; import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; import java.lang.reflect.Method; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Stream; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; class MapUtilsTest { @@ -41,4 +46,29 @@ public static int capacity(Map map) { return 0; } } + + @ParameterizedTest + @MethodSource("getBooleanCase") + void getBoolean(Map map, String key, Predicate asserts) { + asserts.test(MapUtils.getBoolean(map, key)); + } + + static Stream getBooleanCase() { + Map map = new HashMap<>(); + map.put("booleanKey", true); + map.put("stringKey", "true"); + map.put("numberKey", 1); + map.put("key", String.class); + + Predicate asserts1 = bool -> bool; + Predicate asserts2 = bool -> !bool; + + return Stream.of( + arguments(null, null, asserts1), + arguments(map, "booleanKey", asserts2), + arguments(map, "stringKey", asserts2), + arguments(map, "numberKey", asserts2), + arguments(map, "key", asserts1) + ); + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java index e1f5293de..d9ed4238a 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ArexContext.java @@ -3,12 +3,12 @@ import io.arex.agent.bootstrap.util.ConcurrentHashSet; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MergeRecordReplayUtil; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; public class ArexContext { @@ -18,11 +18,13 @@ public class ArexContext { private final long createTime; private final AtomicInteger sequence; private Set methodSignatureHashList; - private Map cachedReplayResultMap; + private Map> cachedReplayResultMap; private Map> excludeMockTemplate; private Map attachments = null; + private LinkedBlockingQueue mergeRecordQueue; + private boolean isRedirectRequest; private boolean isInvalidCase; @@ -72,7 +74,7 @@ public Set getMethodSignatureHashList() { return methodSignatureHashList; } - public Map getCachedReplayResultMap() { + public Map> getCachedReplayResultMap() { if (cachedReplayResultMap == null) { cachedReplayResultMap = new ConcurrentHashMap<>(); } @@ -138,6 +140,13 @@ public boolean isRedirectRequest(String referer) { return isRedirectRequest; } + public LinkedBlockingQueue getMergeRecordQueue() { + if (mergeRecordQueue == null) { + mergeRecordQueue = new LinkedBlockingQueue<>(2048); + } + return mergeRecordQueue; + } + public void clear() { if (methodSignatureHashList != null) { methodSignatureHashList.clear(); @@ -151,5 +160,10 @@ public void clear() { if (attachments != null) { attachments.clear(); } + if (mergeRecordQueue != null) { + // async thread merge record (main entry has ended) + MergeRecordReplayUtil.recordRemain(this); + mergeRecordQueue.clear(); + } } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java index 1d14c9569..55a5368ed 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/context/ContextManager.java @@ -84,7 +84,7 @@ public static void registerListener(ContextListener listener) { private static void publish(ArexContext context, boolean isCreate) { if (CollectionUtil.isNotEmpty(LISTENERS)) { - LISTENERS.stream().forEach(listener -> { + LISTENERS.forEach(listener -> { if (isCreate) { listener.onCreate(context); } else { diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java new file mode 100644 index 000000000..f09038ab4 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AbstractMatchStrategy.java @@ -0,0 +1,42 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; + +public abstract class AbstractMatchStrategy { + static final String MATCH_TITLE = "replay.match.fail"; + + public void match(MatchStrategyContext context) { + try { + if (support(context)) { + process(context); + } + } catch (Exception e) { + LogManager.warn(MATCH_TITLE, e); + } + } + + private boolean support(MatchStrategyContext context) { + if (context == null || context.getRequestMocker() == null || context.isInterrupt()) { + return false; + } + return internalCheck(context); + } + + boolean internalCheck(MatchStrategyContext context) { + return true; + } + abstract void process(MatchStrategyContext context) throws Exception; + + Mocker buildMatchedMocker(Mocker requestMocker, MergeDTO mergeReplayDTO) { + if (mergeReplayDTO == null) { + return null; + } + requestMocker.getTargetResponse().setBody(mergeReplayDTO.getResponse()); + requestMocker.getTargetResponse().setType(mergeReplayDTO.getResponseType()); + requestMocker.getTargetResponse().setAttributes(mergeReplayDTO.getResponseAttributes()); + mergeReplayDTO.setMatched(true); + return requestMocker; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java new file mode 100644 index 000000000..2a86566bc --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/AccurateMatchStrategy.java @@ -0,0 +1,62 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; + +import java.util.ArrayList; +import java.util.List; + +public class AccurateMatchStrategy extends AbstractMatchStrategy{ + private static final String ACCURATE_MATCH_TITLE = "replay.match.accurate"; + /** + * search by operationName + requestBody + */ + void process(MatchStrategyContext context) { + Mocker requestMocker = context.getRequestMocker(); + List mergeReplayList = context.getMergeReplayList(); + int methodSignatureHash = MockUtils.methodSignatureHash(requestMocker); + List matchedList = new ArrayList<>(mergeReplayList.size()); + for (MergeDTO mergeDTO : mergeReplayList) { + if (methodSignatureHash == mergeDTO.getMethodSignatureHash()) { + matchedList.add(mergeDTO); + } + } + int matchedCount = matchedList.size(); + /* + * 1. unmatched + * 2. matched but find last mode (like dynamicClass) + */ + if (matchedCount == 1) { + if (!matchedList.get(0).isMatched() || MockStrategyEnum.FIND_LAST == context.getMockStrategy()) { + context.setMatchMocker(buildMatchedMocker(requestMocker, matchedList.get(0))); + } else { + LogManager.info(ACCURATE_MATCH_TITLE, StringUtil.format("accurate match one result, but cannot be used, " + + "reason: matched: %s, mock strategy: %s, methodSignatureHash: %s, category: %s", + Boolean.toString(matchedList.get(0).isMatched()), context.getMockStrategy().name(), + String.valueOf(methodSignatureHash), requestMocker.getCategoryType().getName())); + } + // other modes can only be matched once, so interrupt and not continue next fuzzy match + context.setInterrupt(true); + return; + } + // matched multiple result(like as redis: incr、decr) only retain matched item for next fuzzy match + if (matchedCount > 1) { + context.setMergeReplayList(matchedList); + return; + } + // if strict match mode and not matched, interrupt and not continue next fuzzy match + if (MockStrategyEnum.STRICT_MATCH == context.getMockStrategy()) { + context.setInterrupt(true); + } + } + + @Override + boolean internalCheck(MatchStrategyContext context) { + // if no request params, do next fuzzy match directly + return StringUtil.isNotEmpty(context.getRequestMocker().getTargetRequest().getBody()); + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java new file mode 100644 index 000000000..0ab3e7bf4 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/EigenMatchStrategy.java @@ -0,0 +1,11 @@ +package io.arex.inst.runtime.match; + +public class EigenMatchStrategy extends AbstractMatchStrategy{ + + /** + * search by eigen value of request + */ + void process(MatchStrategyContext context) { + // to be implemented after database merge replay support + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java new file mode 100644 index 000000000..7395776fc --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/FuzzyMatchStrategy.java @@ -0,0 +1,47 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; + +import java.util.List; + +public class FuzzyMatchStrategy extends AbstractMatchStrategy { + private static final String FUZZY_MATCH_TITLE = "replay.match.fuzzy"; + + /** + * search under the same method signature + * @return unmatched or last one + */ + void process(MatchStrategyContext context) { + Mocker requestMocker = context.getRequestMocker(); + List mergeReplayList = context.getMergeReplayList(); + MergeDTO matchedDTO = null; + int size = mergeReplayList.size(); + for (int i = 0; i < size; i++) { + MergeDTO mergeReplayDTO = mergeReplayList.get(i); + if (!mergeReplayDTO.isMatched()) { + matchedDTO = mergeReplayDTO; + break; + } + } + if (matchedDTO == null && MockStrategyEnum.FIND_LAST == context.getMockStrategy()) { + matchedDTO = mergeReplayList.get(size - 1); + } + if (Config.get().isEnableDebug()) { + String response = matchedDTO != null ? matchedDTO.getResponse() : StringUtil.EMPTY; + LogManager.info(FUZZY_MATCH_TITLE, StringUtil.format("%s%nrequest: %s%nresponse: %s", + requestMocker.logBuilder().toString(), requestMocker.getTargetRequest().getBody(), response)); + } + context.setMatchMocker(buildMatchedMocker(requestMocker, matchedDTO)); + } + + @Override + boolean internalCheck(MatchStrategyContext context) { + return CollectionUtil.isNotEmpty(context.getMergeReplayList()); + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyContext.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyContext.java new file mode 100644 index 000000000..ce90477e5 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyContext.java @@ -0,0 +1,61 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.model.MergeDTO; + +import java.util.List; + +public class MatchStrategyContext { + private Mocker requestMocker; + private List mergeReplayList; + private MockStrategyEnum mockStrategy; + private boolean interrupt; + private Mocker matchMocker; + + public MatchStrategyContext(Mocker requestMocker, List mergeReplayList, MockStrategyEnum mockStrategy) { + this.requestMocker = requestMocker; + this.mergeReplayList = mergeReplayList; + this.mockStrategy = mockStrategy; + } + + public Mocker getRequestMocker() { + return requestMocker; + } + + public void setRequestMocker(Mocker requestMocker) { + this.requestMocker = requestMocker; + } + + public List getMergeReplayList() { + return mergeReplayList; + } + + public void setMergeReplayList(List mergeReplayList) { + this.mergeReplayList = mergeReplayList; + } + + public MockStrategyEnum getMockStrategy() { + return mockStrategy; + } + + public void setMockStrategy(MockStrategyEnum mockStrategy) { + this.mockStrategy = mockStrategy; + } + + public boolean isInterrupt() { + return interrupt; + } + + public void setInterrupt(boolean interrupt) { + this.interrupt = interrupt; + } + + public Mocker getMatchMocker() { + return matchMocker; + } + + public void setMatchMocker(Mocker matchMocker) { + this.matchMocker = matchMocker; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyRegister.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyRegister.java new file mode 100644 index 000000000..aa77ebd70 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/MatchStrategyRegister.java @@ -0,0 +1,28 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.util.CollectionUtil; + +import java.util.*; + +public class MatchStrategyRegister { + private static final AbstractMatchStrategy ACCURATE_STRATEGY = new AccurateMatchStrategy(); + private static final AbstractMatchStrategy FUZZY_STRATEGY = new FuzzyMatchStrategy(); + private static final AbstractMatchStrategy EIGEN_STRATEGY = new EigenMatchStrategy(); + private static final Map> MATCH_STRATEGIES = register(); + + private MatchStrategyRegister() { + } + + public static List getMatchStrategies(MockCategoryType categoryType) { + return MATCH_STRATEGIES.get(categoryType.getName()); + } + + private static Map> register() { + Map> strategyMap = new HashMap<>(); + strategyMap.put(MockCategoryType.DYNAMIC_CLASS.getName(), CollectionUtil.newArrayList(ACCURATE_STRATEGY, FUZZY_STRATEGY)); + strategyMap.put(MockCategoryType.REDIS.getName(), CollectionUtil.newArrayList(ACCURATE_STRATEGY, FUZZY_STRATEGY)); + strategyMap.put(MockCategoryType.DATABASE.getName(), CollectionUtil.newArrayList(ACCURATE_STRATEGY, EIGEN_STRATEGY)); + return strategyMap; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/ReplayMatcher.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/ReplayMatcher.java new file mode 100644 index 000000000..0ab057d50 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/match/ReplayMatcher.java @@ -0,0 +1,45 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; + +import java.util.*; + +public class ReplayMatcher { + private static final String MATCH_TITLE = "replay.match"; + + private ReplayMatcher() { + } + + public static Mocker match(Mocker requestMocker, MockStrategyEnum mockStrategy) { + Map> cachedReplayResultMap = ContextManager.currentContext().getCachedReplayResultMap(); + // first match methodRequestTypeHash: category + operationName + requestType, ensure the same method + List mergeReplayList = cachedReplayResultMap.get(MockUtils.methodRequestTypeHash(requestMocker)); + if (CollectionUtil.isEmpty(mergeReplayList)) { + return null; + } + + List matchStrategyList = MatchStrategyRegister.getMatchStrategies(requestMocker.getCategoryType()); + MatchStrategyContext context = new MatchStrategyContext(requestMocker, mergeReplayList, mockStrategy); + for (AbstractMatchStrategy matchStrategy : matchStrategyList) { + matchStrategy.match(context); + } + + Mocker matchedMocker = context.getMatchMocker(); + if (Config.get().isEnableDebug()) { + String response = matchedMocker != null && matchedMocker.getTargetResponse() != null + ? matchedMocker.getTargetResponse().getBody() : StringUtil.EMPTY; + LogManager.info(MATCH_TITLE, StringUtil.format("%s%nrequest: %s%nresponse: %s", + requestMocker.logBuilder().toString(), requestMocker.getTargetRequest().getBody(), response)); + } + return matchedMocker; + } + +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java index 4fff6f618..f9c222d31 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/ArexConstants.java @@ -31,9 +31,22 @@ private ArexConstants() {} public static final String REPLAY_ORIGINAL_MOCKER = "arex-replay-original-mocker"; public static final String AREX_EXTENSION_ATTRIBUTE = "arex-extension-attribute"; public static final String GSON_SERIALIZER = "gson"; + public static final String JACKSON_SERIALIZER = "jackson"; public static final String CONFIG_DEPENDENCY = "arex_replay_prepare_dependency"; public static final String PREFIX = "arex-"; public static final String CONFIG_VERSION = "configBatchNo"; public static final String SKIP_FLAG = "arex-skip-flag"; public static final String ORIGINAL_REQUEST = "arex-original-request"; + public static final String MERGE_RECORD_NAME = "arex.mergeRecord"; + public static final String MERGE_RECORD_THRESHOLD = "arex.merge.record.threshold"; + public static final String MERGE_REPLAY_THRESHOLD = "arex.merge.replay.threshold"; + public static final int MERGE_RECORD_THRESHOLD_DEFAULT = 10; + public static final int MERGE_REPLAY_THRESHOLD_DEFAULT = 1000; + public static final String MERGE_TYPE = "java.util.ArrayList-io.arex.inst.runtime.model.MergeDTO"; + public static final String MERGE_SPLIT_COUNT = "arex.merge.split.count"; + public static final long MEMORY_SIZE_1MB = 1024L * 1024L; + public static final long MEMORY_SIZE_5MB = 5 * 1024L * 1024L; + public static final String CALL_REPLAY_MAX = "callReplayMax"; + public static final String EXCEED_MAX_SIZE_TITLE = "exceed.max.size"; + public static final String EXCEED_MAX_SIZE_FLAG = "isExceedMaxSize"; } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeDTO.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeDTO.java new file mode 100644 index 000000000..5685b18f6 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeDTO.java @@ -0,0 +1,153 @@ +package io.arex.inst.runtime.model; + +import java.util.Map; +import java.util.Objects; + +public class MergeDTO { + private String category; + private int methodSignatureHash; + private String operationName; + private String request; + private String recordId; + private Map requestAttributes; + private Map responseAttributes; + private int methodRequestTypeHash; + private long creationTime; + private boolean matched; + private String response; + private String responseType; + public MergeDTO() {} + private MergeDTO(String category, int methodSignatureHash, String operationName, String request, String response, String responseType, + Map requestAttributes, Map responseAttributes, String recordId) { + this.category = category; + this.methodSignatureHash = methodSignatureHash; + this.operationName = operationName; + this.request = request; + this.response = response; + this.responseType = responseType; + this.requestAttributes = requestAttributes; + this.responseAttributes = responseAttributes; + this.recordId = recordId; + } + + public static MergeDTO of(String category, int methodSignatureHash, String operationName, String request, String response, String responseType, + Map requestAttributes, Map responseAttributes, String recordId) { + return new MergeDTO(category, methodSignatureHash, operationName, request, response, responseType, requestAttributes, responseAttributes, recordId); + } + + public String getCategory() { + return category; + } + public void setCategory(String category) { + this.category = category; + } + public int getMethodSignatureHash() { + return methodSignatureHash; + } + public void setMethodSignatureHash(int methodSignatureHash) { + this.methodSignatureHash = methodSignatureHash; + } + public String getOperationName() { + return operationName; + } + public void setOperationName(String operationName) { + this.operationName = operationName; + } + public String getRequest() { + return request; + } + public void setRequest(String request) { + this.request = request; + } + public String getRecordId() { + return recordId; + } + public void setRecordId(String recordId) { + this.recordId = recordId; + } + public Map getRequestAttributes() { + return requestAttributes; + } + public void setRequestAttributes(Map requestAttributes) { + this.requestAttributes = requestAttributes; + } + public Map getResponseAttributes() { + return responseAttributes; + } + public void setResponseAttributes(Map responseAttributes) { + this.responseAttributes = responseAttributes; + } + + public int getMethodRequestTypeHash() { + return methodRequestTypeHash; + } + + public void setMethodRequestTypeHash(int methodRequestTypeHash) { + this.methodRequestTypeHash = methodRequestTypeHash; + } + + public long getCreationTime() { + return creationTime; + } + + public void setCreationTime(long creationTime) { + this.creationTime = creationTime; + } + + public boolean isMatched() { + return matched; + } + + public void setMatched(boolean matched) { + this.matched = matched; + } + + public String getResponse() { + return response; + } + + public void setResponse(String response) { + this.response = response; + } + + public String getResponseType() { + return responseType; + } + + public void setResponseType(String responseType) { + this.responseType = responseType; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MergeDTO mergeDTO = (MergeDTO) o; + + return methodSignatureHash == mergeDTO.methodSignatureHash; + } + + @Override + public int hashCode() { + return methodSignatureHash; + } + + @Override + public String toString() { + return "MergeDTO{" + + "category='" + category + '\'' + + ", methodSignatureHash=" + methodSignatureHash + + ", operationName='" + operationName + '\'' + + ", request='" + request + '\'' + + ", recordId='" + recordId + '\'' + + ", requestAttributes=" + requestAttributes + + ", responseAttributes=" + responseAttributes + + ", methodRequestTypeHash=" + methodRequestTypeHash + + ", creationTime=" + creationTime + + ", matched=" + matched + + ", response='" + response + '\'' + + ", responseType='" + responseType + '\'' + + '}'; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeReplayType.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeReplayType.java new file mode 100644 index 000000000..ed84a013b --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/model/MergeReplayType.java @@ -0,0 +1,19 @@ +package io.arex.inst.runtime.model; + +import io.arex.agent.bootstrap.model.MockCategoryType; + +public enum MergeReplayType { + + DYNAMIC_CLASS(MockCategoryType.DYNAMIC_CLASS), + + REDIS(MockCategoryType.REDIS); + + private MockCategoryType mockCategoryType; + MergeReplayType(MockCategoryType mockCategoryType) { + this.mockCategoryType = mockCategoryType; + } + + public MockCategoryType getMockCategoryType() { + return mockCategoryType; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java new file mode 100644 index 000000000..e59fdee9d --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandler.java @@ -0,0 +1,30 @@ +package io.arex.inst.runtime.request; + +import com.google.auto.service.AutoService; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.inst.runtime.util.MergeRecordReplayUtil; + + +@AutoService(RequestHandler.class) +public class MergeRecordDubboRequestHandler implements RequestHandler { + @Override + public String name() { + return MockCategoryType.DUBBO_PROVIDER.getName(); + } + + @Override + public void preHandle(Object request) { + // no need implement + } + + @Override + public void handleAfterCreateContext(Object request) { + // init replay and cached dynamic class + MergeRecordReplayUtil.mergeReplay(); + } + + @Override + public void postHandle(Object request, Object response) { + // no need implement + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java new file mode 100644 index 000000000..67ac8e607 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandler.java @@ -0,0 +1,30 @@ +package io.arex.inst.runtime.request; + +import com.google.auto.service.AutoService; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.inst.runtime.util.MergeRecordReplayUtil; + + +@AutoService(RequestHandler.class) +public class MergeRecordServletRequestHandler implements RequestHandler { + @Override + public String name() { + return MockCategoryType.SERVLET.getName(); + } + + @Override + public void preHandle(Object request) { + // no need implement + } + + @Override + public void handleAfterCreateContext(Object request) { + // init replay and cached dynamic class + MergeRecordReplayUtil.mergeReplay(); + } + + @Override + public void postHandle(Object request, Object response) { + // no need implement + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java index b11317540..1141ab994 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandler.java @@ -10,5 +10,6 @@ public interface RequestHandler { * add or get request information */ void preHandle(Request request); + void handleAfterCreateContext(Request request); void postHandle(Request request, Response response); } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java index a6aafde15..7c3aa5497 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/request/RequestHandlerManager.java @@ -33,6 +33,21 @@ public static void preHandle(Object request, String name) { } } + public static void handleAfterCreateContext(Object request, String name) { + final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); + if (CollectionUtil.isEmpty(requestHandlers)) { + return; + } + for (RequestHandler requestHandler : requestHandlers) { + try { + requestHandler.handleAfterCreateContext(request); + } catch (Throwable ex) { + // avoid affecting the remaining handlers when one handler fails + LogManager.warn("handleAfterCreateContext", ex.getMessage()); + } + } + } + public static void postHandle(Object request, Object response, String name) { try { final List requestHandlers = REQUEST_HANDLER_CACHE.get(name); diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java index 0951e1aa1..19c518856 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/IgnoreUtils.java @@ -118,15 +118,14 @@ private static boolean operationMatched(String targetName, Set searchOpe } public static boolean invalidOperation(String operationSignature) { - return INVALID_OPERATION_HASH_CACHE.contains(operationSignature.hashCode()); + return INVALID_OPERATION_HASH_CACHE.contains(StringUtil.encodeAndHash(operationSignature)); } public static void addInvalidOperation(String operationSignature) { - INVALID_OPERATION_HASH_CACHE.add(operationSignature.hashCode()); + INVALID_OPERATION_HASH_CACHE.add(StringUtil.encodeAndHash(operationSignature)); } public static void clearInvalidOperation() { INVALID_OPERATION_HASH_CACHE.clear(); } - } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java new file mode 100644 index 000000000..bcc08ce69 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MergeRecordReplayUtil.java @@ -0,0 +1,250 @@ +package io.arex.inst.runtime.util; + +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; +import io.arex.agent.bootstrap.util.NumberUtil; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.log.LogManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.model.MergeReplayType; +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; + +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * merge record and replay util + */ +public class MergeRecordReplayUtil { + private static final AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(); + + private MergeRecordReplayUtil() {} + + public static void mergeRecord(Mocker requestMocker) { + int methodSignatureHash = MockUtils.methodSignatureHash(requestMocker); + MergeDTO mergeDTO = MergeDTO.of(requestMocker.getCategoryType().getName(), + methodSignatureHash, + requestMocker.getOperationName(), + requestMocker.getTargetRequest().getBody(), + requestMocker.getTargetResponse().getBody(), + requestMocker.getTargetResponse().getType(), + requestMocker.getTargetRequest().getAttributes(), + requestMocker.getTargetResponse().getAttributes(), + // save recordId prevent miss after clear context at async thread + ContextManager.currentContext() != null ? ContextManager.currentContext().getCaseId() : StringUtil.EMPTY); + mergeDTO.setCreationTime(requestMocker.getCreationTime()); + mergeDTO.setMethodRequestTypeHash(MockUtils.methodRequestTypeHash(requestMocker)); + List> mergeList = merge(mergeDTO); + batchRecord(mergeList); + } + + /** + * merge duplicate mocker to queue and return merged result after reach batch count + * @return if null mean no exceed merge max time limit not need to record or no merge + */ + public static List> merge(MergeDTO mergeDTO) { + try { + ArexContext context = ContextManager.currentContext(); + if (context == null) { + return Collections.emptyList(); + } + + LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); + // offer queue to avoid block current thread + if (!mergeRecordQueue.offer(mergeDTO)) { + // dynamic class not replay compare, log warn temporarily + LogManager.warn("merge.record.fail", "queue is full"); + return Collections.emptyList(); + } + int recordThreshold = Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT); + if (mergeRecordQueue.size() < recordThreshold) { + return Collections.emptyList(); + } + + List mergeList = new ArrayList<>(); + mergeRecordQueue.drainTo(mergeList, recordThreshold); + return checkAndSplit(mergeList); + } catch (Exception e) { + LogManager.warn("merge.record.error", e); + return Collections.emptyList(); + } + } + + /** + * check memory size or split to multiple list if exceed size limit + */ + public static List> checkAndSplit(List mergeList) { + mergeList = CollectionUtil.filterNull(mergeList); + if (CollectionUtil.isEmpty(mergeList)) { + return Collections.emptyList(); + } + List> mergeTotalList = new ArrayList<>(); + Map> mergeRecordGroupMap = group(mergeList); + for (Map.Entry> mergeRecordEntry : mergeRecordGroupMap.entrySet()) { + // check memory size + if (agentSizeOf.checkMemorySizeLimit(mergeRecordEntry.getValue(), ArexConstants.MEMORY_SIZE_5MB)) { + mergeTotalList.add(mergeRecordEntry.getValue()); + } else { + // exceed size limit and split to multiple list + mergeTotalList.addAll(split(mergeRecordEntry.getValue())); + } + } + return mergeTotalList; + } + + /** + * group by category(such as: dynamicClass、redis) + */ + private static Map> group(List mergeList) { + Map> mergeGroupMap = new HashMap<>(); + for (MergeDTO mergeDTO : mergeList) { + if (mergeDTO == null) { + continue; + } + String category = mergeDTO.getCategory(); + mergeGroupMap.computeIfAbsent(category, k -> new ArrayList<>()).add(mergeDTO); + } + return mergeGroupMap; + } + + /** + * split strategy: + * 1. split by config count: list[10] A -> list[5] B、list[5] C + * 2. check memory size separate list + * 3. if memory size not exceed limit return: list[[5]、[5]] R + * 4. if memory size exceed limit, split to single-size list and return: + * list[[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]、[1]] R + */ + private static List> split(List mergeList) { + List> splitTotalList = new ArrayList<>(); + // default split in half + int splitCount = Config.get().getInt(ArexConstants.MERGE_SPLIT_COUNT, 2); + List> splitResultList = CollectionUtil.split(mergeList, splitCount); + for (List splitList : splitResultList) { + if (agentSizeOf.checkMemorySizeLimit(splitList, ArexConstants.MEMORY_SIZE_5MB)) { + splitTotalList.add(splitList); + } else { + // split to single-size list + splitTotalList.addAll(CollectionUtil.split(splitList, splitList.size())); + logBigSize(splitList); + } + } + LogManager.info("merge.record.split", StringUtil.format("original size: %s, split count: %s", + mergeList.size() + "", splitTotalList.size() + "")); + return splitTotalList; + } + + private static void logBigSize(List mergeRecordList) { + for (MergeDTO mergeDTO : mergeRecordList) { + LogManager.warn("merge.record.size.too.large", + StringUtil.format("please check following record data, if is dynamic class, suggest replace it, " + + "category: %s, operationName: %s", + mergeDTO.getCategory(), mergeDTO.getOperationName())); + } + } + + private static void batchRecord(List> splitList) { + MockCategoryType categoryType; + for (List mergeRecords : splitList) { + categoryType = MockCategoryType.of(mergeRecords.get(0).getCategory()); + Mocker mergeMocker = MockUtils.create(categoryType, ArexConstants.MERGE_RECORD_NAME); + mergeMocker.setRecordId(mergeRecords.get(0).getRecordId()); + mergeMocker.getTargetResponse().setBody(Serializer.serialize(mergeRecords)); + mergeMocker.getTargetResponse().setType(ArexConstants.MERGE_TYPE); + MockUtils.executeRecord(mergeMocker); + } + } + + public static void recordRemain(ArexContext context) { + if (context == null) { + return; + } + LinkedBlockingQueue mergeRecordQueue = context.getMergeRecordQueue(); + if (mergeRecordQueue.isEmpty()) { + return; + } + try { + List mergeRecordList = new ArrayList<>(); + mergeRecordQueue.drainTo(mergeRecordList); + List> splitList = checkAndSplit(mergeRecordList); + if (CollectionUtil.isEmpty(splitList)) { + return; + } + batchRecord(splitList); + } catch (Exception e) { + LogManager.warn("merge.record.remain.error", e); + } + } + + /** + * init replay and cached replay result + */ + public static void mergeReplay() { + if (!ContextManager.needReplay()) { + return; + } + Map> cachedReplayResultMap = ContextManager.currentContext().getCachedReplayResultMap(); + // if there are other types that need to be mergeReplay in the future, please add to MergeReplayType + for (MergeReplayType mergeReplayType : MergeReplayType.values()) { + Mocker mergeMocker = MockUtils.create(mergeReplayType.getMockCategoryType(), ArexConstants.MERGE_RECORD_NAME); + int callReplayMax; + int replayCount = 0; + do { + Mocker responseMocker = MockUtils.executeReplay(mergeMocker, MockStrategyEnum.OVER_BREAK); + if (!MockUtils.checkResponseMocker(responseMocker)) { + break; + } + List mergeReplayList = Serializer.deserialize(responseMocker.getTargetResponse().getBody(), + ArexConstants.MERGE_TYPE); + if (CollectionUtil.isEmpty(mergeReplayList)) { + LogManager.warn("merge.replay.fail", "mergeReplayList is empty"); + break; + } + buildReplayResultMap(mergeReplayList, cachedReplayResultMap); + replayCount ++; + callReplayMax = getCallReplayMax(responseMocker); + } while (replayCount < callReplayMax); + } + + // ascending order + sortByCreationTime(cachedReplayResultMap); + } + + private static void buildReplayResultMap(List mergeReplayList, Map> cachedReplayResultMap) { + for (int i = 0; i < mergeReplayList.size(); i++) { + MergeDTO mergeReplayDTO = mergeReplayList.get(i); + if (mergeReplayDTO == null) { + continue; + } + cachedReplayResultMap.computeIfAbsent(mergeReplayDTO.getMethodRequestTypeHash(), k -> new ArrayList<>()).add(mergeReplayDTO); + } + } + + private static void sortByCreationTime(Map> cachedReplayResultMap) { + for (List mergeReplayList : cachedReplayResultMap.values()) { + if (mergeReplayList.size() == 1) { + continue; + } + mergeReplayList.sort((o1, o2) -> { + if (o1.getCreationTime() == o2.getCreationTime()) { + return 0; + } + return o1.getCreationTime() - o2.getCreationTime() > 0 ? 1 : -1; + }); + } + } + + private static int getCallReplayMax(Mocker replayMocker) { + Mocker.Target targetResponse = replayMocker.getTargetResponse(); + int callReplayMax = NumberUtil.toInt(String.valueOf(targetResponse.getAttribute(ArexConstants.CALL_REPLAY_MAX))); + int replayThreshold = Config.get().getInt(ArexConstants.MERGE_REPLAY_THRESHOLD, ArexConstants.MERGE_REPLAY_THRESHOLD_DEFAULT); + return callReplayMax == 0 ? replayThreshold : callReplayMax; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java index 5f506637f..af7210d9f 100644 --- a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/MockUtils.java @@ -5,14 +5,17 @@ import io.arex.agent.bootstrap.model.MockStrategyEnum; import io.arex.agent.bootstrap.model.Mocker; import io.arex.agent.bootstrap.model.Mocker.Target; +import io.arex.agent.bootstrap.util.MapUtils; import io.arex.agent.bootstrap.util.StringUtil; import io.arex.inst.runtime.log.LogManager; import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.match.ReplayMatcher; +import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.service.DataService; - +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; public final class MockUtils { @@ -92,9 +95,23 @@ public static void recordMocker(Mocker requestMocker) { if (CaseManager.isInvalidCase(requestMocker.getRecordId())) { return; } + if (requestMocker.isNeedMerge()) { + MergeRecordReplayUtil.mergeRecord(requestMocker); + return; + } + + executeRecord(requestMocker); + + if (requestMocker.getCategoryType().isEntryPoint()) { + // after main entry record finished, record remain merge mocker that have not reached the merge threshold once(such as dynamicClass) + MergeRecordReplayUtil.recordRemain(ContextManager.currentContext()); + } + } + public static void executeRecord(Mocker requestMocker) { if (Config.get().isEnableDebug()) { - LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", requestMocker.logBuilder().toString(), Serializer.serialize(requestMocker))); + LogManager.info(requestMocker.recordLogTitle(), StringUtil.format("%s%nrequest: %s", + requestMocker.logBuilder().toString(), Serializer.serialize(requestMocker))); } DataService.INSTANCE.save(requestMocker); @@ -110,6 +127,18 @@ public static Mocker replayMocker(Mocker requestMocker, MockStrategyEnum mockStr return null; } + if (requestMocker.isNeedMerge()) { + Mocker matchMocker = ReplayMatcher.match(requestMocker, mockStrategy); + // compatible with old version(fixed case without merge) + if (matchMocker != null) { + return matchMocker; + } + } + + return executeReplay(requestMocker, mockStrategy); + } + + public static Mocker executeReplay(Mocker requestMocker, MockStrategyEnum mockStrategy) { String postJson = Serializer.serialize(requestMocker); String data = DataService.INSTANCE.query(postJson, mockStrategy); @@ -117,7 +146,8 @@ public static Mocker replayMocker(Mocker requestMocker, MockStrategyEnum mockStr boolean isEnableDebug = Config.get().isEnableDebug(); if (isEnableDebug) { - LogManager.info(requestMocker.replayLogTitle(), StringUtil.format("%s%nrequest: %s%nresponse: %s", requestMocker.logBuilder().toString(), postJson, data)); + LogManager.info(requestMocker.replayLogTitle(), StringUtil.format("%s%nrequest: %s%nresponse: %s", + requestMocker.logBuilder().toString(), postJson, data)); } if (StringUtil.isEmpty(data) || EMPTY_JSON.equals(data)) { @@ -163,7 +193,13 @@ public static boolean checkResponseMocker(Mocker responseMocker) { } final String body = targetResponse.getBody(); if (StringUtil.isEmpty(body)) { - LogManager.info(logTitle, "The body of targetResponse is empty"); + String exceedSizeLog = StringUtil.EMPTY; + if (MapUtils.getBoolean(targetResponse.getAttributes(), ArexConstants.EXCEED_MAX_SIZE_FLAG)) { + exceedSizeLog = StringUtil.format( + ", method:%s, because exceed memory max limit:%s, please check method return size, suggest replace it", + responseMocker.getOperationName(), AgentSizeOf.humanReadableUnits(ArexConstants.MEMORY_SIZE_1MB)); + } + LogManager.info(logTitle, "The body of targetResponse is empty" + exceedSizeLog); return false; } final String clazzType = targetResponse.getType(); @@ -174,4 +210,17 @@ public static boolean checkResponseMocker(Mocker responseMocker) { return true; } + + public static int methodSignatureHash(Mocker requestMocker) { + return StringUtil.encodeAndHash(String.format("%s_%s", + requestMocker.getOperationName(), + requestMocker.getTargetRequest().getBody())); + } + + public static int methodRequestTypeHash(Mocker requestMocker) { + return StringUtil.encodeAndHash(String.format("%s_%s_%s", + requestMocker.getCategoryType().getName(), + requestMocker.getOperationName(), + requestMocker.getTargetRequest().getType())); + } } diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java new file mode 100644 index 000000000..2d9d089e6 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/AgentSizeOf.java @@ -0,0 +1,98 @@ +package io.arex.inst.runtime.util.sizeof; + +import io.arex.agent.bootstrap.InstrumentationHolder; +import io.arex.agent.bootstrap.util.StringUtil; +import io.arex.inst.runtime.log.LogManager; + +import java.lang.instrument.Instrumentation; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.util.Locale; + +/** + * refer: https://github.com/ehcache/sizeof + */ +public class AgentSizeOf { + /** One kilobyte bytes. */ + public static final long ONE_KB = 1024; + /** One megabyte bytes. */ + public static final long ONE_MB = ONE_KB * ONE_KB; + /** One gigabyte bytes.*/ + public static final long ONE_GB = ONE_KB * ONE_MB; + private final Instrumentation instrumentation; + private final ObjectGraphWalker walker; + private AgentSizeOf(SizeOfFilter fieldFilter) { + this.instrumentation = InstrumentationHolder.getInstrumentation(); + ObjectGraphWalker.Visitor visitor = new SizeOfVisitor(); + this.walker = new ObjectGraphWalker(visitor, fieldFilter); + } + + /** + * Measures the size in memory (heap) of the objects passed in, walking their graph down + * Any overlap of the graphs being passed in will be recognized and only measured once + * + * @return the total size in bytes for these objects + */ + public long deepSizeOf(long sizeThreshold, Object... obj) { + return walker.walk(null, sizeThreshold, obj); + } + + public static AgentSizeOf newInstance(final SizeOfFilter... filters) { + final SizeOfFilter filter = new CombinationSizeOfFilter(filters); + return new AgentSizeOf(filter); + } + + /** + * Will return the sizeOf each instance + */ + private class SizeOfVisitor implements ObjectGraphWalker.Visitor { + public long visit(Object object) { + return sizeOf(object); + } + } + + public long sizeOf(Object obj) { + if (instrumentation == null) { + return 0; + } + return instrumentation.getObjectSize(obj); + } + + /** + * Returns size in human-readable units (GB, MB, KB or bytes). + */ + public static String humanReadableUnits(long bytes) { + return humanReadableUnits(bytes, + new DecimalFormat("0.#", DecimalFormatSymbols.getInstance(Locale.ROOT))); + } + + /** + * Returns size in human-readable units (GB, MB, KB or bytes). + */ + public static String humanReadableUnits(long bytes, DecimalFormat df) { + if (bytes / ONE_GB > 0) { + return df.format((float) bytes / ONE_GB) + " GB"; + } else if (bytes / ONE_MB > 0) { + return df.format((float) bytes / ONE_MB) + " MB"; + } else if (bytes / ONE_KB > 0) { + return df.format((float) bytes / ONE_KB) + " KB"; + } else { + return bytes + " bytes"; + } + } + + /** + * @return true: not exceed memory size limit + */ + public boolean checkMemorySizeLimit(Object obj, long sizeLimit) { + long start = System.currentTimeMillis(); + long memorySize = deepSizeOf(sizeLimit, obj); + long cost = System.currentTimeMillis() - start; + if (cost > 50) { // used statistical performance can be removed in the future + LogManager.info("check.memory.size", + StringUtil.format("size: %s, cost: %s ms", + humanReadableUnits(memorySize), String.valueOf(cost))); + } + return memorySize < sizeLimit; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java new file mode 100644 index 000000000..6d57ca79f --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilter.java @@ -0,0 +1,44 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.reflect.Field; +import java.util.Collection; + +/** + * Filter combining multiple filters + */ +public class CombinationSizeOfFilter implements SizeOfFilter { + + private final SizeOfFilter[] filters; + + /** + * Constructs a filter combining multiple ones + * + * @param filters the filters to combine + */ + public CombinationSizeOfFilter(SizeOfFilter... filters) { + this.filters = filters; + } + + /** + * {@inheritDoc} + */ + public Collection filterFields(Class klazz, Collection fields) { + Collection current = fields; + for (SizeOfFilter filter : filters) { + current = filter.filterFields(klazz, current); + } + return current; + } + + /** + * {@inheritDoc} + */ + public boolean filterClass(Class klazz) { + for (SizeOfFilter filter : filters) { + if (!filter.filterClass(klazz)) { + return false; + } + } + return true; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java new file mode 100644 index 000000000..922a4d590 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalker.java @@ -0,0 +1,174 @@ +package io.arex.inst.runtime.util.sizeof; + +import io.arex.agent.bootstrap.internal.WeakCache; + +import java.lang.ref.SoftReference; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.IdentityHashMap; +import java.util.Set; + +import static java.util.Collections.newSetFromMap; + +public class ObjectGraphWalker { + private final WeakCache, SoftReference>> fieldCache = new WeakCache<>(); + private final WeakCache, Boolean> classCache = new WeakCache<>(); + private final SizeOfFilter sizeOfFilter; + private final Visitor visitor; + + /** + * Constructor + * + * @param visitor the visitor to use + * @param filter the filtering + * @see Visitor + * @see SizeOfFilter + */ + ObjectGraphWalker(Visitor visitor, SizeOfFilter filter) { + if(visitor == null) { + throw new NullPointerException("Visitor can't be null"); + } + if(filter == null) { + throw new NullPointerException("SizeOfFilter can't be null"); + } + this.visitor = visitor; + this.sizeOfFilter = filter; + } + + /** + * The visitor to execute the function on each node of the graph + * This is only to be used for the sizing of an object graph in memory! + */ + interface Visitor { + /** + * The visit method executed on each node + * + * @param object the reference at that node + * @return a long for you to do things with... + */ + long visit(Object object); + } + + /** + * Walk the graph and call into the "visitor" + * + * @param visitorListener A decorator for the Visitor + * @param sizeThreshold if > 0 and exceed sizeThreshold will stop walk + * @param root the roots of the objects (a shared graph will only be visited once) + * @return the sum of all Visitor#visit returned values + */ + long walk(VisitorListener visitorListener, long sizeThreshold, Object... root) { + long result = 0; + Deque toVisit = new ArrayDeque<>(); + Set visited = newSetFromMap(new IdentityHashMap<>()); + + nullSafeAddArray(toVisit, root); + + while (!toVisit.isEmpty()) { + Object ref = toVisit.pop(); + if (!visited.add(ref) || !shouldWalkClass(ref.getClass())) { + continue; + } + + walkField(ref, toVisit); + + final long visitSize = visitor.visit(ref); + if (visitorListener != null) { + visitorListener.visited(ref, visitSize); + } + result += visitSize; + if (sizeThreshold > 0 && result >= sizeThreshold) { + return result; + } + } + + return result; + } + + private void walkField(Object ref, Deque toVisit) { + Class refClass = ref.getClass(); + if (refClass.isArray() && !refClass.getComponentType().isPrimitive()) { + for (int i = 0; i < Array.getLength(ref); i++) { + nullSafeAdd(toVisit, Array.get(ref, i)); + } + return; + } + for (Field field : getFilteredFields(refClass)) { + try { + nullSafeAdd(toVisit, field.get(ref)); + } catch (IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + } + + /** + * Returns the filtered fields for a particular type + * + * @param refClass the type + * @return A collection of fields to be visited + */ + private Collection getFilteredFields(Class refClass) { + SoftReference> ref = fieldCache.get(refClass); + Collection fieldList = ref != null ? ref.get() : null; + if (fieldList != null) { + return fieldList; + } else { + Collection result; + result = sizeOfFilter.filterFields(refClass, getAllFields(refClass)); + fieldCache.put(refClass, new SoftReference<>(result)); + return result; + } + } + + private boolean shouldWalkClass(Class refClass) { + Boolean cached = classCache.get(refClass); + if (cached == null) { + cached = sizeOfFilter.filterClass(refClass); + classCache.put(refClass, cached); + } + return cached; + } + + private static void nullSafeAddArray(final Deque toVisit, final Object... root) { + if (root != null) { + for (Object object : root) { + nullSafeAdd(toVisit, object); + } + } + } + + private static void nullSafeAdd(final Deque toVisit, final Object o) { + if (o != null) { + toVisit.push(o); + } + } + + /** + * Returns all non-primitive fields for the entire class hierarchy of a type + * + * @param refClass the type + * @return all fields for that type + */ + private static Collection getAllFields(Class refClass) { + Collection fields = new ArrayList<>(); + for (Class klazz = refClass; klazz != null; klazz = klazz.getSuperclass()) { + for (Field field : klazz.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && !field.getType().isPrimitive()) { + try { + field.setAccessible(true); + } catch (RuntimeException e) { + continue; + } + fields.add(field); + } + } + } + return fields; + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java new file mode 100644 index 000000000..348641423 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/SizeOfFilter.java @@ -0,0 +1,27 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.reflect.Field; +import java.util.Collection; + +/** + * Filter to filter types or fields of object graphs passed to a SizeOf engine + */ +public interface SizeOfFilter { + + /** + * Returns the fields to walk and measure for a type + * + * @param klazz the type + * @param fields the fields already "qualified" + * @return the filtered Set + */ + Collection filterFields(Class klazz, Collection fields); + + /** + * Checks whether the type needs to be filtered + * + * @param klazz the type + * @return true, if to be filtered out + */ + boolean filterClass(Class klazz); +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ThrowableFilter.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ThrowableFilter.java new file mode 100644 index 000000000..215da3c41 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/ThrowableFilter.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.util.sizeof; + +import java.lang.reflect.Field; +import java.util.Collection; +import java.util.Collections; + +public class ThrowableFilter implements SizeOfFilter { + public static final ThrowableFilter INSTANCE = new ThrowableFilter(); + @Override + public Collection filterFields(Class klazz, Collection fields) { + return !Throwable.class.isAssignableFrom(klazz) ? fields : Collections.emptyList(); + } + + @Override + public boolean filterClass(Class klazz) { + return !Throwable.class.isAssignableFrom(klazz); + } +} diff --git a/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/VisitorListener.java b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/VisitorListener.java new file mode 100644 index 000000000..65a42b722 --- /dev/null +++ b/arex-instrumentation-api/src/main/java/io/arex/inst/runtime/util/sizeof/VisitorListener.java @@ -0,0 +1,5 @@ +package io.arex.inst.runtime.util.sizeof; + +public interface VisitorListener { + void visited(final Object object, final long size); +} diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java new file mode 100644 index 000000000..29c758752 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AbstractMatchStrategyTest.java @@ -0,0 +1,46 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.model.MergeDTO; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; + +class AbstractMatchStrategyTest { + static AbstractMatchStrategy target; + static ArexMocker mocker; + + @BeforeAll + static void setUp() { + target = new AccurateMatchStrategy(); + mocker = new ArexMocker(); + mocker.setTargetResponse(new Mocker.Target()); + mocker.setTargetRequest(new Mocker.Target()); + mocker.getTargetRequest().setBody("mock"); + } + + @AfterAll + static void tearDown() { + target = null; + } + + @Test + void match() { + assertDoesNotThrow(() -> target.match(null)); + MatchStrategyContext context = new MatchStrategyContext(mocker, new ArrayList<>(), null); + assertDoesNotThrow(() -> target.match(context)); + } + + @Test + void buildMatchedMocker() { + Mocker result = target.buildMatchedMocker(mocker, null); + assertNull(result); + result = target.buildMatchedMocker(mocker, new MergeDTO()); + assertNotNull(result); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java new file mode 100644 index 000000000..1143980a0 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/AccurateMatchStrategyTest.java @@ -0,0 +1,96 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +class AccurateMatchStrategyTest { + + static AccurateMatchStrategy accurateMatchStrategy; + + @BeforeAll + static void setUp() { + accurateMatchStrategy = new AccurateMatchStrategy(); + Mockito.mockStatic(MockUtils.class); + } + + @AfterAll + static void tearDown() { + accurateMatchStrategy = null; + Mockito.clearAllCaches(); + } + + @ParameterizedTest + @MethodSource("processCase") + void process(MatchStrategyContext context, Predicate asserts) { + accurateMatchStrategy.process(context); + asserts.test(context); + } + + static Stream processCase() { + Supplier contextSupplier1 = () -> { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetResponse(new Mocker.Target()); + mocker.setTargetRequest(new Mocker.Target()); + mocker.setCategoryType(MockCategoryType.DYNAMIC_CLASS); + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeReplayList.add(mergeDTO); + return new MatchStrategyContext(mocker, mergeReplayList, MockStrategyEnum.FIND_LAST); + }; + Supplier contextSupplier2 = () -> { + MatchStrategyContext context = contextSupplier1.get(); + context.getMergeReplayList().get(0).setMatched(true); + context.setMockStrategy(MockStrategyEnum.STRICT_MATCH); + return context; + }; + Supplier contextSupplier3 = () -> { + MatchStrategyContext context = contextSupplier1.get(); + MergeDTO mergeDTO = new MergeDTO(); + context.getMergeReplayList().add(mergeDTO); + return context; + }; + Supplier contextSupplier4 = () -> { + MatchStrategyContext context = contextSupplier1.get(); + context.getMergeReplayList().get(0).setMethodSignatureHash(1); + context.setMockStrategy(MockStrategyEnum.STRICT_MATCH); + return context; + }; + + Predicate asserts1 = context -> !context.isInterrupt(); + Predicate asserts2 = MatchStrategyContext::isInterrupt; + + return Stream.of( + arguments(contextSupplier1.get(), asserts1), + arguments(contextSupplier2.get(), asserts2), + arguments(contextSupplier3.get(), asserts1), + arguments(contextSupplier4.get(), asserts2) + ); + } + + @Test + void internalCheck() { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetRequest(new Mocker.Target()); + assertFalse(accurateMatchStrategy.internalCheck(new MatchStrategyContext(mocker, null, null))); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java new file mode 100644 index 000000000..bf993f056 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/EigenMatchStrategyTest.java @@ -0,0 +1,21 @@ +package io.arex.inst.runtime.match; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class EigenMatchStrategyTest { + static EigenMatchStrategy eigenMatchStrategy; + + @BeforeAll + static void setUp() { + eigenMatchStrategy = new EigenMatchStrategy(); + } + + @AfterAll + static void tearDown() { + eigenMatchStrategy = null; + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java new file mode 100644 index 000000000..8b10bb098 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/FuzzyMatchStrategyTest.java @@ -0,0 +1,60 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.util.MockUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class FuzzyMatchStrategyTest { + static FuzzyMatchStrategy fuzzyMatchStrategy; + + @BeforeAll + static void setUp() { + fuzzyMatchStrategy = new FuzzyMatchStrategy(); + Mockito.mockStatic(Config.class); + Mockito.when(Config.get()).thenReturn(Mockito.mock(Config.class)); + Mockito.mockStatic(MockUtils.class); + } + + @AfterAll + static void tearDown() { + fuzzyMatchStrategy = null; + Mockito.clearAllCaches(); + } + + @Test + void process() { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetResponse(new Mocker.Target()); + mocker.setTargetRequest(new Mocker.Target()); + mocker.setCategoryType(MockCategoryType.DYNAMIC_CLASS); + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeReplayList.add(mergeDTO); + MatchStrategyContext context =new MatchStrategyContext(mocker, mergeReplayList, MockStrategyEnum.FIND_LAST); + Mockito.when(Config.get().isEnableDebug()).thenReturn(true); + fuzzyMatchStrategy.process(context); + assertNotNull(context.getMatchMocker()); + + mergeDTO.setMatched(true); + fuzzyMatchStrategy.process(context); + assertNotNull(context.getMatchMocker()); + } + + @Test + void internalCheck() { + assertFalse(fuzzyMatchStrategy.internalCheck(new MatchStrategyContext(null, null, null))); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyContextTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyContextTest.java new file mode 100644 index 000000000..ec00f59cd --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyContextTest.java @@ -0,0 +1,65 @@ +package io.arex.inst.runtime.match; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MatchStrategyContextTest { + static MatchStrategyContext context; + + @BeforeAll + static void setUp() { + context = new MatchStrategyContext(null, null, null); + } + + @Test + void getRequestMocker() { + assertNull(context.getRequestMocker()); + } + + @Test + void setRequestMocker() { + assertDoesNotThrow(() -> context.setRequestMocker(null)); + } + + @Test + void getMergeReplayList() { + assertNull(context.getMergeReplayList()); + } + + @Test + void setMergeReplayList() { + assertDoesNotThrow(() -> context.setMergeReplayList(null)); + } + + @Test + void getMockStrategy() { + assertNull(context.getMockStrategy()); + } + + @Test + void setMockStrategy() { + assertDoesNotThrow(() -> context.setMockStrategy(null)); + } + + @Test + void isInterrupt() { + assertFalse(context.isInterrupt()); + } + + @Test + void setInterrupt() { + assertDoesNotThrow(() -> context.setInterrupt(false)); + } + + @Test + void getMatchMocker() { + assertNull(context.getMatchMocker()); + } + + @Test + void setMatchMocker() { + assertDoesNotThrow(() -> context.setMatchMocker(null)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyRegisterTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyRegisterTest.java new file mode 100644 index 000000000..8357b34ea --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/MatchStrategyRegisterTest.java @@ -0,0 +1,14 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.MockCategoryType; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MatchStrategyRegisterTest { + + @Test + void getMatchStrategies() { + assertNotNull(MatchStrategyRegister.getMatchStrategies(MockCategoryType.DYNAMIC_CLASS)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/ReplayMatcherTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/ReplayMatcherTest.java new file mode 100644 index 000000000..58aa075ab --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/match/ReplayMatcherTest.java @@ -0,0 +1,60 @@ +package io.arex.inst.runtime.match; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.MockStrategyEnum; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.MockUtils; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +class ReplayMatcherTest { + + @BeforeAll + static void setUp() { + Mockito.mockStatic(MockUtils.class); + Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(Config.class); + Mockito.when(Config.get()).thenReturn(Mockito.mock(Config.class)); + Mockito.mockStatic(MatchStrategyRegister.class); + } + + @AfterAll + static void tearDown() { + Mockito.clearAllCaches(); + } + + @Test + void match() { + ArexMocker requestMocker = new ArexMocker(MockCategoryType.DYNAMIC_CLASS); + requestMocker.setOperationName("mock"); + requestMocker.setTargetRequest(new Mocker.Target()); + requestMocker.setTargetResponse(new Mocker.Target()); + ArexContext context = Mockito.mock(ArexContext.class); + Mockito.when(ContextManager.currentContext()).thenReturn(context); + Map> cachedReplayResultMap = new HashMap<>(); + Mockito.when(context.getCachedReplayResultMap()).thenReturn(cachedReplayResultMap); + assertNull(ReplayMatcher.match(requestMocker, MockStrategyEnum.FIND_LAST)); + + Mockito.when(MockUtils.methodRequestTypeHash(requestMocker)).thenReturn(1); + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeReplayList.add(mergeDTO); + cachedReplayResultMap.put(1, mergeReplayList); + Mockito.when(MatchStrategyRegister.getMatchStrategies(any())).thenReturn(Collections.singletonList(new AccurateMatchStrategy())); + Mockito.when(Config.get().isEnableDebug()).thenReturn(true); + assertNull(ReplayMatcher.match(requestMocker, MockStrategyEnum.FIND_LAST)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandlerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandlerTest.java new file mode 100644 index 000000000..57783b3b4 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordDubboRequestHandlerTest.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.request; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MergeRecordDubboRequestHandlerTest { + + @Test + void name() { + assertNotNull(new MergeRecordDubboRequestHandler().name()); + } + + @Test + void handleAfterCreateContext() { + assertDoesNotThrow(() -> new MergeRecordDubboRequestHandler().handleAfterCreateContext("mock")); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandlerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandlerTest.java new file mode 100644 index 000000000..74cf18180 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/MergeRecordServletRequestHandlerTest.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.request; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class MergeRecordServletRequestHandlerTest { + + @Test + void name() { + assertNotNull(new MergeRecordServletRequestHandler().name()); + } + + @Test + void handleAfterCreateContext() { + assertDoesNotThrow(() -> new MergeRecordServletRequestHandler().handleAfterCreateContext("mock")); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java index fab9cf0f9..2337364f8 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/request/RequestHandlerManagerTest.java @@ -21,6 +21,7 @@ static void setUp() { Mockito.when(requestHandler.name()).thenReturn("test"); Mockito.when(requestHandlerError.name()).thenReturn("testError"); Mockito.doThrow(new RuntimeException()).when(requestHandlerError).preHandle("request"); + Mockito.doThrow(new RuntimeException()).when(requestHandlerError).handleAfterCreateContext("request"); Mockito.doThrow(new RuntimeException()).when(requestHandlerError).postHandle("request", "response"); Mockito.when(ServiceLoader.load(RequestHandler.class)).thenReturn(Arrays.asList(requestHandler, requestHandlerError)); RequestHandlerManager.init(); @@ -46,6 +47,19 @@ void preHandle() { } + @Test + void handleAfterCreateContext() { + // null handler + RequestHandlerManager.handleAfterCreateContext(null, "test2"); + Mockito.verify(requestHandler, Mockito.never()).handleAfterCreateContext("test2"); + // normal + RequestHandlerManager.handleAfterCreateContext("request", "test"); + Mockito.verify(requestHandler, Mockito.times(1)).handleAfterCreateContext("request"); + // error + assertDoesNotThrow(() -> RequestHandlerManager.handleAfterCreateContext("request", "testError")); + + } + @Test void postHandle() { // null handler diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MergeRecordReplayUtilTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MergeRecordReplayUtilTest.java new file mode 100644 index 000000000..be2df729a --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MergeRecordReplayUtilTest.java @@ -0,0 +1,180 @@ +package io.arex.inst.runtime.util; + +import io.arex.agent.bootstrap.model.ArexMocker; +import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.Assert; +import io.arex.inst.runtime.config.Config; +import io.arex.inst.runtime.context.ArexContext; +import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.ArexConstants; +import io.arex.inst.runtime.model.MergeDTO; +import io.arex.inst.runtime.serializer.Serializer; +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.*; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.params.provider.Arguments.arguments; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.times; + +@ExtendWith(MockitoExtension.class) +class MergeRecordReplayUtilTest { + static MockedStatic mockUtils; + static AgentSizeOf agentSizeOf; + static ArexMocker requestMocker; + + @BeforeAll + static void setUp() { + agentSizeOf = Mockito.mock(AgentSizeOf.class); + Mockito.mockStatic(AgentSizeOf.class); + Mockito.when(AgentSizeOf.newInstance()).thenReturn(agentSizeOf); + mockUtils = Mockito.mockStatic(MockUtils.class); + Mockito.mockStatic(ContextManager.class); + Mockito.mockStatic(Config.class); + Mockito.when(Config.get()).thenReturn(Mockito.mock(Config.class)); + requestMocker = new ArexMocker(MockCategoryType.DYNAMIC_CLASS); + requestMocker.setOperationName("mock"); + requestMocker.setTargetRequest(new Mocker.Target()); + requestMocker.setTargetResponse(new Mocker.Target()); + Mockito.when(MockUtils.create(any(), any())).thenReturn(requestMocker); + Mockito.mockStatic(Serializer.class); + } + + @AfterAll + static void tearDown() { + mockUtils = null; + agentSizeOf = null; + requestMocker = null; + Mockito.clearAllCaches(); + } + + @ParameterizedTest + @MethodSource("mergeRecordCase") + void mergeRecord(Runnable mocker, Mocker requestMocker, Assert asserts) { + mocker.run(); + MergeRecordReplayUtil.mergeRecord(requestMocker); + assertDoesNotThrow(asserts::verity); + } + + static Stream mergeRecordCase() { + Runnable emptyMocker = () -> {}; + ArexContext context = Mockito.mock(ArexContext.class); + LinkedBlockingQueue mergeRecordQueue = new LinkedBlockingQueue<>(1); + Runnable mocker1 = () -> { + mergeRecordQueue.offer(new MergeDTO()); + Mockito.when(context.getMergeRecordQueue()).thenReturn(mergeRecordQueue); + Mockito.when(ContextManager.currentContext()).thenReturn(context); + }; + Runnable mocker2 = () -> { + mergeRecordQueue.poll(); + Mockito.when(Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT)) + .thenReturn(ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT); + }; + Runnable mocker3 = () -> { + mergeRecordQueue.poll(); + Mockito.when(Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT)) + .thenReturn(0); + }; + Runnable mocker4 = () -> { + mergeRecordQueue.poll(); + Mockito.when(Config.get().getInt(ArexConstants.MERGE_RECORD_THRESHOLD, ArexConstants.MERGE_RECORD_THRESHOLD_DEFAULT)) + .thenReturn(1); + Mockito.when(agentSizeOf.checkMemorySizeLimit(any(), any(long.class))).thenReturn(true); + }; + Runnable mocker5 = () -> { + mergeRecordQueue.poll(); + Mockito.when(agentSizeOf.checkMemorySizeLimit(any(), any(long.class))).thenReturn(false); + }; + Assert asserts1 = () -> { + mockUtils.verify(() -> MockUtils.executeRecord(any()), times(0)); + }; + Assert asserts2 = () -> { + mockUtils.verify(() -> MockUtils.executeRecord(any()), atLeastOnce()); + }; + return Stream.of( + arguments(emptyMocker, requestMocker, asserts1), + arguments(mocker1, requestMocker, asserts1), + arguments(mocker2, requestMocker, asserts1), + arguments(mocker3, requestMocker, asserts1), + arguments(mocker4, requestMocker, asserts2), + arguments(mocker5, requestMocker, asserts2) + ); + } + + @ParameterizedTest + @MethodSource("recordRemainCase") + void recordRemain(ArexContext context, Predicate asserts) { + MergeRecordReplayUtil.recordRemain(context); + asserts.test(context); + } + + static Stream recordRemainCase() { + Supplier contextSupplier1 = () -> ArexContext.of("mock"); + Supplier contextSupplier2 = () -> { + ArexContext arexContext = contextSupplier1.get(); + arexContext.getMergeRecordQueue().offer(new MergeDTO()); + return arexContext; + }; + + Predicate asserts1 = Objects::isNull; + Predicate asserts2 = Objects::nonNull; + return Stream.of( + arguments(null, asserts1), + arguments(contextSupplier1.get(), asserts2), + arguments(contextSupplier2.get(), asserts2) + ); + } + + @ParameterizedTest + @MethodSource("mergeReplayCase") + void mergeReplay(Runnable mocker) { + mocker.run(); + assertDoesNotThrow(MergeRecordReplayUtil::mergeReplay); + } + + static Stream mergeReplayCase() { + Runnable emptyMocker = () -> {}; + Runnable mocker1 = () -> { + Mockito.when(ContextManager.needReplay()).thenReturn(true); + ArexContext context = Mockito.mock(ArexContext.class); + Mockito.when(ContextManager.currentContext()).thenReturn(context); + Map> cachedReplayResultMap = new HashMap<>(); + Mockito.when(context.getCachedReplayResultMap()).thenReturn(cachedReplayResultMap); + }; + Runnable mocker2 = () -> { + Mockito.when(MockUtils.checkResponseMocker(any())).thenReturn(true); + requestMocker.getTargetResponse().setBody("mock"); + Mockito.when(MockUtils.executeReplay(any(), any())).thenReturn(requestMocker); + }; + Runnable mocker3 = () -> { + List mergeReplayList = new ArrayList<>(); + MergeDTO mergeDTO = new MergeDTO(); + mergeDTO.setMethodRequestTypeHash(1); + mergeReplayList.add(mergeDTO); + Mockito.when(Serializer.deserialize(anyString(), anyString())).thenReturn(mergeReplayList); + }; + return Stream.of( + arguments(emptyMocker), + arguments(mocker1), + arguments(mocker2), + arguments(mocker3) + ); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java index 9c9a83a7c..5284b0977 100644 --- a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/MockUtilsTest.java @@ -6,11 +6,14 @@ import io.arex.agent.bootstrap.model.ArexMocker; import io.arex.agent.bootstrap.model.MockCategoryType; +import io.arex.agent.bootstrap.model.Mocker; import io.arex.inst.runtime.config.ConfigBuilder; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; import io.arex.inst.runtime.listener.EventProcessorTest.TestGsonSerializer; import io.arex.inst.runtime.listener.EventProcessorTest.TestJacksonSerializable; +import io.arex.inst.runtime.match.ReplayMatcher; +import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.serializer.StringSerializable; import io.arex.inst.runtime.service.DataCollector; @@ -39,6 +42,8 @@ static void setUp() { list.add(new TestJacksonSerializable()); list.add(new TestGsonSerializer()); Serializer.builder(list).build(); + Mockito.mockStatic(MergeRecordReplayUtil.class); + Mockito.mockStatic(ReplayMatcher.class); } @AfterAll @@ -57,6 +62,16 @@ void recordMocker() { // invalid case Mockito.when(CaseManager.isInvalidCase(any())).thenReturn(true); Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(dynamicClass)); + + // merge case + Mockito.when(CaseManager.isInvalidCase(any())).thenReturn(false); + ArexMocker servletMocker = MockUtils.createServlet("mock"); + servletMocker.setNeedMerge(true); + Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(servletMocker)); + + // remain case + servletMocker.setNeedMerge(false); + Assertions.assertDoesNotThrow(() -> MockUtils.recordMocker(servletMocker)); } @Test @@ -90,6 +105,11 @@ void replayMocker() { // null replayId and is config file ArexMocker configFile = MockUtils.createConfigFile("test"); assertNotNull(MockUtils.replayBody(configFile)); + + // merge case + configFile.setNeedMerge(true); + Mockito.when(ReplayMatcher.match(any(), any())).thenReturn(configFile); + assertNull(MockUtils.replayBody(configFile)); } @Test @@ -113,6 +133,11 @@ void checkResponseMocker() { // normal mocker dynamicClass.getTargetResponse().setType("java.lang.String"); assertTrue(MockUtils.checkResponseMocker(dynamicClass)); + + // test exceed size limit + dynamicClass.getTargetResponse().setBody(null); + dynamicClass.getTargetResponse().setAttribute(ArexConstants.EXCEED_MAX_SIZE_FLAG, true); + assertFalse(MockUtils.checkResponseMocker(dynamicClass)); } @Test @@ -150,5 +175,24 @@ void createMocker() { actualResult = MockUtils.createDubboStreamProvider("query"); assertEquals(MockCategoryType.DUBBO_STREAM_PROVIDER, actualResult.getCategoryType()); + + actualResult = MockUtils.createNettyProvider("query"); + assertEquals(MockCategoryType.NETTY_PROVIDER, actualResult.getCategoryType()); + } + + @Test + void methodSignatureHash() { + ArexMocker mocker = new ArexMocker(); + mocker.setTargetRequest(new Mocker.Target()); + mocker.getTargetRequest().setBody("mock"); + assertTrue(MockUtils.methodSignatureHash(mocker) > 0); + } + + @Test + void methodRequestTypeHash() { + ArexMocker mocker = new ArexMocker(MockCategoryType.DYNAMIC_CLASS); + mocker.setTargetRequest(new Mocker.Target()); + mocker.getTargetRequest().setBody("mock"); + assertTrue(MockUtils.methodRequestTypeHash(mocker) > 0); } } diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java new file mode 100644 index 000000000..c90800e32 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/AgentSizeOfTest.java @@ -0,0 +1,62 @@ +package io.arex.inst.runtime.util.sizeof; + +import io.arex.agent.bootstrap.InstrumentationHolder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; + +import java.lang.instrument.Instrumentation; + +import static org.junit.jupiter.api.Assertions.*; + +class AgentSizeOfTest { + + static AgentSizeOf caller; + static Instrumentation instrumentation; + + @BeforeAll + static void setUp() { + caller = AgentSizeOf.newInstance(); + Mockito.mockStatic(InstrumentationHolder.class); + instrumentation = Mockito.mock(Instrumentation.class); + } + + @AfterAll + static void tearDown() { + caller = null; + instrumentation = null; + Mockito.clearAllCaches(); + } + + @Test + void deepSizeOf() { + assertEquals(caller.deepSizeOf(1, "mock"), 0); + } + + @Test + void sizeOf() { + assertEquals(caller.sizeOf("mock"), 0); + Mockito.when(InstrumentationHolder.getInstrumentation()).thenReturn(instrumentation); + caller = AgentSizeOf.newInstance(); + assertEquals(caller.sizeOf("mock"), 0); + } + + @ParameterizedTest + @CsvSource({ + "1, 1 bytes", + "1024, 1 KB", + "1048576, 1 MB", + "1073741824, 1 GB" + }) + void humanReadableUnits(long bytes, String expected) { + assertEquals(AgentSizeOf.humanReadableUnits(bytes), expected); + } + + @Test + void checkMemorySizeLimit() { + assertTrue(caller.checkMemorySizeLimit("mock", 1)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilterTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilterTest.java new file mode 100644 index 000000000..8b53e0b45 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/CombinationSizeOfFilterTest.java @@ -0,0 +1,40 @@ +package io.arex.inst.runtime.util.sizeof; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +class CombinationSizeOfFilterTest { + + static CombinationSizeOfFilter caller; + static SizeOfFilter filter; + + @BeforeAll + static void setUp() { + filter = Mockito.mock(SizeOfFilter.class); + caller = new CombinationSizeOfFilter(filter); + } + + @AfterAll + static void tearDown() { + caller = null; + filter = null; + Mockito.clearAllCaches(); + } + + @Test + void filterFields() { + assertNotNull(caller.filterFields(null, null)); + } + + @Test + void filterClass() { + assertFalse(caller.filterClass(null)); + Mockito.when(filter.filterClass(any())).thenReturn(true); + assertTrue(caller.filterClass(null)); + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java new file mode 100644 index 000000000..56706183b --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ObjectGraphWalkerTest.java @@ -0,0 +1,51 @@ +package io.arex.inst.runtime.util.sizeof; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; + +import static org.mockito.ArgumentMatchers.any; + +class ObjectGraphWalkerTest { + static ObjectGraphWalker caller; + static SizeOfFilter sizeOfFilter; + static ObjectGraphWalker.Visitor visitor; + + @BeforeAll + static void setUp() { + sizeOfFilter = Mockito.mock(SizeOfFilter.class); + visitor = Mockito.mock(ObjectGraphWalker.Visitor.class); + caller = new ObjectGraphWalker(visitor, sizeOfFilter); + } + + @AfterAll + static void tearDown() { + caller = null; + sizeOfFilter = null; + visitor = null; + Mockito.clearAllCaches(); + } + + @Test + void walk() { + caller.walk(null, 1); + } + + @Test + void testWalk() { + VisitorListener visitorListener = Mockito.mock(VisitorListener.class); + Mockito.when(sizeOfFilter.filterClass(any())).thenReturn(true); + Collection result = new ArrayList<>(); + result.add(TestWalker.class.getDeclaredFields()[0]); + Mockito.when(sizeOfFilter.filterFields(any(), any())).thenReturn(result); + caller.walk(visitorListener, 1, new Object[]{new TestWalker[]{new TestWalker()}}); + } + + static class TestWalker { + String name; + } +} \ No newline at end of file diff --git a/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ThrowableFilterTest.java b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ThrowableFilterTest.java new file mode 100644 index 000000000..6f4fdc578 --- /dev/null +++ b/arex-instrumentation-api/src/test/java/io/arex/inst/runtime/util/sizeof/ThrowableFilterTest.java @@ -0,0 +1,18 @@ +package io.arex.inst.runtime.util.sizeof; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ThrowableFilterTest { + + @Test + void filterFields() { + assertNull(ThrowableFilter.INSTANCE.filterFields(String.class, null)); + } + + @Test + void filterClass() { + assertFalse(ThrowableFilter.INSTANCE.filterClass(Throwable.class)); + } +} \ No newline at end of file diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java index be6666369..6231d8a0c 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloDubboRequestHandler.java @@ -17,6 +17,11 @@ public String name() { @Override public void preHandle(Map request) { + // no need implement + } + + @Override + public void handleAfterCreateContext(Map request) { // check business application if loaded apollo-client if (ApolloConfigChecker.unloadApollo()) { return; diff --git a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java index 1c2c3767d..8f1e524a8 100644 --- a/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java +++ b/arex-instrumentation/config/arex-apollo/src/main/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandler.java @@ -17,6 +17,11 @@ public String name() { @Override public void preHandle(HttpServletRequest request) { + // no need implement + } + + @Override + public void handleAfterCreateContext(HttpServletRequest request) { // check business application if loaded apollo-client if (ApolloConfigChecker.unloadApollo()) { return; diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java index dbc07151f..04f7d7aa3 100644 --- a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloDubboRequestHandlerTest.java @@ -47,8 +47,8 @@ void name() { } @Test - void preHandle() { - target.preHandle(new HashMap<>()); + void handleAfterCreateContext() { + target.handleAfterCreateContext(new HashMap<>()); mockStaticHelper.verify(() -> ApolloConfigHelper.initAndRecord(any(), any()), atLeastOnce()); } diff --git a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java index 2389fdf6e..27dcab3ed 100644 --- a/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java +++ b/arex-instrumentation/config/arex-apollo/src/test/java/io/arex/inst/config/apollo/ApolloServletV3RequestHandlerTest.java @@ -47,9 +47,9 @@ void name() { } @Test - void preHandle() { + void handleAfterCreateContext() { HttpServletRequest request = Mockito.mock(HttpServletRequest.class); - target.preHandle(request); + target.handleAfterCreateContext(request); mockStaticHelper.verify(() -> ApolloConfigHelper.initAndRecord(any(), any()), atLeastOnce()); } diff --git a/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java b/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java index ccc030921..ffa42c483 100644 --- a/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java +++ b/arex-instrumentation/dubbo/arex-dubbo-alibaba/src/main/java/io/arex/inst/dubbo/alibaba/DubboProviderExtractor.java @@ -25,8 +25,9 @@ public static void onServiceEnter(Invoker invoker, Invocation invocation) { } String caseId = adapter.getCaseId(); String excludeMockTemplate = adapter.getExcludeMockTemplate(); - CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); RequestHandlerManager.preHandle(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); + CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); + RequestHandlerManager.handleAfterCreateContext(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); invocation.getAttachments().put(ArexConstants.ORIGINAL_REQUEST, Serializer.serialize(invocation.getArguments())); } public static void onServiceExit(Invoker invoker, Invocation invocation, Result result) { diff --git a/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java b/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java index 9e2f0ad4e..d66176113 100644 --- a/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java +++ b/arex-instrumentation/dubbo/arex-dubbo-apache-v2/src/main/java/io/arex/inst/dubbo/apache/v2/DubboProviderExtractor.java @@ -28,8 +28,9 @@ public static void onServiceEnter(Invoker invoker, Invocation invocation) { } String caseId = adapter.getCaseId(); String excludeMockTemplate = adapter.getExcludeMockTemplate(); - CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); RequestHandlerManager.preHandle(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); + CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); + RequestHandlerManager.handleAfterCreateContext(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); invocation.setAttachment(ArexConstants.ORIGINAL_REQUEST, Serializer.serialize(invocation.getArguments())); } public static void onServiceExit(Invoker invoker, Invocation invocation, Result result) { diff --git a/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java b/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java index 9ff8d1760..7101bc34d 100644 --- a/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java +++ b/arex-instrumentation/dubbo/arex-dubbo-apache-v3/src/main/java/io/arex/inst/dubbo/apache/v3/DubboProviderExtractor.java @@ -25,8 +25,9 @@ public static void onServiceEnter(Invoker invoker, Invocation invocation) { } String caseId = adapter.getCaseId(); String excludeMockTemplate = adapter.getExcludeMockTemplate(); - CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); RequestHandlerManager.preHandle(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); + CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); + RequestHandlerManager.handleAfterCreateContext(invocation.getAttachments(), MockCategoryType.DUBBO_PROVIDER.getName()); invocation.getAttributes().put(ArexConstants.ORIGINAL_REQUEST, Serializer.serialize(invocation.getArguments())); } public static void onServiceExit(Invoker invoker, Invocation invocation, Result result) { diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java index bcbfc05f1..2df8fe90a 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/main/java/io/arex/inst/dynamic/common/DynamicClassExtractor.java @@ -17,30 +17,28 @@ import io.arex.inst.runtime.config.Config; import io.arex.inst.runtime.context.ArexContext; import io.arex.inst.runtime.context.ContextManager; +import io.arex.inst.runtime.model.ArexConstants; import io.arex.inst.runtime.model.DynamicClassEntity; import io.arex.inst.runtime.serializer.Serializer; -import io.arex.inst.runtime.util.IgnoreUtils; +import io.arex.inst.runtime.util.*; import io.arex.inst.runtime.log.LogManager; -import io.arex.inst.runtime.util.MockUtils; -import io.arex.inst.runtime.util.TypeUtil; + import java.lang.reflect.Array; import java.lang.reflect.Method; import java.time.LocalDateTime; import java.time.LocalTime; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.function.Function; + +import io.arex.inst.runtime.util.sizeof.ThrowableFilter; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; + public class DynamicClassExtractor { - private static final int RESULT_SIZE_MAX = Integer.parseInt(System.getProperty("arex.dynamic.result.size.limit", "1000")); - private static final String SERIALIZER = "gson"; private static final String LISTENABLE_FUTURE = "com.google.common.util.concurrent.ListenableFuture"; private static final String COMPLETABLE_FUTURE = "java.util.concurrent.CompletableFuture"; private static final String NEED_RECORD_TITLE = "dynamic.needRecord"; @@ -66,6 +64,9 @@ public class DynamicClassExtractor { private final Class actualType; private final Object[] args; private final String dynamicSignature; + private final String requestType; + private boolean isExceedMaxSize; + private static final AgentSizeOf agentSizeOf = AgentSizeOf.newInstance(ThrowableFilter.INSTANCE); public DynamicClassExtractor(Method method, Object[] args, String keyExpression, Class actualType) { this.clazzName = method.getDeclaringClass().getName(); @@ -75,6 +76,7 @@ public DynamicClassExtractor(Method method, Object[] args, String keyExpression, this.methodKey = buildMethodKey(method, args, keyExpression); this.methodReturnType = TypeUtil.getName(method.getReturnType()); this.actualType = actualType; + this.requestType = buildRequestType(method); } public DynamicClassExtractor(Method method, Object[] args) { @@ -85,12 +87,14 @@ public DynamicClassExtractor(Method method, Object[] args) { this.methodKey = buildMethodKey(method, args); this.methodReturnType = TypeUtil.getName(method.getReturnType()); this.actualType = null; + this.requestType = buildRequestType(method); } public Object recordResponse(Object response) { if (IgnoreUtils.invalidOperation(dynamicSignature)) { - LogManager.warn(NEED_RECORD_TITLE, - StringUtil.format("do not record invalid operation: %s, can not serialize args or response", dynamicSignature)); + LogManager.warn(NEED_RECORD_TITLE, StringUtil.format( + "do not record invalid operation: %s, can not serialize request or response", + dynamicSignature)); return response; } if (response instanceof Future) { @@ -118,8 +122,8 @@ public Object recordResponse(Object response) { if (needRecord()) { this.resultClazz = buildResultClazz(TypeUtil.getName(response)); Mocker mocker = makeMocker(); - this.serializedResult = serialize(this.result); - mocker.getTargetResponse().setBody(this.serializedResult); + mocker.getTargetResponse().setBody(getSerializedResult()); + mocker.getTargetResponse().setAttribute(ArexConstants.EXCEED_MAX_SIZE_FLAG, this.isExceedMaxSize); MockUtils.recordMocker(mocker); cacheMethodSignature(); } @@ -132,34 +136,20 @@ public MockResult replay() { StringUtil.format("do not replay invalid operation: %s, can not serialize args or response", dynamicSignature)); return MockResult.IGNORE_MOCK_RESULT; } - String key = buildCacheKey(); - Map cachedReplayResultMap = ContextManager.currentContext() - .getCachedReplayResultMap(); Object replayResult = null; - // First get replay result from cache - if (key != null) { - replayResult = cachedReplayResultMap.get(key); - } - - // If not in cache, get replay result from mock server - if (replayResult == null) { - Mocker replayMocker = MockUtils.replayMocker(makeMocker(), MockStrategyEnum.FIND_LAST); - if (MockUtils.checkResponseMocker(replayMocker)) { - String typeName = replayMocker.getTargetResponse().getType(); - replayResult = deserializeResult(replayMocker, typeName); - } - replayResult = restoreResponse(replayResult); - // no key no cache, no parameter methods may return different values - if (key != null && replayResult != null) { - cachedReplayResultMap.put(key, replayResult); - } + Mocker replayMocker = MockUtils.replayMocker(makeMocker(), MockStrategyEnum.FIND_LAST); + if (MockUtils.checkResponseMocker(replayMocker)) { + String typeName = replayMocker.getTargetResponse().getType(); + String replayBody = replayMocker.getTargetResponse().getBody(); + replayResult = deserializeResult(replayBody, typeName); } + replayResult = restoreResponse(replayResult); boolean ignoreMockResult = IgnoreUtils.ignoreMockResult(clazzName, methodName); return MockResult.success(ignoreMockResult, replayResult); } - private Object deserializeResult(Mocker replayMocker, String typeName) { - return Serializer.deserialize(replayMocker.getTargetResponse().getBody(), typeName, SERIALIZER); + private Object deserializeResult(String replayResult, String typeName) { + return Serializer.deserialize(replayResult, typeName, ArexConstants.GSON_SERIALIZER); } void setFutureResponse(Future result) { @@ -210,6 +200,10 @@ String buildMethodKey(Method method, Object[] args, String keyExpression) { return serialize(normalizeArgs(args)); } + String buildRequestType(Method method) { + return ArrayUtils.toString(method.getParameterTypes(), obj -> ((Class)obj).getTypeName()); + } + /** * There will be a second-level difference between time type recording and playback, * resulting in inability to accurately match data. And in order to be compatible with previously recorded data, @@ -295,9 +289,10 @@ private String getDynamicEntitySignature() { private Mocker makeMocker() { Mocker mocker = MockUtils.createDynamicClass(this.clazzName, this.methodName); + mocker.setNeedMerge(true); mocker.getTargetRequest().setBody(this.methodKey); - mocker.getTargetResponse().setBody(this.serializedResult); mocker.getTargetResponse().setType(this.resultClazz); + mocker.getTargetRequest().setType(this.requestType); return mocker; } @@ -337,46 +332,32 @@ Object restoreResponse(Object result) { } private boolean needRecord() { - /* - * Judge whether the hash value of the method signature has been recorded to avoid repeated recording. - * The nonparametric method may return different results and needs to be recorded - * */ + // Judge whether the hash value of the method signature has been recorded to avoid repeated recording. ArexContext context = ContextManager.currentContext(); - if (context != null && methodKey != null) { + if (context != null) { this.methodSignatureKey = buildDuplicateMethodKey(); - this.methodSignatureKeyHash = StringUtil.encodeAndHash(methodSignatureKey); - if (context.getMethodSignatureHashList().contains(methodSignatureKeyHash)) { + if (methodKey != null) { + this.methodSignatureKeyHash = StringUtil.encodeAndHash(this.methodSignatureKey); + } else { + /* + * no argument method check repeat first by className + methodName + * avoid serialize big result(maybe last record result exceed size limit and already was set to empty) + * so first only check className + methodName + */ + this.methodSignatureKeyHash = buildNoArgMethodSignatureHash(false); + if (!context.getMethodSignatureHashList().contains(this.methodSignatureKeyHash)) { + // if missed means no exceed size limit, check className + methodName + result + this.methodSignatureKeyHash = buildNoArgMethodSignatureHash(true); + } + } + if (context.getMethodSignatureHashList().contains(this.methodSignatureKeyHash)) { if (Config.get().isEnableDebug()) { LogManager.warn(NEED_RECORD_TITLE, - StringUtil.format("do not record method, cuz exist same method signature: %s", methodSignatureKey)); + StringUtil.format("do not record method, cuz exist same method signature: %s", this.methodSignatureKey)); } return false; } } - - if (result == null || result instanceof Throwable) { - return true; - } - - try { - int size = 0; - if (result instanceof Collection) { - size = ((Collection) result).size(); - } else if (result instanceof Map) { - size = ((Map) result).size(); - } else if (result.getClass().isArray()) { - size = Array.getLength(result); - } - if (size > RESULT_SIZE_MAX) { - String methodInfo = methodSignatureKey == null ? buildDuplicateMethodKey() : methodSignatureKey; - LogManager.warn(NEED_RECORD_TITLE, - StringUtil.format("do not record method, cuz result size:%s > max limit: %s, method info: %s", - String.valueOf(size), String.valueOf(RESULT_SIZE_MAX), methodInfo)); - return false; - } - } catch (Throwable e) { - LogManager.warn(NEED_RECORD_TITLE, e); - } return true; } @@ -406,20 +387,23 @@ private String getResultKey() { */ private void cacheMethodSignature() { ArexContext context = ContextManager.currentContext(); - if (context != null && this.methodKey != null && this.methodSignatureKey != null) { + if (context != null) { context.getMethodSignatureHashList().add(this.methodSignatureKeyHash); } } - private String buildCacheKey() { - if (StringUtil.isNotEmpty(this.methodKey)) { - return String.format("%s_%s_%s", this.clazzName, this.methodName, this.methodKey); - } - return null; - } - public String getSerializedResult() { - return serializedResult; + if (this.serializedResult == null && !this.isExceedMaxSize) { + if (!agentSizeOf.checkMemorySizeLimit(this.result, ArexConstants.MEMORY_SIZE_1MB)) { + this.isExceedMaxSize = true; + LogManager.warn(ArexConstants.EXCEED_MAX_SIZE_TITLE, StringUtil.format("method:%s, exceed memory max limit:%s, " + + "record result will be null, please check method return size, suggest replace it", + this.dynamicSignature, AgentSizeOf.humanReadableUnits(ArexConstants.MEMORY_SIZE_1MB))); + return null; + } + this.serializedResult = serialize(this.result); + } + return this.serializedResult; } private String serialize(Object object) { @@ -427,11 +411,18 @@ private String serialize(Object object) { return null; } try { - return Serializer.serializeWithException(object, SERIALIZER); + return Serializer.serializeWithException(object, ArexConstants.GSON_SERIALIZER); } catch (Throwable ex) { IgnoreUtils.addInvalidOperation(dynamicSignature); LogManager.warn("serializeWithException", StringUtil.format("can not serialize object: %s, cause: %s", TypeUtil.errorSerializeToString(object), ex.toString())); return null; } } + + private int buildNoArgMethodSignatureHash(boolean isNeedResult) { + if (isNeedResult) { + return StringUtil.encodeAndHash(String.format("%s_%s_%s", this.clazzName, this.methodName, getSerializedResult())); + } + return StringUtil.encodeAndHash(String.format("%s_%s_%s", this.clazzName, this.methodName, null)); + } } diff --git a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java index 130f338e2..1825eb31e 100644 --- a/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java +++ b/arex-instrumentation/dynamic/arex-dynamic-common/src/test/java/io/arex/inst/dynamic/common/DynamicClassExtractorTest.java @@ -29,6 +29,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; + +import io.arex.inst.runtime.util.sizeof.AgentSizeOf; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -61,15 +63,21 @@ @ExtendWith(MockitoExtension.class) class DynamicClassExtractorTest { + static AgentSizeOf agentSizeOf; + @BeforeAll static void setUp() { Mockito.mockStatic(ContextManager.class); Mockito.mockStatic(Serializer.class); ConfigBuilder.create("test").enableDebug(true).build(); + agentSizeOf = Mockito.mock(AgentSizeOf.class); + Mockito.mockStatic(AgentSizeOf.class); + Mockito.when(AgentSizeOf.newInstance(any())).thenReturn(agentSizeOf); } @AfterAll static void tearDown() { + agentSizeOf = null; Mockito.clearAllCaches(); } @@ -149,7 +157,7 @@ static Stream recordCase() { Runnable signatureContains = () -> { Set methodSignatureHashList = new HashSet<>(); methodSignatureHashList.add(StringUtil.encodeAndHash( - "io.arex.inst.dynamic.common.DynamicClassExtractorTest_testWithArexMock_mock Serializer.serialize_no_result" + "io.arex.inst.dynamic.common.DynamicClassExtractorTest_testWithArexMock_mock Serializer.serialize_has_result_java.lang.String" )); Mockito.when(context.getMethodSignatureHashList()).thenReturn(methodSignatureHashList); try { @@ -158,13 +166,6 @@ static Stream recordCase() { } }; - Runnable serializeThrowException = () -> { - try { - Mockito.when(Serializer.serializeWithException(any(), anyString())).thenThrow(new RuntimeException("mock Serializer.serializeThrowException")); - } catch (Throwable ignored) { - } - }; - Runnable resultIsNull = () -> { Mockito.when(context.getMethodSignatureHashList()).thenReturn(new HashSet<>()); }; @@ -172,15 +173,15 @@ static Stream recordCase() { Predicate isNull = Objects::isNull; Predicate nonNull = Objects::nonNull; return Stream.of( - arguments(signatureContains, new Object[]{"mock"}, "mock1", nonNull), - arguments(resultIsNull, new Object[]{"mock"}, null, isNull), - arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonList("mock"), nonNull), - arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonMap("key", "val"), nonNull), - arguments(resultIsNull, new Object[]{"mock"}, new int[1001], nonNull), - arguments(resultIsNull, null, null, isNull), - arguments(resultIsNull, null, Mono.just("mono test"), nonNull), - arguments(resultIsNull, null, Futures.immediateFuture("mock-future"), nonNull), - arguments(resultIsNull, null, Flux.just("mock-exception"), nonNull) + arguments(signatureContains, new Object[]{"mock"}, "mock1", nonNull), + arguments(resultIsNull, new Object[]{"mock"}, null, isNull), + arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonList("mock"), nonNull), + arguments(resultIsNull, new Object[]{"mock"}, Collections.singletonMap("key", "val"), nonNull), + arguments(resultIsNull, new Object[]{"mock"}, new int[1001], nonNull), + arguments(resultIsNull, null, null, isNull), + arguments(resultIsNull, null, Mono.just("mono test"), nonNull), + arguments(resultIsNull, null, Futures.immediateFuture("mock-future"), nonNull), + arguments(resultIsNull, null, Flux.just("mock-exception"), nonNull) ); } @@ -244,12 +245,12 @@ void testSetFutureResponse() throws NoSuchMethodException { // result instanceOf CompletableFuture CompletableFuture completableFuture = CompletableFuture.completedFuture("CompletableFuture-result"); extractor.setFutureResponse(completableFuture); - assertNull(extractor.getSerializedResult()); + assertDoesNotThrow(() -> extractor.getSerializedResult()); // result instanceOf ListenableFuture ListenableFuture resultFuture = Futures.immediateFuture("result"); extractor.setFutureResponse(resultFuture); - assertNull(extractor.getSerializedResult()); + assertDoesNotThrow(() -> extractor.getSerializedResult()); } @Test @@ -325,7 +326,7 @@ void testBuildResultClazz() throws NoSuchMethodException { actualResult = extractor.buildResultClazz("Java.util.List"); assertEquals("Java.util.List-java.time.LocalDateTime", actualResult); - ConfigBuilder.create("mock-service").build(); + ConfigBuilder.create("mock-service").enableDebug(true).build(); extractor = new DynamicClassExtractor(testWithArexMock, new Object[]{"mock"}, "#val", null); // DynamicEntityMap is empty actualResult = extractor.buildResultClazz("Java.util.List"); @@ -334,14 +335,14 @@ void testBuildResultClazz() throws NoSuchMethodException { // DynamicEntityMap is not empty, actualType is empty List list = new ArrayList<>(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildResultClazz("Java.util.List"); assertEquals("Java.util.List", actualResult); // actualType is not empty list.clear(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "T:java.lang.String")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildResultClazz("Java.util.List"); assertEquals("Java.util.List-java.lang.String", actualResult); } @@ -356,7 +357,7 @@ void testBuildMethodKey() throws Throwable { assertNull(actualResult); // getDynamicClassSignatureMap is empty - ConfigBuilder.create("mock-service").build(); + ConfigBuilder.create("mock-service").enableDebug(true).build(); Mockito.when(Serializer.serializeWithException(any(), anyString())).thenReturn("mock Serializer.serialize"); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock"}); assertEquals("mock Serializer.serialize", actualResult); @@ -364,14 +365,14 @@ void testBuildMethodKey() throws Throwable { // getDynamicClassSignatureMap is not empty, additionalSignature is empty List list = new ArrayList<>(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock"}); assertEquals("mock Serializer.serialize", actualResult); // additionalSignature is not empty list.clear(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "", "$1")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock-method-key"}); assertEquals("mock-method-key", actualResult); @@ -379,7 +380,7 @@ void testBuildMethodKey() throws Throwable { extractor = new DynamicClassExtractor(testWithArexMock, new Object[]{"mock"}, "#val", String.class); list.clear(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "$1")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildMethodKey(testWithArexMock, new Object[]{"mock-method-key"}); assertEquals("mock-method-key", actualResult); @@ -388,7 +389,7 @@ void testBuildMethodKey() throws Throwable { extractor = new DynamicClassExtractor(testWithArexMockList, new Object[]{new ArrayList<>()}, null, String.class); list.clear(); list.add(new DynamicClassEntity("io.arex.inst.dynamic.common.DynamicClassExtractorTest", "testWithArexMock", "mock", "$1.get(0)")); - ConfigBuilder.create("mock-service").dynamicClassList(list).build(); + ConfigBuilder.create("mock-service").enableDebug(true).dynamicClassList(list).build(); actualResult = extractor.buildMethodKey(testWithArexMockList, new Object[]{new ArrayList<>()}); assertNull(actualResult); } @@ -421,7 +422,7 @@ public CompletableFuture testReturnCompletableFuture(String val, Throwab void invalidOperation() throws Throwable { Method testWithArexMock = DynamicClassExtractorTest.class.getDeclaredMethod("testWithArexMock", String.class); final Object[] args = {"errorSerialize"}; - ConfigBuilder.create("invalid-operation").build(); + ConfigBuilder.create("invalid-operation").enableDebug(true).build(); Mockito.when(Serializer.serializeWithException(any(), anyString())).thenThrow(new RuntimeException("errorSerialize")); DynamicClassExtractor extractor = new DynamicClassExtractor(testWithArexMock, args); extractor.recordResponse("errorSerialize"); @@ -435,6 +436,11 @@ void invalidOperation() throws Throwable { // invalid operation replay return ignore final MockResult replay = extractor.replay(); assertEquals(MockResult.IGNORE_MOCK_RESULT, replay); + + // test getSerializedResult serialize + Mockito.when(agentSizeOf.checkMemorySizeLimit(any(), any(long.class))).thenReturn(true); + extractor = new DynamicClassExtractor(testWithArexMock, args); + assertNull(extractor.getSerializedResult()); } @Test diff --git a/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java b/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java index 9a0dc4bc6..fe4940bc3 100644 --- a/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java +++ b/arex-instrumentation/redis/arex-jedis-v2/src/main/java/io/arex/inst/jedis/v2/JedisWrapper.java @@ -86,7 +86,7 @@ public Long expire(String key, int seconds) { @Override public Long expire(byte[] key, int seconds) { - return call("expire", Base64.getEncoder().encodeToString(key), () -> super.expire(key, seconds), 0L); + return call("expire", key, () -> super.expire(key, seconds), 0L); } @Override @@ -161,7 +161,7 @@ public Long append(String key, String value) { @Override public Long append(byte[] key, byte[] value) { - return call("append", Base64.getEncoder().encodeToString(key), () -> super.append(key, value), 0L); + return call("append", key, () -> super.append(key, value), 0L); } @Override @@ -172,7 +172,7 @@ public String substr(String key, int start, int end) { @Override public byte[] substr(byte[] key, int start, int end) { - return call("substr", Base64.getEncoder().encodeToString(key), + return call("substr", key, RedisKeyUtil.generate("start", String.valueOf(start), "end", String.valueOf(end)), () -> super.substr(key, start, end), null); } @@ -184,20 +184,17 @@ public Long hset(String key, String field, String value) { @Override public Long hset(byte[] key, byte[] field, byte[] value) { - return call("hset", - Base64.getEncoder().encodeToString(key), Base64.getEncoder().encodeToString(field), - () -> super.hset(key, field, value), 0L); + return call("hset", key, field, () -> super.hset(key, field, value), 0L); } @Override public Long hset(byte[] key, Map hash) { - return call("hset", Base64.getEncoder().encodeToString(key), - Serializer.serialize(hash.keySet()), () -> super.hset(key, hash), 0L); + return call("hset", key, hash.keySet(), () -> super.hset(key, hash), 0L); } @Override public Long hset(final String key, final Map hash) { - return call("hset", key, Serializer.serialize(hash.keySet()), () -> super.hset(key, hash), 0L); + return call("hset", key, hash.keySet(), () -> super.hset(key, hash), 0L); } @Override @@ -207,9 +204,7 @@ public String hget(String key, String field) { @Override public byte[] hget(byte[] key, byte[] field) { - return call("hget", - Base64.getEncoder().encodeToString(key), Base64.getEncoder().encodeToString(field), - () -> super.hget(key, field), null); + return call("hget", key, field, () -> super.hget(key, field), null); } @Override @@ -251,7 +246,7 @@ public Long hdel(String key, String... fields) { @Override public Long hdel(byte[] key, byte[]... fields) { - return call("hdel", Base64.getEncoder().encodeToString(key), RedisKeyUtil.generate(fields), () -> super.hdel(key, fields), 0L); + return call("hdel", key, RedisKeyUtil.generate(fields), () -> super.hdel(key, fields), 0L); } @Override @@ -271,7 +266,7 @@ public List hvals(String key) { @Override public List hvals(byte[] key) { - return call("hvals", Base64.getEncoder().encodeToString(key), () -> super.hvals(key), Collections.EMPTY_LIST); + return call("hvals", key, () -> super.hvals(key), Collections.EMPTY_LIST); } @Override @@ -281,7 +276,7 @@ public Map hgetAll(String key) { @Override public Map hgetAll(byte[] key) { - return call("hgetAll", Base64.getEncoder().encodeToString(key), () -> super.hgetAll(key), Collections.EMPTY_MAP); + return call("hgetAll", key, () -> super.hgetAll(key), Collections.EMPTY_MAP); } @Override @@ -415,22 +410,22 @@ public String psetex(String key, long milliseconds, String value) { @Override public String set(final byte[] key, final byte[] value) { - return call("set", Base64.getEncoder().encodeToString(key), () -> super.set(key, value), null); + return call("set", key, () -> super.set(key, value), null); } @Override public String set(byte[] key, byte[] value, byte[] nxxx, byte[] expx, long time) { - return call("set", Base64.getEncoder().encodeToString(key), () -> super.set(key, value, nxxx, expx, time), null); + return call("set", key, () -> super.set(key, value, nxxx, expx, time), null); } @Override public String set(byte[] key, byte[] value, byte[] expx, long time) { - return call("set", Base64.getEncoder().encodeToString(key), () -> super.set(key, value, expx, time), null); + return call("set", key, () -> super.set(key, value, expx, time), null); } @Override public byte[] get(final byte[] key) { - return call("get", Base64.getEncoder().encodeToString(key), () -> super.get(key), null); + return call("get", key, () -> super.get(key), null); } @Override @@ -440,17 +435,17 @@ public Long exists(final byte[]... keys) { @Override public Boolean exists(final byte[] key) { - return call("exists", Base64.getEncoder().encodeToString(key), () -> super.exists(key), false); + return call("exists", key, () -> super.exists(key), false); } @Override public String type(final byte[] key) { - return call("type", Base64.getEncoder().encodeToString(key), () -> super.type(key), "none"); + return call("type", key, () -> super.type(key), "none"); } @Override public byte[] getSet(final byte[] key, final byte[] value) { - return call("getSet", Base64.getEncoder().encodeToString(key), () -> super.getSet(key, value), null); + return call("getSet", key, () -> super.getSet(key, value), null); } @Override @@ -460,12 +455,12 @@ public List mget(final byte[]... keys) { @Override public Long setnx(final byte[] key, final byte[] value) { - return call("setnx", Base64.getEncoder().encodeToString(key), () -> super.setnx(key, value), 0L); + return call("setnx", key, () -> super.setnx(key, value), 0L); } @Override public String setex(byte[] key, int seconds, byte[] value) { - return call("setex", Base64.getEncoder().encodeToString(key), () -> super.setex(key, seconds, value), + return call("setex", key, () -> super.setex(key, seconds, value), null); } @@ -486,7 +481,7 @@ public Long unlink(byte[]... keys) { @Override public Long unlink(byte[] key) { - return call("unlink", Base64.getEncoder().encodeToString(key), () -> super.unlink(key), 0L); + return call("unlink", key, () -> super.unlink(key), 0L); } @Override @@ -516,7 +511,7 @@ public String ping() { @Override public byte[] ping(byte[] message) { - return call("ping", Base64.getEncoder().encodeToString(message), () -> super.ping(message), null); + return call("ping", message, () -> super.ping(message), null); } @Override @@ -546,11 +541,11 @@ private U call(String command, String[] keysValues, Callable callable, U return call(command, keyBuilder.toString(), null, callable, defaultValue); } - private U call(String command, String key, Callable callable, U defaultValue) { + private U call(String command, Object key, Callable callable, U defaultValue) { return call(command, key, null, callable, defaultValue); } - private U call(String command, String key, String field, Callable callable, U defaultValue) { + private U call(String command, Object key, Object field, Callable callable, U defaultValue) { if (ContextManager.needReplay()) { RedisExtractor extractor = new RedisExtractor(this.url, command, key, field); MockResult mockResult = extractor.replay(); diff --git a/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java b/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java index c1255b09e..e2c146db8 100644 --- a/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java +++ b/arex-instrumentation/redis/arex-jedis-v2/src/test/java/io/arex/inst/jedis/v2/JedisWrapperTest.java @@ -20,6 +20,8 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocketFactory; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Stream; @@ -117,4 +119,31 @@ static Stream callCase() { arguments(mocker3, predicate2) ); } -} + + @Test + void testApi() { + assertDoesNotThrow(() -> target.expire("key".getBytes(), 1)); + assertDoesNotThrow(() -> target.append("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.substr("key".getBytes(), 1, 2)); + assertDoesNotThrow(() -> target.hset("key".getBytes(), "field".getBytes(), "value".getBytes())); + Map hash = new HashMap<>(); + assertDoesNotThrow(() -> target.hset("key".getBytes(), hash)); + Map hash1 = new HashMap<>(); + assertDoesNotThrow(() -> target.hset("key", hash1)); + assertDoesNotThrow(() -> target.hget("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.hdel("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.hvals("key".getBytes())); + assertDoesNotThrow(() -> target.hgetAll("key".getBytes())); + assertDoesNotThrow(() -> target.set("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.set("key".getBytes(), "value".getBytes(), "value".getBytes(), "value".getBytes(), 1L)); + assertDoesNotThrow(() -> target.set("key".getBytes(), "value".getBytes(), "value".getBytes(), 1L)); + assertDoesNotThrow(() -> target.get("key".getBytes())); + assertDoesNotThrow(() -> target.exists("key".getBytes())); + assertDoesNotThrow(() -> target.type("key".getBytes())); + assertDoesNotThrow(() -> target.getSet("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.setnx("key".getBytes(), "value".getBytes())); + assertDoesNotThrow(() -> target.setex("key".getBytes(), 1, "value".getBytes())); + assertDoesNotThrow(() -> target.unlink("key".getBytes())); + assertDoesNotThrow(() -> target.ping("key".getBytes())); + } +} \ No newline at end of file diff --git a/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java b/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java index 880e1dce2..a350439e9 100644 --- a/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java +++ b/arex-instrumentation/redis/arex-redis-common/src/main/java/io/arex/inst/redis/common/RedisExtractor.java @@ -3,10 +3,12 @@ import io.arex.agent.bootstrap.model.MockResult; import io.arex.agent.bootstrap.model.Mocker; +import io.arex.agent.bootstrap.util.CollectionUtil; import io.arex.inst.runtime.serializer.Serializer; import io.arex.inst.runtime.util.IgnoreUtils; import io.arex.inst.runtime.util.MockUtils; import io.arex.inst.runtime.util.TypeUtil; + import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -15,10 +17,10 @@ public class RedisExtractor { private final String clusterName; private final String command; - private final String key; - private final String field; + private final Object key; + private final Object field; - public RedisExtractor(String url, String method, String key, String field) { + public RedisExtractor(String url, String method, Object key, Object field) { this.clusterName = RedisCluster.get(url); this.command = method; this.key = key; @@ -26,29 +28,29 @@ public RedisExtractor(String url, String method, String key, String field) { } public static class RedisMultiKey { - private String key; - private String field; + private Object key; + private Object field; public RedisMultiKey() {} - public RedisMultiKey(String key, String field) { + public RedisMultiKey(Object key, Object field) { this.key = key; this.field = field; } - public String getKey() { + public Object getKey() { return key; } - public void setKey(String key) { + public void setKey(Object key) { this.key = key; } - public String getField() { + public Object getField() { return field; } - public void setField(String field) { + public void setField(Object field) { this.field = field; } } @@ -73,10 +75,12 @@ public MockResult replay() { private Mocker makeMocker(Object response) { Mocker mocker = MockUtils.createRedis(this.command); + mocker.setNeedMerge(true); mocker.getTargetRequest().setBody(Serializer.serialize(new RedisMultiKey(key, field))); mocker.getTargetRequest().setAttribute("clusterName", this.clusterName); mocker.getTargetResponse().setBody(Serializer.serialize(response)); mocker.getTargetResponse().setType(normalizeTypeName(response)); + mocker.getTargetRequest().setType(TypeUtil.getName(CollectionUtil.newArrayList(this.key, this.field))); return mocker; } diff --git a/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java b/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java index 009f13ce3..962e44c8c 100644 --- a/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java +++ b/arex-instrumentation/redis/arex-redis-common/src/test/java/io/arex/inst/redis/common/RedisExtractorTest.java @@ -16,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.verify; @ExtendWith(MockitoExtension.class) class RedisExtractorTest { @@ -66,4 +65,13 @@ void replay() { assertNotNull(target.replay()); } } + + @Test + void redisMultiKey() { + RedisExtractor.RedisMultiKey redisMultiKey = new RedisExtractor.RedisMultiKey(); + redisMultiKey.setKey("mock"); + redisMultiKey.getKey(); + redisMultiKey.setField("mock"); + assertNotNull(redisMultiKey.getField()); + } } \ No newline at end of file diff --git a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java index 790dec8cc..2a1bea7d4 100644 --- a/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java +++ b/arex-instrumentation/servlet/arex-httpservlet/src/main/java/io/arex/inst/httpservlet/ServletAdviceHelper.java @@ -117,7 +117,7 @@ public static Pair onServiceEnter( String caseId = adapter.getRequestHeader(httpServletRequest, ArexConstants.RECORD_ID); String excludeMockTemplate = adapter.getRequestHeader(httpServletRequest, ArexConstants.HEADER_EXCLUDE_MOCK); CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent(EventSource.of(caseId, excludeMockTemplate))); - + RequestHandlerManager.handleAfterCreateContext(httpServletRequest, MockCategoryType.SERVLET.getName()); } if (ContextManager.needRecordOrReplay()) {