- 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_SLOT_CHAIN_SIZE = 6000; | |||
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), | |||
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.ContextUtil; | |||
import com.alibaba.csp.sentinel.context.NullContext; | |||
import com.alibaba.csp.sentinel.node.Node; | |||
import com.alibaba.csp.sentinel.slotchain.ProcessorSlot; | |||
import com.alibaba.csp.sentinel.slotchain.ResourceWrapper; | |||
@@ -44,6 +45,10 @@ class CtEntry extends Entry { | |||
} | |||
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; | |||
@@ -58,14 +63,21 @@ class CtEntry extends Entry { | |||
protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException { | |||
if (context != null) { | |||
// Null context should exit without clean-up. | |||
if (context instanceof NullContext) { | |||
return; | |||
} | |||
if (context.getCurEntry() != this) { | |||
String curEntryNameInContext = context.getCurEntry() == null ? null : context.getCurEntry().getResourceWrapper().getName(); | |||
// Clean previous call stack. | |||
CtEntry e = (CtEntry)context.getCurEntry(); | |||
while (e != null) { | |||
e.exit(count, args); | |||
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 { | |||
if (chain != null) { | |||
chain.exit(context, resourceWrapper, count, args); | |||
@@ -76,7 +88,8 @@ class CtEntry extends Entry { | |||
((CtEntry)parent).child = 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(); | |||
} | |||
// 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 { | |||
Context context = ContextUtil.getContext(); | |||
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); | |||
} | |||
if (context == null) { | |||
@@ -112,7 +113,8 @@ public class CtSph implements Sph { | |||
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException { | |||
Context context = ContextUtil.getContext(); | |||
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); | |||
} | |||
@@ -142,6 +144,7 @@ public class CtSph implements Sph { | |||
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; | |||
@@ -187,4 +187,15 @@ public class Context { | |||
public Node 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>(); | |||
/** | |||
* Holds all {@link EntranceNode} | |||
* Holds all {@link EntranceNode}. | |||
*/ | |||
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<String, DefaultNode>(); | |||
private static final ReentrantLock LOCK = new ReentrantLock(); | |||
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> | |||
* Enter the invocation context. The context is ThreadLocal, meaning that | |||
@@ -99,6 +121,7 @@ public class ContextUtil { | |||
DefaultNode node = localCacheNameMap.get(name); | |||
if (node == null) { | |||
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { | |||
contextHolder.set(NULL_CONTEXT); | |||
return NULL_CONTEXT; | |||
} else { | |||
try { | |||
@@ -106,6 +129,7 @@ public class ContextUtil { | |||
node = contextNameNodeMap.get(name); | |||
if (node == null) { | |||
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) { | |||
contextHolder.set(NULL_CONTEXT); | |||
return NULL_CONTEXT; | |||
} else { | |||
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 NullContext() { | |||
super(null, null); | |||
super(null, "null_context_internal"); | |||
} | |||
} |
@@ -15,7 +15,11 @@ | |||
*/ | |||
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.Before; | |||
import org.junit.Test; | |||
import static org.junit.Assert.*; | |||
@@ -28,9 +32,56 @@ import static org.junit.Assert.*; | |||
*/ | |||
public class ContextTest { | |||
@Before | |||
public void setUp() { | |||
resetContextMap(); | |||
} | |||
@After | |||
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 | |||