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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions