40 Add Meal Simulate Remote Invocation Mock Mechanism to Help You

40 Add Meal Simulate Remote Invocation Mock Mechanism to Help You #

Hello, I’m Yang Sizheng. Today, I am going to share with you the topic of Mock Mechanism in Dubbo.

Mock Mechanism is a very common and useful feature in RPC frameworks. It can not only be used for service degradation, but also for simulating various exceptional situations in testing. Dubbo’s Mock Mechanism is implemented on the Consumer side, specifically in the Cluster layer.

In Lesson 38, we extensively discussed the various Cluster implementations provided by Dubbo and the related Cluster Invoker implementations. One of these implementations, ZoneAwareClusterInvoker, involves the MockClusterInvoker. In this lesson, we will introduce the complete process of Dubbo’s Mock Mechanism, including the MockClusterWrapper and MockClusterInvoker related to the Cluster interface. We will also review the Router and Protocol interfaces from previous lessons and analyze their implementations related to the Mock Mechanism.

MockClusterWrapper #

The Cluster interface has two inheritance paths, as shown in the following diagram: one path is the AbstractCluster abstract class, which we have thoroughly analyzed in [Lesson 37]; the other path is the MockClusterWrapper.

Drawing 0.png

Cluster inheritance diagram

MockClusterWrapper is a wrapper class for the Cluster object. In a previous lesson [Lesson 4], we have analyzed the functionality of the Wrapper during the introduction of Dubbo’s SPI mechanism. The MockClusterWrapper class wraps the Cluster. The following is the specific implementation of MockClusterWrapper. It wraps the Cluster Invoker object with the MockClusterInvoker:

public class MockClusterWrapper implements Cluster {

    private Cluster cluster;

    // Wrapper classes all have a copy constructor

    public MockClusterWrapper(Cluster cluster) {

        this.cluster = cluster;

    }

    @Override

    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {

        // wrap with MockClusterInvoker

        return new MockClusterInvoker<T>(directory,

                this.cluster.join(directory));

    }

}

MockClusterInvoker #

MockClusterInvoker is the core of Dubbo’s Mock Mechanism. It mainly implements the Mock Mechanism through its three core methods: invoke(), doMockInvoke(), and selectMockInvoker().

Now let’s introduce the specific implementation of these three methods one by one.

First, let’s take a look at the invoke() method of MockClusterInvoker. It first determines whether the Mock Mechanism needs to be enabled. If the force mode is configured in the mock parameter, it directly calls the doMockInvoke() method to perform mock. If the fail mode is configured in the mock parameter, it will initiate the request normally. When the request fails, it will invoke the doMockInvoke() method for mock. The following is the specific implementation of the invoke() method of MockClusterInvoker:

public Result invoke(Invocation invocation) throws RpcException {

    Result result = null;

    // Get the method's mock configuration from the URL

    String value = getUrl().getMethodParameter(invocation.getMethodName(), MOCK_KEY, Boolean.FALSE.toString()).trim();

    if (value.length() == 0 || "false".equalsIgnoreCase(value)) {

        // If the mock parameter is not configured or is configured as false, the Mock Mechanism will not be enabled, and the underlying Invoker will be called directly

        result = this.invoker.invoke(invocation);

    } else if (value.startsWith("force")) {

        // force: direct mock

        // If the mock parameter is configured as force, it means that forced mock is required, and the doMockInvoke() method is called directly

        result = doMockInvoke(invocation, null);

    } else {

        // If the mock parameter is not force, it is configured as fail, and the invoke() method of the Invoker object will be called again

        try {

            result = this.invoker.invoke(invocation);

        } catch (RpcException e) {

            if (e.isBiz()) { // If it is a business exception, it will be directly thrown

                throw e;

            }

            // If it is a non-business exception, the doMockInvoke() method will be called to return the mock result

            result = doMockInvoke(invocation, e);

        }

    }

    return result;

}

In the doMockInvoke() method, selectMockInvoker() is called first to retrieve the MockInvoker object, and then its invoke() method is called to perform the mock operation. The following is the specific implementation of the doMockInvoke() method:

