diff --git a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java index a6175350..752ea34e 100755 --- a/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java +++ b/sentinel-core/src/main/java/com/alibaba/csp/sentinel/context/ContextUtil.java @@ -190,7 +190,7 @@ public class ContextUtil { * @return old context * @since 0.2.0 */ - private static Context replaceContext(Context newContext) { + static Context replaceContext(Context newContext) { Context backupContext = contextHolder.get(); if (newContext == null) { contextHolder.remove(); diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/AsyncEntryIntegrationTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/AsyncEntryIntegrationTest.java new file mode 100644 index 00000000..589ac19a --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/AsyncEntryIntegrationTest.java @@ -0,0 +1,224 @@ +/* + * 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; + +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.node.DefaultNode; +import com.alibaba.csp.sentinel.node.EntranceNode; +import com.alibaba.csp.sentinel.node.Node; +import com.alibaba.csp.sentinel.slots.block.BlockException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Integration test for asynchronous entry, including common scenarios. + * + * @author Eric Zhao + */ +public class AsyncEntryIntegrationTest { + + @Before + public void clearContext() { + if (ContextUtil.getContext() != null) { + ContextUtil.getContext().setCurEntry(null); + ContextUtil.exit(); + } + } + + private final ExecutorService pool = Executors.newFixedThreadPool(10); + + private void anotherAsync() { + try { + final AsyncEntry entry = SphU.asyncEntry("test-another-async"); + + runAsync(new Runnable() { + @Override + public void run() { + ContextUtil.runOnContext(entry.getAsyncContext(), new Runnable() { + @Override + public void run() { + try { + TimeUnit.SECONDS.sleep(2); + anotherSyncInAsync(); + System.out.println("Async result: 666"); + } catch (InterruptedException e) { + // Ignore. + } finally { + entry.exit(); + } + } + }); + } + }); + } catch (BlockException ex) { + ex.printStackTrace(); + } + } + + private void fetchSync() { + Entry entry = null; + try { + entry = SphU.entry("test-sync"); + } catch (BlockException ex) { + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + } + } + + private void fetchSyncInAsync() { + Entry entry = null; + try { + entry = SphU.entry("test-sync-in-async"); + } catch (BlockException ex) { + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + } + } + + public void anotherSyncInAsync() { + Entry entry = null; + try { + entry = SphU.entry("test-another-in-async"); + } catch (BlockException ex) { + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + } + } + + private void doAsyncThenSync() { + try { + // First we call an asynchronous resource. + final AsyncEntry entry = SphU.asyncEntry("test-async"); + this.invoke("abc", new Consumer() { + @Override + public void accept(final String resp) { + // The thread is different from original caller thread for async entry. + // So we need to wrap in the async context so that nested sync invocation entry + // can be linked to the parent asynchronous entry. + ContextUtil.runOnContext(entry.getAsyncContext(), new Runnable() { + @Override + public void run() { + try { + // In the callback, we do another async invocation under the async context. + anotherAsync(); + + System.out.println(resp); + + // Then we do a sync entry under current async context. + fetchSyncInAsync(); + } finally { + // Exit the async entry. + entry.exit(); + } + } + }); + } + }); + // Then we call a sync resource. + fetchSync(); + } catch (BlockException ex) { + // Request blocked, handle the exception. + ex.printStackTrace(); + } + } + + @Test + public void testAsyncEntryUnderSyncEntry() throws Exception { + // Expected invocation chain: + // EntranceNode: machine-root + // -EntranceNode: async-context + // --test-top + // ---test-async + // ----test-sync-in-async + // ----test-another-async + // -----test-another-in-async + // ---test-sync + ContextUtil.enter(contextName, origin); + Entry entry = null; + try { + entry = SphU.entry("test-top"); + doAsyncThenSync(); + } catch (BlockException ex) { + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + + TimeUnit.SECONDS.sleep(10); + testTreeCorrect(); + } + + private void testTreeCorrect() { + DefaultNode root = Constants.ROOT; + Set childListForRoot = root.getChildList(); + // TODO: check child tree + } + + @After + public void shutdown() { + pool.shutdownNow(); + ContextUtil.exit(); + } + + private void runAsync(Runnable f) { + // In Java 8, we can use CompletableFuture.runAsync(f) instead. + pool.submit(f); + } + + private void invoke(final String arg, final Consumer handler) { + runAsync(new Runnable() { + @Override + public void run() { + try { + TimeUnit.SECONDS.sleep(3); + String resp = arg + ": " + System.currentTimeMillis(); + handler.accept(resp); + + } catch (Exception ex) { + ex.printStackTrace(); + } + } + }); + } + + private interface Consumer { + void accept(T t); + } + + private final String contextName = "async-context"; + private final String origin = "originA"; +} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/AsyncEntryTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/AsyncEntryTest.java new file mode 100644 index 00000000..c09dfb29 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/AsyncEntryTest.java @@ -0,0 +1,76 @@ +package com.alibaba.csp.sentinel; + +import com.alibaba.csp.sentinel.context.Context; +import com.alibaba.csp.sentinel.context.ContextUtil; +import com.alibaba.csp.sentinel.slotchain.StringResourceWrapper; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link AsyncEntry}. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class AsyncEntryTest { + + @Test + public void testCleanCurrentEntryInLocal() { + final String contextName = "abc"; + try { + ContextUtil.enter(contextName); + Context curContext = ContextUtil.getContext(); + AsyncEntry entry = new AsyncEntry(new StringResourceWrapper("testCleanCurrentEntryInLocal", EntryType.OUT), + null, curContext); + + assertSame(entry, curContext.getCurEntry()); + + entry.cleanCurrentEntryInLocal(); + assertNotSame(entry, curContext.getCurEntry()); + } finally { + ContextUtil.getContext().setCurEntry(null); + ContextUtil.exit(); + } + } + + @Test + public void testInitAndGetAsyncContext() { + final String contextName = "abc"; + final String origin = "xxx"; + try { + ContextUtil.enter(contextName, origin); + Context curContext = ContextUtil.getContext(); + AsyncEntry entry = new AsyncEntry(new StringResourceWrapper("testInitAndGetAsyncContext", EntryType.OUT), + null, curContext); + assertNull(entry.getAsyncContext()); + + entry.initAsyncContext(); + System.out.println(curContext.getName()); + System.out.println(curContext.getOrigin()); + + Context asyncContext = entry.getAsyncContext(); + assertNotNull(asyncContext); + assertEquals(contextName, asyncContext.getName()); + assertEquals(origin, asyncContext.getOrigin()); + assertSame(curContext.getEntranceNode(), asyncContext.getEntranceNode()); + assertSame(entry, asyncContext.getCurEntry()); + assertTrue(asyncContext.isAsync()); + } finally { + ContextUtil.getContext().setCurEntry(null); + ContextUtil.exit(); + } + } + + @Test(expected = IllegalStateException.class) + public void testDuplicateInitAsyncContext() { + Context context = new Context(null, "abc"); + AsyncEntry entry = new AsyncEntry(new StringResourceWrapper("testDuplicateInitAsyncContext", EntryType.OUT), + null, context); + entry.initAsyncContext(); + + // Duplicate init. + entry.initAsyncContext(); + } +} \ No newline at end of file diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ContextTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ContextTest.java deleted file mode 100755 index cb09f44a..00000000 --- a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/ContextTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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; - -import org.junit.Test; - -import com.alibaba.csp.sentinel.context.ContextUtil; - -/** - * @author jialiang.linjl - */ -public class ContextTest { - - @Test - public void testEnterContext() { - // TODO: rewrite this unit test - ContextUtil.enter("entry", "origin"); - - ContextUtil.exit(); - } - -} diff --git a/sentinel-core/src/test/java/com/alibaba/csp/sentinel/context/ContextTest.java b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/context/ContextTest.java new file mode 100755 index 00000000..09656a61 --- /dev/null +++ b/sentinel-core/src/test/java/com/alibaba/csp/sentinel/context/ContextTest.java @@ -0,0 +1,93 @@ +/* + * 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.context; + +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Test cases for {@link Context} and {@link ContextUtil}. + * + * @author jialiang.linjl + * @author Eric Zhao + */ +public class ContextTest { + + @After + public void cleanUp() { + ContextUtil.exit(); + } + + @Test + public void testEnterContext() { + final String contextName = "contextA"; + final String origin = "originA"; + ContextUtil.enter(contextName, origin); + + Context curContext = ContextUtil.getContext(); + assertEquals(contextName, curContext.getName()); + assertEquals(origin, curContext.getOrigin()); + assertFalse(curContext.isAsync()); + + ContextUtil.exit(); + assertNull(ContextUtil.getContext()); + } + + @Test + public void testReplaceContext() { + final String contextName = "contextA"; + final String origin = "originA"; + ContextUtil.enter(contextName, origin); + + Context contextB = Context.newAsyncContext(null, "contextB") + .setOrigin("originA"); + Context contextA = ContextUtil.replaceContext(contextB); + assertEquals(contextName, contextA.getName()); + assertEquals(origin, contextA.getOrigin()); + assertFalse(contextA.isAsync()); + + Context curContextAfterReplace = ContextUtil.getContext(); + assertEquals(contextB.getName(), curContextAfterReplace.getName()); + assertEquals(contextB.getOrigin(), curContextAfterReplace.getOrigin()); + assertTrue(curContextAfterReplace.isAsync()); + + ContextUtil.replaceContext(null); + assertNull(ContextUtil.getContext()); + } + + @Test + public void testRunOnContext() { + final String contextName = "contextA"; + final String origin = "originA"; + ContextUtil.enter(contextName, origin); + + final Context contextB = Context.newAsyncContext(null, "contextB") + .setOrigin("originB"); + assertEquals(contextName, ContextUtil.getContext().getName()); + ContextUtil.runOnContext(contextB, new Runnable() { + @Override + public void run() { + Context curContext = ContextUtil.getContext(); + assertEquals(contextB.getName(), curContext.getName()); + assertEquals(contextB.getOrigin(), curContext.getOrigin()); + assertTrue(curContext.isAsync()); + } + }); + assertEquals(contextName, ContextUtil.getContext().getName()); + } +} diff --git a/sentinel-demo/pom.xml b/sentinel-demo/pom.xml index 31cb99a4..1ca9b159 100755 --- a/sentinel-demo/pom.xml +++ b/sentinel-demo/pom.xml @@ -12,6 +12,11 @@ pom sentinel-demo + + 1.8 + 1.8 + + sentinel-demo-basic sentinel-demo-dynamic-file-rule diff --git a/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/AsyncEntryDemo.java b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/AsyncEntryDemo.java new file mode 100644 index 00000000..d9f5c391 --- /dev/null +++ b/sentinel-demo/sentinel-demo-basic/src/main/java/com/alibaba/csp/sentinel/demo/AsyncEntryDemo.java @@ -0,0 +1,218 @@ +/* + * 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.demo; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import com.alibaba.csp.sentinel.AsyncEntry; +import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.SphU; +import com.alibaba.csp.sentinel.context.ContextUtil; +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; + +/** + * An example for asynchronous entry in Sentinel. + * + * @author Eric Zhao + * @since 0.2.0 + */ +public class AsyncEntryDemo { + + private void invoke(String arg, Consumer handler) { + CompletableFuture.runAsync(() -> { + try { + TimeUnit.SECONDS.sleep(3); + String resp = arg + ": " + System.currentTimeMillis(); + handler.accept(resp); + } catch (Exception ex) { + ex.printStackTrace(); + } + }); + } + + private void anotherAsync() { + try { + final AsyncEntry entry = SphU.asyncEntry("test-another-async"); + + CompletableFuture.runAsync(() -> { + ContextUtil.runOnContext(entry.getAsyncContext(), () -> { + try { + TimeUnit.SECONDS.sleep(2); + anotherSyncInAsync(); + System.out.println("Async result: 666"); + } catch (InterruptedException e) { + // Ignore. + } finally { + entry.exit(); + } + }); + }); + } catch (BlockException ex) { + ex.printStackTrace(); + } + } + + private void fetchSync() { + Entry entry = null; + try { + entry = SphU.entry("test-sync"); + } catch (BlockException ex) { + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + } + } + + private void fetchSyncInAsync() { + Entry entry = null; + try { + entry = SphU.entry("test-sync-in-async"); + } catch (BlockException ex) { + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + } + } + + private void anotherSyncInAsync() { + Entry entry = null; + try { + entry = SphU.entry("test-another-sync-in-async"); + } catch (BlockException ex) { + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + } + } + + private void directlyAsync() { + try { + final AsyncEntry entry = SphU.asyncEntry("test-async-not-nested"); + + CompletableFuture.runAsync(() -> { + // If no nested entry later, we don't have to wrap in `ContextUtil.runOnContext()`. + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException ex) { + // Ignore. + } finally { + // Exit the async entry. + entry.exit(); + } + }); + } catch (BlockException e) { + // Request blocked, handle the exception. + e.printStackTrace(); + } + } + + private void doAsyncThenSync() { + try { + // First we call an asynchronous resource. + final AsyncEntry entry = SphU.asyncEntry("test-async"); + this.invoke("abc", resp -> { + // The thread is different from original caller thread for async entry. + // So we need to wrap in the async context so that nested sync invocation entry + // can be linked to the parent asynchronous entry. + ContextUtil.runOnContext(entry.getAsyncContext(), () -> { + try { + // In the callback, we do another async invocation several times under the async context. + for (int i = 0; i < 7; i++) { + anotherAsync(); + } + + System.out.println(resp); + + // Then we do a sync entry under current async context. + fetchSyncInAsync(); + } finally { + // Exit the async entry. + entry.exit(); + } + }); + }); + // Then we call a sync resource. + fetchSync(); + } catch (BlockException ex) { + // Request blocked, handle the exception. + ex.printStackTrace(); + } + } + + public static void main(String[] args) { + initFlowRule(); + + AsyncEntryDemo service = new AsyncEntryDemo(); + + // Expected invocation chain: + // + // EntranceNode: machine-root + // -EntranceNode: async-context + // --test-top + // ---test-sync + // ---test-async + // ----test-another-async + // -----test-another-sync-in-async + // ----test-sync-in-async + ContextUtil.enter("async-context", "originA"); + Entry entry = null; + try { + entry = SphU.entry("test-top"); + System.out.println("Do something..."); + service.doAsyncThenSync(); + } catch (BlockException ex) { + // Request blocked, handle the exception. + ex.printStackTrace(); + } finally { + if (entry != null) { + entry.exit(); + } + ContextUtil.exit(); + } + } + + private static void initFlowRule() { + // Rule 1 won't take effect as the limitApp doesn't match. + FlowRule rule1 = new FlowRule() + .setResource("test-another-sync-in-async") + .setLimitApp("originB") + .as(FlowRule.class) + .setCount(4) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + // Rule 2 will take effect. + FlowRule rule2 = new FlowRule() + .setResource("test-another-async") + .setLimitApp("default") + .as(FlowRule.class) + .setCount(5) + .setGrade(RuleConstant.FLOW_GRADE_QPS); + List ruleList = Arrays.asList(rule1, rule2); + FlowRuleManager.loadRules(ruleList); + } +}