├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── come │ └── ezlippi │ └── isolation │ ├── AbstractCommand.java │ ├── DefaultIsolationThreadPool.java │ ├── Invokable.java │ ├── IsolationCommand.java │ ├── IsolationCommandMetrics.java │ ├── IsolationCommandProperties.java │ ├── IsolationCounters.java │ ├── IsolationPropertiesFactory.java │ ├── IsolationThreadPool.java │ ├── IsolationThreadPoolFactory.java │ ├── IsolationThreadPoolMetrics.java │ ├── IsolationThreadPoolProperties.java │ ├── exception │ └── IsolationRuntimeException.java │ ├── execution │ └── IsolationScheduler.java │ ├── hook │ └── CommandExecutionHook.java │ └── timer │ └── IsolationTimer.java └── test └── java └── com └── ezlippi └── isolation └── CommandTest.java /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # isolation-threadpool 2 | 从Hystrix核心代码中提取出来的线程池隔离的代码,可以非常方便的在Web应用中实现线程池隔离 3 | 4 | # 使用场景 5 | 6 | 我们的应用在使用Jetty服务时,多个HTTP服务会共享同一个线程池,当其中一个服务依赖的其他服务响应慢时造成服务响应时间增加,大多数线程阻塞等待数据响应返回,新的请求无法建立SSL连接,导致整个Jetty线程池都被 7 | 该服务占用,最终拖垮了整个Jetty,因此我们有必要能把不同HTTP服务隔离到不同的线程池中,即使其中某个HTTP服务的线程池满了也不会影响其他的服务 8 | 9 | # 如何使用 10 | 11 | 1. 通过Builder创建命令参数,可以配置任务执行的时间,超时后执行线程会被中断 12 | 13 | ``` java 14 | IsolationCommandProperties.Builder builder1 = new IsolationCommandProperties.Builder() 15 | .withExecutionTimeoutEnabled(true) 16 | .withExecutionTimeoutInMilliseconds(1000) 17 | .withExecutionIsolationThreadInterruptOnTimeout(true); 18 | ``` 19 | 20 | 2. 创建线程池的核心参数 21 | 22 | ``` java 23 | IsolationThreadPoolProperties.Builder threadPoolBuilder1 = new IsolationThreadPoolProperties.Builder() 24 | .withCoreSize(1) 25 | .withMaximumSize(1) 26 | .withKeepAliveTimeMinutes(2) 27 | .withMaxQueueSize(10); 28 | ``` 29 | 3. 创建一个IsolationCommand,一个IsolationCommand对应一个任务的执行,比如第三方服务的调用,需要指定commandName和threadPoolKey,不同的threadPoolKey会创建不同线程池 30 | 31 | ``` java 32 | IsolationCommand command1 = new IsolationCommand("command1", "pool1", 33 | builder1, threadPoolBuilder1, new DefaultExecutionHook()) { 34 | @Override 35 | protected String run() throws Exception { 36 | return "hello"; 37 | } 38 | }; 39 | IsolationCommand command2 = new IsolationCommand("command2", "pool2", 40 | builder1, threadPoolBuilder1, new DefaultExecutionHook()) { 41 | @Override 42 | protected String run() throws Exception { 43 | return "isolation"; 44 | } 45 | }; 46 | //将任务提交给线程池处理,queue()方法返回一个Future,调用者可以通过轮询或者阻塞获取返回值 47 | command1.queue(); 48 | //调用线程同步获取返回值 49 | command2.execute(); 50 | ``` 51 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.ezlippi 8 | isolation.threadpool 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | io.reactivex 14 | rxjava 15 | 1.2.0 16 | 17 | 18 | 19 | org.slf4j 20 | slf4j-api 21 | 1.7.6 22 | 23 | 24 | junit 25 | junit 26 | 4.12 27 | test 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.apache.maven.plugins 35 | maven-compiler-plugin 36 | 37 | 1.8 38 | 1.8 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/AbstractCommand.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import come.ezlippi.isolation.exception.IsolationRuntimeException; 4 | import come.ezlippi.isolation.hook.CommandExecutionHook; 5 | import come.ezlippi.isolation.timer.IsolationTimer; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import rx.Observable; 9 | import rx.Subscriber; 10 | import rx.functions.Action0; 11 | import rx.functions.Func0; 12 | import rx.functions.Func1; 13 | import rx.subscriptions.CompositeSubscription; 14 | 15 | import java.lang.ref.Reference; 16 | import java.util.concurrent.RejectedExecutionException; 17 | import java.util.concurrent.atomic.AtomicReference; 18 | 19 | public abstract class AbstractCommand implements Invokable { 20 | private static final Logger logger = LoggerFactory.getLogger(AbstractCommand.class); 21 | 22 | protected final IsolationThreadPool threadPool; 23 | 24 | protected final String threadPoolKey; 25 | 26 | protected final IsolationCommandProperties properties; 27 | 28 | protected enum TimedOutStatus { 29 | NOT_EXECUTED, COMPLETED, TIMED_OUT 30 | } 31 | 32 | protected enum CommandState { 33 | NOT_STARTED, OBSERVABLE_CHAIN_CREATED, USER_CODE_EXECUTED, UNSUBSCRIBED, TERMINAL 34 | } 35 | 36 | protected enum ThreadState { 37 | NOT_USING_THREAD, STARTED, UNSUBSCRIBED, TERMINAL 38 | } 39 | 40 | protected final IsolationCommandMetrics metrics; 41 | 42 | protected final String commandKey; 43 | 44 | protected volatile long commandStartTimestamp = -1L; 45 | 46 | protected CommandExecutionHook executionHook; 47 | 48 | protected final AtomicReference isCommandTimedOut = new AtomicReference(TimedOutStatus.NOT_EXECUTED); 49 | 50 | protected AtomicReference commandState = new AtomicReference(CommandState.NOT_STARTED); 51 | protected AtomicReference threadState = new AtomicReference(ThreadState.NOT_USING_THREAD); 52 | 53 | protected AbstractCommand(String key, String threadPoolKey, IsolationCommandProperties.Builder commandPropertiesDefaults, 54 | IsolationThreadPoolProperties.Builder threadPoolPropertiesDefaults, CommandExecutionHook executionHook) { 55 | 56 | this.commandKey = key; 57 | this.properties = IsolationPropertiesFactory.getCommandProperties(key, commandPropertiesDefaults); 58 | this.metrics = IsolationCommandMetrics.getInstance(commandKey, properties); 59 | this.threadPoolKey = threadPoolKey; 60 | this.threadPool = IsolationThreadPoolFactory.getInstance(this.threadPoolKey, threadPoolPropertiesDefaults); 61 | this.executionHook = initExecutionHook(executionHook); 62 | 63 | } 64 | 65 | 66 | 67 | protected abstract Observable getExecutionObservable(); 68 | 69 | public Observable toObservable() { 70 | final AbstractCommand _cmd = this; 71 | 72 | //标记命令执行完毕 73 | final Action0 terminateCommandCleanup = new Action0() { 74 | 75 | @Override 76 | public void call() { 77 | if (_cmd.commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.TERMINAL)) { 78 | metrics.markCommandDone(); 79 | } else if (_cmd.commandState.compareAndSet(CommandState.USER_CODE_EXECUTED, CommandState.TERMINAL)) { 80 | metrics.markCommandDone(); 81 | } 82 | } 83 | }; 84 | 85 | //触发执行的hook 86 | final Action0 unsubscribeCommandCleanup = new Action0() { 87 | @Override 88 | public void call() { 89 | try { 90 | executionHook.onUnsubscribe(_cmd); 91 | } catch (Throwable hookEx) { 92 | logger.warn("Error calling CommandExecutionHook.onUnsubscribe", hookEx); 93 | } 94 | } 95 | }; 96 | 97 | //管理命令执行的生命周期 98 | final Func0> applySemantics = new Func0>() { 99 | @Override 100 | public Observable call() { 101 | if (commandState.get().equals(CommandState.UNSUBSCRIBED)) { 102 | return Observable.never(); 103 | } 104 | return applySemantics(_cmd); 105 | } 106 | }; 107 | 108 | final Func1 wrapWithAllOnNextHooks = new Func1() { 109 | @Override 110 | public R call(R r) { 111 | 112 | try { 113 | return executionHook.onEmit(_cmd, r); 114 | } catch (Throwable hookEx) { 115 | logger.warn("Error calling CommandExecutionHook.onEmit", hookEx); 116 | return r; 117 | } 118 | } 119 | }; 120 | 121 | final Action0 fireOnCompletedHook = new Action0() { 122 | @Override 123 | public void call() { 124 | try { 125 | executionHook.onSuccess(_cmd); 126 | } catch (Throwable hookEx) { 127 | logger.warn("Error calling CommandExecutionHook.onSuccess", hookEx); 128 | } 129 | } 130 | }; 131 | 132 | return Observable.defer(new Func0>() { 133 | @Override 134 | public Observable call() { 135 | /* this is a stateful object so can only be used once */ 136 | if (!commandState.compareAndSet(CommandState.NOT_STARTED, CommandState.OBSERVABLE_CHAIN_CREATED)) { 137 | IllegalStateException ex = new IllegalStateException("This instance can only be executed once. " + 138 | "Please instantiate a new instance."); 139 | throw new IsolationRuntimeException(_cmd.getClass(), getCommandKey() + " command executed multiple " + 140 | "times - this is not permitted.", ex, null); 141 | } 142 | 143 | commandStartTimestamp = System.currentTimeMillis(); 144 | 145 | Observable hystrixObservable = 146 | Observable.defer(applySemantics) 147 | .map(wrapWithAllOnNextHooks); 148 | 149 | return hystrixObservable 150 | .doOnTerminate(terminateCommandCleanup) // perform cleanup once (either on normal terminal state (this line), or unsubscribe (next line)) 151 | .doOnUnsubscribe(unsubscribeCommandCleanup) // perform cleanup once 152 | .doOnCompleted(fireOnCompletedHook); 153 | } 154 | }); 155 | } 156 | 157 | 158 | 159 | private Observable applySemantics(final AbstractCommand _cmd) { 160 | // mark that we're starting execution on the ExecutionHook 161 | executionHook.onStart(_cmd); 162 | 163 | Observable execution; 164 | if (properties.executionTimeoutEnabled()) { 165 | execution = executeCommandWithSpecifiedIsolation(_cmd) 166 | .lift(new ObservableTimeoutOperator(_cmd)); 167 | } else { 168 | execution = executeCommandWithSpecifiedIsolation(_cmd); 169 | } 170 | 171 | return execution; 172 | } 173 | 174 | private Observable executeCommandWithSpecifiedIsolation(final AbstractCommand _cmd) { 175 | // mark that we are executing in a thread (even if we end up being rejected we still were a THREAD execution and not SEMAPHORE) 176 | return Observable.defer(new Func0>() { 177 | @Override 178 | public Observable call() { 179 | if (!commandState.compareAndSet(CommandState.OBSERVABLE_CHAIN_CREATED, CommandState.USER_CODE_EXECUTED)) { 180 | return Observable.error(new IllegalStateException("execution attempted while in state : " + commandState.get().name())); 181 | } 182 | 183 | metrics.markCommandStart(); 184 | 185 | if (isCommandTimedOut.get() == TimedOutStatus.TIMED_OUT) { 186 | // the command timed out in the wrapping thread so we will return immediately 187 | // and not increment any of the counters below or other such logic 188 | return Observable.error(new RuntimeException("timed out before executing run()")); 189 | } 190 | if (threadState.compareAndSet(ThreadState.NOT_USING_THREAD, ThreadState.STARTED)) { 191 | //we have not been unsubscribed, so should proceed 192 | IsolationCounters.incrementGlobalConcurrentThreads(); 193 | threadPool.markThreadExecution(); 194 | /** 195 | * If any of these hooks throw an exception, then it appears as if the actual execution threw an error 196 | */ 197 | try { 198 | executionHook.onThreadStart(_cmd); 199 | executionHook.onExecutionStart(_cmd); 200 | return getUserExecutionObservable(_cmd); 201 | } catch (Throwable ex) { 202 | return Observable.error(ex); 203 | } 204 | } else { 205 | //command has already been unsubscribed, so return immediately 206 | return Observable.empty(); 207 | } 208 | } 209 | }).doOnTerminate(new Action0() { 210 | @Override 211 | public void call() { 212 | if (threadState.compareAndSet(ThreadState.STARTED, ThreadState.TERMINAL)) { 213 | handleThreadEnd(_cmd); 214 | } 215 | if (threadState.compareAndSet(ThreadState.NOT_USING_THREAD, ThreadState.TERMINAL)) { 216 | //if it was never started and received terminal, then no need to clean up 217 | } 218 | } 219 | }).doOnUnsubscribe(new Action0() { 220 | @Override 221 | public void call() { 222 | if (threadState.compareAndSet(ThreadState.STARTED, ThreadState.UNSUBSCRIBED)) { 223 | handleThreadEnd(_cmd); 224 | } 225 | if (threadState.compareAndSet(ThreadState.NOT_USING_THREAD, ThreadState.UNSUBSCRIBED)) { 226 | //if it was never started and was cancelled, then no need to clean up 227 | } 228 | //if it was terminal, then other cleanup handled it 229 | } 230 | }).subscribeOn(threadPool.getScheduler(new Func0() { 231 | @Override 232 | public Boolean call() { 233 | return properties.executionIsolationThreadInterruptOnTimeout() && 234 | _cmd.isCommandTimedOut.get() == TimedOutStatus.TIMED_OUT; 235 | } 236 | })).onErrorResumeNext(new Func1>() { 237 | @Override 238 | public Observable call(Throwable throwable) { 239 | Exception e = getExceptionFromThrowable(throwable); 240 | return handleFallback(e); 241 | } 242 | }); 243 | } 244 | 245 | 246 | private Observable getUserExecutionObservable(final AbstractCommand _cmd) { 247 | Observable userObservable; 248 | 249 | try { 250 | userObservable = getExecutionObservable(); 251 | } catch (Throwable ex) { 252 | // the run() method is a user provided implementation so can throw instead of using Observable.onError 253 | // so we catch it here and turn it into Observable.error 254 | userObservable = Observable.error(ex); 255 | } 256 | 257 | return userObservable 258 | .lift(new ExecutionHookApplication(_cmd)); 259 | } 260 | 261 | private class ExecutionHookApplication implements Observable.Operator { 262 | private final Invokable cmd; 263 | 264 | ExecutionHookApplication(Invokable cmd) { 265 | this.cmd = cmd; 266 | } 267 | 268 | @Override 269 | public Subscriber call(final Subscriber subscriber) { 270 | return new Subscriber(subscriber) { 271 | @Override 272 | public void onCompleted() { 273 | try { 274 | executionHook.onExecutionSuccess(cmd); 275 | } catch (Throwable hookEx) { 276 | logger.warn("Error calling CommandExecutionHook.onExecutionSuccess", hookEx); 277 | } 278 | subscriber.onCompleted(); 279 | } 280 | 281 | @Override 282 | public void onError(Throwable e) { 283 | Exception wrappedEx = wrapWithOnExecutionErrorHook(e); 284 | subscriber.onError(wrappedEx); 285 | } 286 | 287 | @Override 288 | public void onNext(R r) { 289 | R wrappedValue = wrapWithOnExecutionEmitHook(r); 290 | subscriber.onNext(wrappedValue); 291 | } 292 | }; 293 | } 294 | } 295 | 296 | private Exception wrapWithOnExecutionErrorHook(Throwable t) { 297 | Exception e = getExceptionFromThrowable(t); 298 | try { 299 | return executionHook.onExecutionError(this, e); 300 | } catch (Throwable hookEx) { 301 | logger.warn("Error calling CommandExecutionHook.onExecutionError", hookEx); 302 | return e; 303 | } 304 | } 305 | 306 | private R wrapWithOnExecutionEmitHook(R r) { 307 | try { 308 | return executionHook.onExecutionEmit(this, r); 309 | } catch (Throwable hookEx) { 310 | logger.warn("Error calling CommandExecutionHook.onExecutionEmit", hookEx); 311 | return r; 312 | } 313 | } 314 | 315 | protected Observable handleFallback(Exception e) { 316 | if (e instanceof RejectedExecutionException) { 317 | threadPool.markThreadRejection(); 318 | } 319 | Exception wrapped = wrapWithOnErrorHook(e); 320 | return Observable.error(new IsolationRuntimeException(this.getClass(), getCommandKey() + 321 | " could not be queued for " + 322 | "execution and fallback disabled.", 323 | wrapped, 324 | null)); 325 | 326 | } 327 | 328 | private Exception wrapWithOnErrorHook(Throwable t) { 329 | Exception e = getExceptionFromThrowable(t); 330 | try { 331 | return executionHook.onError(this, e); 332 | } catch (Throwable hookEx) { 333 | logger.warn("Error calling CommandExecutionHook.onError", hookEx); 334 | return e; 335 | } 336 | } 337 | 338 | protected Exception getExceptionFromThrowable(Throwable t) { 339 | Exception e; 340 | if (t instanceof Exception) { 341 | e = (Exception) t; 342 | } else { 343 | e = new Exception("Throwable caught while executing.", t); 344 | } 345 | return e; 346 | } 347 | 348 | protected void handleThreadEnd(AbstractCommand _cmd) { 349 | IsolationCounters.decrementGlobalConcurrentThreads(); 350 | threadPool.markThreadCompletion(); 351 | try { 352 | executionHook.onThreadComplete(_cmd); 353 | } catch (Throwable hookEx) { 354 | logger.warn("Error calling CommandExecutionHook.onThreadComplete", hookEx); 355 | } 356 | } 357 | 358 | @Override 359 | public String getCommandKey() { 360 | return commandKey; 361 | } 362 | 363 | @Override 364 | public String getThreadPoolKey() { 365 | return threadPoolKey; 366 | } 367 | 368 | @Override 369 | public IsolationCommandMetrics getMetrics() { 370 | return metrics; 371 | } 372 | 373 | @Override 374 | public IsolationCommandProperties getProperties() { 375 | return properties; 376 | } 377 | 378 | @Override 379 | public boolean isExecutionComplete() { 380 | return commandState.get() == CommandState.TERMINAL; 381 | } 382 | 383 | 384 | @Override 385 | public long getCommandRunStartTimeInNanos() { 386 | return commandStartTimestamp; 387 | } 388 | 389 | /** 390 | * 处理任务是否超时 391 | * @param 392 | */ 393 | private static class ObservableTimeoutOperator implements Observable.Operator { 394 | 395 | final AbstractCommand originalCommand; 396 | 397 | public ObservableTimeoutOperator(final AbstractCommand originalCommand) { 398 | this.originalCommand = originalCommand; 399 | } 400 | 401 | @Override 402 | public Subscriber call(final Subscriber children) { 403 | final CompositeSubscription s = new CompositeSubscription(); 404 | // if the children unsubscribes we unsubscribe our parent as well 405 | children.add(s); 406 | 407 | 408 | IsolationTimer.TimerListener listener = new IsolationTimer.TimerListener() { 409 | 410 | @Override 411 | public void tick() { 412 | // if we can go from NOT_EXECUTED to TIMED_OUT then we do the timeout codepath 413 | // otherwise it means we lost a race and the run() execution completed or did not start 414 | if (originalCommand.isCommandTimedOut.compareAndSet(TimedOutStatus.NOT_EXECUTED, TimedOutStatus.TIMED_OUT)) { 415 | 416 | // shut down the original request 417 | s.unsubscribe(); 418 | 419 | } 420 | } 421 | 422 | @Override 423 | public int getIntervalTimeInMilliseconds() { 424 | return originalCommand.properties.executionTimeoutInMilliseconds(); 425 | } 426 | }; 427 | 428 | final Reference tl = IsolationTimer.getInstance().addTimerListener(listener); 429 | 430 | /** 431 | * If this subscriber receives values it means the parent succeeded/completed 432 | */ 433 | Subscriber parent = new Subscriber() { 434 | 435 | @Override 436 | public void onCompleted() { 437 | if (isNotTimedOut()) { 438 | // stop timer and pass notification through 439 | tl.clear(); 440 | children.onCompleted(); 441 | } 442 | } 443 | 444 | @Override 445 | public void onError(Throwable e) { 446 | if (isNotTimedOut()) { 447 | // stop timer and pass notification through 448 | tl.clear(); 449 | children.onError(e); 450 | } 451 | } 452 | 453 | @Override 454 | public void onNext(R v) { 455 | if (isNotTimedOut()) { 456 | children.onNext(v); 457 | } 458 | } 459 | 460 | private boolean isNotTimedOut() { 461 | // if already marked COMPLETED (by onNext) or succeeds in setting to COMPLETED 462 | return originalCommand.isCommandTimedOut.get() == TimedOutStatus.COMPLETED || 463 | originalCommand.isCommandTimedOut.compareAndSet(TimedOutStatus.NOT_EXECUTED, TimedOutStatus.COMPLETED); 464 | } 465 | 466 | }; 467 | 468 | s.add(parent); 469 | 470 | return parent; 471 | } 472 | 473 | } 474 | 475 | 476 | private CommandExecutionHook initExecutionHook(CommandExecutionHook executionHook) { 477 | if (executionHook == null) { 478 | executionHook = new CommandExecutionHook() { 479 | @Override 480 | public void onStart(Invokable commandInstance) { 481 | super.onStart(commandInstance); 482 | } 483 | 484 | @Override 485 | public T onEmit(Invokable commandInstance, T value) { 486 | return super.onEmit(commandInstance, value); 487 | } 488 | 489 | @Override 490 | public Exception onError(Invokable commandInstance, Exception e) { 491 | return super.onError(commandInstance, e); 492 | } 493 | 494 | @Override 495 | public void onSuccess(Invokable commandInstance) { 496 | super.onSuccess(commandInstance); 497 | } 498 | 499 | @Override 500 | public void onThreadStart(Invokable commandInstance) { 501 | super.onThreadStart(commandInstance); 502 | } 503 | 504 | @Override 505 | public void onThreadComplete(Invokable commandInstance) { 506 | super.onThreadComplete(commandInstance); 507 | } 508 | 509 | @Override 510 | public void onExecutionStart(Invokable commandInstance) { 511 | super.onExecutionStart(commandInstance); 512 | } 513 | 514 | @Override 515 | public T onExecutionEmit(Invokable commandInstance, T value) { 516 | return super.onExecutionEmit(commandInstance, value); 517 | } 518 | 519 | @Override 520 | public Exception onExecutionError(Invokable commandInstance, Exception e) { 521 | return super.onExecutionError(commandInstance, e); 522 | } 523 | 524 | @Override 525 | public void onExecutionSuccess(Invokable commandInstance) { 526 | super.onExecutionSuccess(commandInstance); 527 | } 528 | 529 | @Override 530 | public void onUnsubscribe(Invokable commandInstance) { 531 | super.onUnsubscribe(commandInstance); 532 | } 533 | }; 534 | } 535 | return executionHook; 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/DefaultIsolationThreadPool.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import come.ezlippi.isolation.execution.IsolationScheduler; 4 | import rx.Scheduler; 5 | import rx.functions.Func0; 6 | 7 | import java.util.concurrent.*; 8 | import java.util.concurrent.atomic.AtomicInteger; 9 | 10 | /** 11 | * 实现线程池隔离的核心部分,每个线程用来执行{@link IsolationCommand#run()}方法,不同的服务应设置不同的key来区分不同的线程池 12 | */ 13 | public class DefaultIsolationThreadPool implements IsolationThreadPool { 14 | private final IsolationThreadPoolProperties properties; 15 | 16 | private final ThreadPoolExecutor threadPool; 17 | 18 | private final IsolationThreadPoolMetrics metrics; 19 | 20 | private final BlockingQueue queue; 21 | 22 | private final int queueSize; 23 | 24 | public DefaultIsolationThreadPool(String threadPoolKey, IsolationThreadPoolProperties.Builder builder) { 25 | this.properties = IsolationPropertiesFactory.getThreadPoolProperties(threadPoolKey, builder); 26 | 27 | this.queueSize = properties.getWorkingQueueSize(); 28 | 29 | this.threadPool = getThreadPollExecutor(threadPoolKey, properties); 30 | 31 | this.metrics = IsolationThreadPoolMetrics.getInstance(threadPoolKey, threadPool, properties); 32 | 33 | this.queue = threadPool.getQueue(); 34 | } 35 | 36 | private ThreadPoolExecutor getThreadPollExecutor(String threadPoolKey, IsolationThreadPoolProperties properties) { 37 | final int dynamicCoreSize = properties.getCoreSize(); 38 | final int maxCoreSize = properties.getMaxSize(); 39 | final int keepAliveTime = properties.getKeepAliveTimeInMinutes(); 40 | final int maxQueueSize = properties.getWorkingQueueSize(); 41 | final BlockingQueue workQueue = getBlockingQueue(maxQueueSize); 42 | 43 | final ThreadFactory threadFactory = getThreadFactory(threadPoolKey); 44 | 45 | return new ThreadPoolExecutor(dynamicCoreSize, maxCoreSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory); 46 | 47 | } 48 | 49 | private static ThreadFactory getThreadFactory(final String threadPoolKey) { 50 | return new ThreadFactory() { 51 | private final AtomicInteger threadNumber = new AtomicInteger(0); 52 | 53 | @Override 54 | public Thread newThread(Runnable r) { 55 | Thread thread = new Thread(r, "isolation-" + threadPoolKey + "-" + threadNumber.incrementAndGet()); 56 | // thread.setDaemon(true); 57 | return thread; 58 | } 59 | 60 | }; 61 | } 62 | 63 | private BlockingQueue getBlockingQueue(int maxQueueSize) { 64 | if (maxQueueSize < 0) { 65 | return new LinkedBlockingQueue<>(); 66 | } else { 67 | return new LinkedBlockingQueue<>(maxQueueSize); 68 | } 69 | } 70 | 71 | public ExecutorService getExecutor() { 72 | return threadPool; 73 | } 74 | 75 | @Override 76 | public Scheduler getScheduler() { 77 | //by default, interrupt underlying threads on timeout 78 | return getScheduler(new Func0() { 79 | @Override 80 | public Boolean call() { 81 | return true; 82 | } 83 | }); 84 | } 85 | 86 | @Override 87 | public Scheduler getScheduler(Func0 shouldInterruptThread) { 88 | return new IsolationScheduler(this, shouldInterruptThread); 89 | } 90 | 91 | public void markThreadExecution() { 92 | metrics.markThreadExecution(); 93 | } 94 | 95 | public void markThreadCompletion() { 96 | metrics.markThreadCompletion(); 97 | } 98 | 99 | public void markThreadRejection() { 100 | metrics.markThreadRejection(); 101 | } 102 | 103 | public boolean isQueueSpaceAvailable() { 104 | if (queueSize <= 0) { 105 | //无界队列 106 | return true; 107 | } else { 108 | return threadPool.getQueue().size() < properties.getWorkingQueueSize(); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/Invokable.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | /** 4 | * 所有可以被线程池执行的任务 5 | */ 6 | public interface Invokable { 7 | 8 | /** 9 | * 命令的key 10 | * @return 命令的key 11 | */ 12 | String getCommandKey(); 13 | 14 | /** 15 | * 线程池key,标识用哪一组线程池来执行这个任务 16 | * @return 线程池key 17 | */ 18 | String getThreadPoolKey(); 19 | 20 | IsolationCommandMetrics getMetrics(); 21 | 22 | IsolationCommandProperties getProperties(); 23 | 24 | boolean isExecutionComplete(); 25 | 26 | long getCommandRunStartTimeInNanos(); 27 | 28 | Thread getExecutionThread(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationCommand.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import come.ezlippi.isolation.exception.IsolationRuntimeException; 4 | import come.ezlippi.isolation.hook.CommandExecutionHook; 5 | import rx.Observable; 6 | import rx.functions.Action0; 7 | import rx.functions.Func0; 8 | 9 | import java.util.concurrent.ExecutionException; 10 | import java.util.concurrent.Future; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.TimeoutException; 13 | import java.util.concurrent.atomic.AtomicBoolean; 14 | import java.util.concurrent.atomic.AtomicReference; 15 | 16 | public abstract class IsolationCommand extends AbstractCommand { 17 | 18 | protected IsolationCommand(Builder builder) { 19 | this(builder.commandKey, builder.threadPoolKey, builder.commandPropertiesBuilder, 20 | builder.threadPoolPropertiesBuilder, null); 21 | } 22 | 23 | protected IsolationCommand( String key, String threadPoolKey, 24 | IsolationCommandProperties.Builder commandPropertiesDefaults, IsolationThreadPoolProperties.Builder threadPoolPropertiesDefaults, 25 | CommandExecutionHook executionHook) { 26 | super(key, threadPoolKey, commandPropertiesDefaults, threadPoolPropertiesDefaults, executionHook); 27 | } 28 | 29 | public static class Builder { 30 | 31 | protected String commandKey; 32 | protected String threadPoolKey; 33 | protected IsolationCommandProperties.Builder commandPropertiesBuilder; 34 | protected IsolationThreadPoolProperties.Builder threadPoolPropertiesBuilder; 35 | protected CommandExecutionHook commandExecutionHook; 36 | 37 | 38 | public Builder withCommandKey(String commandKey) { 39 | this.commandKey = commandKey; 40 | return this; 41 | } 42 | 43 | public Builder withThreadPoolKey(String threadPoolKey) { 44 | this.threadPoolKey = threadPoolKey; 45 | return this; 46 | } 47 | 48 | public Builder withCommandPropertiesBuilder(IsolationCommandProperties.Builder commandPropertiesBuilder) { 49 | this.commandPropertiesBuilder = commandPropertiesBuilder; 50 | return this; 51 | } 52 | 53 | public Builder withThreadPoolPropertiesBuilder(IsolationThreadPoolProperties.Builder threadPoolPropertiesBuilder) { 54 | this.threadPoolPropertiesBuilder = threadPoolPropertiesBuilder; 55 | return this; 56 | } 57 | 58 | public Builder withCommandExecutionHook(CommandExecutionHook executionHook) { 59 | this.commandExecutionHook = executionHook; 60 | return this; 61 | } 62 | 63 | } 64 | 65 | /** 66 | * 执行这个命令的线程,用于中断执行的线程 67 | */ 68 | private final AtomicReference executionThread = new AtomicReference(); 69 | 70 | private final AtomicBoolean interruptOnFutureCancel = new AtomicBoolean(false); 71 | 72 | /** 73 | * 具体的命令,需要在run方法里实现具体的业务逻辑,比如调用第三方服务的接口 74 | * @return 响应 75 | * @throws Exception 可能抛出异常 76 | */ 77 | protected abstract R run() throws Exception; 78 | 79 | @Override 80 | final protected Observable getExecutionObservable() { 81 | //创建Observable对象 82 | return Observable.defer(new Func0>() { 83 | @Override 84 | public Observable call() { 85 | try { 86 | //执行run()方法里的业务逻辑 87 | return Observable.just(run()); 88 | } catch (Throwable t) { 89 | return Observable.error(t); 90 | } 91 | } 92 | }).doOnSubscribe(new Action0() { 93 | @Override 94 | public void call() { 95 | // 保存这个任务关联的线程,如果有需要我们可以中断它 96 | executionThread.set(Thread.currentThread()); 97 | } 98 | }); 99 | } 100 | 101 | /** 102 | * 同步调用获取执行结果 103 | * @return 返回结果 104 | */ 105 | public R execute() { 106 | try { 107 | return queue().get(); 108 | } catch (ExecutionException e) { 109 | throw new IsolationRuntimeException(this.getClass(), "executionException", e, e); 110 | } catch (InterruptedException e) { 111 | throw new IsolationRuntimeException(this.getClass(), "InterruptException", e, e); 112 | } catch (Exception e) { 113 | throw e; 114 | } 115 | } 116 | 117 | /** 118 | * 异步执行一个命令,会把命令提交到线程池中,返回一个future给调用者,调用者可以通过future来获取返回结果 119 | */ 120 | public Future queue() { 121 | 122 | //Observable.toBlocking().toFuture()返回的Future在调用Future.cancel(true)时没有实现中断执行线程的机制,因此这里做一个封装 123 | final Future originFuture = toObservable().toBlocking().toFuture(); 124 | 125 | final Future wrapedFuture = new Future() { 126 | 127 | @Override 128 | public boolean cancel(boolean mayInterruptIfRunning) { 129 | if (originFuture.isCancelled()) { 130 | return false; 131 | } 132 | 133 | if (IsolationCommand.this.getProperties().executionIsolationThreadInterruptOnFutureCancel()) { 134 | /* 135 | * The only valid transition here is false -> true. If there are two futures, say f1 and f2, created by this command 136 | * (which is super-weird, but has never been prohibited), and calls to f1.cancel(true) and to f2.cancel(false) are 137 | * issued by different threads, it's unclear about what value would be used by the time mayInterruptOnCancel is checked. 138 | * The most consistent way to deal with this scenario is to say that if *any* cancellation is invoked with interruption, 139 | * than that interruption request cannot be taken back. 140 | */ 141 | interruptOnFutureCancel.compareAndSet(false, mayInterruptIfRunning); 142 | } 143 | 144 | final boolean result = originFuture.cancel(interruptOnFutureCancel.get()); 145 | 146 | if (!isExecutionComplete() && interruptOnFutureCancel.get()) { 147 | final Thread t = executionThread.get(); 148 | if (t != null && !t.equals(Thread.currentThread())) { 149 | t.interrupt(); 150 | } 151 | } 152 | 153 | return result; 154 | } 155 | 156 | @Override 157 | public boolean isCancelled() { 158 | return originFuture.isCancelled(); 159 | } 160 | 161 | @Override 162 | public boolean isDone() { 163 | return originFuture.isDone(); 164 | } 165 | 166 | @Override 167 | public R get() throws InterruptedException, ExecutionException { 168 | return originFuture.get(); 169 | } 170 | 171 | @Override 172 | public R get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { 173 | return originFuture.get(timeout, unit); 174 | } 175 | 176 | }; 177 | 178 | return wrapedFuture; 179 | } 180 | 181 | @Override 182 | public Thread getExecutionThread() { 183 | return executionThread.get(); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationCommandMetrics.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | /** 8 | * 度量执行的命令数 9 | */ 10 | public class IsolationCommandMetrics { 11 | 12 | private static final Map metrics = new ConcurrentHashMap(); 13 | 14 | 15 | private final IsolationCommandProperties properties; 16 | 17 | private final String key; 18 | 19 | private final String threadPoolKey; 20 | 21 | private final AtomicInteger concurrentExecutionCount = new AtomicInteger(); 22 | 23 | 24 | public IsolationCommandMetrics(String key, String threadPoolKey, IsolationCommandProperties properties) { 25 | this.key = key; 26 | this.threadPoolKey = threadPoolKey; 27 | this.properties = properties; 28 | } 29 | 30 | /** 31 | * Get or create the {@link IsolationCommandMetrics} instance for a given String. 32 | *

