Regarding the case of ~S in the identity of the traffic sub-call recorded by the repeater

Some time ago, my colleague asked me, in the traffic we recorded, especially the sub-call of dubbo, what does it mean that there is often a small tail at the end of it? In fact, I didn’t pay much attention to this matter before, but my colleagues were so suspicious. Really piqued the curiosity, so it was just a matter of what it was

Let’s first look at what kind of phenomenon, as shown in the following figure:

As above, there will be a small tail of ~AP here, so what does this mean, is it the result of a certain hash? It is not difficult to understand this, just look at how the identity of the dubbo sub-call is assigned. So let’s look at the specific logic

During the recording process, when the event comes, if it is Before, it will enter the following content,

protected void doBefore(BeforeEvent event) throws ProcessControlException {<!-- -->
    // Playback traffic; if it is an entry, give up; if the sub-call is mocked
    if (RepeatCache.isRepeatFlow(Tracer.getTraceId())) {<!-- -->
        processor.doMock(event, entrance, invokeType);
        return;
    }
    Invocation invocation = initInvocation(event);
    invocation.setStart(System.currentTimeMillis());
    invocation.setTraceId(Tracer.getTraceId());
    invocation.setIndex(entrance? 0 : SequenceGenerator.generate(Tracer.getTraceId()));
    invocation.setIdentity(processor.assembleIdentity(event));
    invocation. setEntrance(entrance);
    invocation. setType(invokeType);
    invocation.setProcessId(event.processId);
    invocation.setInvokeId(event.invokeId);
    invocation.setRequest(processor.assembleRequest(event));
    invocation.setResponse(processor.assembleResponse(event));
    invocation.setSerializeToken(ClassloaderBridge.instance().encode(event.javaClassLoader));
    try {<!-- -->
        // fix issue#14: useGeneratedKeys
        if (processor.inTimeSerializeRequest(invocation, event)) {<!-- -->
            SerializerWrapper.inTimeSerialize(invocation);
        }
    } catch (SerializeException e) {<!-- -->
        Tracer.getContext().setSampled(false);
        log.error("Error occurred serialize", e);
    }
    RecordCache.cacheInvocation(event.invokeId, invocation);
}

So identity is the identity generated after processing by the respective sub-call processors. Here we specifically call dubbo, so let’s look at the logic of DubboConsumerInvocationProcessor

public Identity assembleIdentity(BeforeEvent event) {<!-- -->
    Object invoker;
    Object invocation;
    if (ON_RESPONSE.equals(event.javaMethodName)) {<!-- -->
        // for record identity assemble
        // onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {}
        invoker = event.argumentArray[1];
        invocation = event.argumentArray[2];
    } else {<!-- -->
        // for repeater identity assemble
        // invoke(Invoker<?> invoker, Invocation invocation)
        invoker = event.argumentArray[0];
        invocation = event.argumentArray[1];
    }

    try {<!-- -->
        // methodName
        String methodName = (String) MethodUtils. invokeMethod(invocation, "getMethodName");
        Class<?>[] parameterTypes = (Class<?>[]) MethodUtils. invokeMethod(invocation, "getParameterTypes");
        // interfaceName
        String interfaceName = ((Class)MethodUtils.invokeMethod(invoker, "getInterface")).getCanonicalName();
        return new Identity(InvokeType.DUBBO.name(), interfaceName, getMethodDesc(methodName, parameterTypes), getExtra());
    } catch (Exception e) {<!-- -->
        // ignore
        LogUtil.error("error occurred when assemble dubbo request", e);
    }
    return new Identity(InvokeType.DUBBO.name(), "unknown", "unknown", null);
}

We see that Identity is directly completed by new Identity(InvokeType.DUBBO.name(), interfaceName, getMethodDesc(methodName, parameterTypes), getExtra()). So if the parameters here are viewed according to the previous screenshots,

  1. InvokeType.DUBBO.name() is dubbo,

  2. interfaceName is com.xx.xx.api.service.UserAgreementApiService

  3. getMethodDesc(methodName, parameterTypes) After looking at the implementation, it turns out that it is the reason we have been looking for with a small tail. Let’s take a look.

    protected String getMethodDesc(String methodName, Class<?>[] parameterTypes) {<!-- -->
            StringBuilder builder = new StringBuilder(methodName);
            if (parameterTypes != null & amp; & amp; parameterTypes. length > 0) {<!-- -->
                builder.append("~");
                for (Class<?> parameterType : parameterTypes) {<!-- -->
                    String className = parameterType. getSimpleName();
                    builder.append(className.subSequence(0, 1));
                }
            }
            return builder.toString();
        }
    

    We can see that it is splicing the method and the parameter type here, but it does not take all the content of the parameter type, but the class name of the parameter type. For example, the result of Java.lang.String is string , and then take the first character here.

  4. The temporary position of getExtra() seems to be the query field of http, but seeing that all the recorded sub-call recording logic is basically null, we will ignore it here for the time being.

Then let’s take a look at what the constructor of Identity looks like.

public Identity(String scheme, String location, String endpoint, Map<String, String> extra) {<!-- -->
        this.scheme = scheme;
        this. location = location;
        this.endpoint = endpoint;
        this. extra = extra;
        StringBuilder sb = new StringBuilder();
        sb.append(scheme).append(HOST_SPLITTER).append(Joiner.on("/").join(location, endpoint));
        if (extra != null & amp; & amp; !extra.isEmpty()) {<!-- -->
            boolean firstKey = true;
            for (Map.Entry<String, String> entry : extra.entrySet()) {<!-- -->
                if (firstKey) {<!-- -->
                    firstKey = false;
                    sb.append(QUERY_STRING_COLLECTOR);
                } else {<!-- -->
                    sb.append(KEY_VALUE_SPLITTER);
                }
                sb.append(entry.getKey()).append(KEY_VALUE_COLLECTOR).append(entry.getValue());
            }
        }
        this.uri = sb.toString();
    }

It can be seen that the logic in the constructor actually focuses on constructing a uri, and this uri should be the content of the identity we saw when we first took the screenshot. In fact, it is connected by various separators to form the data we pass in.