preface

Based on Sentinel1.8, this chapter analyzes the execution flow of Sph. Entry and Entry. Exit.

1, the SPI

Take a look at the Service Provider Interface (SPI) mechanism of Sentinel.

Sentinel’s Spi mechanism is similar to the JDK’s Spi mechanism in that its extension points relative to the JDK are condensed in Spi annotations.

public @interface Spi {
    String value() default "";
    boolean isSingleton() default true;
    boolean isDefault() default false;
    int order() default 0;
    int ORDER_HIGHEST = Integer.MIN_VALUE;
    int ORDER_LOWEST = Integer.MAX_VALUE;
}
Copy the code
  1. Value: alias. If an alias is set, only one implementation class of the same alias can exist in a SpiLoader.
  2. IsSingleton: whether to be singleton. Default is true.
  3. IsDefault: whether the default implementation class is false. There can only be one default implementation class in a SpiLoader.
  4. Order C.

To sum up, Sentinel’s SPI mechanism supports: alias mutual exclusion, singleton, default implementation, priority.

SpiLoader implements these four functions by loading implementation classes for the SPI interface with the generic S and caching them in several member variables.

Public final class SpiLoader<S> {// SPI configuration file path private static final String SPI_FILE_PREFIX = "meta-INF /services/"; // SPI interface - SpiLoader implementation private static final ConcurrentHashMap<String, SpiLoader> SPI_LOADER_MAP = new ConcurrentHashMap<>(); Private final List<Class<? extends S>> classList = Collections.synchronizedList(new ArrayList<Class<? extends S>>()); // Current SpiLoader cache SPI interface implementation classes (ordered) Private final List<Class<? extends S>> sortedClassList = Collections.synchronizedList(new ArrayList<Class<? extends S>>()); Private Final ConcurrentHashMap<String, Class<? extends S>> classMap = new ConcurrentHashMap<>(); // Singleton map of current SpiLoader cache k-className V-singleton private Final ConcurrentHashMap<String, S> singletonMap = new ConcurrentHashMap<>(); Private final AtomicBoolean loaded = new AtomicBoolean(false); // The current SpiLoader default implementation of the Spi interface private Class<? extends S> defaultClass = null; // current SpiLoader corresponding Spi interface private Class<S> service; }Copy the code

For example, Sentinel’s various ProcessorSlot slots are loaded through the SPI mechanism.

Spiloader.of (ProcessorSlot.class) creates DefaultSlotChainBuilder, which loads the implementation class for the ProcessorSlot interface.

@Spi(isDefault = true) public class DefaultSlotChainBuilder implements SlotChainBuilder { @Override public ProcessorSlotChain build() { ProcessorSlotChain chain = new DefaultProcessorSlotChain(); List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted(); for (ProcessorSlot slot : sortedSlotList) { if (! (slot instanceof AbstractLinkedProcessorSlot)) { continue; } chain.addLast((AbstractLinkedProcessorSlot<? >) slot); } return chain; }}Copy the code

SpiLoader’s logic is relatively simple, and is very similar to the JDK’s ServiceLoader source code, so skip it.

2, SphO or SphU

A simple example of using Sentinel is as follows:

Entry entry = null; Try {// Get Entry Entry = SphU. Entry (KEY); } catch (BlockException e1) {// Rule verification failed, BlockException block.incrementandGet () occurs; } catch (Exception e2) {finally {total.incrementandget (); // Release Entry if (Entry! = null) { entry.exit(); }}Copy the code

SphO entry method that captures BlockException and returns false if the Sentinel rule fails.

// SphO.java
public static boolean entry(Method method, EntryType trafficType, int batchCount, Object... args) {
    try {
        Env.sph.entry(method, trafficType, batchCount, args);
    } catch (BlockException e) {
        return false;
    } catch (Throwable e) {
        RecordLog.warn("SphO fatal error", e);
        return true;
    }
    return true;
}
Copy the code

The entry method of SphU calls env.sph.entry directly without doing any processing.

// SphU.java
public static Entry entry(String name, EntryType trafficType, int batchCount, Object... args)
  throws BlockException {
  return Env.sph.entry(name, trafficType, batchCount, args);
}
Copy the code

Both are underlying calls to the entry method of the static CtSph instance in Env.

public class Env {
    public static final Sph sph = new CtSph();
}
Copy the code

3, CtSph

Resources of either type String or Method are wrapped as StringResourceWrapper or MethodResourceWrapper when they enter CtSph.

// CtSph.java
@Override
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    StringResourceWrapper resource = new StringResourceWrapper(name, type);
    return entry(resource, count, args);
}
Copy the code

Finally, the entryWithPriority method is entered, and the prioritized parameter defaults to false.

// CtSph.java private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // 1. Context Context = contextutil.getContext (); If (Context instanceof NullContext) {return new CtEntry(resourceWrapper, null, Context); } // 2. If the user does not actively create the Context, Use default context sentinel_default_context if (context == null) {context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); } if (! Constants.ON) { return new CtEntry(resourceWrapper, null, context); ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); if (chain == null) { return new CtEntry(resourceWrapper, null, context); Entry e = new CtEntry(resourceWrapper, chain, Context); Try {// 5. Execute all rules to validate chain.entry(context, resourceWrapper, NULL, count, prioritized, args); } catch (BlockException e1) { e.exit(count, args); throw e1; } catch (Throwable e1) { // This should not happen, unless there are errors existing in Sentinel internal. RecordLog.info("Sentinel unexpected exception", e1); } return e; }Copy the code

CtSph. EntryWithPriority can be divided into 5 steps. The first two steps are to get the Context, the third step is to get all processorSlots to execute, the fourth step is to construct Entry, and the fifth step is to execute all ProcessorSlots.

Next, analyze the Entry method step by step.

4, the Context

The first two steps of CtSph. EntryWithPriority involve Context Context.

Context is stored in ContextUtil via ThreadLocal, and each Context name uniquely corresponds to an EntranceNode.

public class ContextUtil {
    private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
    // k=context.name v=EntranceNode
    private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
}
Copy the code

When ContextUtil is initialized, an EntranceNode corresponding to the Context called sentinel_DEFAULt_context is created.

This EntranceNode will be added to the children of the Constants.root node.

// ContextUtil.java
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
static {
    initDefaultContext();
}
private static void initDefaultContext() {
    // sentinel_default_context
    String defaultContextName = Constants.CONTEXT_DEFAULT_NAME;
    EntranceNode node = new EntranceNode(new StringResourceWrapper(defaultContextName, EntryType.IN), null);
    Constants.ROOT.addChild(node);
    contextNameNodeMap.put(defaultContextName, node);
}
Copy the code

The first step in CtSph. EntryWithPriority is to get the Context held by the current thread.

If the current thread holds a NullContext, no rule check is performed (SlotChain is null in CtEntry) and a CtEntry is returned.

// CtSph.java private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... Contextutil.getcontext (); contextutil.getContext (); If (Context instanceof NullContext) {// If (Context instanceof NullContext) {// If (Context instanceof NullContext) { Return new CtEntry(resourceWrapper, null, context); } } // ContextUtil.java private static ThreadLocal<Context> contextHolder = new ThreadLocal<>(); public static Context getContext() { return contextHolder.get(); }Copy the code

The second step of ctSPh.entryWithPriority sets the current thread Context to the sentinel_DEFAULt_context default if there is no corresponding Context for the current thread.

// CtSph.java private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // .. // 2. If the user does not actively create the Context, Use default context sentinel_default_context if (context == null) {context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME); }}Copy the code

InternalContextUtil. InternalEnter call bottom is ContextUtil# trueEnter method, this method is the core of creating context.

// InternalContextUtil.java /** * Holds all {@link EntranceNode}. Each {@link EntranceNode} is associated with a distinct context name. */ private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>(); protected static Context trueEnter(String name, String origin) { // 1. Context = contextholder.get (); Context = contextholder.get (); If (context == null) {// get EntranceNode corresponding to context name If no Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap; DefaultNode node = localCacheNameMap.get(name); if (node == null) { if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { // 3. If the number of contexts exceeds MAX_CONTEXT_NAME_SIZE (2000), NullContext is returned. SetNullContext () does not create the actual Context. return NULL_CONTEXT; } else { LOCK.lock(); try { node = contextNameNodeMap.get(name); if (node == null) { if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { setNullContext(); return NULL_CONTEXT; } else {// 4. EntranceNode node = new EntranceNode(new StringResourceWrapper(name, entryType. IN), null); Constants.ROOT.addChild(node); Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1); newMap.putAll(contextNameNodeMap); newMap.put(name, node); contextNameNodeMap = newMap; } } } finally { LOCK.unlock(); ThreadLocal Context = new Context(node, name); context.setOrigin(origin); contextHolder.set(context); } return context; }Copy the code

As you can see from the above code:

  1. Each context name corresponds to an EntranceNode of the same name, stored in the contextNameNodeMap static variable of InternalContextUtil. The key is the context name and the value is the EntranceNode.
  2. In a process, Sentinel can only allow a maximum of 2000 contexts (hard coded). If the number exceeds 2000, NullContext will be returned and no rule verification will be performed.

User code can create a non-default context by executing the public Enter method of InternalContextUtil.

// InternalContextUtil.java public static Context enter(String name, String origin) { if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) { throw new ContextNameDefineException( "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!" ); } return trueEnter(name, origin); }Copy the code

5, ProcessorSlot

The third step of CtSph. EntryWithPriority is to get the ProcessorSlot chain to execute according to the resource name. If chain is null, CtEntry is returned without rule verification.

// CtSph.java private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // ... ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper); if (chain == null) { return new CtEntry(resourceWrapper, null, context); } / /... }Copy the code

The lookProcessChain method loads the Resource corresponding to ProcessorSlotChain, which contains all processorSlots loaded through the SPI mechanism for subsequent rule verification of this Resource.

Note that:

  1. A resource of the same name corresponds to the same ProcessorSlotChain instance (equals and hasCode methods of ResourceWrapper);
  2. LookProcessChain methods will return null if the number of resources exceeds 6000. LookProcessChain methods will not perform any rule verification.
// CtSph.java /** * Same resource({@link ResourceWrapper#equals(Object)}) will share the same * {@link ProcessorSlotChain}, no matter in which {@link Context}. */ private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap<ResourceWrapper, ProcessorSlotChain>() ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) { // 1. ProcessorSlotChain Chain = ChainMap. get(resourceWrapper); if (chain == null) { synchronized (LOCK) { chain = chainMap.get(resourceWrapper); If (chain == null) {// 2. If the number of resources exceeds MAX_SLOT_CHAIN_SIZE (6000), null is returned. Constants.MAX_SLOT_CHAIN_SIZE) {return null; } / / 3. Load all ProcessorSlot SPI mechanism, structure of DefaultProcessorSlotChain returns chain = SlotChainProvider. NewSlotChain (); Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>( chainMap.size() + 1); newMap.putAll(chainMap); newMap.put(resourceWrapper, chain); chainMap = newMap; } } } return chain; }Copy the code

6, CtEntry

The fourth step in ctSPh.entryWithPriority constructs the CtEntry, which is the Entry returned to the user code.

// CtSph.java private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // ... Entry e = new CtEntry(resourceWrapper, chain, Context); / /... }Copy the code

CtEntry aggregates Context, Slot, and Resource, and in the constructor, setUpEntryFor adds the current Entry to the Entry call chain in the Context.

Class CtEntry extends Entry {// Last Entry protected Entry parent = null; // Next Entry protected Entry child = null; // Slot Slot protected ProcessorSlot<Object> chain; // Context protected Context Context; protected LinkedList<BiConsumer<Context, Entry>> exitHandlers; CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) { super(resourceWrapper); this.chain = chain; this.context = context; setUpEntryFor(context); } private void setUpEntryFor(Context Context) {// The Entry should not be associated to NullContext. if (context instanceof NullContext) { return; } this.parent = context.getCurEntry(); if (parent ! = null) { ((CtEntry) parent).child = this; } context.setCurEntry(this); }}Copy the code

7. Rule verification

The fifth step of CtSph. EntryWithPriority executes the entry method of ProcessorSlotChain, which throws a BlockException if the rule verification fails, and returns the CtEntry obtained in step 4 if the rule verification succeeds.

// CtSph.java private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // 1. Get the current thread Context // 2. Use the default Context sentinel_default_context if the user has not actively created the Context. // get Slot chain // 4. Construct a CtEntry that connects the Entry to the end of the Entry list in the Context. Execute all rules to validate chain.entry(Context, resourceWrapper, NULL, count, prioritized, ARgs); } catch (BlockException e1) { e.exit(count, args); throw e1; } catch (Throwable e1) { RecordLog.info("Sentinel unexpected exception", e1); } return e; }Copy the code

The implementation class is DefaultProcessorSlotChain ProcessorSlotChain.

DefaultProcessorSlotChain loaded many ProcessorSlot slot according to the SPI mechanism, by first. TransformEntry method, perform the first ProcessorSlot.

public class DefaultProcessorSlotChain extends ProcessorSlotChain { AbstractLinkedProcessorSlot<? > first = new AbstractLinkedProcessorSlot<Object>() { @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable { super.fireEntry(context, resourceWrapper, t, count, prioritized, args); }}; @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args) throws Throwable { first.transformEntry(context, resourceWrapper, t, count, prioritized, args); }}Copy the code

All ProcessorSlot inherited AbstractLinkedProcessorSlot abstract class, form list, each time the current Slot after perform their duties (chain), is called fireEntry methods of an abstract class, execute the next Slot entry method.

Public abstract class AbstractLinkedProcessorSlot < T > implements ProcessorSlot < T > {/ / the next slot private AbstractLinkedProcessorSlot<? > next = null; @override public void fireEntry(Context Context, ResourceWrapper ResourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable { if (next ! = null) { next.transformEntry(context, resourceWrapper, obj, count, prioritized, args); }} // generics, Void transformEntry(Context Context, ResourceWrapper ResourceWrapper, Object O, int count, boolean prioritized, Object... args) throws Throwable { T t = (T)o; entry(context, resourceWrapper, t, count, prioritized, args); }}Copy the code

For example, after NodeSelectorSlot executes its own services, it calls fireEntry to execute the next Slot.

@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT) public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> { @Override public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args) throws Throwable { // 1. Execute your own logic //... // 2. Execute the next Slot fireEntry(context, resourceWrapper, node, count, prioritized, args); }}Copy the code

The first three provide data support, and the last five provide rule verification (throwing blockexceptions) :

  1. NodeSelectorSlot: Build the path of a Resource DefaultNode, stored in a tree structure.
  2. ClusterBuilderSlot: A ClusterNode is built to record statistics about resource dimensions.
  3. StatisticSlot: Use Node to record performance indicators, such as RT and Pass/Block Count, to provide data support for subsequent rule verification.
  4. AuthoritySlot: verifies the authorization rule
  5. SystemSlot: verifies system rules
  6. ParamFlowSlot: verifies flow control rules for hotspot parameters
  7. FlowSlot: verifies flow control rules
  8. DegradeSlot: degrades rules

The source code for each Slot will be detailed in the next chapter.

8 the exit.

Sph. Entry performs rule verification, returns user entry, and then the user code executes the business logic. When the business logic processing is complete, the user code calls the entry.exit method in the finally block.

Entry entry = null; Try {// Perform rule verification, return CtEntry Entry = sphu.entry (KEY); / /... Business logic} finally {// Entry exit if (Entry! = null) { entry.exit(); }}Copy the code

In addition, if a BlockException occurs, CtSph actively calls the entry.exit method.

// CtSph.java private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args) throws BlockException { // ... Try {// 5. Execute all rules to validate chain.entry(context, resourceWrapper, NULL, count, prioritized, args); } catch (BlockException e1) { e.exit(count, args); throw e1; }}Copy the code

The entry. exit method removes the current Entry from the context and eventually calls the exitForContext method that implements the CtEntry class.

// ctentry. Java // last Entry protected Entry parent = null; // Next Entry protected Entry child = null; // Slot Slot protected ProcessorSlot<Object> chain; // Context protected Context Context; protected LinkedList<BiConsumer<Context, Entry>> exitHandlers; @Override protected Entry trueExit(int count, Object... args) throws ErrorEntryFreeException { exitForContext(context, count, args); return parent; } protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException { if (context ! // NullContext ignores if (context instanceof NullContext) {return; } if (context.getCurEntry() ! = this) {// The wrong entry exits, empties the context of all entries and throws an exception String curEntryNameInContext = context.getCurenTry () == null? null : context.getCurEntry().getResourceWrapper().getName(); CtEntry e = (CtEntry) context.getCurEntry(); while (e ! = null) { e.exit(count, args); e = (CtEntry) e.parent; } String errorMessage = String.format("The order of entry exit can't be paired with the order of entry" + ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext, resourceWrapper.getName()); throw new ErrorEntryFreeException(errorMessage); } else {// Normal entry exit // 1. Execute exit method for all slots if (chain! = null) { chain.exit(context, resourceWrapper, count, args); } / / 2. Perform all exitHandlers (degradation rule is used) callExitHandlersAndCleanUp (context); // 3. Remove the current entry context.setCurentry (parent); if (parent ! = null) { ((CtEntry) parent).child = null; } if (parent == null) { if (ContextUtil.isDefaultContext(context)) { ContextUtil.exit(); }} // 4. Current entry.context = null to prevent repeated exit clearEntryContext(); }}}Copy the code

Normal business logic will only be executed if the exit Entry is the last Entry in the current context, otherwise ErrorEntryFreeException will be thrown.

There are four steps to exit Entry:

  1. Perform all ProcessorSlot DefaultProcessorSlotChain once upon a time
public class DefaultProcessorSlotChain extends ProcessorSlotChain {

    AbstractLinkedProcessorSlot<?> first = ...
    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        first.exit(context, resourceWrapper, count, args);
    }

}
Copy the code
  1. CallExitHandlersAndCleanUp perform all exitHandlers on the current Entry, this basically is to Degrade service degradation rule of circuit breaker, later to say again
// CtEntry.java protected LinkedList<BiConsumer<Context, Entry>> exitHandlers; private void callExitHandlersAndCleanUp(Context ctx) { if (exitHandlers ! = null && ! exitHandlers.isEmpty()) { for (BiConsumer<Context, Entry> handler : this.exitHandlers) { try { handler.accept(ctx, this); } catch (Exception e) { RecordLog.warn("Error occurred when invoking entry exit handler, current entry: " + resourceWrapper.getName(), e); } } exitHandlers = null; }}Copy the code
  1. List operation to remove the current CtEntry from the Entry list of the Context Context. (See section 6, Constructor of ctentries, for how to make a list of calls.)
  2. ClearEntryContext, which sets the Context associated with the current CtEntry to NULL
// CtEntry.java
protected void clearEntryContext() {
    this.context = null;
}
Copy the code

conclusion

This chapter reads the execution flow of Sph. Entry and Entry. Exit.

Sph.entry

Before the user code is executed, the sph. entry rule can be verified. If the verification passes, the execution continues; otherwise, BlockException will be thrown.

Sph. Entry is divided into the following steps:

  1. Optional, and can be accessed by the user code InternalContextUtil. Enter method, create a default Context, at the same time will create the corresponding EntranceNode Context;
  2. Enter the ctsph. entry method and create a Resource object corresponding to the Resource name.
  3. The ctSPh. entryWithPriority method is used to get the current thread’s Context in ThreadLocal. If it doesn’t exist, use the default sentinel_default_context.
  4. The ProcessorSlot linked list is loaded through the SPI mechanism with the same Resource name corresponding to the same ProcessorSlot linked list instance.
  5. Construct an instance of CtEntry, which adds the Entry instance to the Context’s Entry list.
  6. Perform all processorSlot.entry, perform rule verification;
  7. If the rule verification fails, throw a BlockException. Otherwise, execute the user code normally;

Entry.exit

After the user code is executed, the resource needs to be released through entry.exit.

Entry. Exit is divided into the following steps:

  1. Execute all processorSlot.exit;
  2. Execute all Entry. ExitHandlers, used only by the demote rule;
  3. Remove the relationship between Entry and Context: Remove the current Entry from the Entry list of Context, and set the Context corresponding to the current Entry to NULL.