├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── phantomthief │ └── scope │ ├── JdkThreadLocal.java │ ├── LongCostTrack.java │ ├── MyThreadLocal.java │ ├── MyThreadLocalFactory.java │ ├── NettyFastThreadLocal.java │ ├── RetryPolicy.java │ ├── Scope.java │ ├── ScopeAsyncRetry.java │ ├── ScopeKey.java │ ├── ScopeUtils.java │ └── SubstituteThreadLocal.java └── test ├── java └── com │ └── github │ └── phantomthief │ └── scope │ ├── FastThreadLocalEnabledTest.java │ ├── FastThreadLocalExecutor.java │ ├── ScopeAsyncRetryBenchMark.java │ ├── ScopeAsyncRetryTest.java │ ├── ScopeKeyTest.java │ ├── ScopeTest.java │ ├── ScopeThreadLocalBenchmark.java │ └── ScopeUtilsTest.java └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .idea 3 | *.iml 4 | 5 | # Mobile Tools for Java (J2ME) 6 | .mtj.tmp/ 7 | 8 | # Package Files # 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 14 | hs_err_pid* 15 | /target/ 16 | 17 | .project 18 | .settings 19 | .classpath 20 | 21 | .DS_Store 22 | 23 | .README.md.html 24 | 25 | pom.xml.releaseBackup 26 | release.properties 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | after_success: 5 | - mvn clean test jacoco:report coveralls:report -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2014 w.vela 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | scope 2 | ======================= 3 | [![Build Status](https://travis-ci.org/PhantomThief/scope.svg)](https://travis-ci.org/PhantomThief/scope) 4 | [![Coverage Status](https://coveralls.io/repos/PhantomThief/scope/badge.svg?branch=master)](https://coveralls.io/r/PhantomThief/scope?branch=master) 5 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/PhantomThief/scope.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/PhantomThief/scope/alerts/) 6 | [![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/PhantomThief/scope.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/PhantomThief/scope/context:java) 7 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.phantomthief/scope)](https://search.maven.org/artifact/com.github.phantomthief/scope/) 8 | 9 | 对ThreadLocal的高级封装 10 | 11 | * 显示的声明Scope的范围 12 | * 强类型 13 | * 可以在线程池中安全的使用,并防止泄露 14 | * 只支持jdk1.8 15 | 16 | ## Usage 17 | 18 | ```Java 19 | 20 | private static final ScopeKey TEST_KEY = allocate(); 21 | 22 | public void basicUse() { 23 | runWithNewScope(() -> { 24 | TEST_KEY.set("abc"); 25 | String result = TEST_KEY.get(); // get "abc" 26 | 27 | runAsyncWithCurrentScope(()-> { 28 | String resultInScope = TEST_KEY.get(); // get "abc" 29 | }, executor); 30 | }); 31 | } 32 | 33 | // 或者声明一个Scope友好的ExecutorService,方法如下: 34 | private static class ScopeThreadPoolExecutor extends ThreadPoolExecutor { 35 | 36 | ScopeThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 37 | TimeUnit unit, BlockingQueue workQueue) { 38 | super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); 39 | } 40 | 41 | /** 42 | * same as {@link java.util.concurrent.Executors#newFixedThreadPool(int)} 43 | */ 44 | static ScopeThreadPoolExecutor newFixedThreadPool(int nThreads) { 45 | return new ScopeThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, 46 | new LinkedBlockingQueue()); 47 | } 48 | 49 | /** 50 | * 只要override这一个方法就可以 51 | * 所有submit, invokeAll等方法都会代理到这里来 52 | */ 53 | @Override 54 | public void execute(Runnable command) { 55 | Scope scope = getCurrentScope(); 56 | super.execute(() -> runWithExistScope(scope, command::run)); 57 | } 58 | } 59 | 60 | private ExecutorService executor = ScopeThreadPoolExecutor.newFixedThreadPool(10); 61 | 62 | public void executeTest() { 63 | runWithNewScope(() -> { 64 | TEST_KEY.set("abc"); 65 | executor.submit(() -> { 66 | TEST_KEY.get(); // get abc 67 | }); 68 | }); 69 | } 70 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.github.phantomthief 4 | scope 5 | 1.0.24-SNAPSHOT 6 | 7 | 8 | 0.1.8 9 | 4.1.32.Final 10 | 3.0.2 11 | 12 | 13 | 28.1-jre 14 | 5.7.2 15 | 1.2.3 16 | 1.23 17 | 18 | 19 | 3.8.1 20 | 3.2.1 21 | 3.3.0 22 | 1.7.0 23 | 0.8.7 24 | 4.3.0 25 | 3.0.0-M5 26 | 2.2.6 27 | 3.2.0 28 | 29 | 30 | 31 | org.sonatype.oss 32 | oss-parent 33 | 9 34 | 35 | 36 | scope 37 | A thread local based scope api preventing thread local leaking. 38 | 39 | https://github.com/PhantomThief/scope 40 | 41 | 42 | 43 | w.vela 44 | 45 | 46 | 47 | 48 | 49 | The Artistic License 2.0 50 | http://www.perlfoundation.org/artistic_license_2_0 51 | repo 52 | 53 | 54 | 55 | 56 | scm:git:git@github.com:PhantomThief/scope.git 57 | https://github.com/PhantomThief/scope.git 58 | scm:git:git@github.com:PhantomThief/scope.git 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.junit 66 | junit-bom 67 | ${junit.version} 68 | pom 69 | import 70 | 71 | 72 | 73 | 74 | 75 | 76 | com.google.guava 77 | guava 78 | ${guava.version} 79 | 80 | 81 | com.github.phantomthief 82 | more-lambdas 83 | ${more-lambdas.version} 84 | 85 | 86 | io.netty 87 | netty-common 88 | ${netty-common.version} 89 | true 90 | 91 | 92 | com.google.code.findbugs 93 | jsr305 94 | ${jsr305.version} 95 | true 96 | 97 | 98 | 99 | org.junit.jupiter 100 | junit-jupiter-api 101 | test 102 | 103 | 104 | org.junit.jupiter 105 | junit-jupiter-engine 106 | test 107 | 108 | 109 | ch.qos.logback 110 | logback-classic 111 | ${logback-classic.version} 112 | test 113 | 114 | 115 | org.openjdk.jmh 116 | jmh-core 117 | ${jmh.version} 118 | test 119 | 120 | 121 | org.openjdk.jmh 122 | jmh-generator-annprocess 123 | ${jmh.version} 124 | test 125 | 126 | 127 | 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-compiler-plugin 133 | ${maven-compiler-plugin.version} 134 | 135 | 1.8 136 | 1.8 137 | true 138 | true 139 | UTF-8 140 | true 141 | 142 | -parameters 143 | 144 | 145 | 146 | 147 | maven-surefire-plugin 148 | ${maven-surefire-plugin.version} 149 | 150 | 151 | org.apache.maven.plugins 152 | maven-source-plugin 153 | ${maven-source-plugin.version} 154 | 155 | 156 | attach-sources 157 | 158 | jar 159 | 160 | 161 | 162 | 163 | 164 | org.apache.maven.plugins 165 | maven-javadoc-plugin 166 | ${maven-javadoc-plugin.version} 167 | 168 | none 169 | 8 170 | 171 | 172 | 173 | attach-javadocs 174 | 175 | jar 176 | 177 | 178 | 179 | 180 | 181 | org.sonatype.plugins 182 | nexus-staging-maven-plugin 183 | ${nexus-staging-maven-plugin.version} 184 | true 185 | 186 | sonatype-nexus-staging 187 | https://oss.sonatype.org/ 188 | true 189 | 190 | 191 | 192 | org.jacoco 193 | jacoco-maven-plugin 194 | ${jacoco-maven-plugin.version} 195 | 196 | 197 | prepare-agent 198 | 199 | prepare-agent 200 | 201 | 202 | 203 | 204 | 205 | org.eluder.coveralls 206 | coveralls-maven-plugin 207 | ${coveralls-maven-plugin.version} 208 | 209 | 210 | pl.project13.maven 211 | git-commit-id-plugin 212 | ${git-commit-id-plugin.version} 213 | 214 | 215 | get-the-git-infos 216 | 217 | revision 218 | 219 | 220 | 221 | 222 | false 223 | 8 224 | yyyyMMddHHmmssSSS 225 | false 226 | false 227 | true 228 | 229 | true 230 | 231 | 232 | git.branch 233 | git.build 234 | git.commit.id 235 | git.commit.time 236 | git.commit.user 237 | git.remote.origin.url 238 | 239 | 240 | 241 | 242 | org.apache.maven.plugins 243 | maven-jar-plugin 244 | ${maven-jar-plugin.version} 245 | 246 | 247 | 248 | true 249 | 250 | 251 | ${git.commit.id} 252 | ${git.build.time} 253 | ${git.branch} 254 | ${java.version} 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | sonatype-nexus-snapshots 265 | https://oss.sonatype.org/content/repositories/snapshots 266 | 267 | 268 | sonatype-nexus-staging 269 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 270 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/JdkThreadLocal.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | /** 4 | * @author w.vela 5 | * Created on 2019-07-03. 6 | */ 7 | class JdkThreadLocal implements MyThreadLocal { 8 | 9 | private final ThreadLocal threadLocal = new ThreadLocal<>(); 10 | 11 | @Override 12 | public T get() { 13 | return threadLocal.get(); 14 | } 15 | 16 | @Override 17 | public void set(T value) { 18 | threadLocal.set(value); 19 | } 20 | 21 | @Override 22 | public void remove() { 23 | threadLocal.remove(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/LongCostTrack.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | /** 4 | * 当请求结束时,调用{@link #close()} 关闭追踪 5 | * 6 | * @author w.vela 7 | * Created on 2019-10-22. 8 | */ 9 | public interface LongCostTrack extends AutoCloseable { 10 | 11 | @Override 12 | void close(); // override for remove exception declaration. 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/MyThreadLocal.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | /** 4 | * @author w.vela 5 | * Created on 2019-07-03. 6 | */ 7 | interface MyThreadLocal { 8 | 9 | T get(); 10 | 11 | void set(T value); 12 | 13 | void remove(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/MyThreadLocalFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | /** 7 | * @author w.vela 8 | * Created on 2019-07-03. 9 | */ 10 | class MyThreadLocalFactory { 11 | 12 | private static final Logger logger = LoggerFactory.getLogger(MyThreadLocalFactory.class); 13 | 14 | static final String USE_FAST_THREAD_LOCAL = "USE_FAST_THREAD_LOCAL"; 15 | 16 | static SubstituteThreadLocal create() { 17 | MyThreadLocal real = null; 18 | if (Boolean.getBoolean(USE_FAST_THREAD_LOCAL)) { 19 | try { 20 | NettyFastThreadLocal nettyFastThreadLocal = new NettyFastThreadLocal<>(); 21 | logger.info("using fast thread local as scope implements."); 22 | real = nettyFastThreadLocal; 23 | } catch (Error e) { 24 | logger.warn("cannot use fast thread local as scope implements."); 25 | } 26 | } 27 | // TODO auto adaptive thread local between jdk thread local and netty fast thread local? 28 | if (real == null) { 29 | real = new JdkThreadLocal<>(); 30 | } 31 | return new SubstituteThreadLocal<>(real); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/NettyFastThreadLocal.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import io.netty.util.concurrent.FastThreadLocal; 4 | 5 | /** 6 | * @author w.vela 7 | * Created on 2019-07-03. 8 | */ 9 | class NettyFastThreadLocal implements MyThreadLocal { 10 | 11 | private final FastThreadLocal fastThreadLocal = new FastThreadLocal<>(); 12 | 13 | @Override 14 | public T get() { 15 | return fastThreadLocal.get(); 16 | } 17 | 18 | @Override 19 | public void set(T value) { 20 | fastThreadLocal.set(value); 21 | } 22 | 23 | @Override 24 | public void remove() { 25 | fastThreadLocal.remove(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/RetryPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.google.common.base.Preconditions.checkArgument; 4 | 5 | import javax.annotation.Nonnegative; 6 | 7 | /** 8 | * @author w.vela 9 | * Created on 2019-01-07. 10 | */ 11 | public interface RetryPolicy { 12 | 13 | long NO_RETRY = -1L; 14 | 15 | static RetryPolicy noRetry() { 16 | return retryNTimes(0); 17 | } 18 | 19 | static RetryPolicy retryNTimes(int times) { 20 | return retryNTimes(times, 0); 21 | } 22 | 23 | static RetryPolicy retryNTimes(int times, @Nonnegative long delayInMs) { 24 | return retryNTimes(times, delayInMs, true); 25 | } 26 | 27 | static RetryPolicy retryNTimes(int times, @Nonnegative long delayInMs, boolean hedge) { 28 | checkArgument(delayInMs >= 0, "delayInMs must be non-negative."); 29 | return new RetryPolicy() { 30 | 31 | @Override 32 | public long retry(int retryCount) { 33 | return retryCount <= times ? delayInMs : NO_RETRY; 34 | } 35 | 36 | @Override 37 | public boolean hedge() { 38 | return hedge; 39 | } 40 | }; 41 | } 42 | 43 | /** 44 | * @param retryCount 当前重试的次数(1为第一次重试) 45 | * @return 下次重试的间隔时间,或者返回 {@link #NO_RETRY} 46 | */ 47 | long retry(int retryCount); 48 | 49 | /** 50 | * 返回true则不cancel任何一次重试,重试过程中任何一次返回成功都拿来做最终结果 51 | * 返回false则开始下一次重试时,之前超时的请求就算后来结果成功返回也没有用 52 | */ 53 | default boolean hedge() { 54 | return true; 55 | } 56 | 57 | default boolean triggerGetOnTimeout() { 58 | return true; 59 | } 60 | 61 | /** 62 | * 判断抛出某个异常后,是否要终止重试 63 | */ 64 | default boolean abortRetry(Throwable t) { 65 | return false; 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/Scope.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | import java.util.concurrent.ConcurrentMap; 5 | import java.util.function.Supplier; 6 | 7 | import javax.annotation.Nonnull; 8 | import javax.annotation.Nullable; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import com.github.phantomthief.util.ThrowableRunnable; 14 | import com.github.phantomthief.util.ThrowableSupplier; 15 | import com.google.common.annotations.Beta; 16 | 17 | /** 18 | * 自定义Scope,支持如下功能: 19 | * 20 | *
    21 | *
  • 开启一个自定义的Scope,在Scope范围内,可以通过 {@link Scope} 各个方法读写数据
  • 22 | *
  • 可以通过 {@link #supplyWithExistScope} 或者 {@link #runWithExistScope} 绑定已经存在的scope
  • 23 | *
24 | * 25 | * 举个栗子: 26 | *
 {@code
 27 |  * ScopeKey<String> TEST_KEY = allocate();
 28 |  *
 29 |  * runWithNewScope(() -> {
 30 |  *      TEST_KEY.set("abc");
 31 |  *      String result = TEST_KEY.get(); // get "abc"
 32 |  *
 33 |  *      Scope scope = getCurrentScope();
 34 |  *      executor.execute(wrapRunnableExistScope(scope, () -> {
 35 |  *          String resultInScope = TEST_KEY.get(); // get "abc"
 36 |  *      });
 37 |  * });
 38 |  * }
39 | * 40 | * TODO: 当前实现是一种比较简易的方式,直接把所有Scope放到一个ThreadLocal里。 41 | * 实际上这样在使用过程中会有二次hash查询的问题,对性能会有些许的影响,更好的做法是: 42 | * 直接使用ThreadLocal(也就是使用内部的ThreadLocalMap),同时在Scope拷贝和清理时,维护一个额外的Set,进行ThreadLocal拷贝。 43 | *

44 | * 这样的优化考虑是:Scope正常访问的频率很高,而线程切换拷贝的概率比较低。 45 | * 目前这个实现参考了 GRPC 的 Context API 以及 Spring 的 RequestContext, 46 | * 相对比较简单,目前效率也可以接受。等到需要榨取性能时再对这个实现动手吧。 47 | *

48 | *

49 | * 注意: 本实现并不充当对 ThreadLocal 性能提升的作用(虽然在有 FastThreadLocal 使用条件下并开启开关后,会优先使用 FastThreadLocal 以提升性能); 50 | *

51 | *

52 | * 注意: 在Scope提供的传播已有Scope的方法中,没有对Scope做拷贝,如果使用{@link #supplyWithExistScope(Scope, ThrowableSupplier)}, {@link #runWithExistScope(Scope, ThrowableRunnable)} 53 | * 等方法在不同的线程传递Scope,那么多个线程会共享同一个Scope实例。 54 | * 如果多个线程共享了一个Scope,那么他们对于{@link ScopeKey}的get/set调用是操作的同一个值,这一点和{@link ThreadLocal}不一样,{@link ThreadLocal}每个线程总是访问自己的一份变量。 55 | * 因而,{@link ScopeKey}也不能在所有场合都无脑替换{@link ThreadLocal}。 56 | *

57 | * @author w.vela 58 | */ 59 | public final class Scope { 60 | 61 | private static final Logger logger = LoggerFactory.getLogger(Scope.class); 62 | 63 | private static final SubstituteThreadLocal SCOPE_THREAD_LOCAL = MyThreadLocalFactory.create(); 64 | 65 | private final ConcurrentMap, Holder> values = new ConcurrentHashMap<>(); 66 | 67 | private final ConcurrentMap, Boolean> enableNullProtections = new ConcurrentHashMap<>(); 68 | 69 | @Beta 70 | public static boolean fastThreadLocalEnabled() { 71 | try { 72 | return SCOPE_THREAD_LOCAL.getRealThreadLocal() instanceof NettyFastThreadLocal; 73 | } catch (Error e) { 74 | return false; 75 | } 76 | } 77 | 78 | /** 79 | * @return {@code true} if fast thread local was enabled. 80 | */ 81 | @Beta 82 | public static boolean tryEnableFastThreadLocal() { 83 | return setFastThreadLocal(true); 84 | } 85 | 86 | static boolean setFastThreadLocal(boolean usingFastThreadLocal) { 87 | // no lock need here, for benchmark friendly 88 | // unnecessary to copy content of thread local. it's only for switch on initial stage or testing. 89 | if (usingFastThreadLocal) { 90 | try { 91 | if (!(SCOPE_THREAD_LOCAL.getRealThreadLocal() instanceof NettyFastThreadLocal)) { 92 | SCOPE_THREAD_LOCAL.setRealThreadLocal(new NettyFastThreadLocal<>()); 93 | logger.info("change current scope's implements to fast thread local."); 94 | } 95 | } catch (Error e) { 96 | logger.warn("fail to change scope's implements to fast thread local."); 97 | return false; 98 | } 99 | } else { 100 | if (!(SCOPE_THREAD_LOCAL.getRealThreadLocal() instanceof JdkThreadLocal)) { 101 | SCOPE_THREAD_LOCAL.setRealThreadLocal(new JdkThreadLocal<>()); 102 | logger.info("change current scope's implements to jdk thread local."); 103 | } 104 | } 105 | return true; 106 | } 107 | 108 | public static void runWithExistScope(@Nullable Scope scope, 109 | ThrowableRunnable runnable) throws X { 110 | supplyWithExistScope(scope, () -> { 111 | runnable.run(); 112 | return null; 113 | }); 114 | } 115 | 116 | public static T supplyWithExistScope(@Nullable Scope scope, 117 | ThrowableSupplier supplier) throws X { 118 | Scope oldScope = SCOPE_THREAD_LOCAL.get(); 119 | SCOPE_THREAD_LOCAL.set(scope); 120 | try { 121 | return supplier.get(); 122 | } finally { 123 | if (oldScope != null) { 124 | SCOPE_THREAD_LOCAL.set(oldScope); 125 | } else { 126 | SCOPE_THREAD_LOCAL.remove(); 127 | } 128 | } 129 | } 130 | 131 | public static void runWithNewScope(@Nonnull ThrowableRunnable runnable) 132 | throws X { 133 | supplyWithNewScope(() -> { 134 | runnable.run(); 135 | return null; 136 | }); 137 | } 138 | 139 | public static T 140 | supplyWithNewScope(@Nonnull ThrowableSupplier supplier) throws X { 141 | beginScope(); 142 | try { 143 | return supplier.get(); 144 | } finally { 145 | endScope(); 146 | } 147 | } 148 | 149 | /** 150 | * 正常应该优先使用 {@link #supplyWithNewScope} 或者 {@link #runWithNewScope} 151 | * 152 | * 手工使用beginScope和endScope的场景只有在: 153 | *
    154 | *
  • 上面两个方法当需要抛出多个正交异常时会造成不必要的try/catch代码
  • 155 | *
  • 开始scope和结束scope不在一个代码块中
  • 156 | *
157 | * 158 | * @throws IllegalStateException if try to start a new scope in an exist scope. 159 | */ 160 | @Nonnull 161 | public static Scope beginScope() { 162 | Scope scope = SCOPE_THREAD_LOCAL.get(); 163 | if (scope != null) { 164 | throw new IllegalStateException("start a scope in an exist scope."); 165 | } 166 | scope = new Scope(); 167 | SCOPE_THREAD_LOCAL.set(scope); 168 | return scope; 169 | } 170 | 171 | /** 172 | * @see #beginScope 173 | */ 174 | public static void endScope() { 175 | SCOPE_THREAD_LOCAL.remove(); 176 | } 177 | 178 | /** 179 | * @return 返回当前请求的 {@link Scope},当请求线程不在 {@link Scope} 绑定状态时,返回 {@code null} 180 | */ 181 | @Nullable 182 | public static Scope getCurrentScope() { 183 | return SCOPE_THREAD_LOCAL.get(); 184 | } 185 | 186 | public void set(@Nonnull ScopeKey key, T value) { 187 | if (value != null) { 188 | values.put(key, new Holder<>(value)); 189 | } else { 190 | values.remove(key); 191 | } 192 | } 193 | 194 | @SuppressWarnings("unchecked") 195 | public T get(@Nonnull ScopeKey key) { 196 | Holder holder = (Holder) values.get(key); 197 | if (holder == null) { 198 | holder = (Holder) values.computeIfAbsent(key, k -> new Holder()); 199 | } 200 | 201 | return holder.getOrCreate(key, enableNullProtections); 202 | } 203 | 204 | private static class Holder { 205 | private T value; 206 | 207 | public Holder() { 208 | } 209 | 210 | public Holder(T value) { 211 | this.value = value; 212 | } 213 | 214 | public T getOrCreate(ScopeKey key, ConcurrentMap, Boolean> enableNullProtections) { 215 | if (value != null) { 216 | return value; 217 | } 218 | if (key.initializer() == null) { 219 | return key.defaultValue(); 220 | } 221 | 222 | return create(key, enableNullProtections); 223 | } 224 | 225 | private synchronized T create(ScopeKey key, ConcurrentMap, Boolean> enableNullProtections) { 226 | if (value != null) { 227 | return value; 228 | } 229 | final Supplier initializer = key.initializer(); 230 | if (initializer == null) { 231 | return key.defaultValue(); 232 | } 233 | 234 | if(enableNullProtections.containsKey(key)){ 235 | return null; 236 | } 237 | 238 | final T v = initializer.get(); 239 | if(v != null){ 240 | this.value = v; 241 | return v; 242 | } 243 | 244 | if(key.enableNullProtection()){ 245 | enableNullProtections.put(key, true); 246 | } 247 | 248 | return key.defaultValue(); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/ScopeAsyncRetry.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.Scope.getCurrentScope; 4 | import static com.github.phantomthief.scope.Scope.supplyWithExistScope; 5 | import static com.google.common.base.Preconditions.checkArgument; 6 | import static com.google.common.base.Preconditions.checkNotNull; 7 | import static com.google.common.util.concurrent.Futures.addCallback; 8 | import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 9 | import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; 10 | import static java.lang.Thread.MAX_PRIORITY; 11 | import static java.util.concurrent.Executors.newFixedThreadPool; 12 | import static java.util.concurrent.Executors.newScheduledThreadPool; 13 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 14 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 15 | 16 | import java.util.concurrent.Executor; 17 | import java.util.concurrent.Future; 18 | import java.util.concurrent.ScheduledExecutorService; 19 | import java.util.concurrent.TimeoutException; 20 | import java.util.concurrent.atomic.AtomicBoolean; 21 | import java.util.concurrent.atomic.AtomicInteger; 22 | import java.util.function.Predicate; 23 | import java.util.function.Supplier; 24 | 25 | import javax.annotation.Nonnegative; 26 | import javax.annotation.Nonnull; 27 | import javax.annotation.Nullable; 28 | 29 | import com.github.phantomthief.util.ThrowableSupplier; 30 | import com.google.common.util.concurrent.FutureCallback; 31 | import com.google.common.util.concurrent.ListenableFuture; 32 | import com.google.common.util.concurrent.ListeningScheduledExecutorService; 33 | import com.google.common.util.concurrent.SettableFuture; 34 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 35 | 36 | /** 37 | * 支持 {@link Scope} 级联,并且支持单次调用独立设置超时的异步重试封装 38 | *

39 | * 使用方法: 40 | *

{@code
 41 |  *
 42 |  * class MyClass {
 43 |  *
 44 |  *   private final ScopeAsyncRetry retrier = ScopeAsyncRetry.shared();
 45 |  *   private final ListeningExecutorService executor = listeningDecorator(newFixedThreadPool(10);
 46 |  *
 47 |  *   ListenableFuture<String> asyncCall() {
 48 |  *     return executor.submit(() -> {
 49 |  *       sleepUninterruptibly(ThreadLocalRandom.current().nextLong(150L), MILLISECONDS);
 50 |  *       return "myTest"
 51 |  *     });
 52 |  *   }
 53 |  *
 54 |  *   void foo() throws ExecutionException, TimeoutException {
 55 |  *     ListenableFuture<String> future = retrier.callWithRetry(100, retryNTimes(3), () -> asyncCall());
 56 |  *     String unwrapped = getUninterruptibly(future, 200, MILLISECONDS);
 57 |  *     System.out.println("result is:" + unwrapped);
 58 |  *   }
 59 |  * }
 60 |  *
 61 |  * }
 62 |  * 
63 | *

64 | * 注意: 如果最终外部的ListenableFuture.get(timeout)没有超时,但是内部请求都失败了,则上抛 65 | * 会上抛 {@link java.util.concurrent.ExecutionException} 并包含最后一次重试的结果 66 | * 特别的,如果最后一次请求超时 {@link java.util.concurrent.ExecutionException#getCause()} 为 {@link TimeoutException} 67 | *

68 | * 注意: 需要重试的方法应该是幂等的操作,不应有任何副作用。 69 | * 70 | * @author myco 71 | * Created on 2019-01-20 72 | */ 73 | public class ScopeAsyncRetry { 74 | 75 | private final ListeningScheduledExecutorService scheduler; 76 | private final Executor callbackExecutor; 77 | 78 | /** 79 | * 因为使用 directExecutor 执行 callback 操作,导致 callback 任务占用 ScheduledExecutorService, 80 | * 从而导致超时控制的有效性可能会随着负载提高而急剧下降 81 | * 请使用 {@link ScopeAsyncRetry#createScopeAsyncRetry(ScheduledExecutorService, Executor)} 82 | */ 83 | @Deprecated 84 | public static ScopeAsyncRetry createScopeAsyncRetry(@Nonnegative ScheduledExecutorService executor) { 85 | return new ScopeAsyncRetry(executor); 86 | } 87 | 88 | public static ScopeAsyncRetry createScopeAsyncRetry(@Nonnegative ScheduledExecutorService executor, 89 | Executor callbackExecutor) { 90 | return new ScopeAsyncRetry(executor, callbackExecutor); 91 | } 92 | 93 | /** 94 | * 共享的 ScopeAsyncRetry 实例 95 | *

96 | * 建议不同业务使用不同的实例,因为其中通过 ScheduledExecutorService 来检测超时 和 实现间隔重试 97 | * 大量使用共享实例,这里可能成为瓶颈 98 | */ 99 | public static ScopeAsyncRetry shared() { 100 | return LazyHolder.INSTANCE; 101 | } 102 | 103 | @Deprecated 104 | ScopeAsyncRetry(ScheduledExecutorService scheduler) { 105 | this(scheduler, directExecutor()); 106 | } 107 | 108 | ScopeAsyncRetry(ScheduledExecutorService scheduler, Executor callbackExecutor) { 109 | this.scheduler = listeningDecorator(scheduler); 110 | this.callbackExecutor = callbackExecutor; 111 | } 112 | 113 | /** 114 | * 内部工具方法,将future结果代理到另一个SettableFuture上 115 | */ 116 | private static FutureCallback setAllResultToOtherSettableFuture(SettableFuture target) { 117 | return new FutureCallback() { 118 | 119 | @Override 120 | public void onSuccess(@Nullable T result) { 121 | target.set(result); 122 | } 123 | 124 | @Override 125 | public void onFailure(Throwable t) { 126 | target.setException(t); 127 | } 128 | }; 129 | } 130 | 131 | private static FutureCallback cancelOtherFuture(Future target, 132 | boolean mayInterruptIfRunning) { 133 | return new FutureCallback() { 134 | 135 | @Override 136 | public void onSuccess(@Nullable T result) { 137 | target.cancel(mayInterruptIfRunning); 138 | } 139 | 140 | @Override 141 | public void onFailure(Throwable t) { 142 | target.cancel(mayInterruptIfRunning); 143 | } 144 | }; 145 | } 146 | 147 | private static FutureCallback setSuccessResultToOtherSettableFuture(SettableFuture target) { 148 | return new FutureCallback() { 149 | 150 | @Override 151 | public void onSuccess(@Nullable T result) { 152 | target.set(result); 153 | } 154 | 155 | @Override 156 | public void onFailure(Throwable t) { 157 | } 158 | }; 159 | } 160 | 161 | private static void addCallbackWithDirectExecutor(ListenableFuture future, 162 | FutureCallback callback) { 163 | addCallback(future, callback, directExecutor()); 164 | } 165 | 166 | private void addCallbackWithCallbackExecutor(ListenableFuture future, 167 | FutureCallback callback) { 168 | addCallback(future, callback, callbackExecutor); 169 | } 170 | 171 | private static class RetryConfig { 172 | 173 | private final long retryInterval; 174 | private final boolean hedge; 175 | private final boolean triggerGetOnTimeout; 176 | private final Predicate abortRetry; 177 | 178 | private RetryConfig(long retryInterval, boolean hedge, boolean triggerGetOnTimeout, 179 | Predicate abortRetry) { 180 | this.retryInterval = retryInterval; 181 | this.hedge = hedge; 182 | this.triggerGetOnTimeout = triggerGetOnTimeout; 183 | this.abortRetry = abortRetry; 184 | } 185 | } 186 | 187 | /** 188 | * 带重试的调用 189 | * 190 | * @param singleCallTimeoutMs 单次调用超时限制,单位:ms 191 | * @param func 需要重试的调用 192 | * @return 带重试的future 193 | */ 194 | @Nonnull 195 | public ListenableFuture callWithRetry(long singleCallTimeoutMs, 196 | RetryPolicy retryPolicy, @Nonnull ThrowableSupplier, X> func) { 197 | return callWithRetry(singleCallTimeoutMs, retryPolicy, func, null); 198 | } 199 | 200 | @Nonnull 201 | public ListenableFuture callWithRetry(long singleCallTimeoutMs, 202 | RetryPolicy retryPolicy, @Nonnull ThrowableSupplier, X> func, 203 | @Nullable FutureCallback eachRetryCallback) { 204 | checkNotNull(retryPolicy); 205 | checkNotNull(func); 206 | checkArgument(singleCallTimeoutMs > 0); 207 | 208 | // 用来保存最终的结果 209 | SettableFuture resultFuture = SettableFuture.create(); 210 | 211 | AtomicInteger retryTime = new AtomicInteger(0); 212 | Supplier retryConfigSupplier = () -> new RetryConfig( 213 | retryPolicy.retry(retryTime.incrementAndGet()), retryPolicy.hedge(), retryPolicy.triggerGetOnTimeout(), 214 | retryPolicy::abortRetry); 215 | 216 | Scope scope = getCurrentScope(); 217 | ThrowableSupplier, X> scopeWrappedFunc = () -> supplyWithExistScope( 218 | scope, func); 219 | 220 | return callWithRetry(scopeWrappedFunc, singleCallTimeoutMs, retryConfigSupplier, 221 | resultFuture, eachRetryCallback); 222 | } 223 | 224 | /** 225 | * 内部递归方法,返回值是最终的挂了多个retry callback的future 226 | */ 227 | private SettableFuture callWithRetry( 228 | @Nonnull ThrowableSupplier, X> func, long singleCallTimeoutMs, 229 | Supplier retryConfigSupplier, SettableFuture resultFuture, 230 | FutureCallback eachRetryCallback) { 231 | 232 | // 如果外部主动 cancel 了,那就不用再做后边没完成的 retry 了 233 | if (resultFuture.isDone()) { 234 | return resultFuture; 235 | } 236 | 237 | RetryConfig retryConfig = retryConfigSupplier.get(); 238 | 239 | // 开始当前一次调用尝试 240 | final SettableFuture currentTry = SettableFuture.create(); 241 | if (eachRetryCallback != null) { 242 | addCallback(currentTry, eachRetryCallback, callbackExecutor); 243 | } 244 | AtomicBoolean currentTrySetted = new AtomicBoolean(false); 245 | RefHolder> callingFuture = new RefHolder<>(); 246 | try { 247 | callingFuture.set(func.get()); 248 | addCallbackWithDirectExecutor(callingFuture.get(), 249 | new FutureCallback() { 250 | @Override 251 | public void onSuccess(@Nullable T result) { 252 | if (currentTrySetted.compareAndSet(false, true)) { 253 | currentTry.set(result); 254 | } 255 | } 256 | 257 | @Override 258 | public void onFailure(Throwable t) { 259 | if (currentTrySetted.compareAndSet(false, true)) { 260 | currentTry.setException(t); 261 | } 262 | } 263 | }); 264 | } catch (Throwable t) { 265 | currentTry.setException(t); 266 | } 267 | if (callingFuture.get() != null) { 268 | // 看是先超时还是先执行完成或者执行抛异常 269 | scheduler.schedule(() -> { 270 | if (retryConfig.triggerGetOnTimeout) { 271 | if (currentTrySetted.compareAndSet(false, true)) { 272 | try { 273 | // 这里get一下是为了触发一些 listener,例子参考 ScopeAsyncRetryTest.testTimeoutListenableFuture 274 | T result = callingFuture.get().get(0, NANOSECONDS); 275 | // 如果这会儿成功了还是把结果 set 给 currentTry 276 | currentTry.set(result); 277 | } catch (Throwable t) { 278 | currentTry.setException(t); 279 | } 280 | } 281 | } else { 282 | currentTry.setException(new TimeoutException()); 283 | } 284 | if (!retryConfig.hedge) { 285 | // 普通模式下,这次重试超时就把这次的future cancel掉 286 | callingFuture.get().cancel(false); 287 | } else { 288 | // hedge模式下,这次重试等到最终结果确定下来之后再cancel 289 | addCallbackWithDirectExecutor(resultFuture, 290 | cancelOtherFuture(callingFuture.get(), false)); 291 | } 292 | }, singleCallTimeoutMs, MILLISECONDS); 293 | } 294 | 295 | if (retryConfig.hedge && callingFuture.get() != null) { 296 | // hedge模式下,不cancel之前的尝试,之前的调用一旦成功就set到最终结果里 297 | addCallbackWithDirectExecutor(callingFuture.get(), 298 | setSuccessResultToOtherSettableFuture(resultFuture)); 299 | } 300 | 301 | if (retryConfig.retryInterval < 0) { 302 | // 如果不会再重试了,那就不管什么结果都set到最终结果里吧 303 | addCallbackWithCallbackExecutor(currentTry, 304 | setAllResultToOtherSettableFuture(resultFuture)); 305 | } else { 306 | // 本次尝试如果成功,直接给最终结果set上;超时或者异常的话,后边的重试操作都挂在catching里 307 | addCallbackWithCallbackExecutor(currentTry, 308 | setSuccessResultToOtherSettableFuture(resultFuture)); 309 | } 310 | 311 | // hedge模式下,resultFuture可能被之前的调用成功set值,所里这里不仅检查是否需要重试,也检查下是否已经取到了最终结果 312 | if (!resultFuture.isDone() && retryConfig.retryInterval >= 0) { 313 | // 没拿到最终结果,且重试次数还没用完,那我们接着加重试callback 314 | addCallbackWithCallbackExecutor(currentTry, new FutureCallback() { 315 | 316 | @Override 317 | public void onSuccess(@Nullable T result) { 318 | // 只有失败了才需要再进行重试,所以这里就啥也不干了 319 | } 320 | 321 | @Override 322 | public void onFailure(Throwable t) { 323 | // 判定这个异常是否需要重试 324 | if (retryConfig.abortRetry.test(t)) { 325 | resultFuture.setException(t); 326 | } else { 327 | // 不管之前是超时还是执行失败了,只要最终结果没拿到,且重试次数还没用完,就会到这里来 328 | if (retryConfig.retryInterval > 0) { 329 | // 延迟一会儿再重试 330 | scheduler.schedule(() -> { 331 | callWithRetry(func, singleCallTimeoutMs, retryConfigSupplier, 332 | resultFuture, eachRetryCallback); 333 | }, retryConfig.retryInterval, MILLISECONDS); 334 | } else { 335 | // 直接重试 336 | callWithRetry(func, singleCallTimeoutMs, retryConfigSupplier, resultFuture, 337 | eachRetryCallback); 338 | } 339 | } 340 | } 341 | }); 342 | } 343 | 344 | return resultFuture; 345 | } 346 | 347 | private static class RefHolder { 348 | private R r; 349 | 350 | public void set(R ref) { 351 | this.r = ref; 352 | } 353 | 354 | public R get() { 355 | return r; 356 | } 357 | } 358 | 359 | private static final class LazyHolder { 360 | 361 | private static final ScopeAsyncRetry INSTANCE = createScopeAsyncRetry( 362 | newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), 363 | new ThreadFactoryBuilder() 364 | .setPriority(MAX_PRIORITY) 365 | .setNameFormat("default-retrier-%d") 366 | .build()), 367 | newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2, 368 | new ThreadFactoryBuilder() 369 | .setPriority(MAX_PRIORITY) 370 | .setNameFormat("default-callback-%d") 371 | .build())); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/ScopeKey.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.Scope.getCurrentScope; 4 | 5 | import java.util.function.Supplier; 6 | 7 | import javax.annotation.Nonnull; 8 | 9 | /** 10 | * 强类型数据读写的封装 11 | * 12 | *

13 | * 如果多个线程共享了一个Scope,那么他们对于{@link ScopeKey}的get/set调用是操作的同一个值,这一点和{@link ThreadLocal}不一样,{@link ThreadLocal} 每个线程总是访问自己的一份变量。 14 | * 因而,{@link ScopeKey}也不能在所有场合都无脑替换{@link ThreadLocal}。 15 | * 更多信息参考 {@link Scope}的文档。 16 | *

17 | * 18 | * @author w.vela 19 | */ 20 | public final class ScopeKey { 21 | 22 | private final T defaultValue; 23 | private final Supplier initializer; 24 | private final boolean enableNullProtection; 25 | 26 | private ScopeKey(T defaultValue, Supplier initializer) { 27 | this(defaultValue, initializer, false); 28 | } 29 | 30 | private ScopeKey(T defaultValue, Supplier initializer, boolean enableNullProtection) { 31 | this.defaultValue = defaultValue; 32 | this.initializer = initializer; 33 | this.enableNullProtection = enableNullProtection; 34 | } 35 | 36 | @Nonnull 37 | public static ScopeKey allocate() { 38 | return withDefaultValue0(null); 39 | } 40 | 41 | @Nonnull 42 | private static ScopeKey withDefaultValue0(T defaultValue) { 43 | return new ScopeKey<>(defaultValue, null); 44 | } 45 | 46 | @Nonnull 47 | public static ScopeKey withDefaultValue(boolean defaultValue) { 48 | return withDefaultValue0(defaultValue); 49 | } 50 | 51 | @Nonnull 52 | public static ScopeKey withDefaultValue(int defaultValue) { 53 | return withDefaultValue0(defaultValue); 54 | } 55 | 56 | @Nonnull 57 | public static ScopeKey withDefaultValue(long defaultValue) { 58 | return withDefaultValue0(defaultValue); 59 | } 60 | 61 | @Nonnull 62 | public static ScopeKey withDefaultValue(double defaultValue) { 63 | return withDefaultValue0(defaultValue); 64 | } 65 | 66 | @Nonnull 67 | public static ScopeKey withDefaultValue(String defaultValue) { 68 | return withDefaultValue0(defaultValue); 69 | } 70 | 71 | @Nonnull 72 | public static > ScopeKey withDefaultValue(T defaultValue) { 73 | return withDefaultValue0(defaultValue); 74 | } 75 | 76 | /** 77 | * @param initializer 初始化ScopeKey(仅在Scope有效时) 78 | *

79 | * 等效代码:(调用 {@link #get} 时) 80 | *

81 | *

 {@code
 82 |      * T obj = SCOPE_KEY.get();
 83 |      * if (obj == null) {
 84 |      *     obj = initializer.get();
 85 |      *     SCOPE_KEY.set(obj);
 86 |      * }
 87 |      * return obj;
 88 |      * }
89 | *

90 | * 注意,如果 initializer 返回 {@code null},每次访问时都会重复初始化执行; 91 | * 虽然该问题是预期外的,但是考虑到业务如果刚好依赖了此 bug,可能直接修复会产生行为异常,并因此产生不容易发现的意外,所以提供了重载版本修正: 92 | * 对于可能返回 {@code null} 的场景,请使用 {@link #withInitializer(boolean, Supplier)} 版本,并传递参数 {@code true} 93 | */ 94 | @Nonnull 95 | public static ScopeKey withInitializer(Supplier initializer) { 96 | return withInitializer(false, initializer); 97 | } 98 | 99 | /** 100 | * @param initializer 初始化ScopeKey(仅在Scope有效时) 101 | *

102 | * 等效代码:(调用 {@link #get} 时) 103 | *

104 | *

 {@code
105 |      * T obj = SCOPE_KEY.get();
106 |      * if (obj == null) {
107 |      *     obj = initializer.get();
108 |      *     SCOPE_KEY.set(obj);
109 |      * }
110 |      * return obj;
111 |      * }
112 | */ 113 | @Nonnull 114 | public static ScopeKey withInitializer(boolean enableNullProtection, Supplier initializer) { 115 | return new ScopeKey<>(null, initializer, enableNullProtection); 116 | } 117 | 118 | public T get() { 119 | Scope currentScope = getCurrentScope(); 120 | if (currentScope == null) { 121 | return defaultValue(); 122 | } 123 | return currentScope.get(this); 124 | } 125 | 126 | Supplier initializer() { 127 | return initializer; 128 | } 129 | 130 | T defaultValue() { 131 | return defaultValue; 132 | } 133 | 134 | boolean enableNullProtection() { 135 | return enableNullProtection; 136 | } 137 | 138 | /** 139 | * @return {@code true} if in a scope and set success. 140 | */ 141 | public boolean set(T value) { 142 | Scope currentScope = getCurrentScope(); 143 | if (currentScope != null) { 144 | currentScope.set(this, value); 145 | return true; 146 | } else { 147 | return false; 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/ScopeUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.Scope.getCurrentScope; 4 | import static com.github.phantomthief.scope.Scope.runWithExistScope; 5 | import static com.github.phantomthief.scope.Scope.supplyWithExistScope; 6 | import static com.github.phantomthief.util.MoreSuppliers.lazy; 7 | import static java.lang.Boolean.TRUE; 8 | import static java.lang.System.nanoTime; 9 | import static java.lang.Thread.MIN_PRIORITY; 10 | import static java.time.Duration.ofNanos; 11 | import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; 12 | import static java.util.concurrent.TimeUnit.SECONDS; 13 | 14 | import java.time.Duration; 15 | import java.util.Iterator; 16 | import java.util.Map.Entry; 17 | import java.util.concurrent.ConcurrentMap; 18 | import java.util.concurrent.Executor; 19 | import java.util.concurrent.ExecutorService; 20 | import java.util.concurrent.Future; 21 | import java.util.concurrent.ScheduledFuture; 22 | import java.util.function.Consumer; 23 | import java.util.function.Supplier; 24 | 25 | import javax.annotation.Nonnull; 26 | import javax.annotation.Nullable; 27 | 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import com.google.common.base.Preconditions; 32 | import com.google.common.collect.MapMaker; 33 | import com.google.common.util.concurrent.FutureCallback; 34 | import com.google.common.util.concurrent.Futures; 35 | import com.google.common.util.concurrent.ListenableFuture; 36 | import com.google.common.util.concurrent.ListeningExecutorService; 37 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 38 | 39 | /** 40 | * @author w.vela 41 | */ 42 | public final class ScopeUtils { 43 | 44 | private static final Logger logger = LoggerFactory.getLogger(ScopeUtils.class); 45 | private static final ConcurrentMap MAP = new MapMaker() 46 | .weakKeys() 47 | .concurrencyLevel(64) 48 | .makeMap(); 49 | 50 | private static final int CHECK_PERIOD = 1; 51 | 52 | private static final Supplier> SCHEDULER = lazy(() -> 53 | newSingleThreadScheduledExecutor(new ThreadFactoryBuilder() 54 | .setDaemon(true) 55 | .setNameFormat("long-cost-track") 56 | .setPriority(MIN_PRIORITY) 57 | .build()) 58 | .scheduleWithFixedDelay(ScopeUtils::doReport, CHECK_PERIOD, CHECK_PERIOD, SECONDS)); 59 | 60 | private ScopeUtils() { 61 | } 62 | 63 | private static Runnable wrapRunnableExistScope(@Nullable Scope scope, 64 | @Nonnull Runnable runnable) { 65 | return () -> runWithExistScope(scope, runnable::run); 66 | } 67 | 68 | private static Supplier wrapSupplierExistScope(@Nullable Scope scope, 69 | @Nonnull Supplier supplier) { 70 | return () -> supplyWithExistScope(scope, supplier::get); 71 | } 72 | 73 | public static void runAsyncWithCurrentScope(@Nonnull Runnable runnable, 74 | @Nonnull Executor executor) { 75 | executor.execute(wrapRunnableExistScope(getCurrentScope(), runnable)); 76 | } 77 | 78 | @Nonnull 79 | public static ListenableFuture runAsyncWithCurrentScope(@Nonnull Runnable runnable, 80 | @Nonnull ListeningExecutorService executor) { 81 | return executor.submit(wrapRunnableExistScope(getCurrentScope(), runnable)); 82 | } 83 | 84 | @Nonnull 85 | public static Future supplyAsyncWithCurrentScope(@Nonnull Supplier supplier, 86 | @Nonnull ExecutorService executor) { 87 | return executor.submit(() -> wrapSupplierExistScope(getCurrentScope(), supplier).get()); 88 | } 89 | 90 | @Nonnull 91 | public static ListenableFuture supplyAsyncWithCurrentScope(@Nonnull Supplier supplier, 92 | @Nonnull ListeningExecutorService executor) { 93 | return executor.submit(() -> wrapSupplierExistScope(getCurrentScope(), supplier).get()); 94 | } 95 | 96 | /** 97 | * @param onTimeoutReportRunnable accept a time duration in nano-seconds. 98 | */ 99 | public static LongCostTrack trackLongCost(Duration timeoutForReport, Consumer onTimeoutReportRunnable) { 100 | SCHEDULER.get(); 101 | Scope scope = getCurrentScope(); 102 | long nano = nanoTime(); 103 | LongCostTrackImpl context = 104 | new LongCostTrackImpl(onTimeoutReportRunnable, nano, nano + timeoutForReport.toNanos(), scope); 105 | MAP.put(context, TRUE); 106 | return context; 107 | } 108 | 109 | private static void doReport() { 110 | Iterator> iterator = MAP.entrySet().iterator(); 111 | while (iterator.hasNext()) { 112 | Entry entry = iterator.next(); 113 | LongCostTrackImpl key = entry.getKey(); 114 | if (key.closed) { 115 | iterator.remove(); 116 | continue; 117 | } 118 | long now = nanoTime(); 119 | long currentCost = now - key.deadline; 120 | if (currentCost > 0) { 121 | runWithExistScope(key.scope, () -> { 122 | try { 123 | key.runnable.accept(ofNanos(now - key.start)); 124 | } catch (Throwable e) { 125 | logger.error("", e); 126 | } finally { 127 | iterator.remove(); 128 | } 129 | }); 130 | } 131 | } 132 | } 133 | 134 | private static class LongCostTrackImpl implements LongCostTrack { 135 | 136 | private final Consumer runnable; 137 | private final long start; 138 | private final long deadline; 139 | private final Scope scope; 140 | private volatile boolean closed; 141 | 142 | private LongCostTrackImpl(Consumer runnable, long start, long deadline, Scope scope) { 143 | this.runnable = runnable; 144 | this.start = start; 145 | this.deadline = deadline; 146 | this.scope = scope; 147 | } 148 | 149 | @Override 150 | public void close() { 151 | closed = true; 152 | // 希望业务调用这个,这样可以更早的回收,而不用等到GC阶段 153 | MAP.remove(this); 154 | } 155 | } 156 | 157 | /** 158 | * for {@link Futures#addCallback} 159 | */ 160 | @Nonnull 161 | public static FutureCallback wrapWithScope(@Nonnull FutureCallback futureCallback) { 162 | 163 | Preconditions.checkNotNull(futureCallback); 164 | Scope currentScope = getCurrentScope(); 165 | return new FutureCallback() { 166 | @Override 167 | public void onSuccess(@Nullable U u) { 168 | runWithExistScope(currentScope, () -> futureCallback.onSuccess(u)); 169 | } 170 | 171 | @Override 172 | public void onFailure(Throwable throwable) { 173 | runWithExistScope(currentScope, () -> futureCallback.onFailure(throwable)); 174 | } 175 | }; 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/scope/SubstituteThreadLocal.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | /** 6 | * @author w.vela 7 | * Created on 2019-07-09. 8 | */ 9 | class SubstituteThreadLocal implements MyThreadLocal { 10 | 11 | private MyThreadLocal realThreadLocal; 12 | 13 | SubstituteThreadLocal(@Nonnull MyThreadLocal realThreadLocal) { 14 | this.realThreadLocal = realThreadLocal; 15 | } 16 | 17 | @Nonnull 18 | MyThreadLocal getRealThreadLocal() { 19 | return realThreadLocal; 20 | } 21 | 22 | void setRealThreadLocal(@Nonnull MyThreadLocal realThreadLocal) { 23 | this.realThreadLocal = realThreadLocal; 24 | } 25 | 26 | @Override 27 | public T get() { 28 | return realThreadLocal.get(); 29 | } 30 | 31 | @Override 32 | public void set(T value) { 33 | realThreadLocal.set(value); 34 | } 35 | 36 | @Override 37 | public void remove() { 38 | realThreadLocal.remove(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/FastThreadLocalEnabledTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.MyThreadLocalFactory.USE_FAST_THREAD_LOCAL; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import org.junit.jupiter.api.Disabled; 8 | import org.junit.jupiter.api.Test; 9 | 10 | /** 11 | * @author w.vela 12 | * Created on 2019-07-08. 13 | */ 14 | class FastThreadLocalEnabledTest { 15 | 16 | /** 17 | * 由于netty依赖是optional的,所以这个测试用例只在IDEA中手工运行确认 18 | */ 19 | @Disabled 20 | @Test 21 | void test() { 22 | System.setProperty(USE_FAST_THREAD_LOCAL, "true"); 23 | assertTrue(Scope.fastThreadLocalEnabled()); 24 | assertTrue(Scope.setFastThreadLocal(false)); 25 | assertFalse(Scope.fastThreadLocalEnabled()); 26 | assertTrue(Scope.tryEnableFastThreadLocal()); 27 | assertTrue(Scope.fastThreadLocalEnabled()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/FastThreadLocalExecutor.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 4 | 5 | import java.util.concurrent.LinkedBlockingQueue; 6 | import java.util.concurrent.ThreadPoolExecutor; 7 | 8 | import io.netty.util.concurrent.DefaultThreadFactory; 9 | 10 | /** 11 | * @author w.vela 12 | * Created on 2019-07-08. 13 | */ 14 | public class FastThreadLocalExecutor extends ThreadPoolExecutor { 15 | 16 | private static final DefaultThreadFactory NETTY_FACTORY = new DefaultThreadFactory(FastThreadLocalExecutor.class); 17 | 18 | public FastThreadLocalExecutor(int nThread, String prefix) { 19 | super(nThread, nThread, 0L, MILLISECONDS, new LinkedBlockingQueue<>(), NETTY_FACTORY::newThread); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/ScopeAsyncRetryBenchMark.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.RetryPolicy.retryNTimes; 4 | import static com.github.phantomthief.scope.Scope.beginScope; 5 | import static com.github.phantomthief.scope.Scope.endScope; 6 | import static com.github.phantomthief.scope.ScopeAsyncRetry.createScopeAsyncRetry; 7 | import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; 8 | import static java.lang.Thread.MAX_PRIORITY; 9 | 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import org.openjdk.jmh.annotations.Benchmark; 14 | import org.openjdk.jmh.annotations.BenchmarkMode; 15 | import org.openjdk.jmh.annotations.Fork; 16 | import org.openjdk.jmh.annotations.Measurement; 17 | import org.openjdk.jmh.annotations.Mode; 18 | import org.openjdk.jmh.annotations.OutputTimeUnit; 19 | import org.openjdk.jmh.annotations.Scope; 20 | import org.openjdk.jmh.annotations.Setup; 21 | import org.openjdk.jmh.annotations.State; 22 | import org.openjdk.jmh.annotations.TearDown; 23 | import org.openjdk.jmh.annotations.Threads; 24 | import org.openjdk.jmh.annotations.Warmup; 25 | import org.openjdk.jmh.runner.Runner; 26 | import org.openjdk.jmh.runner.options.Options; 27 | import org.openjdk.jmh.runner.options.OptionsBuilder; 28 | 29 | import com.google.common.util.concurrent.ListenableFuture; 30 | import com.google.common.util.concurrent.ListeningScheduledExecutorService; 31 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 32 | 33 | /** 34 | * @author myco 35 | * Created on 2019-06-05 36 | */ 37 | @BenchmarkMode(Mode.Throughput) 38 | @Warmup(iterations = 1, time = 2) 39 | @Measurement(iterations = 5, time = 1) 40 | @Threads(8) 41 | @Fork(1) 42 | @OutputTimeUnit(TimeUnit.SECONDS) 43 | @State(Scope.Benchmark) 44 | public class ScopeAsyncRetryBenchMark { 45 | 46 | private static ListeningScheduledExecutorService listeningScheduledExecutorService = listeningDecorator( 47 | Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors())); 48 | 49 | private static ListenableFuture successAfter(String expected, long timeout) { 50 | return listeningScheduledExecutorService.schedule(() -> expected, timeout, TimeUnit.MILLISECONDS); 51 | } 52 | 53 | private static ScopeAsyncRetry directCallbackRetry; 54 | private static ScopeAsyncRetry isolateCallbackRetry; 55 | 56 | @Setup 57 | public static void init() { 58 | directCallbackRetry = 59 | createScopeAsyncRetry(Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), 60 | new ThreadFactoryBuilder() 61 | .setPriority(MAX_PRIORITY) 62 | .setNameFormat("default-directCallbackRetry-%d") 63 | .build())); 64 | isolateCallbackRetry = 65 | createScopeAsyncRetry(Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), 66 | new ThreadFactoryBuilder() 67 | .setPriority(MAX_PRIORITY) 68 | .setNameFormat("default-isolateCallbackRetry-%d") 69 | .build()), 70 | Executors.newCachedThreadPool()); 71 | beginScope(); 72 | } 73 | 74 | @TearDown 75 | public static void destroy() { 76 | endScope(); 77 | } 78 | 79 | @Benchmark 80 | public static void testAllTimeout() { 81 | directCallbackRetry.callWithRetry(10, retryNTimes(3, 10), 82 | () -> successAfter("test", 1000)); 83 | } 84 | 85 | @Benchmark 86 | public static void testAllTimeout2() { 87 | directCallbackRetry.callWithRetry(10, new RetryPolicy() { 88 | @Override 89 | public long retry(int retryCount) { 90 | return retryCount <= 3 ? 10 : NO_RETRY; 91 | } 92 | 93 | @Override 94 | public boolean triggerGetOnTimeout() { 95 | return true; 96 | } 97 | }, 98 | () -> successAfter("test", 1000)); 99 | } 100 | 101 | @Benchmark 102 | public static void testAllTimeout3() { 103 | isolateCallbackRetry.callWithRetry(10, retryNTimes(3, 10), 104 | () -> successAfter("test", 1000)); 105 | } 106 | 107 | @Benchmark 108 | public static void testAllTimeout4() { 109 | isolateCallbackRetry.callWithRetry(10, new RetryPolicy() { 110 | @Override 111 | public long retry(int retryCount) { 112 | return retryCount <= 3 ? 10 : NO_RETRY; 113 | } 114 | 115 | @Override 116 | public boolean triggerGetOnTimeout() { 117 | return true; 118 | } 119 | }, 120 | () -> successAfter("test", 1000)); 121 | } 122 | 123 | public static void main(String[] args) throws Exception { 124 | Options options = new OptionsBuilder() 125 | .include(ScopeAsyncRetryBenchMark.class.getName()) 126 | .build(); 127 | new Runner(options).run(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/ScopeAsyncRetryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.RetryPolicy.retryNTimes; 4 | import static com.github.phantomthief.scope.Scope.beginScope; 5 | import static com.github.phantomthief.scope.Scope.endScope; 6 | import static com.github.phantomthief.scope.ScopeAsyncRetry.createScopeAsyncRetry; 7 | import static com.github.phantomthief.scope.ScopeAsyncRetry.shared; 8 | import static com.github.phantomthief.scope.ScopeKey.allocate; 9 | import static com.google.common.util.concurrent.Futures.addCallback; 10 | import static com.google.common.util.concurrent.MoreExecutors.directExecutor; 11 | import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; 12 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 13 | import static java.lang.Thread.MAX_PRIORITY; 14 | import static java.time.Duration.ofMillis; 15 | import static java.util.concurrent.Executors.newFixedThreadPool; 16 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 17 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 18 | import static java.util.concurrent.TimeUnit.SECONDS; 19 | import static org.junit.jupiter.api.Assertions.assertEquals; 20 | import static org.junit.jupiter.api.Assertions.assertFalse; 21 | import static org.junit.jupiter.api.Assertions.assertNull; 22 | import static org.junit.jupiter.api.Assertions.assertSame; 23 | import static org.junit.jupiter.api.Assertions.assertThrows; 24 | import static org.junit.jupiter.api.Assertions.assertTimeout; 25 | import static org.junit.jupiter.api.Assertions.assertTrue; 26 | 27 | import java.util.concurrent.ConcurrentLinkedQueue; 28 | import java.util.concurrent.ExecutionException; 29 | import java.util.concurrent.Executor; 30 | import java.util.concurrent.ExecutorService; 31 | import java.util.concurrent.Executors; 32 | import java.util.concurrent.Future; 33 | import java.util.concurrent.ThreadLocalRandom; 34 | import java.util.concurrent.TimeUnit; 35 | import java.util.concurrent.TimeoutException; 36 | import java.util.concurrent.atomic.AtomicBoolean; 37 | import java.util.concurrent.atomic.AtomicInteger; 38 | import java.util.concurrent.atomic.AtomicLong; 39 | 40 | import javax.annotation.Nullable; 41 | 42 | import org.junit.jupiter.api.Assertions; 43 | import org.junit.jupiter.api.Disabled; 44 | import org.junit.jupiter.api.Test; 45 | import org.junit.jupiter.api.function.ThrowingSupplier; 46 | import org.slf4j.Logger; 47 | import org.slf4j.LoggerFactory; 48 | 49 | import com.github.phantomthief.util.ThrowableSupplier; 50 | import com.google.common.base.Supplier; 51 | import com.google.common.base.Throwables; 52 | import com.google.common.util.concurrent.FutureCallback; 53 | import com.google.common.util.concurrent.Futures; 54 | import com.google.common.util.concurrent.ListenableFuture; 55 | import com.google.common.util.concurrent.ListeningExecutorService; 56 | import com.google.common.util.concurrent.ListeningScheduledExecutorService; 57 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 58 | 59 | /** 60 | * @author w.vela 61 | * Created on 2019-01-11. 62 | */ 63 | class ScopeAsyncRetryTest { 64 | 65 | private static final Logger logger = LoggerFactory.getLogger(ScopeAsyncRetryTest.class); 66 | private final ScopeAsyncRetry retrier = shared(); 67 | private final ListeningExecutorService executor = 68 | listeningDecorator(newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 4)); 69 | private final ScopeKey context = allocate(); 70 | private final ListeningScheduledExecutorService scheduler = 71 | listeningDecorator(Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors())); 72 | 73 | private void initKey() { 74 | context.set("test"); 75 | } 76 | 77 | private void assertContext() { 78 | assertEquals("test", context.get()); 79 | } 80 | 81 | @Test 82 | void testTimeout() throws Throwable { 83 | beginScope(); 84 | try { 85 | initKey(); 86 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10, false), 87 | () -> successAfter("test", 200)); 88 | try { 89 | future.get(); 90 | } catch (Throwable t) { 91 | assertTrue(t instanceof ExecutionException); 92 | assertTrue(t.getCause() instanceof TimeoutException); 93 | } 94 | } finally { 95 | endScope(); 96 | } 97 | } 98 | 99 | @Test 100 | void testTimeout2() throws Throwable { 101 | beginScope(); 102 | try { 103 | initKey(); 104 | ListenableFuture future = retrier.callWithRetry(10, retryNTimes(3, 10), 105 | () -> successAfter("test", 100)); 106 | addCallback(future, new FutureCallback() { 107 | @Override 108 | public void onSuccess(@Nullable String result) { 109 | logger.info("{}", result); 110 | } 111 | 112 | @Override 113 | public void onFailure(Throwable t) { 114 | t.printStackTrace(); 115 | } 116 | }, directExecutor()); 117 | try { 118 | future.get(); 119 | } catch (Throwable t) { 120 | assertTrue(t instanceof ExecutionException); 121 | assertTrue(t.getCause() instanceof TimeoutException); 122 | } 123 | } finally { 124 | endScope(); 125 | } 126 | } 127 | 128 | @Test 129 | void testAllTimeout() { 130 | beginScope(); 131 | try { 132 | initKey(); 133 | for (int i = 0; i < 10; i++) { 134 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10), 135 | () -> successAfter("test", 200)); 136 | assertThrows(TimeoutException.class, 137 | () -> assertTimeout(ofMillis(60), () -> future.get(50, MILLISECONDS))); 138 | } 139 | } finally { 140 | endScope(); 141 | } 142 | } 143 | 144 | @Test 145 | void test() throws InterruptedException, ExecutionException, TimeoutException { 146 | beginScope(); 147 | try { 148 | initKey(); 149 | for (int i = 0; i < 10; i++) { 150 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10), 151 | sleepySuccess(new long[] {300L, 200L, 50L})); 152 | assertEquals("2", future.get(350, MILLISECONDS)); 153 | } 154 | } finally { 155 | endScope(); 156 | } 157 | } 158 | 159 | @Test 160 | void testBreak() { 161 | beginScope(); 162 | try { 163 | initKey(); 164 | for (int i = 0; i < 10; i++) { 165 | MySupplier1 func = sleepySuccess(new long[] {300L, 200L, 50L}); 166 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10), func); 167 | assertThrows(TimeoutException.class, () -> future.get(50, MILLISECONDS)); 168 | future.cancel(false); 169 | sleepUninterruptibly(1, SECONDS); 170 | assertEquals(1, func.current.get()); 171 | } 172 | } finally { 173 | endScope(); 174 | } 175 | } 176 | 177 | @Test 178 | void testException() { 179 | beginScope(); 180 | try { 181 | initKey(); 182 | for (int i = 0; i < 10; i++) { 183 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10), () -> { 184 | assertContext(); 185 | return executor.submit(() -> { 186 | sleepUninterruptibly(500, MILLISECONDS); 187 | throw new IllegalArgumentException("test"); 188 | }); 189 | }); 190 | assertThrows(TimeoutException.class, () -> future.get(300, MILLISECONDS)); 191 | } 192 | 193 | for (int i = 0; i < 10; i++) { 194 | ListenableFuture future2 = retrier.callWithRetry(100, retryNTimes(3, 10), () -> { 195 | assertContext(); 196 | return executor.submit(() -> { 197 | sleepUninterruptibly(50, MILLISECONDS); 198 | throw new IllegalArgumentException("test"); 199 | }); 200 | }); 201 | ExecutionException exception = assertThrows(ExecutionException.class, 202 | () -> future2.get(1600, MILLISECONDS)); 203 | assertSame(IllegalArgumentException.class, exception.getCause().getClass()); 204 | } 205 | 206 | for (int i = 0; i < 10; i++) { 207 | ListenableFuture future3 = retrier.callWithRetry(20, retryNTimes(3, 10), () -> { 208 | assertContext(); 209 | return executor.submit(() -> { 210 | sleepUninterruptibly(100, MILLISECONDS); 211 | throw new IllegalArgumentException("test"); 212 | }); 213 | }); 214 | ExecutionException exception3 = assertThrows(ExecutionException.class, 215 | () -> future3.get(1600, MILLISECONDS)); 216 | assertSame(TimeoutException.class, exception3.getCause().getClass()); 217 | } 218 | 219 | for (int i = 0; i < 10; i++) { 220 | ListenableFuture future4 = retrier.callWithRetry(20, retryNTimes(3, 10), () -> { 221 | assertContext(); 222 | throw new IllegalArgumentException("test"); 223 | }); 224 | ExecutionException exception4 = assertThrows(ExecutionException.class, 225 | () -> future4.get(1600, MILLISECONDS)); 226 | assertSame(IllegalArgumentException.class, exception4.getCause().getClass()); 227 | } 228 | } finally { 229 | endScope(); 230 | } 231 | } 232 | 233 | private static AtomicInteger idx = new AtomicInteger(0); 234 | private static final long[] delayTimeArray = {300, 900, 500, 900}; 235 | 236 | private static void delaySomeTime() { 237 | sleepUninterruptibly(delayTimeArray[idx.getAndIncrement()], 238 | TimeUnit.MILLISECONDS); 239 | } 240 | 241 | @Test 242 | void testNotHedge() throws Throwable { 243 | beginScope(); 244 | try { 245 | initKey(); 246 | idx.set(0); 247 | 248 | AtomicInteger calledTimes = new AtomicInteger(0); 249 | 250 | ListenableFuture future = retrier.callWithRetry(200, retryNTimes(3, 10, false), 251 | () -> executor.submit(() -> { 252 | int id = calledTimes.incrementAndGet(); 253 | delaySomeTime(); 254 | // throw new IllegalStateException(); 255 | return id + " -- done!"; 256 | })); 257 | String result = null; 258 | try { 259 | result = future.get(); 260 | } catch (Throwable t) { 261 | // ignore 262 | } 263 | assertNull(result); 264 | assertEquals(4, calledTimes.get()); 265 | } finally { 266 | endScope(); 267 | } 268 | } 269 | 270 | @Test 271 | void testHedge() throws Throwable { 272 | beginScope(); 273 | try { 274 | initKey(); 275 | idx.set(0); 276 | 277 | AtomicInteger calledTimes = new AtomicInteger(0); 278 | 279 | ListenableFuture future = retrier.callWithRetry(200, retryNTimes(3, 10, true), 280 | () -> executor.submit(() -> { 281 | int id = calledTimes.incrementAndGet(); 282 | delaySomeTime(); 283 | // throw new IllegalStateException(); 284 | return id + " -- done!"; 285 | })); 286 | String result = null; 287 | try { 288 | result = future.get(); 289 | } catch (Throwable t) { 290 | // ignore 291 | } 292 | assertEquals("1 -- done!", result); 293 | assertEquals(2, calledTimes.get()); 294 | } finally { 295 | endScope(); 296 | } 297 | } 298 | 299 | private final AtomicLong callTimes = new AtomicLong(0); 300 | 301 | private void clearCallTimes() { 302 | callTimes.set(0); 303 | } 304 | 305 | private long getCallTimes() { 306 | return callTimes.get(); 307 | } 308 | 309 | private ListenableFuture successAfter(String expected, long timeout) { 310 | callTimes.incrementAndGet(); 311 | assertContext(); 312 | return scheduler.schedule(() -> expected, timeout, MILLISECONDS); 313 | } 314 | 315 | private ListenableFuture exceptionAfter(long timeout) { 316 | callTimes.incrementAndGet(); 317 | return scheduler.schedule(() -> { 318 | throw new RuntimeException(); 319 | }, timeout, MILLISECONDS); 320 | } 321 | 322 | private ListenableFuture successAfterBySleep(String expected, long timeout) { 323 | callTimes.incrementAndGet(); 324 | assertContext(); 325 | return executor.submit(() -> { 326 | sleepUninterruptibly(timeout, MILLISECONDS); 327 | return expected; 328 | }); 329 | } 330 | 331 | private MySupplier2 exceptionsAndThenSuccess(Throwable[] exceptionArray) { 332 | assertContext(); 333 | return new MySupplier2(exceptionArray); 334 | } 335 | 336 | private class MySupplier2 implements ThrowableSupplier, RuntimeException> { 337 | 338 | private final Throwable[] exceptionArray; 339 | private int current = 0; 340 | 341 | MySupplier2(Throwable[] exceptionArray) { 342 | this.exceptionArray = exceptionArray; 343 | } 344 | 345 | @Override 346 | public ListenableFuture get() throws RuntimeException { 347 | callTimes.incrementAndGet(); 348 | int idx = current++; 349 | if (idx < exceptionArray.length) { 350 | return Futures.immediateFailedFuture(exceptionArray[idx]); 351 | } else { 352 | return Futures.immediateFuture("test"); 353 | } 354 | } 355 | } 356 | 357 | private MySupplier1 sleepySuccess(long[] sleepArray) { 358 | assertContext(); 359 | return new MySupplier1(sleepArray); 360 | } 361 | 362 | private class MySupplier1 implements 363 | ThrowableSupplier, RuntimeException> { 364 | 365 | private final long[] sleepArray; 366 | private AtomicInteger current = new AtomicInteger(0); 367 | 368 | public MySupplier1(long[] sleepArray) { 369 | this.sleepArray = sleepArray; 370 | } 371 | 372 | @Override 373 | public ListenableFuture get() { 374 | callTimes.incrementAndGet(); 375 | return executor.submit(() -> { 376 | int index = current.getAndIncrement(); 377 | long sleepFor = sleepArray[index]; 378 | sleepUninterruptibly(sleepFor, MILLISECONDS); 379 | return index + ""; 380 | }); 381 | } 382 | } 383 | 384 | @Test 385 | void testAllTimeoutWithEachListener() { 386 | clearCallTimes(); 387 | beginScope(); 388 | try { 389 | initKey(); 390 | AtomicInteger succNum = new AtomicInteger(0); 391 | AtomicInteger failedNum = new AtomicInteger(0); 392 | for (int i = 0; i < 10; i++) { 393 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10, false), 394 | () -> successAfter("test", 200), new FutureCallback() { 395 | @Override 396 | public void onSuccess(@Nullable String result) { 397 | succNum.incrementAndGet(); 398 | } 399 | 400 | @Override 401 | public void onFailure(Throwable t) { 402 | failedNum.incrementAndGet(); 403 | } 404 | }); 405 | assertThrows(TimeoutException.class, 406 | () -> assertTimeout(ofMillis(60), () -> future.get(5, MILLISECONDS))); 407 | } 408 | sleepUninterruptibly(1, SECONDS); 409 | assertEquals(40, getCallTimes()); 410 | assertEquals(0, succNum.get()); 411 | assertEquals(40, failedNum.get()); 412 | } finally { 413 | endScope(); 414 | } 415 | } 416 | 417 | @Test 418 | void testAllTimeoutWithEachListenerWithHedgeMode() { 419 | clearCallTimes(); 420 | beginScope(); 421 | try { 422 | initKey(); 423 | AtomicInteger succNum = new AtomicInteger(0); 424 | AtomicInteger failedNum = new AtomicInteger(0); 425 | for (int i = 0; i < 10; i++) { 426 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10), 427 | () -> successAfter("test", 200), new FutureCallback() { 428 | @Override 429 | public void onSuccess(@Nullable String result) { 430 | succNum.incrementAndGet(); 431 | } 432 | 433 | @Override 434 | public void onFailure(Throwable t) { 435 | failedNum.incrementAndGet(); 436 | } 437 | }); 438 | assertThrows(TimeoutException.class, 439 | () -> assertTimeout(ofMillis(60), () -> future.get(5, MILLISECONDS))); 440 | } 441 | sleepUninterruptibly(1, SECONDS); 442 | assertEquals(20, getCallTimes()); 443 | assertEquals(0, succNum.get()); 444 | assertEquals(20, failedNum.get()); 445 | } finally { 446 | endScope(); 447 | } 448 | } 449 | 450 | @Test 451 | void testWithEachListener() throws InterruptedException, ExecutionException, TimeoutException { 452 | beginScope(); 453 | try { 454 | initKey(); 455 | AtomicInteger succNum = new AtomicInteger(0); 456 | AtomicInteger failedNum = new AtomicInteger(0); 457 | for (int i = 0; i < 10; i++) { 458 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(4, 10), 459 | sleepySuccess(new long[] {300L, 200L, 50L, 100L}), new FutureCallback() { 460 | @Override 461 | public void onSuccess(@Nullable String result) { 462 | succNum.incrementAndGet(); 463 | } 464 | 465 | @Override 466 | public void onFailure(Throwable t) { 467 | failedNum.incrementAndGet(); 468 | } 469 | }); 470 | assertEquals("2", future.get(350, MILLISECONDS)); 471 | } 472 | sleepUninterruptibly(1, SECONDS); 473 | assertEquals(10, succNum.get()); 474 | assertEquals(20, failedNum.get()); 475 | } finally { 476 | endScope(); 477 | } 478 | } 479 | 480 | @Test 481 | void testGetFuture() { 482 | beginScope(); 483 | try { 484 | initKey(); 485 | assertThrows(TimeoutException.class, () -> successAfter("test", 200).get(0, NANOSECONDS)); 486 | } finally { 487 | endScope(); 488 | } 489 | } 490 | 491 | private static class TimeoutListenableFuture implements ListenableFuture { 492 | 493 | private final ListenableFuture originFuture; 494 | private final Runnable listener; 495 | 496 | TimeoutListenableFuture(ListenableFuture originFuture, Runnable listener) { 497 | this.originFuture = originFuture; 498 | this.listener = listener; 499 | } 500 | 501 | @Override 502 | public void addListener(Runnable listener, Executor executor) { 503 | originFuture.addListener(listener, executor); 504 | } 505 | 506 | @Override 507 | public boolean cancel(boolean mayInterruptIfRunning) { 508 | return originFuture.cancel(mayInterruptIfRunning); 509 | } 510 | 511 | @Override 512 | public boolean isCancelled() { 513 | return originFuture.isCancelled(); 514 | } 515 | 516 | @Override 517 | public boolean isDone() { 518 | return originFuture.isDone(); 519 | } 520 | 521 | @Override 522 | public T get() throws InterruptedException, ExecutionException { 523 | return originFuture.get(); 524 | } 525 | 526 | @Override 527 | public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { 528 | try { 529 | return originFuture.get(timeout, unit); 530 | } catch (Throwable t) { 531 | if (t instanceof TimeoutException) { 532 | listener.run(); 533 | } 534 | throw t; 535 | } 536 | } 537 | } 538 | 539 | @Test 540 | void testTimeoutListenableFuture() { 541 | for (int i = 0; i < 10000; i++) { 542 | AtomicBoolean timeout = new AtomicBoolean(false); 543 | TimeoutListenableFuture future = new TimeoutListenableFuture<>(executor.submit(() -> { 544 | sleepUninterruptibly(10, MILLISECONDS); 545 | return "haha"; 546 | }), () -> timeout.set(true)); 547 | try { 548 | future.get(1, NANOSECONDS); 549 | } catch (Throwable t) { 550 | // ignore 551 | } 552 | assertTrue(timeout.get()); 553 | } 554 | } 555 | 556 | @Disabled // 目前这个测试用例还不稳定,后面还需要完善和回查 557 | @Test 558 | void testCallerTimeoutListener() throws InterruptedException, ExecutionException { 559 | String expectResult = "hahaha"; 560 | for (int i = 0; i < 10000; i++) { 561 | AtomicBoolean timeoutListenerTriggered = new AtomicBoolean(false); 562 | try { 563 | String result = retrier.callWithRetry(1, retryNTimes(0, 0, false), 564 | () -> new TimeoutListenableFuture<>(executor.submit(() -> { 565 | sleepUninterruptibly(ThreadLocalRandom.current().nextInt(2), MILLISECONDS); 566 | return expectResult; 567 | }), () -> { 568 | timeoutListenerTriggered.set(true); 569 | })).get(ThreadLocalRandom.current().nextInt(3), MILLISECONDS); 570 | // 这里验证下没抛 TimeoutException 的时候一定没有调用 timeout listener 571 | assertEquals(expectResult, result); 572 | assertFalse(timeoutListenerTriggered.get()); 573 | logger.info("nothing."); 574 | } catch (TimeoutException e) { 575 | logger.info("timeout by caller."); 576 | } catch (ExecutionException e) { 577 | if (Throwables.getRootCause(e) instanceof TimeoutException) { 578 | logger.info("timeout by retrier."); 579 | // 这里验证下抛 TimeoutException 的时候一定都调用了 timeout listener 580 | assertTrue(timeoutListenerTriggered.get()); 581 | } else { 582 | throw e; 583 | } 584 | } 585 | } 586 | } 587 | 588 | @Test 589 | void testRetryForException() throws Throwable { 590 | clearCallTimes(); 591 | beginScope(); 592 | try { 593 | initKey(); 594 | for (int i = 0; i < 10; i++) { 595 | ListenableFuture future = 596 | retrier.callWithRetry(100, retryNTimes(3, 10, false), 597 | sleepySuccess(new long[] {300L, 200L, 50L})); 598 | Assertions.assertDoesNotThrow((ThrowingSupplier) future::get); 599 | } 600 | sleepUninterruptibly(1, SECONDS); 601 | } finally { 602 | endScope(); 603 | } 604 | logger.info("{}", getCallTimes()); 605 | assertEquals(30, getCallTimes()); 606 | 607 | clearCallTimes(); 608 | beginScope(); 609 | try { 610 | initKey(); 611 | for (int i = 0; i < 10; i++) { 612 | ListenableFuture future = 613 | retrier.callWithRetry(100, retryNTimes(3, 10, false), 614 | sleepySuccess(new long[] {300L, 200L, 50L})); 615 | future.cancel(false); 616 | } 617 | sleepUninterruptibly(1, SECONDS); 618 | } finally { 619 | endScope(); 620 | } 621 | logger.info("{}", getCallTimes()); 622 | assertEquals(10, getCallTimes()); 623 | 624 | clearCallTimes(); 625 | beginScope(); 626 | try { 627 | initKey(); 628 | for (int i = 0; i < 10; i++) { 629 | ListenableFuture future = 630 | retrier.callWithRetry(1, retryNTimes(3, 10, false), exceptionsAndThenSuccess( 631 | new Throwable[] {new RuntimeException(), new IllegalStateException()})); 632 | future.get(); 633 | } 634 | sleepUninterruptibly(1, SECONDS); 635 | } finally { 636 | endScope(); 637 | } 638 | logger.info("{}", getCallTimes()); 639 | assertEquals(30, getCallTimes()); 640 | 641 | clearCallTimes(); 642 | beginScope(); 643 | try { 644 | initKey(); 645 | for (int i = 0; i < 10; i++) { 646 | ListenableFuture future = 647 | retrier.callWithRetry(100, retryNTimes(3, 10, false), exceptionsAndThenSuccess( 648 | new Throwable[] {new RuntimeException(), new IllegalStateException(), 649 | new IllegalArgumentException()})); 650 | future.cancel(false); 651 | } 652 | sleepUninterruptibly(1, SECONDS); 653 | } finally { 654 | endScope(); 655 | } 656 | logger.info("{}", getCallTimes()); 657 | assertEquals(10, getCallTimes()); 658 | } 659 | 660 | @Test 661 | void testNoMoreNewRetryAfterCancel() { 662 | clearCallTimes(); 663 | beginScope(); 664 | try { 665 | initKey(); 666 | for (int i = 0; i < 10; i++) { 667 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10, false), 668 | () -> successAfter("test", 200)); 669 | assertThrows(TimeoutException.class, 670 | () -> assertTimeout(ofMillis(60), () -> { 671 | future.get(5, MILLISECONDS); 672 | })); 673 | } 674 | sleepUninterruptibly(1, SECONDS); 675 | } finally { 676 | endScope(); 677 | } 678 | logger.info("{}", getCallTimes()); 679 | assertEquals(40, getCallTimes()); 680 | 681 | clearCallTimes(); 682 | beginScope(); 683 | try { 684 | initKey(); 685 | for (int i = 0; i < 10; i++) { 686 | ListenableFuture future = retrier.callWithRetry(100, retryNTimes(3, 10, false), 687 | () -> successAfter("test", 200)); 688 | assertThrows(TimeoutException.class, 689 | () -> assertTimeout(ofMillis(60), () -> { 690 | future.get(5, MILLISECONDS); 691 | })); 692 | future.cancel(false); 693 | } 694 | sleepUninterruptibly(1, SECONDS); 695 | } finally { 696 | endScope(); 697 | } 698 | logger.info("{}", getCallTimes()); 699 | assertEquals(10, getCallTimes()); 700 | } 701 | 702 | private static final ScopeAsyncRetry directCallbackRetry = 703 | createScopeAsyncRetry(Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(), 704 | new ThreadFactoryBuilder() // 705 | .setPriority(MAX_PRIORITY) // 706 | .setNameFormat("default-directCallbackRetry-%d") // 707 | .build())); 708 | 709 | private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool(); 710 | 711 | @Test 712 | void testPerformance() throws Throwable { 713 | beginScope(); 714 | try { 715 | initKey(); 716 | AtomicLong succCount = new AtomicLong(0); 717 | AtomicLong failCount = new AtomicLong(0); 718 | long calls = 100000; 719 | clearCallTimes(); 720 | ConcurrentLinkedQueue> futures = new ConcurrentLinkedQueue<>(); 721 | for (int i = 0; i < calls; i++) { 722 | EXECUTOR_SERVICE.submit(() -> { 723 | ListenableFuture future = 724 | directCallbackRetry 725 | .callWithRetry(1000, retryNTimes(3, 10, false), 726 | () -> successAfterBySleep("test", 200)); 727 | addCallback(future, new FutureCallback() { 728 | @Override 729 | public void onSuccess(@Nullable String result) { 730 | succCount.incrementAndGet(); 731 | } 732 | 733 | @Override 734 | public void onFailure(Throwable t) { 735 | failCount.incrementAndGet(); 736 | } 737 | }, directExecutor()); 738 | futures.add(future); 739 | }); 740 | } 741 | sleepUninterruptibly(1, SECONDS); 742 | while (futures.peek() != null) { 743 | Future tmpFuture = futures.poll(); 744 | assertThrows(Throwable.class, tmpFuture::get); 745 | } 746 | assertEquals(calls, failCount.get()); 747 | assertEquals(0, succCount.get()); 748 | } finally { 749 | endScope(); 750 | } 751 | } 752 | 753 | private static class AbortRetryException extends RuntimeException { 754 | } 755 | 756 | @Test 757 | void testAbortRetry() { 758 | AtomicInteger callTime = new AtomicInteger(0); 759 | Supplier> callFunction = () -> { 760 | if (callTime.incrementAndGet() > 3) { 761 | throw new AbortRetryException(); 762 | } else { 763 | throw new RuntimeException(); 764 | } 765 | }; 766 | 767 | ListenableFuture resultFuture = retrier.callWithRetry(100, new RetryPolicy() { 768 | private final AtomicInteger retryTime = new AtomicInteger(); 769 | 770 | @Override 771 | public long retry(int retryCount) { 772 | int rt = retryTime.incrementAndGet(); 773 | if (rt < 10) { 774 | return rt * 100; 775 | } else { 776 | return NO_RETRY; 777 | } 778 | } 779 | 780 | @Override 781 | public boolean abortRetry(Throwable t) { 782 | return t instanceof AbortRetryException; 783 | } 784 | }, callFunction::get); 785 | 786 | try { 787 | resultFuture.get(); 788 | } catch (Throwable t) { 789 | Assertions.assertTrue(Throwables.getRootCause(t) instanceof AbortRetryException); 790 | } 791 | Assertions.assertEquals(4, callTime.get()); 792 | } 793 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/ScopeKeyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.ScopeKey.allocate; 4 | import static com.github.phantomthief.scope.ScopeKey.withDefaultValue; 5 | import static com.github.phantomthief.scope.ScopeKeyTest.TestEnum.ABC; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | import static org.junit.jupiter.api.Assertions.assertNull; 8 | import static org.junit.jupiter.api.Assertions.assertSame; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import org.junit.jupiter.api.Test; 12 | 13 | /** 14 | * @author w.vela 15 | * Created on 2018-05-30. 16 | */ 17 | class ScopeKeyTest { 18 | 19 | private static final ScopeKey INT_SCOPE_KEY = withDefaultValue(1); 20 | private static final ScopeKey LONG_SCOPE_KEY = withDefaultValue(1L); 21 | private static final ScopeKey DOUBLE_SCOPE_KEY = withDefaultValue(0.0D); 22 | private static final ScopeKey STRING_SCOPE_KEY = withDefaultValue("abc"); 23 | private static final ScopeKey ENUM_SCOPE_KEY = withDefaultValue(ABC); 24 | private static final ScopeKey BOOLEAN_SCOPE_KEY = withDefaultValue(true); 25 | private static final ScopeKey SOME_SCOPE_KEY = allocate(); 26 | 27 | @Test 28 | void testApiCompile() { 29 | assertEquals(Integer.valueOf(1), INT_SCOPE_KEY.get()); 30 | assertEquals(Long.valueOf(1), LONG_SCOPE_KEY.get()); 31 | assertEquals(Double.valueOf(0.0D), DOUBLE_SCOPE_KEY.get()); 32 | assertEquals("abc", STRING_SCOPE_KEY.get()); 33 | assertSame(ABC, ENUM_SCOPE_KEY.get()); 34 | assertTrue(BOOLEAN_SCOPE_KEY.get()); 35 | assertNull(SOME_SCOPE_KEY.get()); 36 | } 37 | 38 | enum TestEnum { 39 | ABC 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/ScopeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.Scope.getCurrentScope; 4 | import static com.github.phantomthief.scope.Scope.runWithExistScope; 5 | import static com.github.phantomthief.scope.Scope.runWithNewScope; 6 | import static com.github.phantomthief.scope.ScopeKey.allocate; 7 | import static com.github.phantomthief.scope.ScopeKey.withDefaultValue; 8 | import static com.github.phantomthief.scope.ScopeKey.withInitializer; 9 | import static com.github.phantomthief.scope.ScopeUtils.runAsyncWithCurrentScope; 10 | import static com.github.phantomthief.scope.ScopeUtils.supplyAsyncWithCurrentScope; 11 | import static com.github.phantomthief.util.MoreFunctions.throwing; 12 | import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; 13 | import static com.google.common.util.concurrent.MoreExecutors.shutdownAndAwaitTermination; 14 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 15 | import static java.util.concurrent.CompletableFuture.supplyAsync; 16 | import static java.util.concurrent.Executors.newFixedThreadPool; 17 | import static java.util.concurrent.TimeUnit.DAYS; 18 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 19 | import static java.util.stream.Collectors.toList; 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | import static org.junit.jupiter.api.Assertions.assertFalse; 22 | import static org.junit.jupiter.api.Assertions.assertNotNull; 23 | import static org.junit.jupiter.api.Assertions.assertNull; 24 | import static org.junit.jupiter.api.Assertions.assertThrows; 25 | import static org.junit.jupiter.api.Assertions.assertTrue; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.concurrent.BlockingQueue; 30 | import java.util.concurrent.Callable; 31 | import java.util.concurrent.CountDownLatch; 32 | import java.util.concurrent.ExecutorService; 33 | import java.util.concurrent.Executors; 34 | import java.util.concurrent.LinkedBlockingQueue; 35 | import java.util.concurrent.ThreadPoolExecutor; 36 | import java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy; 37 | import java.util.concurrent.TimeUnit; 38 | import java.util.concurrent.atomic.AtomicBoolean; 39 | import java.util.concurrent.atomic.AtomicInteger; 40 | 41 | import javax.annotation.Nullable; 42 | 43 | import org.junit.jupiter.api.Assertions; 44 | import org.junit.jupiter.api.Test; 45 | import org.slf4j.Logger; 46 | import org.slf4j.LoggerFactory; 47 | 48 | import com.google.common.util.concurrent.FutureCallback; 49 | import com.google.common.util.concurrent.Futures; 50 | import com.google.common.util.concurrent.ListeningExecutorService; 51 | import com.google.common.util.concurrent.SettableFuture; 52 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 53 | 54 | /** 55 | * @author w.vela 56 | */ 57 | class ScopeTest { 58 | 59 | private static final Logger logger = LoggerFactory.getLogger(ScopeTest.class); 60 | private static final ScopeKey TEST_KEY = allocate(); 61 | 62 | @Test 63 | void testScope() { 64 | ExecutorService executorService = newBlockingThreadPool(20, "main-%d"); 65 | ExecutorService anotherExecutor = newBlockingThreadPool(20, "another-%d"); 66 | ListeningExecutorService anotherExecutor2 = listeningDecorator( 67 | newBlockingThreadPool(20, "another-%d")); 68 | for (int i = 0; i < 10; i++) { 69 | int j = i; 70 | supplyAsyncWithCurrentScope(() -> { 71 | runWithNewScope(() -> { 72 | TEST_KEY.set(j); 73 | Integer fromScope = TEST_KEY.get(); 74 | assertEquals(j, fromScope.intValue()); 75 | for (int k = 0; k < 10; k++) { 76 | runAsyncWithCurrentScope(() -> { 77 | Integer fromScope1 = TEST_KEY.get(); 78 | assertEquals(j, fromScope1.intValue()); 79 | }, anotherExecutor); 80 | runAsyncWithCurrentScope(() -> { 81 | Integer fromScope1 = TEST_KEY.get(); 82 | assertEquals(j, fromScope1.intValue()); 83 | }, anotherExecutor2); 84 | } 85 | }); 86 | return "NONE"; 87 | }, executorService); 88 | assertNull(TEST_KEY.get()); 89 | } 90 | shutdownAndAwaitTermination(executorService, 1, DAYS); 91 | shutdownAndAwaitTermination(anotherExecutor, 1, DAYS); 92 | shutdownAndAwaitTermination(anotherExecutor2, 1, DAYS); 93 | } 94 | 95 | @Test 96 | void testInit() { 97 | ScopeKey test2 = withInitializer(Object::new); 98 | assertNull(test2.get()); 99 | runWithNewScope(() -> { 100 | Object x = test2.get(); 101 | assertEquals(test2.get(), x); 102 | Scope currentScope = getCurrentScope(); 103 | assertEquals(currentScope.get(test2), x); 104 | }); 105 | } 106 | 107 | @Test 108 | void testInitNull() { 109 | AtomicInteger counter = new AtomicInteger(); 110 | ScopeKey test2 = withInitializer(() -> { 111 | counter.incrementAndGet(); 112 | return null; 113 | }); 114 | assertNull(test2.get()); 115 | assertEquals(0, counter.get()); 116 | runWithNewScope(() -> { 117 | assertNull(test2.get()); 118 | assertEquals(1, counter.get()); 119 | assertNull(test2.get()); 120 | assertEquals(2, counter.get()); 121 | }); 122 | } 123 | 124 | @Test 125 | void testInitNullProtection() { 126 | AtomicInteger counter = new AtomicInteger(); 127 | ScopeKey test2 = withInitializer(true,() -> { 128 | counter.incrementAndGet(); 129 | return null; 130 | }); 131 | assertNull(test2.get()); 132 | assertEquals(0, counter.get()); 133 | runWithNewScope(() -> { 134 | assertNull(test2.get()); 135 | assertEquals(1, counter.get()); 136 | assertNull(test2.get()); 137 | assertEquals(1, counter.get()); 138 | }); 139 | } 140 | 141 | @Test 142 | void testDefaultValue() { 143 | ScopeKey key1 = withDefaultValue("test"); 144 | assertEquals(key1.get(), "test"); // anyway, default value is ok. 145 | runWithNewScope(() -> { 146 | assertEquals(key1.get(), "test"); 147 | Scope currentScope = getCurrentScope(); 148 | assertEquals(currentScope.get(key1), "test"); 149 | key1.set("t1"); 150 | assertEquals(key1.get(), "t1"); 151 | assertEquals(currentScope.get(key1), "t1"); 152 | }); 153 | } 154 | 155 | @Test 156 | void testSet() { 157 | ScopeKey key1 = allocate(); 158 | assertFalse(key1.set("test")); 159 | assertNull(key1.get()); 160 | runWithNewScope(() -> { 161 | assertTrue(key1.set("test")); 162 | assertEquals(key1.get(), "test"); 163 | }); 164 | } 165 | 166 | @Test 167 | void testDuplicateStartScope() { 168 | runWithNewScope(() -> { 169 | assertThrows(IllegalStateException.class, () -> runWithNewScope(() -> { 170 | Assertions.fail(""); 171 | })); 172 | }); 173 | } 174 | 175 | @Test 176 | void testRemoveKey() { 177 | runWithNewScope(() -> { 178 | TEST_KEY.set(999); 179 | assertEquals(TEST_KEY.get(), Integer.valueOf(999)); 180 | TEST_KEY.set(null); 181 | assertNull(TEST_KEY.get()); 182 | }); 183 | } 184 | 185 | @Test 186 | void testScopeOverride() { 187 | Scope[] scope = { null }; 188 | Thread thread = new Thread(() -> { 189 | runWithNewScope(() -> { 190 | TEST_KEY.set(2); 191 | assertEquals(TEST_KEY.get(), Integer.valueOf(2)); 192 | while (scope[0] == null) { 193 | sleepUninterruptibly(10, MILLISECONDS); 194 | runWithExistScope(scope[0], () -> { 195 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 196 | }); 197 | } 198 | assertEquals(TEST_KEY.get(), Integer.valueOf(2)); 199 | }); 200 | }); 201 | thread.start(); 202 | try { 203 | runWithNewScope(() -> { 204 | TEST_KEY.set(1); 205 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 206 | Scope currentScope = getCurrentScope(); 207 | scope[0] = currentScope; 208 | thread.join(); 209 | }); 210 | } catch (InterruptedException e) { 211 | e.printStackTrace(); 212 | } 213 | } 214 | 215 | @Test 216 | void testRestoreOldScope() { 217 | Scope[] scope = { null }; 218 | new Thread(() -> { 219 | runWithNewScope(() -> { 220 | TEST_KEY.set(2); 221 | scope[0] = getCurrentScope(); 222 | }); 223 | }).start(); 224 | runWithNewScope(() -> { 225 | TEST_KEY.set(1); 226 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 227 | while (scope[0] == null) { 228 | sleepUninterruptibly(10, MILLISECONDS); 229 | } 230 | runWithExistScope(scope[0], () -> assertEquals(TEST_KEY.get(), Integer.valueOf(2))); 231 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 232 | }); 233 | } 234 | 235 | @Test 236 | void testScopeExecutor() throws Exception { 237 | ExecutorService executorService = ScopeThreadPoolExecutor.newFixedThreadPool(10); 238 | runWithNewScope(() -> { 239 | TEST_KEY.set(1); 240 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 241 | executorService.execute(() -> { 242 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 243 | }); 244 | executorService.submit(() -> { 245 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 246 | return 1; 247 | }).get(); 248 | List> callableList = new ArrayList<>(); 249 | for (int i = 0; i < 50; i++) { 250 | int j = i; 251 | callableList.add(() -> { 252 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 253 | return j; 254 | }); 255 | } 256 | logger.info("invoke all:{}", executorService.invokeAll(callableList).stream() 257 | .map(f -> throwing(f::get)) 258 | .collect(toList())); 259 | logger.info("supply async:{}", supplyAsync(() -> { 260 | assertEquals(TEST_KEY.get(), Integer.valueOf(1)); 261 | return 1; 262 | }, executorService).get()); 263 | }); 264 | shutdownAndAwaitTermination(executorService, 1, DAYS); 265 | } 266 | 267 | private ExecutorService newBlockingThreadPool(int thread, String name) { 268 | ExecutorService executor = newFixedThreadPool(thread, new ThreadFactoryBuilder() 269 | .setNameFormat(name) 270 | .build()); 271 | ((ThreadPoolExecutor) executor).setRejectedExecutionHandler(new CallerRunsPolicy()); 272 | return executor; 273 | } 274 | 275 | private static class ScopeThreadPoolExecutor extends ThreadPoolExecutor { 276 | 277 | ScopeThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 278 | TimeUnit unit, BlockingQueue workQueue) { 279 | super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue); 280 | } 281 | 282 | static ScopeThreadPoolExecutor newFixedThreadPool(int nThreads) { 283 | return new ScopeThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, 284 | new LinkedBlockingQueue<>()); 285 | } 286 | 287 | @Override 288 | public void execute(Runnable command) { 289 | Scope scope = getCurrentScope(); 290 | assertNotNull(scope); 291 | super.execute(() -> { 292 | Scope scope1 = getCurrentScope(); 293 | assertNull(scope1); 294 | runWithExistScope(scope, command::run); 295 | }); 296 | } 297 | } 298 | 299 | @Test 300 | void testFutureCallback() throws Throwable { 301 | ExecutorService executor = Executors.newSingleThreadExecutor(); 302 | 303 | CountDownLatch latch = new CountDownLatch(1); 304 | AtomicBoolean flag = new AtomicBoolean(false); 305 | 306 | runWithNewScope(() -> { 307 | TEST_KEY.set(1); 308 | SettableFuture future = SettableFuture.create(); 309 | Futures.addCallback(future, ScopeUtils.wrapWithScope(new FutureCallback() { 310 | @Override 311 | public void onSuccess(@Nullable Boolean aBoolean) { 312 | try { 313 | Integer integer = TEST_KEY.get(); 314 | if (integer == 1) { 315 | flag.set(true); 316 | } 317 | } finally { 318 | latch.countDown(); 319 | } 320 | } 321 | 322 | @Override 323 | public void onFailure(Throwable throwable) { 324 | try { 325 | Integer integer = TEST_KEY.get(); 326 | if (integer == 1) { 327 | flag.set(true); 328 | } 329 | } finally { 330 | latch.countDown(); 331 | } 332 | } 333 | }), executor); 334 | future.setException(new Throwable()); 335 | latch.await(); 336 | Assertions.assertTrue(flag.get()); 337 | }); 338 | } 339 | } -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/ScopeThreadLocalBenchmark.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.Scope.setFastThreadLocal; 4 | import static com.github.phantomthief.scope.ScopeKey.withDefaultValue; 5 | import static com.github.phantomthief.scope.ScopeKey.withInitializer; 6 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 7 | import static org.openjdk.jmh.annotations.Mode.Throughput; 8 | 9 | import java.util.Set; 10 | 11 | import org.openjdk.jmh.annotations.Benchmark; 12 | import org.openjdk.jmh.annotations.BenchmarkMode; 13 | import org.openjdk.jmh.annotations.Fork; 14 | import org.openjdk.jmh.annotations.Measurement; 15 | import org.openjdk.jmh.annotations.OutputTimeUnit; 16 | import org.openjdk.jmh.annotations.Scope; 17 | import org.openjdk.jmh.annotations.State; 18 | import org.openjdk.jmh.annotations.Threads; 19 | import org.openjdk.jmh.annotations.Warmup; 20 | 21 | import com.google.common.collect.ImmutableSet; 22 | 23 | /** 24 | * 测试说明 25 | * 通过设置jvm参数来决定是否启用 {@link FastThreadLocalExecutor} : 26 | * 27 | * com.github.phantomthief.scope.ScopeThreadLocalBenchmark.* --jvmArgs "-Djmh.executor=CUSTOM -Djmh.executor.class=com.github.phantomthief.scope.FastThreadLocalExecutor" 28 | * 29 | * @author w.vela 30 | * Created on 2019-07-08. 31 | */ 32 | @BenchmarkMode(Throughput) 33 | @Warmup(iterations = 3, time = 2) 34 | @Measurement(iterations = 3, time = 3) 35 | @Threads(8) 36 | @Fork(1) 37 | @OutputTimeUnit(MILLISECONDS) 38 | @State(Scope.Benchmark) 39 | public class ScopeThreadLocalBenchmark { 40 | 41 | private static ScopeKey longScopeKey = withDefaultValue(0L); 42 | private static ScopeKey stringScopeKey = withDefaultValue("asdasdasd"); 43 | private static ScopeKey intScopeKey = withDefaultValue(122); 44 | private static ScopeKey> setScopeKey = withInitializer(() -> ImmutableSet.of("11", "22", "33")); 45 | 46 | @Benchmark 47 | public void benchmarkGet() { 48 | setFastThreadLocal(false); 49 | longScopeKey.get(); 50 | stringScopeKey.get(); 51 | intScopeKey.get(); 52 | setScopeKey.get(); 53 | } 54 | 55 | @Benchmark 56 | public void benchmarkFastGet() { 57 | setFastThreadLocal(true); 58 | longScopeKey.get(); 59 | stringScopeKey.get(); 60 | intScopeKey.get(); 61 | setScopeKey.get(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/scope/ScopeUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.scope; 2 | 3 | import static com.github.phantomthief.scope.Scope.runWithNewScope; 4 | import static com.github.phantomthief.scope.ScopeUtils.trackLongCost; 5 | import static com.google.common.util.concurrent.Uninterruptibles.sleepUninterruptibly; 6 | import static java.time.Duration.ofSeconds; 7 | import static java.util.concurrent.TimeUnit.SECONDS; 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import java.time.Duration; 12 | import java.util.concurrent.atomic.AtomicLong; 13 | 14 | import org.junit.jupiter.api.Test; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | /** 19 | * @author w.vela 20 | * Created on 2019-10-22. 21 | */ 22 | class ScopeUtilsTest { 23 | 24 | private static final Logger logger = LoggerFactory.getLogger(ScopeUtilsTest.class); 25 | private final ScopeKey key = ScopeKey.allocate(); 26 | private final AtomicLong track = new AtomicLong(); 27 | 28 | @Test 29 | void test() { 30 | runWithNewScope(() -> { 31 | key.set("test"); 32 | assertEquals("test", key.get()); 33 | logger.info("starting..."); 34 | 35 | try (LongCostTrack context = trackLongCost(ofSeconds(3), this::setAtomicLong)) { 36 | longCost(1); 37 | } 38 | assertEquals(track.get(), 0); 39 | 40 | try (LongCostTrack context = trackLongCost(ofSeconds(3), this::setAtomicLong)) { 41 | longCost(5); 42 | } 43 | assertTrue(track.get() > 0); 44 | }); 45 | } 46 | 47 | private void setAtomicLong(Duration t) { 48 | logger.info("setting track:{}", t); 49 | assertTrue(t.toNanos() > SECONDS.toNanos(3)); 50 | assertEquals("test", key.get()); 51 | track.set(t.toNanos()); 52 | } 53 | 54 | private void longCost(long seconds) { 55 | sleepUninterruptibly(seconds, SECONDS); 56 | } 57 | } -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %level \(%F:%L\) - %msg %n 7 | 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------