Skip to content

[BUG] 一个关于 SentinelResourceAspect 的疑似 BUG #3597

@HaganYang

Description

@HaganYang

使用注解和抛出异常两种方式对同样的资源和规则进行流量控制时,执行结果不一样,以下是使用抛出异常方式的代码:

package com.alibaba.csp.sentinel.demo.test;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;

import java.util.ArrayList;
import java.util.List;

public class SentinelFlowThread {

    public static void main(String[] args) throws InterruptedException {
        initFlowRule();

        for (int i = 0; i < 4; i++) {
//        for (int i = 0; i < 1; i++) {
            final int j = i;
            new Thread(() -> {
                Entry entry = null;
                Object[] argArr = {"1"};
                try {
                    if (j == 2) {
                        Thread.sleep(1000);
                    }
                    if (j == 3) {
                        Thread.sleep(4000);
                    }
//                    entry = SphU.entry("doSomething");
                    entry = SphU.entry("doSomething", 0, EntryType.OUT, argArr);
                    Thread.sleep(3000);
                    System.out.println(Thread.currentThread().getName() + " 执行完成");
                } catch (BlockException e) {
                    System.out.println(Thread.currentThread().getName() + " 被限流");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    if (entry != null) {
//                        entry.exit();
                        entry.exit(1, argArr);
                    }
                }
            }, "Thread-" + i).start();
        }

        Thread.sleep(10000);
    }

    private static void initFlowRule() {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        rule.setResource("doSomething");
        rule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
        rule.setCount(2);
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }

}

执行结果为:

INFO: Sentinel log output type is: file
INFO: Sentinel log charset is: utf-8
INFO: Sentinel log base directory is: C:\Users\Administrator\logs\csp
INFO: Sentinel log name use pid is: false
INFO: Sentinel log level is: INFO
Thread-2 被限流
Thread-0 执行完成
Thread-1 执行完成
Thread-3 执行完成

以下是使用注解方式的代码:

package com.alibaba.csp.sentinel.demo.test;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;

import java.util.ArrayList;
import java.util.List;

public class TestAnnotation {

    @SentinelResource(value = "doSomething", blockHandler = "usingAnnotationMethodBlockHandler")
    public void usingAnnotationMethod() throws InterruptedException {
        Thread.sleep(3000);
        System.out.println(Thread.currentThread().getName() + " 执行完成");
    }

    public void usingAnnotationMethodBlockHandler(BlockException e) {
        System.out.println(Thread.currentThread().getName() + " 被限流");
    }

    public static void main(String[] args) throws InterruptedException {
        initFlowRule();

        TestAnnotation testAnnotation = new TestAnnotation();

        for (int i = 0; i < 4; i++) {
//        for (int i = 0; i < 1; i++) {
            final int j = i;
            new Thread(() -> {
                try {
                    if (j == 2) {
                        Thread.sleep(1000);
                    }
                    if (j == 3) {
                        Thread.sleep(4000);
                    }
                    testAnnotation.usingAnnotationMethod();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }, "Thread-" + i).start();
        }

        Thread.sleep(10000);
    }

    private static void initFlowRule() {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        rule.setResource("doSomething");
        rule.setGrade(RuleConstant.FLOW_GRADE_THREAD);
        rule.setCount(2);
//        rule.setCount(3);
        rules.add(rule);
        FlowRuleManager.loadRules(rules);
    }

}

执行结果为:

INFO: Sentinel log output type is: file
INFO: Sentinel log charset is: utf-8
INFO: Sentinel log base directory is: C:\Users\Administrator\logs\csp
INFO: Sentinel log name use pid is: false
INFO: Sentinel log level is: INFO
Thread-0 被限流
Thread-1 被限流
Thread-3 被限流
Thread-2 执行完成

然后,我把 SentinelResourceAspect 类进行修改,改完如下:

/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.alibaba.csp.sentinel.annotation.aspectj;

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.EntryType;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import java.lang.reflect.Method;

/**
 * Aspect for methods with {@link SentinelResource} annotation.
 *
 * @author Eric Zhao
 */
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {

//    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
//    @Pointcut("execution(* *(..)) && @annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    @Pointcut("call(* *(..)) && @annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }

    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {

//        System.out.println(Thread.currentThread().getName());

        Method originMethod = resolveMethod(pjp);

        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            // Should not go through here.
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }
        String resourceName = getResourceName(annotation.value(), originMethod);
        EntryType entryType = annotation.entryType();
        int resourceType = annotation.resourceType();
        Entry entry = null;
        try {
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            return pjp.proceed();
        } catch (BlockException ex) {
            return handleBlockException(pjp, annotation, ex);
        } catch (Throwable ex) {
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            // The ignore list will be checked first.
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                return handleFallback(pjp, annotation, ex);
            }

            // No fallback function can handle the exception, so throw it out.
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit(1, pjp.getArgs());
            }
        }
    }
}

也就是把 pointcut 改为 @pointcut("execution(* (..)) && @annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)") 或者 @pointcut("call( *(..)) && @annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)"),接着再执行使用注解方式的 TestAnnotation,执行结果为:

INFO: Sentinel log output type is: file
INFO: Sentinel log charset is: utf-8
INFO: Sentinel log base directory is: C:\Users\Administrator\logs\csp
INFO: Sentinel log name use pid is: false
INFO: Sentinel log level is: INFO
Thread-2 被限流
Thread-1 执行完成
Thread-0 执行完成
Thread-3 执行完成

与使用抛出异常方式定义资源 SentinelFlowThread 的执行结果已经一样,这说明 AspectJ 在以方法为目标时会存在 call 和 execution 两种 join point,见官方文档 pointcutsjoinPoints,因此,SentinelResourceAspect 的 invokeResourceWithSentinel 方法将会被同一个线程执行两次,导致 Sentinel 统计线程数时不准确,恳请 Sentinel 的贡献者们排查这是不是一个 BUG?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions