Sentinel源码(七)AsyncEntry

695 阅读4分钟

前言

在第二章的时候,我们分析了Sph.entry和Entry.exit的执行流程。

除此以外,Sentinel还提供了一套Async API,本章来学习一下Async API的作用和源码。

一、案例

public class CustomAsyncDemo {
    public static void main(String[] args) {
        ContextUtil.enter("context-0", "appA");
        Entry entry0 = null;
        try {
            entry0 = SphU.entry("entry0");
            // 生成异步调用链路
            AsyncEntry asyncEntry0 = SphU.asyncEntry("asyncEntry0");
            Thread t1 = new Thread(() -> {
                // 切换上下文
                ContextUtil.runOnContext(/*上下文*/asyncEntry0.getAsyncContext(), /*Runnable*/ () -> {
                    try {
                        System.out.println(Thread.currentThread().getName() + " running, current_context = " + ContextUtil.getContext());
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        asyncEntry0.exit();
                    }
                });
            });
            t1.start();
            System.out.println(Thread.currentThread().getName() + " running, current_context = " + ContextUtil.getContext());
            t1.join();
        } catch (BlockException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (entry0 != null) {
                entry0.exit();
            }
            ContextUtil.exit();
        }
    }
}

根据上述demo,链路树长这样。

|_EntranceNode: context_0
   |_ entry0
   |_ asyncEntry0

控制台输出:

main running, current_context = Context{name='context-0', entranceNode=com.alibaba.csp.sentinel.node.EntranceNode@372f7a8d, curEntry=com.alibaba.csp.sentinel.CtEntry@7c3df479, origin='appA', async=false}
Thread-2 running, current_context = Context{name='context-0', entranceNode=com.alibaba.csp.sentinel.node.EntranceNode@372f7a8d, curEntry=com.alibaba.csp.sentinel.AsyncEntry@4c31aaef, origin='appA', async=true}

总结来说,async api做到了几点:

  1. 链路保持:SphU.asyncEntry方法,将async节点与当前节点平级,与父节点相连;
  2. 线程间上下文传递:ContextUtil.runOnContext方法,线程间传递Context。保证父线程Context中除了curEntry以外所有信息传递到子线程;

回顾一下Context的属性:

public class Context {
    // 上下文名称
    private final String name;
    // EntranceNode 入口节点 --- 树根
    private DefaultNode entranceNode;
    // 当前Entry
    private Entry curEntry;
    // 来源
    private String origin = "";
      // 是否是异步上下文
    private final boolean async;
}

二、SphU.asyncEntry

如果连续调用SphU.entry,树的层级会越来越深。

而SphU.asyncEntry能让新生成的节点与当前节点齐平。

核心代码在CtSph#asyncEntryWithPriorityInternal。

// CtSph.java
private AsyncEntry asyncEntryWithPriorityInternal(ResourceWrapper resourceWrapper, int count, boolean prioritized,
                                                  Object... args) throws BlockException {
    // 1. 获取当前线程Context
    Context context = ContextUtil.getContext();
    // 当Context数量超过了阈值,不会做任何规则校验
    if (context instanceof NullContext) {
        return asyncEntryWithNoChain(resourceWrapper, context);
    }
    // 2. 如果用户没有主动创建Context,使用默认上下文sentinel_default_context
    if (context == null) {
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }

    if (!Constants.ON) {
        return asyncEntryWithNoChain(resourceWrapper, context);
    }

    // 3. 获取Slot链
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

    if (chain == null) {
        return asyncEntryWithNoChain(resourceWrapper, context);
    }

    // 4. 构造AsyncEntry,构造时将这个Entry接入Context中的Entry链表尾部
    AsyncEntry asyncEntry = new AsyncEntry(resourceWrapper, chain, context);
    try {
        // 5. 执行所有规则校验
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
        // 6. diff: 创建AsyncContext
        asyncEntry.initAsyncContext();
        // 7. diff: 对原有Context处理
        asyncEntry.cleanCurrentEntryInLocal();
    } catch (BlockException e1) {
        asyncEntry.exitForContext(context, count, args);
        throw e1;
    } catch (Throwable e1) {
        asyncEntry.cleanCurrentEntryInLocal();
    }
    return asyncEntry;
}

