- When amount of context exceeds the threshold, the `NullContext` will be set to current thread local. Thus, when checking context in `CtSph#entry`, once `NullContext` detected, the entry will not do rule checking on the slot chain. - Cache the default context during initialization. Then when amount of context exceeds the threshold, entries under default context can do rule checking under default context. - Enhance the error message Signed-off-by: Eric Zhao <sczyh16@gmail.com>master
@@ -31,7 +31,7 @@ public class Constants { | |||||
public final static int MAX_CONTEXT_NAME_SIZE = 2000; | public final static int MAX_CONTEXT_NAME_SIZE = 2000; | ||||
public final static int MAX_SLOT_CHAIN_SIZE = 6000; | public final static int MAX_SLOT_CHAIN_SIZE = 6000; | ||||
public final static String ROOT_ID = "machine-root"; | public final static String ROOT_ID = "machine-root"; | ||||
public final static String CONTEXT_DEFAULT_NAME = "default_context_name"; | |||||
public final static String CONTEXT_DEFAULT_NAME = "sentinel_default_context"; | |||||
public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN), | public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN), | ||||
Env.nodeBuilder.buildClusterNode()); | Env.nodeBuilder.buildClusterNode()); | ||||
@@ -17,6 +17,7 @@ package com.alibaba.csp.sentinel; | |||||
import com.alibaba.csp.sentinel.context.Context; | import com.alibaba.csp.sentinel.context.Context; | ||||
import com.alibaba.csp.sentinel.context.ContextUtil; | import com.alibaba.csp.sentinel.context.ContextUtil; | ||||
import com.alibaba.csp.sentinel.context.NullContext; | |||||
import com.alibaba.csp.sentinel.node.Node; | import com.alibaba.csp.sentinel.node.Node; | ||||
import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; | import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; | ||||
import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; | import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; | ||||
@@ -44,6 +45,10 @@ class CtEntry extends Entry { | |||||
} | } | ||||
private void setUpEntryFor(Context context) { | private void setUpEntryFor(Context context) { | ||||
// The entry should not be associated to NullContext. | |||||
if (context instanceof NullContext) { | |||||
return; | |||||
} | |||||
this.parent = context.getCurEntry(); | this.parent = context.getCurEntry(); | ||||
if (parent != null) { | if (parent != null) { | ||||
((CtEntry)parent).child = this; | ((CtEntry)parent).child = this; | ||||
@@ -58,14 +63,21 @@ class CtEntry extends Entry { | |||||
protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException { | protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException { | ||||
if (context != null) { | if (context != null) { | ||||
// Null context should exit without clean-up. | |||||
if (context instanceof NullContext) { | |||||
return; | |||||
} | |||||
if (context.getCurEntry() != this) { | if (context.getCurEntry() != this) { | ||||
String curEntryNameInContext = context.getCurEntry() == null ? null : context.getCurEntry().getResourceWrapper().getName(); | |||||
// Clean previous call stack. | // Clean previous call stack. | ||||
CtEntry e = (CtEntry)context.getCurEntry(); | CtEntry e = (CtEntry)context.getCurEntry(); | ||||
while (e != null) { | while (e != null) { | ||||
e.exit(count, args); | e.exit(count, args); | ||||
e = (CtEntry)e.parent; | e = (CtEntry)e.parent; | ||||
} | } | ||||
throw new ErrorEntryFreeException("The order of entry free is can't be paired with the order of entry"); | |||||
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 { | } else { | ||||
if (chain != null) { | if (chain != null) { | ||||
chain.exit(context, resourceWrapper, count, args); | chain.exit(context, resourceWrapper, count, args); | ||||
@@ -76,7 +88,8 @@ class CtEntry extends Entry { | |||||
((CtEntry)parent).child = null; | ((CtEntry)parent).child = null; | ||||
} | } | ||||
if (parent == null) { | if (parent == null) { | ||||
// Auto-created entry indicates immediate exit. | |||||
// Default context (auto entered) will be exited automatically. | |||||
// Note: NullContext won't be exited automatically. | |||||
ContextUtil.exit(); | ContextUtil.exit(); | ||||
} | } | ||||
// Clean the reference of context in current entry to avoid duplicate exit. | // Clean the reference of context in current entry to avoid duplicate exit. | ||||
@@ -56,7 +56,8 @@ public class CtSph implements Sph { | |||||
private AsyncEntry asyncEntryInternal(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { | private AsyncEntry asyncEntryInternal(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { | ||||
Context context = ContextUtil.getContext(); | Context context = ContextUtil.getContext(); | ||||
if (context instanceof NullContext) { | if (context instanceof NullContext) { | ||||
// Init the entry only. No rule checking will occur. | |||||
// The {@link NullContext} indicates that the amount of context has exceeded the threshold, | |||||
// so here init the entry only. No rule checking will be done. | |||||
return new AsyncEntry(resourceWrapper, null, context); | return new AsyncEntry(resourceWrapper, null, context); | ||||
} | } | ||||
if (context == null) { | if (context == null) { | ||||
@@ -112,7 +113,8 @@ public class CtSph implements Sph { | |||||
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { | public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { | ||||
Context context = ContextUtil.getContext(); | Context context = ContextUtil.getContext(); | ||||
if (context instanceof NullContext) { | if (context instanceof NullContext) { | ||||
// Init the entry only. No rule checking will occur. | |||||
// The {@link NullContext} indicates that the amount of context has exceeded the threshold, | |||||
// so here init the entry only. No rule checking will be done. | |||||
return new CtEntry(resourceWrapper, null, context); | return new CtEntry(resourceWrapper, null, context); | ||||
} | } | ||||
@@ -142,6 +144,7 @@ public class CtSph implements Sph { | |||||
e.exit(count, args); | e.exit(count, args); | ||||
throw e1; | throw e1; | ||||
} catch (Throwable e1) { | } catch (Throwable e1) { | ||||
// This should not happen, unless there are errors existing in Sentinel internal. | |||||
RecordLog.info("Sentinel unexpected exception", e1); | RecordLog.info("Sentinel unexpected exception", e1); | ||||
} | } | ||||
return e; | return e; | ||||
@@ -187,4 +187,15 @@ public class Context { | |||||
public Node getOriginNode() { | public Node getOriginNode() { | ||||
return curEntry == null ? null : curEntry.getOriginNode(); | return curEntry == null ? null : curEntry.getOriginNode(); | ||||
} | } | ||||
@Override | |||||
public String toString() { | |||||
return "Context{" + | |||||
"name='" + name + '\'' + | |||||
", entranceNode=" + entranceNode + | |||||
", curEntry=" + curEntry + | |||||
", origin='" + origin + '\'' + | |||||
", async=" + async + | |||||
'}'; | |||||
} | |||||
} | } |
@@ -49,13 +49,35 @@ public class ContextUtil { | |||||
private static ThreadLocal<Context> contextHolder = new ThreadLocal<Context>(); | private static ThreadLocal<Context> contextHolder = new ThreadLocal<Context>(); | ||||
/** | /** | ||||
* Holds all {@link EntranceNode} | |||||
* Holds all {@link EntranceNode}. | |||||
*/ | */ | ||||
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<String, DefaultNode>(); | private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<String, DefaultNode>(); | ||||
private static final ReentrantLock LOCK = new ReentrantLock(); | private static final ReentrantLock LOCK = new ReentrantLock(); | ||||
private static final Context NULL_CONTEXT = new NullContext(); | private static final Context NULL_CONTEXT = new NullContext(); | ||||
static { | |||||
// Cache the entrance node for default context. | |||||
initDefaultContext(); | |||||
} | |||||
private static void initDefaultContext() { | |||||
String defaultContextName = Constants.CONTEXT_DEFAULT_NAME; | |||||
EntranceNode node = new EntranceNode(new StringResourceWrapper(defaultContextName, EntryType.IN), null); | |||||
Constants.ROOT.addChild(node); | |||||
contextNameNodeMap.put(defaultContextName, node); | |||||
} | |||||
/** | |||||
* Not thread-safe, only for test. | |||||
*/ | |||||
static void resetContextMap() { | |||||
if (contextNameNodeMap != null) { | |||||
contextNameNodeMap.clear(); | |||||
initDefaultContext(); | |||||
} | |||||
} | |||||
/** | /** | ||||
* <p> | * <p> | ||||
* Enter the invocation context. The context is ThreadLocal, meaning that | * Enter the invocation context. The context is ThreadLocal, meaning that | ||||
@@ -99,6 +121,7 @@ public class ContextUtil { | |||||
DefaultNode node = localCacheNameMap.get(name); | DefaultNode node = localCacheNameMap.get(name); | ||||
if (node == null) { | if (node == null) { | ||||
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { | if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { | ||||
contextHolder.set(NULL_CONTEXT); | |||||
return NULL_CONTEXT; | return NULL_CONTEXT; | ||||
} else { | } else { | ||||
try { | try { | ||||
@@ -106,6 +129,7 @@ public class ContextUtil { | |||||
node = contextNameNodeMap.get(name); | node = contextNameNodeMap.get(name); | ||||
if (node == null) { | if (node == null) { | ||||
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { | if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { | ||||
contextHolder.set(NULL_CONTEXT); | |||||
return NULL_CONTEXT; | return NULL_CONTEXT; | ||||
} else { | } else { | ||||
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); | node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null); | ||||
@@ -27,7 +27,6 @@ import com.alibaba.csp.sentinel.Constants; | |||||
public class NullContext extends Context { | public class NullContext extends Context { | ||||
public NullContext() { | public NullContext() { | ||||
super(null, null); | |||||
super(null, "null_context_internal"); | |||||
} | } | ||||
} | } |
@@ -15,7 +15,11 @@ | |||||
*/ | */ | ||||
package com.alibaba.csp.sentinel.context; | package com.alibaba.csp.sentinel.context; | ||||
import com.alibaba.csp.sentinel.Constants; | |||||
import com.alibaba.csp.sentinel.TestUtil; | |||||
import org.junit.After; | import org.junit.After; | ||||
import org.junit.Before; | |||||
import org.junit.Test; | import org.junit.Test; | ||||
import static org.junit.Assert.*; | import static org.junit.Assert.*; | ||||
@@ -28,9 +32,56 @@ import static org.junit.Assert.*; | |||||
*/ | */ | ||||
public class ContextTest { | public class ContextTest { | ||||
@Before | |||||
public void setUp() { | |||||
resetContextMap(); | |||||
} | |||||
@After | @After | ||||
public void cleanUp() { | public void cleanUp() { | ||||
ContextUtil.exit(); | |||||
TestUtil.cleanUpContext(); | |||||
} | |||||
@Test | |||||
public void testEnterCustomContextWhenExceedsThreshold() { | |||||
fillContext(); | |||||
try { | |||||
String contextName = "abc"; | |||||
ContextUtil.enter(contextName, "bcd"); | |||||
Context curContext = ContextUtil.getContext(); | |||||
assertNotEquals(contextName, curContext.getName()); | |||||
assertTrue(curContext instanceof NullContext); | |||||
assertEquals("", curContext.getOrigin()); | |||||
} finally { | |||||
ContextUtil.exit(); | |||||
resetContextMap(); | |||||
} | |||||
} | |||||
@Test | |||||
public void testDefaultContextWhenExceedsThreshold() { | |||||
fillContext(); | |||||
try { | |||||
ContextUtil.trueEnter(Constants.CONTEXT_DEFAULT_NAME, ""); | |||||
Context curContext = ContextUtil.getContext(); | |||||
assertEquals(Constants.CONTEXT_DEFAULT_NAME, curContext.getName()); | |||||
assertNotNull(curContext.getEntranceNode()); | |||||
} finally { | |||||
ContextUtil.exit(); | |||||
resetContextMap(); | |||||
} | |||||
} | |||||
private void fillContext() { | |||||
for (int i = 0; i < Constants.MAX_CONTEXT_NAME_SIZE; i++) { | |||||
ContextUtil.enter("test-context-" + i); | |||||
ContextUtil.exit(); | |||||
} | |||||
} | |||||
private void resetContextMap() { | |||||
ContextUtil.resetContextMap(); | |||||
Constants.ROOT.removeChildList(); | |||||
} | } | ||||
@Test | @Test | ||||