33 | * This is thread-safe and ensures only 1 {@link IsolationCommandMetrics} per String. 34 | * 35 | * @param key String of {@link IsolationCommand} instance requesting the {@link IsolationCommandMetrics} 36 | * @param properties Pass-thru to {@link IsolationCommandMetrics} instance on first time when constructed 37 | * @return {@link IsolationCommandMetrics} 38 | */ 39 | public static IsolationCommandMetrics getInstance(String key, IsolationCommandProperties properties) { 40 | return getInstance(key, null, properties); 41 | } 42 | 43 | /** 44 | * Get or create the {@link IsolationCommandMetrics} instance for a given String. 45 | *

46 | * This is thread-safe and ensures only 1 {@link IsolationCommandMetrics} per String. 47 | * 48 | * @param key String of {@link IsolationCommand} instance requesting the {@link IsolationCommandMetrics} 49 | * @param properties Pass-thru to {@link IsolationCommandMetrics} instance on first time when constructed 50 | * @return {@link IsolationCommandMetrics} 51 | */ 52 | public static IsolationCommandMetrics getInstance(String key, String threadPoolKey, IsolationCommandProperties properties) { 53 | // attempt to retrieve from cache first 54 | IsolationCommandMetrics commandMetrics = metrics.get(key); 55 | if (commandMetrics != null) { 56 | return commandMetrics; 57 | } else { 58 | synchronized (IsolationCommandMetrics.class) { 59 | IsolationCommandMetrics existingMetrics = metrics.get(key); 60 | if (existingMetrics != null) { 61 | return existingMetrics; 62 | } else { 63 | IsolationCommandMetrics newCommandMetrics = new IsolationCommandMetrics(key, threadPoolKey, properties); 64 | metrics.putIfAbsent(key, newCommandMetrics); 65 | return newCommandMetrics; 66 | } 67 | } 68 | } 69 | } 70 | 71 | void markCommandStart() { 72 | concurrentExecutionCount.incrementAndGet(); 73 | } 74 | 75 | void markCommandDone() { 76 | concurrentExecutionCount.decrementAndGet(); 77 | } 78 | 79 | static void reset() { 80 | metrics.clear(); 81 | } 82 | 83 | public IsolationCommandProperties getProperties() { 84 | return properties; 85 | } 86 | 87 | public String getKey() { 88 | return key; 89 | } 90 | 91 | public String getThreadPoolKey() { 92 | return threadPoolKey; 93 | } 94 | 95 | public AtomicInteger getConcurrentExecutionCount() { 96 | return concurrentExecutionCount; 97 | } 98 | 99 | public int getCurrentConcurrentExecutionCount() { 100 | return concurrentExecutionCount.get(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationCommandProperties.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | /** 4 | * 隔离参数 5 | */ 6 | public class IsolationCommandProperties { 7 | 8 | private static final Integer default_executionTimeoutInMilliseconds = 5000; 9 | 10 | private static final Boolean default_executionTimeoutEnabled = false; 11 | 12 | private static final Boolean default_InterruptExecuteOnTimeout = true; 13 | 14 | private static final Boolean default_InterruptExecuteOnFutureCancel = false; 15 | 16 | private static final Boolean default_requestLogEnabled = false; 17 | 18 | private final String key; 19 | 20 | /** 21 | * Timeout value in milliseconds for a command 22 | */ 23 | private final Integer executionTimeoutInMilliseconds; 24 | 25 | /** 26 | * Whether timeout should be triggered 27 | */ 28 | private final Boolean executionTimeoutEnabled; 29 | 30 | 31 | /** 32 | * Whether an underlying Future/Thread (when runInSeparateThread == true) should be interrupted after a timeout 33 | */ 34 | private final Boolean interruptExecuteOnTimeout; 35 | 36 | /** 37 | * Whether canceling an underlying Future/Thread (when runInSeparateThread == true) should interrupt the execution thread 38 | */ 39 | private final Boolean interruptExecuteOnFutureCancel; 40 | 41 | 42 | public IsolationCommandProperties(String key) { 43 | this(key, new Builder()); 44 | } 45 | 46 | public IsolationCommandProperties(String key, IsolationCommandProperties.Builder builder) { 47 | this.key = key; 48 | this.executionTimeoutInMilliseconds = builder.executionTimeoutInMilliseconds == null ? 49 | default_executionTimeoutInMilliseconds : builder.executionTimeoutInMilliseconds; 50 | 51 | this.executionTimeoutEnabled = builder.executionTimeoutEnabled == null ? 52 | default_executionTimeoutEnabled : builder.executionTimeoutEnabled; 53 | 54 | this.interruptExecuteOnTimeout = builder.interruptExecutionOnTimeout == null ? 55 | default_InterruptExecuteOnTimeout : builder.interruptExecutionOnTimeout; 56 | 57 | this.interruptExecuteOnFutureCancel = builder.interruptExecutionOnFutureCancel == null ? 58 | default_InterruptExecuteOnFutureCancel : builder.interruptExecutionOnFutureCancel; 59 | 60 | } 61 | 62 | public Boolean executionIsolationThreadInterruptOnTimeout() { 63 | return interruptExecuteOnTimeout; 64 | } 65 | 66 | 67 | public Boolean executionIsolationThreadInterruptOnFutureCancel() { 68 | return interruptExecuteOnFutureCancel; 69 | } 70 | 71 | public Integer executionTimeoutInMilliseconds() { 72 | return executionTimeoutInMilliseconds; 73 | } 74 | 75 | public Boolean executionTimeoutEnabled() { 76 | return executionTimeoutEnabled; 77 | } 78 | 79 | public static class Builder { 80 | 81 | private Boolean interruptExecutionOnTimeout = null; 82 | private Boolean interruptExecutionOnFutureCancel = null; 83 | private Integer executionTimeoutInMilliseconds = null; 84 | private Boolean executionTimeoutEnabled = null; 85 | 86 | public Builder() { 87 | } 88 | 89 | public Boolean getInterruptExecutionOnTimeout() { 90 | return interruptExecutionOnTimeout; 91 | } 92 | 93 | public Boolean getInterruptExecutionOnFutureCancel() { 94 | return interruptExecutionOnFutureCancel; 95 | } 96 | 97 | public Integer getExecutionTimeoutInMilliseconds() { 98 | return executionTimeoutInMilliseconds; 99 | } 100 | 101 | public Boolean getExecutionTimeoutEnabled() { 102 | return executionTimeoutEnabled; 103 | } 104 | 105 | 106 | public Builder withExecutionIsolationThreadInterruptOnTimeout(boolean value) { 107 | this.interruptExecutionOnTimeout = value; 108 | return this; 109 | } 110 | 111 | public Builder withExecutionIsolationThreadInterruptOnFutureCancel(boolean value) { 112 | this.interruptExecutionOnFutureCancel = value; 113 | return this; 114 | } 115 | 116 | public Builder withExecutionTimeoutInMilliseconds(int value) { 117 | this.executionTimeoutInMilliseconds = value; 118 | return this; 119 | } 120 | 121 | public Builder withExecutionTimeoutEnabled(boolean value) { 122 | this.executionTimeoutEnabled = value; 123 | return this; 124 | } 125 | 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationCounters.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | /** 6 | * 全局统计信息 7 | */ 8 | public class IsolationCounters { 9 | private static final AtomicInteger concurrentThreadsExecuting = new AtomicInteger(0); 10 | 11 | static int incrementGlobalConcurrentThreads() { 12 | return concurrentThreadsExecuting.incrementAndGet(); 13 | } 14 | 15 | static int decrementGlobalConcurrentThreads() { 16 | return concurrentThreadsExecuting.decrementAndGet(); 17 | } 18 | 19 | /** 20 | * Return the number of currently-executing threads 21 | * @return number of currently-executing threads 22 | */ 23 | public static int getGlobalConcurrentThreadsExecuting() { 24 | return concurrentThreadsExecuting.get(); 25 | } 26 | 27 | /** 28 | * Return the number of unique {@link IsolationThreadPool}s that have been registered 29 | * @return number of unique {@link IsolationThreadPool}s that have been registered 30 | */ 31 | public static int getThreadPoolCount() { 32 | return IsolationThreadPoolFactory.getThreadPoolCount(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationPropertiesFactory.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | 6 | /** 7 | * 属性工厂类,保存所有命令参数和线程池参数 8 | */ 9 | public class IsolationPropertiesFactory { 10 | 11 | private static final Map THREAD_POOL_PROPERTIES_MAP = 12 | new ConcurrentHashMap(); 13 | 14 | private static final Map commandProperties = 15 | new ConcurrentHashMap(); 16 | 17 | public static void clear() { 18 | THREAD_POOL_PROPERTIES_MAP.clear(); 19 | commandProperties.clear(); 20 | } 21 | 22 | public static IsolationThreadPoolProperties getThreadPoolProperties(String threadPoolKey, IsolationThreadPoolProperties.Builder builder) { 23 | IsolationThreadPoolProperties poolProperties = THREAD_POOL_PROPERTIES_MAP.get(threadPoolKey); 24 | if (poolProperties == null) { 25 | poolProperties = new IsolationThreadPoolProperties(builder); 26 | IsolationThreadPoolProperties exists = THREAD_POOL_PROPERTIES_MAP.putIfAbsent(threadPoolKey, poolProperties); 27 | if (exists == null) { 28 | return poolProperties; 29 | } else { 30 | return exists; 31 | } 32 | } 33 | return poolProperties; 34 | } 35 | 36 | public static IsolationCommandProperties getCommandProperties(String key, IsolationCommandProperties.Builder builder) { 37 | IsolationCommandProperties properties = commandProperties.get(key); 38 | if (properties != null) { 39 | return properties; 40 | } else { 41 | if (builder == null) { 42 | builder = new IsolationCommandProperties.Builder(); 43 | } 44 | // create new instance 45 | properties = new IsolationCommandProperties(key, builder); 46 | // cache and return 47 | IsolationCommandProperties existing = commandProperties.putIfAbsent(key, properties); 48 | if (existing == null) { 49 | return properties; 50 | } else { 51 | return existing; 52 | } 53 | } 54 | } 55 | 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationThreadPool.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import rx.Scheduler; 4 | import rx.functions.Func0; 5 | 6 | import java.util.concurrent.ExecutorService; 7 | 8 | /** 9 | * 隔离线程池,每个线程用来执行{@link IsolationCommand#run()}方法,不同的服务应设置不同的key来区分不同的线程池 10 | */ 11 | public interface IsolationThreadPool { 12 | 13 | /** 14 | * 返回ThreadPoolExecutor的实例 15 | * 16 | * @return ThreadPoolExecutor 17 | */ 18 | ExecutorService getExecutor(); 19 | 20 | Scheduler getScheduler(); 21 | 22 | Scheduler getScheduler(Func0 shouldInterruptThread); 23 | 24 | 25 | /** 26 | * 标记一个线程开始执行一个任务 27 | */ 28 | void markThreadExecution(); 29 | 30 | /** 31 | * 标记线程执行完一个任务 32 | */ 33 | void markThreadCompletion(); 34 | 35 | /** 36 | * 标记一个任务被线程池拒绝执行 37 | */ 38 | void markThreadRejection(); 39 | 40 | 41 | /** 42 | * 线程池工作队列是否已满 43 | * 44 | * @return 队列是否已满 45 | */ 46 | boolean isQueueSpaceAvailable(); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationThreadPoolFactory.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** 8 | * 线程池工厂,用于创建线程池,并保存创建过的线程池 9 | */ 10 | public class IsolationThreadPoolFactory { 11 | private static final Map THREAD_POOLS = new ConcurrentHashMap<>(); 12 | 13 | static IsolationThreadPool getInstance(String threadPoolKey, IsolationThreadPoolProperties.Builder propertiesBuilder) { 14 | 15 | // this should find it for all but the first time 16 | IsolationThreadPool previouslyCached = THREAD_POOLS.get(threadPoolKey); 17 | if (previouslyCached != null) { 18 | return previouslyCached; 19 | } 20 | 21 | // if we get here this is the first time so we need to initialize 22 | synchronized (IsolationThreadPoolFactory.class) { 23 | if (!THREAD_POOLS.containsKey(threadPoolKey)) { 24 | THREAD_POOLS.put(threadPoolKey, new DefaultIsolationThreadPool(threadPoolKey, propertiesBuilder)); 25 | } 26 | } 27 | return THREAD_POOLS.get(threadPoolKey); 28 | } 29 | 30 | static int getThreadPoolCount() { 31 | return THREAD_POOLS.size(); 32 | } 33 | 34 | static synchronized void shutdown() { 35 | for (IsolationThreadPool pool : THREAD_POOLS.values()) { 36 | pool.getExecutor().shutdown(); 37 | } 38 | THREAD_POOLS.clear(); 39 | } 40 | 41 | 42 | static synchronized void shutdown(long timeout, TimeUnit unit) { 43 | for (IsolationThreadPool pool : THREAD_POOLS.values()) { 44 | pool.getExecutor().shutdown(); 45 | } 46 | for (IsolationThreadPool pool : THREAD_POOLS.values()) { 47 | try { 48 | while (!pool.getExecutor().awaitTermination(timeout, unit)) { 49 | } 50 | } catch (InterruptedException e) { 51 | throw new RuntimeException("Interrupted while waiting for thread-pools to terminate. " + 52 | "Pools may not be correctly shutdown or cleared.", e); 53 | } 54 | } 55 | THREAD_POOLS.clear(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationThreadPoolMetrics.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | import java.util.Map; 4 | import java.util.concurrent.ConcurrentHashMap; 5 | import java.util.concurrent.ThreadPoolExecutor; 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | /** 9 | * 记录线程池度量数据 10 | */ 11 | public class IsolationThreadPoolMetrics { 12 | 13 | private static final Map METRICS = new ConcurrentHashMap<>(); 14 | 15 | static void reset() { 16 | METRICS.clear(); 17 | } 18 | 19 | private final String threadPoolKey; 20 | 21 | private final ThreadPoolExecutor threadPool; 22 | 23 | private final IsolationThreadPoolProperties properties; 24 | 25 | private final AtomicInteger concurrentExecutionCount = new AtomicInteger(); 26 | 27 | public IsolationThreadPoolMetrics(String threadPoolKey, ThreadPoolExecutor threadPool, IsolationThreadPoolProperties properties) { 28 | this.threadPoolKey = threadPoolKey; 29 | this.threadPool = threadPool; 30 | this.properties = properties; 31 | } 32 | 33 | public static IsolationThreadPoolMetrics getInstance(String key, ThreadPoolExecutor threadPool, IsolationThreadPoolProperties properties) { 34 | // attempt to retrieve from cache first 35 | IsolationThreadPoolMetrics threadPoolMetrics = METRICS.get(key); 36 | if (threadPoolMetrics != null) { 37 | return threadPoolMetrics; 38 | } else { 39 | synchronized (IsolationThreadPoolMetrics.class) { 40 | IsolationThreadPoolMetrics existingMetrics = METRICS.get(key); 41 | if (existingMetrics != null) { 42 | return existingMetrics; 43 | } else { 44 | IsolationThreadPoolMetrics newThreadPoolMetrics = new IsolationThreadPoolMetrics(key, threadPool, properties); 45 | METRICS.putIfAbsent(key, newThreadPoolMetrics); 46 | return newThreadPoolMetrics; 47 | } 48 | } 49 | } 50 | } 51 | 52 | public static IsolationThreadPoolMetrics getInstance(String key) { 53 | return METRICS.get(key); 54 | } 55 | 56 | public String getThreadPoolKey() { 57 | return threadPoolKey; 58 | } 59 | 60 | public ThreadPoolExecutor getThreadPool() { 61 | return threadPool; 62 | } 63 | 64 | public IsolationThreadPoolProperties getProperties() { 65 | return properties; 66 | } 67 | 68 | public AtomicInteger getConcurrentExecutionCount() { 69 | return concurrentExecutionCount; 70 | } 71 | 72 | public Number getCurrentActiveCount() { 73 | return threadPool.getActiveCount(); 74 | } 75 | 76 | public Number getCurrentCompletedTaskCount() { 77 | return threadPool.getCompletedTaskCount(); 78 | } 79 | 80 | public Number getCurrentCorePoolSize() { 81 | return threadPool.getCorePoolSize(); 82 | } 83 | 84 | public Number getCurrentLargestPoolSize() { 85 | return threadPool.getLargestPoolSize(); 86 | } 87 | 88 | public Number getCurrentMaximumPoolSize() { 89 | return threadPool.getMaximumPoolSize(); 90 | } 91 | 92 | public Number getCurrentPoolSize() { 93 | return threadPool.getPoolSize(); 94 | } 95 | 96 | public Number getCurrentTaskCount() { 97 | return threadPool.getTaskCount(); 98 | } 99 | 100 | public Number getCurrentQueueSize() { 101 | return threadPool.getQueue().size(); 102 | } 103 | 104 | public void markThreadExecution() { 105 | concurrentExecutionCount.incrementAndGet(); 106 | } 107 | 108 | public void markThreadCompletion() { 109 | concurrentExecutionCount.decrementAndGet(); 110 | } 111 | 112 | public void markThreadRejection() { 113 | concurrentExecutionCount.decrementAndGet(); 114 | } 115 | 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/IsolationThreadPoolProperties.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation; 2 | 3 | /** 4 | * 线程池参数配置 5 | */ 6 | public class IsolationThreadPoolProperties { 7 | static int default_coreSize = 10; // core size of thread pool 8 | static int default_maximumSize = 10; // maximum size of thread pool 9 | static int default_keepAliveTimeMinutes = 5; // minutes to keep a thread alive 10 | static int default_maxQueueSize = -1; //working queue size,default in infinity 11 | 12 | 13 | private final Integer coreSize; 14 | 15 | private final Integer maxSize; 16 | 17 | private final Integer keepAliveTimeInMinutes; 18 | 19 | private final Integer workingQueueSize; 20 | 21 | public IsolationThreadPoolProperties() { 22 | this(new Builder()); 23 | } 24 | 25 | public IsolationThreadPoolProperties(IsolationThreadPoolProperties.Builder builder) { 26 | coreSize = builder.coreSize == null ? default_coreSize : builder.coreSize; 27 | maxSize = builder.maxSize == null ? default_maximumSize : builder.maxSize; 28 | keepAliveTimeInMinutes = builder.keepAliveTimeInMinutes == null ? default_keepAliveTimeMinutes : builder.keepAliveTimeInMinutes; 29 | workingQueueSize = builder.workingQueueSize == null ? default_maxQueueSize : builder.workingQueueSize; 30 | } 31 | 32 | public Integer getCoreSize() { 33 | return coreSize; 34 | } 35 | 36 | public Integer getMaxSize() { 37 | return maxSize; 38 | } 39 | 40 | public Integer getKeepAliveTimeInMinutes() { 41 | return keepAliveTimeInMinutes; 42 | } 43 | 44 | public Integer getWorkingQueueSize() { 45 | return workingQueueSize; 46 | } 47 | 48 | public static class Builder { 49 | private Integer coreSize; 50 | 51 | private Integer maxSize; 52 | 53 | private Integer keepAliveTimeInMinutes; 54 | 55 | private Integer workingQueueSize; 56 | 57 | public Builder() {} 58 | 59 | public Integer getCoreSize() { 60 | return coreSize; 61 | } 62 | 63 | public Integer getMaximumSize() { 64 | return maxSize; 65 | } 66 | 67 | public Integer getKeepAliveTimeMinutes() { 68 | return keepAliveTimeInMinutes; 69 | } 70 | 71 | public Integer getMaxQueueSize() { 72 | return workingQueueSize; 73 | } 74 | 75 | public Builder withCoreSize(int value) { 76 | this.coreSize = value; 77 | return this; 78 | } 79 | 80 | public Builder withMaximumSize(int value) { 81 | this.maxSize = value; 82 | return this; 83 | } 84 | 85 | public Builder withKeepAliveTimeMinutes(int value) { 86 | this.keepAliveTimeInMinutes = value; 87 | return this; 88 | } 89 | 90 | public Builder withMaxQueueSize(int value) { 91 | this.workingQueueSize = value; 92 | return this; 93 | } 94 | 95 | public IsolationThreadPoolProperties build() { 96 | return new IsolationThreadPoolProperties(this); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/exception/IsolationRuntimeException.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation.exception; 2 | 3 | import come.ezlippi.isolation.Invokable; 4 | import come.ezlippi.isolation.IsolationCommand; 5 | 6 | public class IsolationRuntimeException extends RuntimeException { 7 | private static final long serialVersionUID = 5219160332476046229L; 8 | 9 | private final Class commandClass; 10 | private final Throwable fallbackException; 11 | 12 | 13 | public IsolationRuntimeException(Class commandClass, String message, Exception cause, Throwable fallbackException) { 14 | super(message, cause); 15 | this.commandClass = commandClass; 16 | this.fallbackException = fallbackException; 17 | } 18 | 19 | public IsolationRuntimeException(Class commandClass, String message, Throwable cause, Throwable fallbackException) { 20 | super(message, cause); 21 | this.commandClass = commandClass; 22 | this.fallbackException = fallbackException; 23 | } 24 | 25 | /** 26 | * The implementing class of the {@link IsolationCommand}. 27 | * 28 | * @return {@code Class } 29 | */ 30 | public Class getImplementingClass() { 31 | return commandClass; 32 | } 33 | 34 | /** 35 | * The {@link Throwable} that was thrown when trying to retrieve a fallback. 36 | * 37 | * @return {@link Throwable} 38 | */ 39 | public Throwable getFallbackException() { 40 | return fallbackException; 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/execution/IsolationScheduler.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation.execution; 2 | 3 | import come.ezlippi.isolation.IsolationThreadPool; 4 | import rx.Scheduler; 5 | import rx.Subscription; 6 | import rx.functions.Action0; 7 | import rx.functions.Func0; 8 | import rx.internal.schedulers.ScheduledAction; 9 | import rx.subscriptions.CompositeSubscription; 10 | import rx.subscriptions.Subscriptions; 11 | 12 | import java.util.concurrent.FutureTask; 13 | import java.util.concurrent.RejectedExecutionException; 14 | import java.util.concurrent.ThreadPoolExecutor; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | /** 18 | * 任务调度 19 | */ 20 | public class IsolationScheduler extends Scheduler { 21 | private final Scheduler actualScheduler; 22 | private final IsolationThreadPool threadPool; 23 | 24 | public IsolationScheduler(Scheduler scheduler) { 25 | this.actualScheduler = scheduler; 26 | this.threadPool = null; 27 | } 28 | 29 | public IsolationScheduler(IsolationThreadPool threadPool) { 30 | this(threadPool, new Func0() { 31 | @Override 32 | public Boolean call() { 33 | return true; 34 | } 35 | }); 36 | } 37 | 38 | public IsolationScheduler(IsolationThreadPool threadPool, Func0 shouldInterruptThread) { 39 | this.threadPool = threadPool; 40 | this.actualScheduler = new ThreadPoolScheduler(threadPool, shouldInterruptThread); 41 | } 42 | 43 | @Override 44 | public Worker createWorker() { 45 | return new IsolationSchedulerWorker(actualScheduler.createWorker()); 46 | } 47 | 48 | private class IsolationSchedulerWorker extends Worker { 49 | 50 | private final Worker worker; 51 | 52 | private IsolationSchedulerWorker(Worker actualWorker) { 53 | this.worker = actualWorker; 54 | } 55 | 56 | @Override 57 | public void unsubscribe() { 58 | worker.unsubscribe(); 59 | } 60 | 61 | @Override 62 | public boolean isUnsubscribed() { 63 | return worker.isUnsubscribed(); 64 | } 65 | 66 | @Override 67 | public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { 68 | if (threadPool != null) { 69 | if (!threadPool.isQueueSpaceAvailable()) { 70 | throw new RejectedExecutionException("Rejected command because thread-pool queueSize is at rejection threshold."); 71 | } 72 | } 73 | return worker.schedule(new Action0() { 74 | @Override 75 | public void call() { 76 | action.call(); 77 | } 78 | }, delayTime, unit); 79 | } 80 | 81 | @Override 82 | public Subscription schedule(Action0 action) { 83 | if (threadPool != null) { 84 | if (!threadPool.isQueueSpaceAvailable()) { 85 | throw new RejectedExecutionException("Rejected command because thread-pool queueSize is at rejection threshold."); 86 | } 87 | } 88 | return worker.schedule(new Action0() { 89 | @Override 90 | public void call() { 91 | action.call(); 92 | } 93 | }); 94 | } 95 | 96 | } 97 | 98 | private static class ThreadPoolScheduler extends Scheduler { 99 | 100 | private final IsolationThreadPool threadPool; 101 | private final Func0 shouldInterruptThread; 102 | 103 | public ThreadPoolScheduler(IsolationThreadPool threadPool, Func0 shouldInterruptThread) { 104 | this.threadPool = threadPool; 105 | this.shouldInterruptThread = shouldInterruptThread; 106 | } 107 | 108 | @Override 109 | public Worker createWorker() { 110 | return new ThreadPoolWorker(threadPool, shouldInterruptThread); 111 | } 112 | 113 | } 114 | 115 | /** 116 | * Purely for scheduling work on a thread-pool. 117 | *

118 | * This is not natively supported by RxJava as of 0.18.0 because thread-pools 119 | * are contrary to sequential execution. 120 | *

121 | * For the Hystrix case, each Command invocation has a single action so the concurrency 122 | * issue is not a problem. 123 | */ 124 | private static class ThreadPoolWorker extends Worker { 125 | 126 | private final IsolationThreadPool threadPool; 127 | private final CompositeSubscription subscription = new CompositeSubscription(); 128 | private final Func0 shouldInterruptThread; 129 | 130 | public ThreadPoolWorker(IsolationThreadPool threadPool, Func0 shouldInterruptThread) { 131 | this.threadPool = threadPool; 132 | this.shouldInterruptThread = shouldInterruptThread; 133 | } 134 | 135 | @Override 136 | public void unsubscribe() { 137 | subscription.unsubscribe(); 138 | } 139 | 140 | @Override 141 | public boolean isUnsubscribed() { 142 | return subscription.isUnsubscribed(); 143 | } 144 | 145 | @Override 146 | public Subscription schedule(final Action0 action) { 147 | if (subscription.isUnsubscribed()) { 148 | // don't schedule, we are unsubscribed 149 | return Subscriptions.unsubscribed(); 150 | } 151 | 152 | ScheduledAction sa = new ScheduledAction(action); 153 | 154 | // 155 | subscription.add(sa); 156 | sa.addParent(subscription); 157 | 158 | ThreadPoolExecutor executor = (ThreadPoolExecutor) threadPool.getExecutor(); 159 | FutureTask f = (FutureTask) executor.submit(sa); 160 | sa.add(new FutureCompleterWithConfigurableInterrupt(f, shouldInterruptThread, executor)); 161 | 162 | return sa; 163 | } 164 | 165 | @Override 166 | public Subscription schedule(Action0 action, long delayTime, TimeUnit unit) { 167 | throw new IllegalStateException("Hystrix does not support delayed scheduling"); 168 | } 169 | } 170 | 171 | /** 172 | * Very similar to rx.internal.schedulers.ScheduledAction.FutureCompleter, but with configurable interrupt behavior 173 | */ 174 | private static class FutureCompleterWithConfigurableInterrupt implements Subscription { 175 | private final FutureTask f; 176 | private final Func0 shouldInterruptThread; 177 | private final ThreadPoolExecutor executor; 178 | 179 | private FutureCompleterWithConfigurableInterrupt(FutureTask f, Func0 shouldInterruptThread, ThreadPoolExecutor executor) { 180 | this.f = f; 181 | this.shouldInterruptThread = shouldInterruptThread; 182 | this.executor = executor; 183 | } 184 | 185 | @Override 186 | public void unsubscribe() { 187 | executor.remove(f); 188 | if (shouldInterruptThread.call()) { 189 | f.cancel(true); 190 | } else { 191 | f.cancel(false); 192 | } 193 | } 194 | 195 | @Override 196 | public boolean isUnsubscribed() { 197 | return f.isCancelled(); 198 | } 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/hook/CommandExecutionHook.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation.hook; 2 | 3 | import come.ezlippi.isolation.Invokable; 4 | import come.ezlippi.isolation.IsolationCommand; 5 | 6 | /** 7 | * 管理命令执行的生命周期 8 | */ 9 | public abstract class CommandExecutionHook { 10 | /** 11 | * Invoked before {@link Invokable} begins executing. 12 | * 13 | * @param commandInstance The executing Invokable instance. 14 | * 15 | */ 16 | public void onStart(Invokable commandInstance) { 17 | //do nothing by default 18 | } 19 | 20 | /** 21 | * Invoked when {@link Invokable} emits a value. 22 | * 23 | * @param commandInstance The executing Invokable instance. 24 | * @param value value emitted 25 | * 26 | */ 27 | public T onEmit(Invokable commandInstance, T value) { 28 | return value; //by default, just pass through 29 | } 30 | 31 | /** 32 | * Invoked when {@link Invokable} fails with an Exception. 33 | * 34 | * @param commandInstance The executing Invokable instance. 35 | * @param e exception object 36 | * 37 | */ 38 | public Exception onError(Invokable commandInstance, Exception e) { 39 | return e; //by default, just pass through 40 | } 41 | 42 | /** 43 | * Invoked when {@link Invokable} finishes a successful execution. 44 | * 45 | * @param commandInstance The executing Invokable instance. 46 | * 47 | */ 48 | public void onSuccess(Invokable commandInstance) { 49 | //do nothing by default 50 | } 51 | 52 | /** 53 | * Invoked at start of thread execution when {@link IsolationCommand} is executed . 54 | * 55 | * @param commandInstance The executing IsolationCommand instance. 56 | * 57 | */ 58 | public void onThreadStart(Invokable commandInstance) { 59 | //do nothing by default 60 | } 61 | 62 | /** 63 | * Invoked at completion of thread execution when {@link IsolationCommand} is executed . 64 | * This will get invoked whenever the Hystrix thread is done executing, regardless of whether the thread finished 65 | * naturally, or was unsubscribed externally 66 | * 67 | * @param commandInstance The executing IsolationCommand instance. 68 | * 69 | */ 70 | public void onThreadComplete(Invokable commandInstance) { 71 | // do nothing by default 72 | } 73 | 74 | /** 75 | * Invoked when the user-defined execution method in {@link Invokable} starts. 76 | * 77 | * @param commandInstance The executing Invokable instance. 78 | * 79 | */ 80 | public void onExecutionStart(Invokable commandInstance) { 81 | //do nothing by default 82 | } 83 | 84 | /** 85 | * Invoked when the user-defined execution method in {@link Invokable} emits a value. 86 | * 87 | * @param commandInstance The executing Invokable instance. 88 | * @param value value emitted 89 | * 90 | */ 91 | public T onExecutionEmit(Invokable commandInstance, T value) { 92 | return value; //by default, just pass through 93 | } 94 | 95 | /** 96 | * Invoked when the user-defined execution method in {@link Invokable} fails with an Exception. 97 | * 98 | * @param commandInstance The executing Invokable instance. 99 | * @param e exception object 100 | * 101 | */ 102 | public Exception onExecutionError(Invokable commandInstance, Exception e) { 103 | return e; //by default, just pass through 104 | } 105 | 106 | /** 107 | * Invoked when the user-defined execution method in {@link Invokable} completes successfully. 108 | * 109 | * @param commandInstance The executing Invokable instance. 110 | * 111 | */ 112 | public void onExecutionSuccess(Invokable commandInstance) { 113 | //do nothing by default 114 | } 115 | 116 | 117 | /** 118 | * Invoked with the command is unsubscribed before a terminal state 119 | * 120 | * @param commandInstance The executing Invokable instance. 121 | * 122 | */ 123 | public void onUnsubscribe(Invokable commandInstance) { 124 | //do nothing by default 125 | } 126 | 127 | 128 | } 129 | 130 | -------------------------------------------------------------------------------- /src/main/java/come/ezlippi/isolation/timer/IsolationTimer.java: -------------------------------------------------------------------------------- 1 | package come.ezlippi.isolation.timer; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.lang.ref.Reference; 7 | import java.lang.ref.SoftReference; 8 | import java.util.concurrent.ScheduledFuture; 9 | import java.util.concurrent.ScheduledThreadPoolExecutor; 10 | import java.util.concurrent.ThreadFactory; 11 | import java.util.concurrent.TimeUnit; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | import java.util.concurrent.atomic.AtomicReference; 14 | 15 | /** 16 | * 定时器 17 | */ 18 | public class IsolationTimer { 19 | private static final Logger logger = LoggerFactory.getLogger(IsolationTimer.class); 20 | 21 | private static IsolationTimer INSTANCE = new IsolationTimer(); 22 | 23 | private IsolationTimer() { 24 | } 25 | 26 | 27 | public static IsolationTimer getInstance() { 28 | return INSTANCE; 29 | } 30 | 31 | /** 32 | * Clears all listeners. 33 | */ 34 | public static void reset() { 35 | ScheduledExecutor ex = INSTANCE.executor.getAndSet(null); 36 | if (ex != null && ex.getThreadPool() != null) { 37 | ex.getThreadPool().shutdownNow(); 38 | } 39 | } 40 | 41 | AtomicReference executor = new AtomicReference(); 42 | 43 | /** 44 | * Add a {@link TimerListener} that will be executed until it is garbage collected or removed by clearing the returned {@link Reference}. 45 | *

46 | * NOTE: It is the responsibility of code that adds a listener via this method to clear this listener when completed. 47 | *

48 | *

49 | *

50 | *

 {@code
 51 |      * // add a TimerListener
 52 |      * Reference listener = IsolationTimer.getInstance().addTimerListener(listenerImpl);
 53 |      *
 54 |      * // sometime later, often in a thread shutdown, request cleanup, servlet filter or something similar the listener must be shutdown via the clear() method
 55 |      * listener.clear();
 56 |      * }
57 | *
58 | * 59 | * @param listener TimerListener implementation that will be triggered according to its getIntervalTimeInMilliseconds() method implementation. 60 | * @return reference to the TimerListener that allows cleanup via the clear() method 61 | */ 62 | public Reference addTimerListener(final TimerListener listener) { 63 | startThreadIfNeeded(); 64 | // add the listener 65 | 66 | Runnable r = new Runnable() { 67 | 68 | @Override 69 | public void run() { 70 | try { 71 | listener.tick(); 72 | } catch (Exception e) { 73 | logger.error("Failed while ticking TimerListener", e); 74 | } 75 | } 76 | }; 77 | 78 | ScheduledFuture f = executor.get().getThreadPool().scheduleAtFixedRate(r, listener.getIntervalTimeInMilliseconds(), listener.getIntervalTimeInMilliseconds(), TimeUnit.MILLISECONDS); 79 | return new TimerReference(listener, f); 80 | } 81 | 82 | private static class TimerReference extends SoftReference { 83 | 84 | private final ScheduledFuture f; 85 | 86 | TimerReference(TimerListener referent, ScheduledFuture f) { 87 | super(referent); 88 | this.f = f; 89 | } 90 | 91 | @Override 92 | public void clear() { 93 | super.clear(); 94 | // stop this ScheduledFuture from any further executions 95 | f.cancel(false); 96 | } 97 | 98 | } 99 | 100 | /** 101 | * Since we allow resetting the timer (shutting down the thread) we need to lazily re-start it if it starts being used again. 102 | *

103 | * This does the lazy initialization and start of the thread in a thread-safe manner while having little cost the rest of the time. 104 | */ 105 | protected void startThreadIfNeeded() { 106 | // create and start thread if one doesn't exist 107 | while (executor.get() == null || !executor.get().isInitialized()) { 108 | if (executor.compareAndSet(null, new ScheduledExecutor())) { 109 | // initialize the executor that we 'won' setting 110 | executor.get().initialize(); 111 | } 112 | } 113 | } 114 | 115 | static class ScheduledExecutor { 116 | volatile ScheduledThreadPoolExecutor executor; 117 | private volatile boolean initialized; 118 | 119 | /** 120 | * We want this only done once when created in compareAndSet so use an initialize method 121 | */ 122 | public void initialize() { 123 | 124 | 125 | ThreadFactory threadFactory = new ThreadFactory() { 126 | final AtomicInteger counter = new AtomicInteger(); 127 | @Override 128 | public Thread newThread(Runnable r) { 129 | Thread thread = new Thread(r, "IsolationTimer-" + counter.incrementAndGet()); 130 | thread.setDaemon(true); 131 | return thread; 132 | } 133 | 134 | }; 135 | 136 | executor = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors(), threadFactory); 137 | initialized = true; 138 | } 139 | 140 | public ScheduledThreadPoolExecutor getThreadPool() { 141 | return executor; 142 | } 143 | 144 | public boolean isInitialized() { 145 | return initialized; 146 | } 147 | } 148 | 149 | public static interface TimerListener { 150 | 151 | /** 152 | * The 'tick' is called each time the interval occurs. 153 | *

154 | * This method should NOT block or do any work but instead fire its work asynchronously to perform on another thread otherwise it will prevent the Timer from functioning. 155 | *

156 | * This contract is used to keep this implementation single-threaded and simplistic. 157 | *

158 | * If you need a ThreadLocal set, you can store the state in the TimerListener, then when tick() is called, set the ThreadLocal to your desired value. 159 | */ 160 | public void tick(); 161 | 162 | /** 163 | * How often this TimerListener should 'tick' defined in milliseconds. 164 | */ 165 | public int getIntervalTimeInMilliseconds(); 166 | } 167 | 168 | } 169 | 170 | -------------------------------------------------------------------------------- /src/test/java/com/ezlippi/isolation/CommandTest.java: -------------------------------------------------------------------------------- 1 | package com.ezlippi.isolation; 2 | 3 | import come.ezlippi.isolation.Invokable; 4 | import come.ezlippi.isolation.IsolationCommand; 5 | import come.ezlippi.isolation.IsolationCommandProperties; 6 | import come.ezlippi.isolation.IsolationThreadPoolProperties; 7 | import come.ezlippi.isolation.hook.CommandExecutionHook; 8 | import org.junit.Test; 9 | 10 | public class CommandTest { 11 | 12 | @Test 13 | public void testIsolationThreadPool() { 14 | IsolationCommandProperties.Builder builder1 = new IsolationCommandProperties.Builder() 15 | .withExecutionTimeoutEnabled(true) 16 | .withExecutionTimeoutInMilliseconds(1000) 17 | .withExecutionIsolationThreadInterruptOnTimeout(true); 18 | 19 | IsolationThreadPoolProperties.Builder threadPoolBuilder1 = new IsolationThreadPoolProperties.Builder() 20 | .withCoreSize(1) 21 | .withMaximumSize(1) 22 | .withKeepAliveTimeMinutes(2) 23 | .withMaxQueueSize(10); 24 | 25 | IsolationCommand command1 = new IsolationCommand("command1", "pool1", 26 | builder1, threadPoolBuilder1, new DefaultExecutionHook()) { 27 | @Override 28 | protected String run() throws Exception { 29 | return "hello"; 30 | } 31 | }; 32 | IsolationCommand command2 = new IsolationCommand("command2", "pool2", 33 | builder1, threadPoolBuilder1, new DefaultExecutionHook()) { 34 | @Override 35 | protected String run() throws Exception { 36 | return "isolation"; 37 | } 38 | }; 39 | command1.execute(); 40 | command2.execute(); 41 | } 42 | 43 | static class DefaultExecutionHook extends CommandExecutionHook { 44 | @Override 45 | public void onStart(Invokable commandInstance) { 46 | } 47 | 48 | @Override 49 | public T onEmit(Invokable commandInstance, T value) { 50 | return value; 51 | } 52 | 53 | @Override 54 | public Exception onError(Invokable commandInstance, Exception e) { 55 | return e; 56 | } 57 | 58 | @Override 59 | public void onSuccess(Invokable commandInstance) { 60 | } 61 | 62 | @Override 63 | public void onThreadStart(Invokable commandInstance) { 64 | } 65 | 66 | @Override 67 | public void onThreadComplete(Invokable commandInstance) { 68 | } 69 | 70 | @Override 71 | public void onExecutionStart(Invokable command) { 72 | } 73 | 74 | @Override 75 | public T onExecutionEmit(Invokable command, T value) { 76 | System.out.println(command.getCommandKey() + " execution on " + command.getExecutionThread().getName()); 77 | return value; 78 | } 79 | 80 | @Override 81 | public Exception onExecutionError(Invokable commandInstance, Exception e) { 82 | return e; 83 | } 84 | 85 | @Override 86 | public void onExecutionSuccess(Invokable commandInstance) { 87 | } 88 | 89 | @Override 90 | public void onUnsubscribe(Invokable commandInstance) { 91 | } 92 | } 93 | } 94 | --------------------------------------------------------------------------------