在entry方法的基础上,asyncEntry多了第6步和第7步。

首先关注AsyncEntry的构造方法(4) 。AsyncEntry在构造时,直接调用了父类的三个参数的构造方法。

public class AsyncEntry extends CtEntry {
    AsyncEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
        super(resourceWrapper, chain, context);
    }
}
// CtEntry.java
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
  super(resourceWrapper);
  this.chain = chain;
  this.context = context;

  setUpEntryFor(context);
}

private void setUpEntryFor(Context context) {
  if (context instanceof NullContext) {
    return;
  }
  this.parent = context.getCurEntry();
  if (parent != null) {
    ((CtEntry) parent).child = this;
  }
  context.setCurEntry(this);
}

这意味着,在做规则校验时(5),当前上下文的curEntry是这个AsyncEntry,且当前AsyncEntry是接在上一个Entry的后面的,调用树如下:

|_EntranceNode: context_0
   |_ entry0
     |_ asyncEntry0

initAsyncContext方法(6) ,在AsyncEntry里创建了一个用于异步线程携带的上下文。在这里创建是为了让Entry被带入其他线程时,用户端代码能够拿到这个异步上下文。

// AsyncEntry.java
private Context asyncContext;
void initAsyncContext() {
    if (asyncContext == null) {
        if (context instanceof NullContext) {
            asyncContext = context;
            return;
        }
        // 拷贝上下文
        this.asyncContext = Context.newAsyncContext(context.getEntranceNode(), context.getName())
            .setOrigin(context.getOrigin())
            .setCurEntry(this);
    }
}

cleanCurrentEntryInLocal方法(7) ,将当前AsyncEntry从原来的Context的调用链中移除,将原有Context调用链和异步Context调用链分开。即AsyncEntry存在于原来的上下文中,仅仅是为了走一遍所有Slot。

public class AsyncEntry extends CtEntry {

    private Context asyncContext;

    void cleanCurrentEntryInLocal() {
        if (context instanceof NullContext) {
            return;
        }
        Context originalContext = context;
        if (originalContext != null) {
            Entry curEntry = originalContext.getCurEntry();
            if (curEntry == this) {
                Entry parent = this.parent;
                originalContext.setCurEntry(parent);
                if (parent != null) {
                    ((CtEntry)parent).child = null;
                }
            } else {
                // ...
                throw new IllegalStateException(msg);
            }
        }
    }
}

至此,异步Context调用树如下:

|_EntranceNode: context_0
   |_ entry0
   |_ asyncEntry0

同步Context调用树如下:

|_EntranceNode: context_0
   |_ entry0

AsyncEntry的规则校验和数据统计都已经走完了,需要用户代码将异步Context传递到子线程中。

三、ContextUtil.runOnContext

用户代码调用ContextUtil.runOnContext,传入异步Context和回调方法(Runnable),可以实现上下文传递。

// 生成异步调用链路
AsyncEntry asyncEntry0 = SphU.asyncEntry("asyncEntry0");
Thread t1 = new Thread(() -> {
    // 切换上下文
    ContextUtil.runOnContext(/*上下文*/asyncEntry0.getAsyncContext(), /*Runnable*/ () -> {
        try {
            System.out.println(Thread.currentThread().getName() + " running, current_context = " + ContextUtil.getContext());
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            asyncEntry0.exit();
        }
    });
});
t1.start();

runOnContext方法没有什么特别之处,就是将用户业务代码外面裹一层replaceContext方法,用于切换上下文。

// ContextUtil.java
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
public static void runOnContext(Context context, Runnable f) {
    // 1. 用入参Context替换当前线程ThreadLocal持有的Context
    Context curContext = replaceContext(context);
    try {
        // 2. 执行callback
        f.run();
    } finally {
        // 3. 复原当前线程ThreadLocal持有的Context
        replaceContext(curContext);
    }
}

static Context replaceContext(Context newContext) {
  Context backupContext = contextHolder.get();
  if (newContext == null) {
    contextHolder.remove();
  } else {
    contextHolder.set(newContext);
  }
  return backupContext;
}

总结

Sentinel的Async API支持在不同线程间传播Context上下文。

更好的支持了流控模式为链路的流控规则(因为链路流控规则涉及到当前上下文)。