private Result doMockInvoke(Invocation invocation, RpcException e) {

    Result result = null;
    ...
Invoker<T> minvoker;

// Call selectMockInvoker() method to filter and get MockInvoker
List<Invoker<T>> mockInvokers = selectMockInvoker(invocation);

if (CollectionUtils.isEmpty(mockInvokers)) {

    // If selectMockInvoker() method does not return a MockInvoker object, create a new MockInvoker
    minvoker = (Invoker<T>) new MockInvoker(getUrl(), directory.getInterface());

} else {

    minvoker = mockInvokers.get(0);

}

try {

    // Call MockInvoker.invoke() method to perform mock
    result = minvoker.invoke(invocation);

} catch (RpcException me) {

    if (me.isBiz()) {

        // If it is a business exception, set the exception in Result
        result = AsyncRpcResult.newDefaultAsyncResult(me.getCause(), invocation);

    } else {

        throw new RpcException(...);

    }

} catch (Throwable me) {

    throw new RpcException(...);

}

return result;

The selectMockInvoker() method does not select or create a MockInvoker. It only sets the invocation.need.mock attribute in the attachment of the Invocation to true and passes it to the Router collection in the Directory for processing. The implementation of the selectMockInvoker() method is as follows:

private List<Invoker<T>> selectMockInvoker(Invocation invocation) {

    List<Invoker<T>> invokers = null;

    if (invocation instanceof RpcInvocation) {

        // Set invocation.need.mock attribute in the attachment of the Invocation to true
        ((RpcInvocation) invocation).setAttachment(INVOCATION_NEED_MOCK, Boolean.TRUE.toString()); 

        invokers = directory.list(invocation);

    }

    return invokers;

}

MockInvokersSelector #

In Lesson 32 and Lesson 33, we introduced multiple Router implementations, but at the time we did not go into depth about the Mock-related Router implementations, specifically MockInvokersSelector. The inheritance relationship of MockInvokersSelector is shown in the following diagram:

Drawing 1.png

MockInvokersSelector inheritance diagram

MockInvokersSelector is a Router implementation related to the Dubbo Mock mechanism. When the Mock mechanism is not enabled, it returns a list of normal Invoker objects. When the Mock mechanism is enabled, it returns a list of MockInvoker objects. The specific implementation of MockInvokersSelector is as follows:

public <T> List<Invoker<T>> route(final List<Invoker<T>> invokers, URL url, final Invocation invocation) throws RpcException {
    if (CollectionUtils.isEmpty(invokers)) {
        return invokers;
    }
    if (invocation.getObjectAttachments() == null) {
        // If attachments are null, filter out MockInvokers and return normal Invoker objects
        return getNormalInvokers(invokers);
    } else {
        String value = (String) invocation.getObjectAttachments().get(INVOCATION_NEED_MOCK);
        if (value == null) {
            // If invocation.need.mock is null, filter out MockInvokers and return normal Invoker objects
            return getNormalInvokers(invokers);
        } else if (Boolean.TRUE.toString().equalsIgnoreCase(value)) {
            // If invocation.need.mock is true, filter out MockInvokers and return normal Invoker objects
            return getMockedInvokers(invokers);
}
}
// If invocation.need.mock is false, the MockInvoker and normal Invoker will be returned together
return invokers;
}

In the getMockedInvokers() method, it filters the Invoker objects based on the Protocol of the URL and only returns objects with Protocol as “mock”. The getNormalInvokers() method only returns Invoker objects with Protocol not equal to “mock”. The specific implementation of these two methods is relatively simple and will not be displayed here. If you are interested, you can refer to the source code for learning.

MockProtocol & MockInvoker #

After introducing the implementation of the Mock function in the Cluster layer, let’s take a look at Dubbo’s support for the Mock mechanism in the RPC layer. This involves the MockProtocol and MockInvoker classes.

First, let’s look at MockProtocol, which is an extension implementation of the Protocol interface, with the extension name “mock”. MockProtocol can only create MockInvokers through the refer() method and cannot expose services through the export() method. The specific implementation is as follows:

final public class MockProtocol extends AbstractProtocol {
    public int getDefaultPort() { return 0;}

    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        // Throws an exception directly, unable to expose the service
        throw new UnsupportedOperationException();
    }

    public <T> Invoker<T> protocolBindingRefer(Class<T> type, URL url) throws RpcException {
        // Creates a MockInvoker object directly
        return new MockInvoker<>(url, type);
    }
}

Next, let’s see how the MockInvoker parses various mock configurations and how it handles different mock configurations. Here we focus on the MockInvoker.invoke() method, which branches into three branches based on the mock parameter.

  • If the mock parameter starts with “return”: directly return the fixed value specified in the mock parameter, such as empty, null, true, false, json, etc. The fixed return value specified in the mock parameter will be parsed by the parseMockValue() method.
  • If the mock parameter starts with “throw”: directly throw an exception. If no exception type is specified in the mock parameter, RpcException will be thrown; otherwise, the specified Exception type will be thrown.
  • If the mock parameter is true or default, it will look for the Mock implementation corresponding to the service interface; if it is any other value, it will be directly used as the Mock implementation of the service interface. After obtaining the Mock implementation, it will be converted into an Invoker for invocation.

The specific implementation of the MockInvoker.invoke() method is as follows:

public Result invoke(Invocation invocation) throws RpcException {
    if (invocation instanceof RpcInvocation) {
        ((RpcInvocation) invocation).setInvoker(this);
    }

    // Get the mock value (will get from the methodName.mock parameter or mock parameter in the URL)
    String mock = null;
    if (getUrl().hasMethodParameter(invocation.getMethodName())) {
        mock = getUrl().getParameter(invocation.getMethodName() + "." + MOCK_KEY);
    }
    if (StringUtils.isBlank(mock)) {
        mock = getUrl().getParameter(MOCK_KEY);
    }
    if (StringUtils.isBlank(mock)) { // If no mock value is configured, throw an exception directly
        throw new RpcException(new IllegalAccessException("mock can not be null. url :" + url));
    }
    // Process the mock value, remove the "force:" and "fail:" prefixes, etc.
    mock = normalizeMock(URL.decode(mock));

    if (mock.startsWith(RETURN_PREFIX)) { // If the mock value starts with "return"
        mock = mock.substring(RETURN_PREFIX.length()).trim();
        try {
            // Get the return result type
            Type[] returnTypes = RpcUtils.getReturnTypes(invocation);
            // Depending on the result type, convert the result value in the mock value
            Object value = parseMockValue(mock, returnTypes);
            // Set the fixed mock value to the Result
            return AsyncRpcResult.newDefaultAsyncResult(value, invocation);
        } catch (Exception ew) {
            throw new RpcException("mock return invoke error. method :" + invocation.getMethodName() + ", mock:" + mock + ", url: " + url, ew);
        }
    } else if (mock.startsWith(THROW_PREFIX)) { // If the mock value starts with "throw"
        mock = mock.substring(THROW_PREFIX.length()).trim();
        if (StringUtils.isBlank(mock)) { // If no exception type is specified, directly throw RpcException
            throw new RpcException("mocked exception for service degradation.");
        } else { // Throw a custom exception
            Throwable t = getThrowable(mock);
            throw new RpcException(RpcException.BIZ_EXCEPTION, t);
        }
    } else { // Execute mockService to get the mock result
        try {
            Invoker<T> invoker = getInvoker(mock);
            return invoker.invoke(invocation);
        } catch (Throwable t) {
            throw new RpcException("Failed to create mock implementation class " + mock, t);
        }
    }
}

The handling logic for return and throw is relatively simple, but the getInvoker() method is slightly more complicated. It involves the reading and writing of the MOCK_MAP cache, finding, generating, and invoking Invokers for Mock implementations. The specific implementation is as follows:

private Invoker<T> getInvoker(String mockService) {
    // Try to get the Invoker object corresponding to mockService from the MOCK_MAP collection
    Invoker<T> invoker = (Invoker<T>) MOCK_MAP.get(mockService);
    if (invoker != null) {
        return invoker;
    }
    // Find the mock implementation class based on serviceType
    Class<T> serviceType = (Class<T>) ReflectUtils.forName(url.getServiceInterface());
    T mockObject = (T) getMockObject(mockService, serviceType);
    // Create the Invoker object
    invoker = PROXY_FACTORY.getInvoker(mockObject, serviceType, url);
    if (MOCK_MAP.size() < 10000) { // Write to the cache
        MOCK_MAP.put(mockService, invoker);
    }
    return invoker;
}

In the getMockObject() method, it checks whether the mockService parameter is true or default. If it is, it adds the “Mock” string after the service interface as the Mock implementation of the service interface. If not, it directly uses the mockService implementation as the Mock implementation of the service interface. The specific implementation of the getMockObject() method is as follows:

public static Object getMockObject(String mockService, Class serviceType) {
    if (ConfigUtils.isDefault(mockService)) {
        // If mock is true or default, adds the Mock string after the service interface to get the corresponding implementation class name and instantiate it
        mockService = serviceType.getName() + "Mock";
    }
    Class<?> mockClass = ReflectUtils.forName(mockService);
    if (!serviceType.isAssignableFrom(mockClass)) {
        // Check if the mockClass inherits from the serviceType interface
        throw new IllegalStateException("...");
    }
    return mockClass.newInstance();
}

Summary #

In this lesson, we have introduced all the relevant content of the Mock mechanism in Dubbo.

  • First, we introduced the MockClusterWrapper, which implements the Cluster interface and is responsible for creating the MockClusterInvoker. It is the entry point for the Dubbo Mock mechanism.
  • Next, we introduced the MockClusterInvoker, which is the core of the Dubbo Mock mechanism in the Cluster layer. It determines whether the request has enabled the Mock mechanism and when the Mock will be triggered based on the configuration.
  • Then, we explained the MockInvokersSelector, which is an implementation of the Router interface. It determines whether to return MockInvoker objects at the level of the routing rules.
  • Finally, we analyzed the implementation of the Protocol layer related to Mock - MockProtocol, and MockInvoker, which actually performs the Mock operation as an Invoker. In MockInvoker, various Mock configurations are parsed, and different Mock operations are performed based on the different Mock configurations.