├── .gitignore ├── .travis.yml ├── checkstyle.xml ├── license.md ├── pom.xml ├── readme.md └── src ├── main └── java │ └── com │ └── github │ └── egetman │ ├── BalancingSubscriber.java │ ├── ColdPublisher.java │ ├── barrier │ ├── Barrier.java │ └── OpenBarrier.java │ ├── etc │ ├── AbstractPool.java │ ├── BlockingPool.java │ ├── BoundedBlockingPool.java │ ├── CustomizableThreadFactory.java │ ├── Pool.java │ ├── PoolFactory.java │ └── package-info.java │ ├── package-info.java │ └── source │ ├── CloseableIterator.java │ ├── JmsQuota.java │ ├── JmsUnit.java │ ├── Source.java │ ├── UnicastJmsQueueSource.java │ └── package-info.java └── test ├── java └── com │ └── github │ └── egetman │ ├── BalancingSubscriberBlackBoxTest.java │ ├── BalancingSubscriberWhiteBoxTest.java │ ├── UnicastJmsQueueAutoAcknowledgePublisherTest.java │ ├── UnicastJmsQueueClientAcknowledgePublisherTest.java │ └── UnicastJmsQueueColdPublisherTest.java └── resources └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Java template 3 | /target 4 | ### JetBrains template 5 | .idea 6 | *.iml 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | after_success: 3 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 86 | 87 | 88 | 89 | 91 | 92 | 93 | 94 | 95 | 96 | 98 | 99 | 100 | 101 | 103 | 104 | 105 | 106 | 108 | 109 | 110 | 111 | 113 | 115 | 117 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # Apache License 2 | **Version 2.0, January 2004** 3 | 4 | [**http://www.apache.org/licenses/**](http://www.apache.org/licenses/) 5 | 6 | ## TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | #### 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 16 | 17 | "You" (or "Your") shall mean an individual or Legal Entity 18 | exercising permissions granted by this License. 19 | 20 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 21 | 22 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 23 | 24 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 25 | 26 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 27 | 28 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 29 | 30 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 31 | 32 | #### 2. Grant of Copyright License. 33 | 34 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 35 | 36 | #### 3. Grant of Patent License. 37 | 38 | Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 39 | 40 | #### 4. Redistribution. 41 | 42 | You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 43 | 44 | **(a)** You must give any other recipients of the Work or Derivative Works a copy of this License; and 45 | 46 | **(b)** You must cause any modified files to carry prominent notices stating that You changed the files; and 47 | 48 | **(c)** You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 49 | 50 | **(d)** If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 51 | 52 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 53 | 54 | #### 5. Submission of Contributions. 55 | 56 | Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 57 | 58 | #### 6. Trademarks. 59 | 60 | This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 61 | 62 | #### 7. Disclaimer of Warranty. 63 | 64 | Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 65 | 66 | #### 8. Limitation of Liability. 67 | 68 | In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 69 | 70 | #### 9. Accepting Warranty or Additional Liability. 71 | 72 | While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 73 | 74 | ***END OF TERMS AND CONDITIONS*** 75 | * * * 76 | 77 | #### APPENDIX: How to apply the Apache License to your work. 78 | 79 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 80 | 81 |
82 | Copyright [yyyy] [name of copyright owner]
83 | 
84 | Licensed under the Apache License, Version 2.0 (the "License");
85 | you may not use this file except in compliance with the License.
86 | You may obtain a copy of the License at
87 | 
88 |     http://www.apache.org/licenses/LICENSE-2.0
89 | 
90 | Unless required by applicable law or agreed to in writing, software
91 | distributed under the License is distributed on an "AS IS" BASIS,
92 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
93 | See the License for the specific language governing permissions and
94 | limitations under the License.
95 | 
96 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.egetman 8 | reactive-jms-wrapper 9 | 0.8 10 | 11 | Reactive JMS wrapper 12 | 13 | 14 | 15 | 1.16.16 16 | 1.7.25 17 | 3.0.2 18 | 1.2.3 19 | 2.0.1 20 | 1.0.2 21 | 22 | 23 | 5.15.0 24 | 25 | 26 | 3.0.1 27 | 0.7.9 28 | 3.0.1 29 | 3.7.0 30 | 2.17 31 | 32 | 33 | 1.8 34 | 1.8 35 | 1.8 36 | UTF-8 37 | UTF-8 38 | 39 | 40 | 41 | 42 | 43 | org.slf4j 44 | slf4j-api 45 | ${slf4j.version} 46 | 47 | 48 | 49 | ch.qos.logback 50 | logback-core 51 | ${logback.version} 52 | 53 | 54 | 55 | ch.qos.logback 56 | logback-classic 57 | ${logback.version} 58 | 59 | 60 | 61 | org.projectlombok 62 | lombok 63 | ${lombok.version} 64 | provided 65 | 66 | 67 | 68 | javax.jms 69 | javax.jms-api 70 | ${javax.jms-api.version} 71 | 72 | 73 | 74 | com.google.code.findbugs 75 | jsr305 76 | ${jsr305.version} 77 | 78 | 79 | 80 | org.reactivestreams 81 | reactive-streams 82 | ${reactive-streams.version} 83 | 84 | 85 | 86 | 87 | org.reactivestreams 88 | reactive-streams-tck 89 | ${reactive-streams.version} 90 | test 91 | 92 | 93 | 94 | org.apache.activemq 95 | activemq-broker 96 | ${activemq-version} 97 | test 98 | 99 | 100 | 101 | 102 | src/main/java 103 | src/test/java 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-compiler-plugin 109 | ${maven.plugin.compiler.version} 110 | 111 | UTF-8 112 | 8 113 | 8 114 | 115 | 116 | 117 | 118 | org.jacoco 119 | jacoco-maven-plugin 120 | ${jacoco-maven-plugin.version} 121 | 122 | 123 | 124 | prepare-agent 125 | 126 | 127 | 128 | report 129 | test 130 | 131 | report 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.apache.maven.plugins 139 | maven-checkstyle-plugin 140 | ${maven-checkstyle-plugin.version} 141 | 142 | 143 | validate 144 | validate 145 | 146 | check 147 | 148 | 149 | checkstyle.xml 150 | UTF-8 151 | true 152 | true 153 | warning 154 | 155 | 156 | 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-source-plugin 162 | ${maven-source-plugin.version} 163 | 164 | 165 | attach-sources 166 | 167 | jar 168 | test-jar 169 | 170 | package 171 | 172 | 173 | 174 | 175 | 176 | org.apache.maven.plugins 177 | maven-javadoc-plugin 178 | ${maven-javadoc-plugin.version} 179 | 180 | 181 | attach-javadocs 182 | 183 | jar 184 | 185 | package 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![hex.pm](https://img.shields.io/hexpm/l/plug.svg) 2 | ![version](https://img.shields.io/badge/version-0.8-blue.svg) 3 | [![build status](https://travis-ci.org/egetman/reactive-jms.svg?branch=master)](https://travis-ci.org/egetman/reactive-jms) 4 | ![code coverage](https://codecov.io/gh/egetman/reactive-jms/branch/master/graph/badge.svg) 5 | # Reactive JMS Publisher (wrapper) 6 | 7 | Reactive JMS publisher is a simple reactive wrapper for JMS API. 8 | 9 | In terms of reactive streams, it is a **Cold** Publisher. i.e. no data will be lost (via unhandled emitting). Emitting 10 | begins right after the client demand's some data. 11 | 12 | JMS publisher or rather its [Source](/src/main/java/com/github/egetman/source/UnicastJmsQueueSource.java) is unicast by its nature. If multiple clients connect to the same **JMS queue**, each one will receive unique messages. (The 13 | same logic as with common interaction with JMS queue through JMS API). 14 | 15 | It's tested with [reactive-streams-jvm](https://github.com/reactive-streams/reactive-streams-jvm) tck, 16 | and verified with **amq** & **wmq** brokers. 17 | 18 | 19 | ## When to use 20 | 21 | If you have some components in your app, that use [reactive](https://github.com/reactive-streams/reactive-streams-jvm/tree/master/api/src/main/java/org/reactivestreams) 22 | interfaces, it's a good choice to use one more for easy integration =) 23 | 24 | If you want to make some manual/dynamic throughput control, you can check the use cases of 25 | [Barrier](/src/main/java/com/github/egetman/barrier/Barrier.java) abstraction. 26 | 27 | ## How to use 28 | 29 | Creation of JMS Publisher as simple as 30 | 31 | ```java 32 | final ConnectionFactory factory = ... 33 | final Function messageToString = message -> { 34 | try { 35 | return ((TextMessage) message).getText(); 36 | } catch (Exception e) { 37 | throw new IllegalStateException(e); 38 | } 39 | }; 40 | Publsiher jmsPublisher = new ColdPublisher<>(new UnicastJmsQueueSource<>(factory, messageToString, "MY_COOL_QUEUE")); 41 | ``` 42 | 43 | Publisher acceps `Source` instance as data & access provider. 44 | ```java 45 | public ColdPublisher(@Nonnull Source source) { 46 | this.source = Objects.requireNonNull(source, "Source must not be null"); 47 | } 48 | ``` 49 | You can implement your own source types, it's quite easy: 50 | ```java 51 | public interface Source { 52 | CloseableIterator iterator(int key); 53 | } 54 | ``` 55 | Jms source has 2 constructors, that accepts following params: 56 | ```java 57 | public UnicastJmsQueueSource(@Nonnull ConnectionFactory factory, @Nonnull Function function, 58 | @Nonnull String queue) { 59 | ... 60 | } 61 | 62 | public UnicastJmsQueueSource(@Nonnull ConnectionFactory factory, @Nonnull Function function, 63 | @Nonnull String queue, String user, String password, boolean transacted, int acknowledgeMode) { 64 | ... 65 | } 66 | ``` 67 | 68 | You can use whatever `Subscriber` you want with `ColdPublisher`. 69 | There is one build in: `BalancingSubscriber`. 70 | 71 | The main idea is you never ask the given `Subscription` for an unbounded sequence of elements (usually through `Long.MAX_VALUE`). 72 | Instead, you say how much elements you want to process for a concrete time interval. In a case when the application throughput rises too high, you can obtain additional control through `Barrier`. 73 | 74 | The simplest way to create a subscriber: 75 | ```java 76 | Subscriber subscriber = new BalancingSubscriber(System.out::println); 77 | ``` 78 | Additionally, **BalancingSubscriber** has several overloaded constructors: 79 | ```java 80 | public BalancingSubscriber(@Nonnull Consumer onNext) { 81 | ... 82 | } 83 | 84 | public BalancingSubscriber(@Nonnull Consumer onNext, @Nonnull Barrier barrier) { 85 | ... 86 | } 87 | 88 | public BalancingSubscriber(@Nonnull Consumer onNext, @Nonnull Barrier barrier, int batchSize, int pollInterval) { 89 | ... 90 | } 91 | 92 | public BalancingSubscriber(@Nonnull Consumer onNext, @Nullable Consumer onError, 93 | @Nullable Runnable onComplete, @Nonnull Barrier barrier, int batchSize, int pollInterval) { 94 | ... 95 | } 96 | ``` 97 | 98 | **Please feel free to send a pr =)** -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/BalancingSubscriber.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.Objects; 5 | import java.util.concurrent.ScheduledExecutorService; 6 | import java.util.concurrent.ThreadFactory; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.atomic.AtomicBoolean; 9 | import java.util.concurrent.atomic.AtomicLong; 10 | import java.util.function.Consumer; 11 | import javax.annotation.Nonnull; 12 | import javax.annotation.Nullable; 13 | 14 | import com.github.egetman.barrier.Barrier; 15 | import com.github.egetman.barrier.OpenBarrier; 16 | import com.github.egetman.etc.CustomizableThreadFactory; 17 | 18 | import org.reactivestreams.Subscriber; 19 | import org.reactivestreams.Subscription; 20 | 21 | import lombok.extern.slf4j.Slf4j; 22 | 23 | import static java.util.concurrent.Executors.newScheduledThreadPool; 24 | 25 | /** 26 | * {@link BalancingSubscriber} is {@link Subscriber} implementation with dynamic throughput. 27 | * The main idea of {@link BalancingSubscriber} is you never ask the given {@literal Subscription} for an unbounded 28 | * sequence of elements (usually through {@literal Long.MAX_VALUE}). 29 | * Instead, you say how much elements you want to process for a concrete time interval. {@link BalancingSubscriber} 30 | * will demand {@literal NOT MORE} elements from it's subscription for that time. 31 | * In a case when the application throughput rises too high, you can obtain additional control through {@link Barrier}. 32 | * 33 | *

The simplest way to create a subscriber: 34 | * {@code 35 | * Subscriber subscriber = new BalancingSubscriber(System.out::println); 36 | * } 37 | * 38 | * @param type of elements, that {@link BalancingSubscriber} handle. 39 | */ 40 | @Slf4j 41 | public class BalancingSubscriber implements Subscriber, AutoCloseable { 42 | 43 | private static final int BATCH_SIZE = 10; 44 | private static final int POLL_INTERVAL = 3000; 45 | 46 | private final int batchSize; 47 | private final int pollInterval; 48 | private final Barrier barrier; 49 | private final Consumer onNext; 50 | private final Runnable onComplete; 51 | private final Consumer onError; 52 | 53 | private Subscription subscription; 54 | private final AtomicLong consumed = new AtomicLong(); 55 | private final AtomicBoolean completed = new AtomicBoolean(); 56 | private final ThreadFactory threadFactory = new CustomizableThreadFactory("bs-worker", true); 57 | private final ScheduledExecutorService executor = newScheduledThreadPool(1, threadFactory); 58 | 59 | public BalancingSubscriber(@Nonnull Consumer onNext) { 60 | this(onNext, null, null, new OpenBarrier(), BATCH_SIZE, POLL_INTERVAL); 61 | } 62 | 63 | public BalancingSubscriber(@Nonnull Consumer onNext, @Nonnull Barrier barrier) { 64 | this(onNext, null, null, barrier, BATCH_SIZE, POLL_INTERVAL); 65 | } 66 | 67 | public BalancingSubscriber(@Nonnull Consumer onNext, @Nonnull Barrier barrier, int batchSize, int pollInterval) { 68 | this(onNext, null, null, barrier, batchSize, pollInterval); 69 | } 70 | 71 | public BalancingSubscriber(@Nonnull Consumer onNext, @Nullable Consumer onError, 72 | @Nullable Runnable onComplete, @Nonnull Barrier barrier, int batchSize, 73 | int pollInterval) { 74 | 75 | this.onNext = Objects.requireNonNull(onNext, "OnNext action must not be null"); 76 | this.onError = onError; 77 | this.onComplete = onComplete; 78 | 79 | this.barrier = Objects.requireNonNull(barrier, "Barrier must not be null"); 80 | this.batchSize = batchSize; 81 | this.pollInterval = pollInterval; 82 | log.debug("{}: initialized with {} batch size and {} barrier", this, batchSize, barrier); 83 | } 84 | 85 | @Override 86 | public void onSubscribe(@Nullable Subscription subscription) { 87 | Objects.requireNonNull(subscription, "Subscription must not be null"); 88 | if (this.subscription != null) { 89 | log.debug("{}: already subscribed: cancelling new subscription {}", this, subscription); 90 | subscription.cancel(); 91 | } else { 92 | log.debug("{}: subscribed with {}", this, subscription); 93 | this.subscription = subscription; 94 | if (barrier.isOpen() && !completed.get()) { 95 | this.subscription.request(batchSize); 96 | } else { 97 | log.debug("{}: can't request any more elements", this); 98 | } 99 | executor.scheduleAtFixedRate(this::tryToRequest, 0, pollInterval, TimeUnit.MILLISECONDS); 100 | } 101 | } 102 | 103 | @Override 104 | public void onNext(@Nullable T next) { 105 | log.debug("{}: received element: {}", this, next); 106 | Objects.requireNonNull(next, "Provided element must not be null"); 107 | 108 | onNext.accept(next); 109 | consumed.incrementAndGet(); 110 | } 111 | 112 | @Override 113 | public void onError(@Nullable Throwable throwable) { 114 | log.warn("{}: received throwable:", this, throwable); 115 | Objects.requireNonNull(throwable, "Provided throwable must not be null"); 116 | 117 | if (onError != null) { 118 | onError.accept(throwable); 119 | } 120 | completed.set(true); 121 | close(); 122 | } 123 | 124 | @Override 125 | public void onComplete() { 126 | log.debug("{}: complete", this); 127 | 128 | if (onComplete != null) { 129 | onComplete.run(); 130 | } 131 | completed.set(true); 132 | close(); 133 | } 134 | 135 | private void tryToRequest() { 136 | if (consumed.compareAndSet(batchSize, 0) && barrier.isOpen() && !completed.get()) { 137 | log.debug("{}: additional {} elements requested", this, batchSize); 138 | subscription.request(batchSize); 139 | } 140 | } 141 | 142 | @Override 143 | public void close() { 144 | if (!completed.get()) { 145 | onComplete(); 146 | } 147 | if (!executor.isShutdown()) { 148 | executor.shutdown(); 149 | log.debug("{}: shutdown complete", this); 150 | } 151 | } 152 | 153 | @Override 154 | public String toString() { 155 | return MessageFormat.format("{0} [batch {1}]", getClass().getSimpleName(), batchSize); 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/ColdPublisher.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman; 2 | 3 | import java.util.Iterator; 4 | import java.util.Map; 5 | import java.util.Objects; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.ThreadFactory; 9 | import java.util.concurrent.atomic.AtomicBoolean; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | import java.util.concurrent.atomic.AtomicLong; 12 | import javax.annotation.Nonnull; 13 | 14 | import com.github.egetman.etc.CustomizableThreadFactory; 15 | import com.github.egetman.source.Source; 16 | 17 | import org.reactivestreams.Publisher; 18 | import org.reactivestreams.Subscriber; 19 | import org.reactivestreams.Subscription; 20 | 21 | import lombok.EqualsAndHashCode; 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | import static java.util.concurrent.Executors.newScheduledThreadPool; 25 | 26 | @Slf4j 27 | public class ColdPublisher implements Publisher, AutoCloseable { 28 | 29 | private final Source source; 30 | private final AtomicInteger demandKey = new AtomicInteger(); 31 | private final Map demands = new ConcurrentHashMap<>(); 32 | 33 | private final int poolSize = Runtime.getRuntime().availableProcessors(); 34 | private final ThreadFactory threadFactory = new CustomizableThreadFactory("cp-worker", true); 35 | private final ExecutorService executor = newScheduledThreadPool(poolSize, threadFactory); 36 | 37 | public ColdPublisher(@Nonnull Source source) { 38 | this.source = Objects.requireNonNull(source, "Source must not be null"); 39 | } 40 | 41 | /** 42 | * {@inheritDoc}. 43 | */ 44 | @Override 45 | public void subscribe(@Nonnull Subscriber subscriber) { 46 | Objects.requireNonNull(subscriber, "Subscriber must not be null"); 47 | try { 48 | final Demand demand = new Demand(subscriber, demandKey.getAndIncrement()); 49 | subscriber.onSubscribe(demand); 50 | log.debug("{}: subscribed with {}", demand, subscriber); 51 | demands.put(demand.key, demand); 52 | log.debug("Total subscriptions count: {}", demands.size()); 53 | executor.execute(() -> sendNext(demand)); 54 | } catch (Exception e) { 55 | log.error("Exception occurred during subscription: " + e, e); 56 | subscriber.onError(e); 57 | } 58 | } 59 | 60 | private void sendNext(@Nonnull Demand demand) { 61 | // if cancel was requested, skip execution. 62 | if (demand.isCancelled()) { 63 | return; 64 | } 65 | 66 | try { 67 | while (true) { 68 | // we could add new requested elements from different threads, but process from one 69 | if (demand.tryLock()) { 70 | log.debug("{}: processing Next for total pool of: {} requests(s)", demand, demand.size()); 71 | final Iterator iterator = source.iterator(demand.key); 72 | 73 | while (!demand.isCancelled() && demand.size() > 0 && iterator.hasNext()) { 74 | final T element = Objects.requireNonNull(iterator.next()); 75 | log.debug("Publishing next element with type {}", element.getClass().getSimpleName()); 76 | demand.onNext(element); 77 | } 78 | completeIfNoMoreElements(demand); 79 | demand.release(); 80 | log.debug("{}: processing Next completed by {}", demand, Thread.currentThread().getName()); 81 | break; 82 | } 83 | } 84 | } catch (Exception e) { 85 | log.error("{}: exception occurred during sending onNext:", demand, e); 86 | demand.onError(e); 87 | } 88 | } 89 | 90 | /** 91 | * Verify that {@link Iterator} for this {@link Demand} has more elements to process. 92 | * 93 | * @param demand is current {@link Demand} to check. 94 | */ 95 | private void completeIfNoMoreElements(@Nonnull Demand demand) { 96 | if (demand.isCancelled() || !source.iterator(demand.key).hasNext()) { 97 | log.debug("{}: no more source to publish", demand); 98 | demand.onComplete(); 99 | } 100 | } 101 | 102 | /** 103 | * {@inheritDoc}. 104 | */ 105 | @Override 106 | public void close() { 107 | if (!executor.isShutdown()) { 108 | log.debug("shutting down {}", this); 109 | demands.values().forEach(Demand::cancel); 110 | executor.shutdownNow(); 111 | } 112 | } 113 | 114 | @EqualsAndHashCode(of = "key") 115 | class Demand implements Subscription { 116 | 117 | private final AtomicBoolean canceled = new AtomicBoolean(); 118 | private final AtomicBoolean processing = new AtomicBoolean(); 119 | 120 | private final int key; 121 | private Subscriber subscriber; 122 | private final AtomicLong requested = new AtomicLong(); 123 | 124 | private Demand(@Nonnull Subscriber subscriber, int key) { 125 | this.key = key; 126 | log.debug("{}: initialization started", this); 127 | this.subscriber = Objects.requireNonNull(subscriber, "Subscriber must not be null"); 128 | log.debug("{}: initialization finished", this); 129 | } 130 | 131 | private void onNext(@Nonnull T next) { 132 | log.debug("{}: received onNext {} signal", this, next.getClass().getSimpleName()); 133 | subscriber.onNext(next); 134 | requested.decrementAndGet(); 135 | } 136 | 137 | private void onError(@Nonnull Throwable error) { 138 | log.debug("{}: received onError signal", this, error); 139 | if (canceled.compareAndSet(false, true)) { 140 | subscriber.onError(error); 141 | clear(); 142 | } 143 | } 144 | 145 | private void onComplete() { 146 | if (canceled.compareAndSet(false, true)) { 147 | log.debug("{}: subscriber completed", this); 148 | subscriber.onComplete(); 149 | clear(); 150 | } 151 | } 152 | 153 | /** 154 | * {@inheritDoc}. 155 | */ 156 | @Override 157 | public void request(long addition) { 158 | if (canceled.get()) { 159 | return; 160 | } 161 | log.debug("{}: requested {} element(s)", this, addition); 162 | if (addition <= 0) { 163 | subscriber.onError(new IllegalArgumentException("Specification rule [3.9] violation")); 164 | return; 165 | } 166 | while (true) { 167 | long count = this.requested.get(); 168 | if (this.requested.compareAndSet(count, count + addition)) { 169 | log.debug("{}: additional request(s) [{}] added to requests pool", this, addition); 170 | break; 171 | } 172 | } 173 | executor.execute(() -> sendNext(this)); 174 | } 175 | 176 | /** 177 | * {@inheritDoc}. 178 | */ 179 | @Override 180 | public void cancel() { 181 | // no need to close resources each time cancel called 182 | if (canceled.compareAndSet(false, true)) { 183 | log.debug("{}: cancelled", this); 184 | clear(); 185 | } 186 | } 187 | 188 | /** 189 | * Indicates if {@link Demand} is cancelled. 190 | * 191 | * @return true if demand is cancelled, false otherwise. 192 | */ 193 | private boolean isCancelled() { 194 | return canceled.get(); 195 | } 196 | 197 | /** 198 | * Try to get exclusive {@literal processing} lock for given {@link Demand}. 199 | * 200 | * @return true, if the acquisition was successful, false otherwise. 201 | */ 202 | private boolean tryLock() { 203 | return processing.compareAndSet(false, true); 204 | } 205 | 206 | /** 207 | * Reseases exclusive {@literal processing} lock for given {@link Demand}. 208 | */ 209 | private void release() { 210 | processing.set(false); 211 | } 212 | 213 | /** 214 | * @return count of demanded elements. 215 | */ 216 | private long size() { 217 | return requested.get(); 218 | } 219 | 220 | /** 221 | * Clean up all internal {@link Demand} resources and drop reference to it's {@link Subscriber}. 222 | * Usage of this method should be synchronized, cause there is no guarantee of it's idempotency. 223 | */ 224 | private void clear() { 225 | subscriber = null; 226 | demands.remove(key); 227 | try { 228 | // we should try to close underlying source, but it's prohibited by the spec to throw any exceptions 229 | // from cancel and etc. 230 | source.iterator(key).close(); 231 | } catch (Exception e) { 232 | log.error("Exception occurred during closing source#closableIterator for " + this, e); 233 | } 234 | } 235 | 236 | @Override 237 | public String toString() { 238 | return "Demand [" + key + "]"; 239 | } 240 | 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/barrier/Barrier.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.barrier; 2 | 3 | /** 4 | * Interface used to limit throughput of the {@link org.reactivestreams.Subscriber} impl. 5 | */ 6 | public interface Barrier { 7 | 8 | /** 9 | * Indicates that barrier is open, i.e. it possible to demand new elements. 10 | * If {@link Barrier} is closed (isOpen() return {@code false}) new elements requests should be avoided. 11 | * Implementations may rely on this mechanism, or may not. 12 | * 13 | * @return true if it's possible to demand new elements. 14 | */ 15 | boolean isOpen(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/barrier/OpenBarrier.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.barrier; 2 | 3 | /** 4 | * Simple {@link Barrier} implementation that always return {@code true} on {@link Barrier#isOpen()}. 5 | */ 6 | public class OpenBarrier implements Barrier { 7 | 8 | @Override 9 | public boolean isOpen() { 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/etc/AbstractPool.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.etc; 2 | 3 | /** 4 | * Represents an abstract pool, that defines the procedure 5 | * of returning an object to the pool. 6 | * 7 | * @param the type of pooled objects. 8 | */ 9 | abstract class AbstractPool implements Pool { 10 | 11 | /** 12 | * Returns the object to the pool. 13 | * The method first validates the object if it is 14 | * re-usable and then puts returns it to the pool. 15 | * 16 | *

If the object validation fails, 17 | * some implementations 18 | * will try to create a new one 19 | * and put it into the pool; however 20 | * this behaviour is subject to change 21 | * from implementation to implementation 22 | */ 23 | @Override 24 | public final void release(T object) { 25 | if (isValid(object)) { 26 | returnToPool(object); 27 | } else { 28 | handleInvalidReturn(object); 29 | } 30 | } 31 | 32 | protected abstract void handleInvalidReturn(T object); 33 | 34 | protected abstract void returnToPool(T object); 35 | 36 | protected abstract boolean isValid(T object); 37 | } -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/etc/BlockingPool.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.etc; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | import javax.annotation.Nonnull; 5 | 6 | /** 7 | * Represents a pool of objects that makes the 8 | * requesting threads wait if no object is available. 9 | * 10 | * @param the type of objects to pool. 11 | */ 12 | @SuppressWarnings("unused") 13 | public interface BlockingPool extends Pool { 14 | 15 | /** 16 | * Returns an instance of type T from the pool. 17 | * 18 | *

The call is a blocking call, 19 | * and client threads are made to wait 20 | * indefinitely until an object is available. 21 | * The call implements a fairness algorithm 22 | * that ensures that a FCFS service is implemented. 23 | * 24 | *

Clients are advised to react to InterruptedException. 25 | * If the thread is interrupted while waiting 26 | * for an object to become available, 27 | * the current implementations 28 | * sets the interrupted state of the thread 29 | * to true and returns null. 30 | * However this is subject to change 31 | * from implementation to implementation. 32 | * 33 | * @return T an instance of the Object of type T from the pool. 34 | */ 35 | @Nonnull 36 | T get(); 37 | 38 | /** 39 | * Returns an instance of type T from the pool, 40 | * waiting up to the 41 | * specified wait time if necessary 42 | * for an object to become available.. 43 | * 44 | *

The call is a blocking call, 45 | * and client threads are made to wait 46 | * for time until an object is available 47 | * or until the timeout occurs. 48 | * The call implements a fairness algorithm 49 | * that ensures that a FCFS service is implemented. 50 | * 51 | *

Clients are advised to react to InterruptedException. 52 | * If the thread is interrupted while waiting 53 | * for an object to become available, 54 | * the current implementations 55 | * set the interrupted state of the thread 56 | * to true and returns null. 57 | * However this is subject to change 58 | * from implementation to implementation. 59 | * 60 | * @param time amount of time to wait before giving up, 61 | * in units of unit 62 | * @param unit a TimeUnit determining 63 | * how to interpret the 64 | * timeout parameter 65 | * @return T an instance of the Object of type T from the pool. 66 | * @throws InterruptedException if interrupted while waiting 67 | */ 68 | T get(long time, TimeUnit unit) throws InterruptedException; 69 | } -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/etc/BoundedBlockingPool.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.etc; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.ArrayBlockingQueue; 5 | import java.util.concurrent.BlockingQueue; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.function.Consumer; 9 | import java.util.function.Predicate; 10 | import java.util.function.Supplier; 11 | import java.util.stream.IntStream; 12 | import javax.annotation.Nonnull; 13 | import javax.annotation.Nullable; 14 | 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import static java.lang.Thread.currentThread; 18 | import static java.util.concurrent.Executors.newCachedThreadPool; 19 | 20 | /** 21 | * {@inheritDoc} 22 | * This class is {@literal Unconditionally thread safe}. 23 | * Thread safety guarantees by internal sync with used {@link BlockingQueue}. 24 | * 25 | * @param is pool elements type. 26 | */ 27 | @Slf4j 28 | final class BoundedBlockingPool extends AbstractPool implements BlockingPool { 29 | 30 | private BlockingQueue objects; 31 | private final Supplier factory; 32 | private final Consumer cleaner; 33 | private final Predicate validator; 34 | 35 | private static final String SHUTDOWN_CAUSE = "Object pool is already shutdown"; 36 | 37 | private volatile boolean shutdownCalled = false; 38 | private final ExecutorService executor = newCachedThreadPool(new CustomizableThreadFactory(true)); 39 | 40 | // cleaner is optional 41 | BoundedBlockingPool(int size, @Nonnull Predicate validator, @Nonnull Supplier factory, Consumer cleaner) { 42 | this.cleaner = cleaner; 43 | this.factory = Objects.requireNonNull(factory); 44 | this.validator = Objects.requireNonNull(validator); 45 | 46 | objects = new ArrayBlockingQueue<>(size); 47 | IntStream.range(0, size).forEach(ignored -> objects.add(factory.get())); 48 | } 49 | 50 | @Nullable 51 | @Override 52 | public T get(long timeOut, TimeUnit unit) { 53 | if (!shutdownCalled) { 54 | try { 55 | return objects.poll(timeOut, unit); 56 | } catch (InterruptedException ie) { 57 | currentThread().interrupt(); 58 | } 59 | return null; 60 | } 61 | throw new IllegalStateException(SHUTDOWN_CAUSE); 62 | } 63 | 64 | @Nonnull 65 | @Override 66 | public T get() { 67 | if (!shutdownCalled) { 68 | try { 69 | return objects.take(); 70 | } catch (InterruptedException ie) { 71 | currentThread().interrupt(); 72 | } 73 | // unbounded? 74 | return get(); 75 | } 76 | throw new IllegalStateException(SHUTDOWN_CAUSE); 77 | } 78 | 79 | /** 80 | * Shutdowns pool and release all acquired resources. 81 | */ 82 | @Override 83 | public void shutdown() { 84 | log.info("Pool shutdown requested"); 85 | shutdownCalled = true; 86 | executor.shutdownNow(); 87 | if (cleaner != null) { 88 | objects.forEach(cleaner); 89 | } 90 | } 91 | 92 | @Override 93 | protected void returnToPool(T object) { 94 | if (shutdownCalled) { 95 | throw new IllegalStateException(SHUTDOWN_CAUSE); 96 | } 97 | execute(() -> put(object)); 98 | } 99 | 100 | @Override 101 | protected void handleInvalidReturn(T object) { 102 | if (shutdownCalled) { 103 | throw new IllegalStateException(SHUTDOWN_CAUSE); 104 | } 105 | execute(() -> put(factory.get())); 106 | } 107 | 108 | private void execute(@Nonnull Runnable runnable) { 109 | if (executor.isShutdown() || executor.isTerminated()) { 110 | log.warn("Can't execute task: executor is shutdown"); 111 | } else { 112 | executor.execute(runnable); 113 | } 114 | } 115 | 116 | @Override 117 | protected boolean isValid(T object) { 118 | return validator.test(object); 119 | } 120 | 121 | private void put(@Nonnull T object) { 122 | while (true) { 123 | try { 124 | objects.put(object); 125 | break; 126 | } catch (InterruptedException ie) { 127 | currentThread().interrupt(); 128 | } 129 | } 130 | } 131 | 132 | } -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/etc/CustomizableThreadFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.etc; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.concurrent.ThreadFactory; 5 | import java.util.concurrent.atomic.AtomicLong; 6 | 7 | import javax.annotation.Nonnull; 8 | 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | @SuppressWarnings("unused") 12 | public class CustomizableThreadFactory implements ThreadFactory { 13 | 14 | private static final String DEFAULT_THREAD_NAME = "worker"; 15 | private final AtomicLong workerNumber = new AtomicLong(); 16 | private final TracePrinter tracePrinter = new TracePrinter(); 17 | private final boolean isDaemon; 18 | private final String threadName; 19 | 20 | CustomizableThreadFactory(boolean isDaemon) { 21 | this(DEFAULT_THREAD_NAME, isDaemon); 22 | } 23 | 24 | public CustomizableThreadFactory(String threadName) { 25 | this(threadName, false); 26 | } 27 | 28 | public CustomizableThreadFactory(String threadName, boolean isDaemon) { 29 | this.threadName = threadName; 30 | this.isDaemon = isDaemon; 31 | } 32 | 33 | @Override 34 | public Thread newThread(@Nonnull Runnable runnable) { 35 | String name = MessageFormat.format("{0} [{1}]", threadName, workerNumber.getAndIncrement()); 36 | Thread thread = new Thread(runnable, name); 37 | thread.setDaemon(isDaemon); 38 | thread.setUncaughtExceptionHandler(tracePrinter); 39 | return thread; 40 | } 41 | 42 | @Slf4j 43 | private static class TracePrinter implements Thread.UncaughtExceptionHandler { 44 | 45 | @Override 46 | public void uncaughtException(@Nonnull Thread thread, @Nonnull Throwable ex) { 47 | log.error("Uncaught exception for {}:", thread.getName(), ex); 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/etc/Pool.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.etc; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | /** 6 | * Represents a cached pool of objects. 7 | * 8 | * @param the type of object to pool. 9 | */ 10 | @SuppressWarnings("unused") 11 | public interface Pool { 12 | 13 | /** 14 | * Returns an instance from the pool. 15 | * The call may be a blocking one or a non-blocking one 16 | * and that is determined by the internal implementation. 17 | * 18 | *

If the call is a blocking call, 19 | * the call returns immediately with a valid object 20 | * if available, else the thread is made to wait 21 | * until an object becomes available. 22 | * In case of a blocking call, 23 | * it is advised that clients react 24 | * to {@link InterruptedException} which might be thrown 25 | * when the thread waits for an object to become available. 26 | * 27 | *

If the call is a non-blocking one, 28 | * the call returns immediately irrespective of 29 | * whether an object is available or not. 30 | * If any object is available the call returns it 31 | * else the call returns null. 32 | * 33 | *

The validity of the objects are determined using the 34 | * {@link java.util.function.Predicate} interface, such that 35 | * an object o is valid if 36 | * Predicate.test(o) == true . 37 | * 38 | * @return T one of the pooled objects. 39 | */ 40 | @Nonnull 41 | T get(); 42 | 43 | /** 44 | * Releases the object and puts it back to the pool. 45 | * 46 | *

The mechanism of putting the object back to the pool is 47 | * generally asynchronous, 48 | * however future implementations might differ. 49 | * 50 | * @param object the object to return to the pool 51 | */ 52 | 53 | void release(T object); 54 | 55 | /** 56 | * Shuts down the pool. In essence this call will not 57 | * accept any more requests 58 | * and will release all resources. 59 | */ 60 | 61 | void shutdown(); 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/etc/PoolFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.etc; 2 | 3 | 4 | import java.util.Objects; 5 | import java.util.function.Consumer; 6 | import java.util.function.Predicate; 7 | import java.util.function.Supplier; 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | 11 | /** 12 | * Factory and utility methods for 13 | * {@link Pool} and {@link BlockingPool} classes 14 | * defined in this package. 15 | * This class supports the following kinds of methods: 16 | * 17 | *

    18 | *
  • Method that creates and returns a default non-blocking 19 | * implementation of the {@link Pool} interface. 20 | *
  • 21 | * 22 | *
  • Method that creates and returns a 23 | * default implementation of 24 | * the {@link BlockingPool} interface. 25 | *
  • 26 | *
27 | */ 28 | @SuppressWarnings("unused") 29 | public interface PoolFactory { 30 | 31 | /** 32 | * Creates a and returns a new object pool, 33 | * that is an implementation of the {@link BlockingPool}, 34 | * whose size is limited by 35 | * the size parameter. 36 | * 37 | * @param size the number of objects in the pool. 38 | * @param factory the factory to create new objects. 39 | * @param validator the validator to 40 | * validate the re-usability of returned objects. 41 | * @param cleaner the cleaner to clean up the resources. (optional - may be null) 42 | * @param type of elements in the pool. 43 | * @return a blocking object pool bounded by size 44 | */ 45 | @Nonnull 46 | static Pool newBoundedBlockingPool(int size, @Nonnull Supplier factory, @Nonnull Predicate validator, 47 | @Nullable Consumer cleaner) { 48 | return new BoundedBlockingPool<>(size, validator, factory, cleaner); 49 | } 50 | 51 | /** 52 | * Creates a and returns a new object pool, 53 | * that is an implementation of the {@link BlockingPool}, 54 | * whose size is limited by 55 | * the size parameter. 56 | * 57 | * @param size the number of objects in the pool. 58 | * @param factory the factory to create new objects. 59 | * @param type of elements in the pool. 60 | * @return a blocking object pool bounded by size 61 | */ 62 | @Nonnull 63 | static Pool newBoundedBlockingPool(int size, @Nonnull Supplier factory) { 64 | return new BoundedBlockingPool<>(size, Objects::nonNull, factory, null); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/etc/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNullableByDefault 2 | 3 | package com.github.egetman.etc; 4 | 5 | import javax.annotation.ParametersAreNullableByDefault; -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNullableByDefault 2 | 3 | package com.github.egetman; 4 | 5 | import javax.annotation.ParametersAreNullableByDefault; -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/source/CloseableIterator.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.source; 2 | 3 | import java.util.Iterator; 4 | 5 | /** 6 | * An iterator over a data. 7 | * It could be manually closed as it's implement {@link AutoCloseable}. 8 | * 9 | * @param is type of elements returned by this iterator. 10 | */ 11 | @SuppressWarnings("WeakerAccess") 12 | public interface CloseableIterator extends Iterator, AutoCloseable { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/source/JmsQuota.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.source; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.Objects; 5 | import java.util.concurrent.TimeUnit; 6 | import java.util.concurrent.locks.Lock; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | import java.util.function.Function; 9 | import java.util.function.Predicate; 10 | import java.util.function.Supplier; 11 | import javax.annotation.Nonnull; 12 | import javax.annotation.Nullable; 13 | import javax.jms.Connection; 14 | import javax.jms.ConnectionFactory; 15 | import javax.jms.JMSException; 16 | import javax.jms.Message; 17 | import javax.jms.Session; 18 | 19 | import com.github.egetman.etc.Pool; 20 | import com.github.egetman.etc.PoolFactory; 21 | 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | import static javax.jms.Session.AUTO_ACKNOWLEDGE; 25 | 26 | /** 27 | * Transacted passed inside the {@link JmsQuota} cause it could be changed for source later, but not for already 28 | * created {@link JmsQuota}. 29 | */ 30 | @Slf4j 31 | class JmsQuota implements CloseableIterator { 32 | 33 | private static final int POOL_SIZE = 3; 34 | private static final int RETRY_TIME_SECONDS = 10; 35 | 36 | private final int key; 37 | private final String queue; 38 | private final boolean transacted; 39 | private final int acknowledgeMode; 40 | 41 | private final Pool units; 42 | private final Function function; 43 | private final Lock lock = new ReentrantLock(); 44 | 45 | @SuppressWarnings("unused") 46 | JmsQuota(int key, 47 | @Nonnull Function function, 48 | @Nonnull ConnectionFactory factory, 49 | @Nonnull String queue, 50 | String user, String password, boolean transacted) { 51 | this(key, function, factory, queue, user, password, transacted, AUTO_ACKNOWLEDGE); 52 | } 53 | 54 | @SuppressWarnings("squid:S00107") 55 | JmsQuota(int key, 56 | @Nonnull Function function, 57 | @Nonnull ConnectionFactory factory, 58 | @Nonnull String queue, 59 | String user, String password, boolean transacted, int acknowledgeMode) { 60 | try { 61 | // initialization should be sync'ed 62 | lock.lock(); 63 | this.key = key; 64 | this.queue = Objects.requireNonNull(queue); 65 | this.function = Objects.requireNonNull(function); 66 | this.transacted = transacted; 67 | this.acknowledgeMode = acknowledgeMode; 68 | 69 | this.units = newPool(Objects.requireNonNull(factory), queue, user, password, transacted, acknowledgeMode); 70 | } finally { 71 | lock.unlock(); 72 | } 73 | } 74 | 75 | private Pool newPool(@Nonnull ConnectionFactory factory, 76 | @Nonnull String queue, 77 | String user, 78 | String password, boolean transacted, int acknowledgeMode) { 79 | 80 | final Supplier supplier = () -> { 81 | final Connection connection; 82 | try { 83 | // password could be missing? 84 | if (user != null) { 85 | connection = factory.createConnection(user, password); 86 | } else { 87 | connection = factory.createConnection(); 88 | } 89 | return new JmsUnit(connection, queue, transacted, acknowledgeMode); 90 | } catch (JMSException e) { 91 | log.error("Exception during pool connection initialization: {}", e.getMessage()); 92 | throw new IllegalStateException("Exception during pool connection initialization", e); 93 | } 94 | }; 95 | final Predicate validator = jmsUnit -> !jmsUnit.isFailed(); 96 | return PoolFactory.newBoundedBlockingPool(POOL_SIZE, supplier, validator, JmsUnit::close); 97 | } 98 | 99 | @Override 100 | public boolean hasNext() { 101 | // it's always true for infinite source 102 | return true; 103 | } 104 | 105 | @Nonnull 106 | @Override 107 | public T next() { 108 | // iterator contract 109 | if (!hasNext()) { 110 | throw new NoSuchElementException(); 111 | } 112 | T result = null; 113 | while (result == null) { 114 | // outside of try-catch for exit 'next' when pool is closed 115 | JmsUnit unit = units.get(); 116 | try { 117 | result = next(unit); 118 | } catch (Exception e) { 119 | log.error("", e); 120 | } finally { 121 | units.release(unit); 122 | } 123 | } 124 | return result; 125 | } 126 | 127 | @Nullable 128 | private T next(@Nonnull JmsUnit unit) { 129 | Message message = null; 130 | try { 131 | message = unit.receive(); 132 | return function.apply(message); 133 | } catch (Exception ex) { 134 | log.error("Exception during message receiving for {}: {}", this, ex.getMessage()); 135 | log.warn("Prepare to recover session {}", this); 136 | try { 137 | if (!recover(unit)) { 138 | unit.fail(); 139 | wait(RETRY_TIME_SECONDS, TimeUnit.SECONDS); 140 | return null; 141 | } 142 | message = unit.receive(); 143 | return function.apply(message); 144 | } catch (Exception e) { 145 | log.warn("Session recovery failed for {} with cause {}. Releasing unit", this, e.getMessage()); 146 | unit.fail(); 147 | wait(RETRY_TIME_SECONDS, TimeUnit.SECONDS); 148 | return null; 149 | } 150 | } finally { 151 | acknowledge(message); 152 | } 153 | } 154 | 155 | private boolean recover(@Nonnull JmsUnit unit) { 156 | try { 157 | // try to recover with exclusive lock 158 | lock.lock(); 159 | unit.recover(); 160 | return true; 161 | } catch (Exception e) { 162 | log.error("Recovery failed for {}", this); 163 | return false; 164 | } finally { 165 | lock.unlock(); 166 | } 167 | } 168 | 169 | private void acknowledge(@Nullable Message message) { 170 | if (message != null && Session.CLIENT_ACKNOWLEDGE == acknowledgeMode) { 171 | try { 172 | message.acknowledge(); 173 | } catch (JMSException e) { 174 | log.error("Fail to acknowledge message {}: {}", message, e.getMessage()); 175 | } 176 | } 177 | } 178 | 179 | @SuppressWarnings("SameParameterValue") 180 | private void wait(long time, @Nonnull TimeUnit timeUnit) { 181 | try { 182 | Thread.sleep(timeUnit.toMillis(time)); 183 | } catch (InterruptedException e) { 184 | Thread.currentThread().interrupt(); 185 | } 186 | } 187 | 188 | @Override 189 | public void remove() { 190 | // noop 191 | } 192 | 193 | @Override 194 | public String toString() { 195 | String name = "JmsQuota [" + key + "]" + "[" + queue + "]"; 196 | return transacted ? "Transacted " + name : name; 197 | } 198 | 199 | @Override 200 | public boolean equals(Object other) { 201 | if (this == other) { 202 | return true; 203 | } 204 | if (other == null || getClass() != other.getClass()) { 205 | return false; 206 | } 207 | JmsQuota quota = (JmsQuota) other; 208 | return key == quota.key && Objects.equals(queue, quota.queue); 209 | } 210 | 211 | @Override 212 | public int hashCode() { 213 | return Objects.hash(key, queue); 214 | } 215 | 216 | @Override 217 | public void close() { 218 | try { 219 | // utilization should be sync'ed 220 | lock.lock(); 221 | units.shutdown(); 222 | } finally { 223 | lock.unlock(); 224 | } 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/source/JmsUnit.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.source; 2 | 3 | import java.util.HashSet; 4 | import java.util.Set; 5 | import java.util.concurrent.atomic.AtomicBoolean; 6 | import java.util.concurrent.locks.Lock; 7 | import java.util.concurrent.locks.ReentrantLock; 8 | import java.util.function.Function; 9 | import java.util.function.Supplier; 10 | import javax.annotation.Nonnull; 11 | import javax.jms.Connection; 12 | import javax.jms.IllegalStateException; 13 | import javax.jms.JMSException; 14 | import javax.jms.Message; 15 | import javax.jms.MessageConsumer; 16 | import javax.jms.Session; 17 | import javax.jms.TransactionInProgressException; 18 | 19 | import lombok.SneakyThrows; 20 | import lombok.extern.slf4j.Slf4j; 21 | 22 | import static java.util.Arrays.asList; 23 | import static javax.jms.Session.AUTO_ACKNOWLEDGE; 24 | import static javax.jms.Session.CLIENT_ACKNOWLEDGE; 25 | 26 | @Slf4j 27 | class JmsUnit implements AutoCloseable { 28 | 29 | private static final Set ALLOWED_ACKNOWLEDGE_MODES = new HashSet<>(asList(AUTO_ACKNOWLEDGE, 30 | CLIENT_ACKNOWLEDGE)); 31 | 32 | private final AtomicBoolean failed = new AtomicBoolean(); 33 | private final Lock initializationLock = new ReentrantLock(); 34 | private final AtomicBoolean initialized = new AtomicBoolean(); 35 | 36 | // one session and consumer per connection 37 | private Session session; 38 | private MessageConsumer consumer; 39 | private final boolean transacted; 40 | private final Connection connection; 41 | 42 | private final Supplier sessionSupplier; 43 | private final Function sessionToConsumer; 44 | 45 | JmsUnit(@Nonnull Connection connection, @Nonnull String queue, boolean transacted, int acknowledgeMode) { 46 | this.connection = connection; 47 | this.transacted = transacted; 48 | if (!ALLOWED_ACKNOWLEDGE_MODES.contains(acknowledgeMode)) { 49 | throw new IllegalArgumentException("Acknowledge mode " + acknowledgeMode + " not supported"); 50 | } 51 | 52 | this.sessionSupplier = () -> { 53 | try { 54 | return connection.createSession(transacted, acknowledgeMode); 55 | } catch (JMSException e) { 56 | log.error("Exception during session creation: {}", e.getMessage()); 57 | throw new java.lang.IllegalStateException(e); 58 | } 59 | }; 60 | this.sessionToConsumer = currentSession -> { 61 | try { 62 | return currentSession.createConsumer(currentSession.createQueue(queue)); 63 | } catch (JMSException e) { 64 | log.error("Exception during consumer creation: {}", e.getMessage()); 65 | throw new java.lang.IllegalStateException(e); 66 | } 67 | }; 68 | } 69 | 70 | /** 71 | * It's ok to sync this method. Called just ones. 72 | */ 73 | @SneakyThrows 74 | private void initialize() { 75 | try { 76 | initializationLock.lock(); 77 | if (initialized.get()) { 78 | // check if already initialized 79 | return; 80 | } 81 | session = sessionSupplier.get(); 82 | consumer = sessionToConsumer.apply(session); 83 | 84 | connection.start(); 85 | log.debug("Unit {} successfully initialized", super.hashCode()); 86 | initialized.set(true); 87 | } finally { 88 | initializationLock.unlock(); 89 | } 90 | } 91 | 92 | @Nonnull 93 | private Session session() { 94 | waitUntilInitialized(); 95 | return session; 96 | } 97 | 98 | @Nonnull 99 | private MessageConsumer consumer() { 100 | waitUntilInitialized(); 101 | return consumer; 102 | } 103 | 104 | @SneakyThrows 105 | Message receive() { 106 | final Message message = consumer().receive(); 107 | if (transacted && session().getTransacted()) { 108 | try { 109 | session().commit(); 110 | } catch (IllegalStateException | TransactionInProgressException e) { 111 | log.trace("Can't commit. Possible JTA transaction:", e); 112 | } 113 | } 114 | return message; 115 | } 116 | 117 | @SneakyThrows 118 | void recover() { 119 | session().recover(); 120 | } 121 | 122 | private void waitUntilInitialized() { 123 | if (!initialized.get()) { 124 | initialize(); 125 | } 126 | } 127 | 128 | void fail() { 129 | failed.set(true); 130 | } 131 | 132 | boolean isFailed() { 133 | return failed.get(); 134 | } 135 | 136 | @Override 137 | public void close() { 138 | try { 139 | closeResources(consumer, session, connection); 140 | } catch (Exception e) { 141 | log.error("Exception during resource close: " + e.getMessage()); 142 | } 143 | } 144 | 145 | /** 146 | * Closes all resources. 147 | * 148 | * @param resources - resources to close. 149 | */ 150 | private void closeResources(@Nonnull AutoCloseable... resources) { 151 | Exception toThrowUp = null; 152 | for (AutoCloseable resource : resources) { 153 | toThrowUp = closeAndReturnException(resource, toThrowUp); 154 | } 155 | throwIfNonNull(toThrowUp); 156 | } 157 | 158 | private Exception closeAndReturnException(AutoCloseable closeable, Exception thrown) { 159 | if (closeable != null) { 160 | try { 161 | closeable.close(); 162 | } catch (Exception cause) { 163 | if (thrown != null) { 164 | thrown.addSuppressed(cause); 165 | } else { 166 | return cause; 167 | } 168 | } 169 | } 170 | return thrown; 171 | } 172 | 173 | @SneakyThrows 174 | private static void throwIfNonNull(Exception exception) { 175 | if (exception != null) { 176 | throw exception; 177 | } 178 | } 179 | } 180 | 181 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/source/Source.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.source; 2 | 3 | import java.util.Iterator; 4 | import javax.annotation.Nonnull; 5 | 6 | /** 7 | * Abstraction of elements source, that could give an {@link Iterator} for that source. 8 | * 9 | * @param type of elements returned by this source. 10 | */ 11 | public interface Source { 12 | 13 | /** 14 | * Return iterator for given key. 15 | * Source should create new iterator for given {@code key} and cache it. 16 | * Otherwise it could be inconsistent state of the Publisher, that uses that source. 17 | * Note: if source supports concurrent processing, it should synchronize correctly data access. 18 | * 19 | * @param key uniq key to obtain iterator instance. 20 | * @return {@link CloseableIterator}. 21 | */ 22 | @Nonnull 23 | CloseableIterator iterator(int key); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/source/UnicastJmsQueueSource.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman.source; 2 | 3 | import java.util.Map; 4 | import java.util.Objects; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.function.Function; 7 | import javax.annotation.Nonnull; 8 | import javax.jms.ConnectionFactory; 9 | import javax.jms.Message; 10 | import javax.jms.Session; 11 | 12 | public class UnicastJmsQueueSource implements Source { 13 | 14 | private boolean tx; 15 | private String user; 16 | private String password; 17 | 18 | private final String queue; 19 | private final int acknowledgeMode; 20 | private final ConnectionFactory factory; 21 | private final Function function; 22 | 23 | private final Map> pool = new ConcurrentHashMap<>(); 24 | 25 | public UnicastJmsQueueSource(@Nonnull ConnectionFactory factory, @Nonnull Function function, 26 | @Nonnull String queue) { 27 | this(factory, function, queue, null, null, false, Session.AUTO_ACKNOWLEDGE); 28 | } 29 | 30 | public UnicastJmsQueueSource(@Nonnull ConnectionFactory factory, @Nonnull Function function, 31 | @Nonnull String queue, String user, String password, boolean transacted, 32 | int acknowledgeMode) { 33 | this.queue = Objects.requireNonNull(queue, "Queue name ust not be null"); 34 | this.factory = Objects.requireNonNull(factory, "Factory must not be null"); 35 | this.function = Objects.requireNonNull(function, "Function must not be null"); 36 | 37 | this.user = user; 38 | this.password = password; 39 | 40 | this.tx = transacted; 41 | this.acknowledgeMode = acknowledgeMode; 42 | } 43 | 44 | /** 45 | * {@inheritDoc}. 46 | * 47 | * @param key uniq key to obtain iterator instance. 48 | * @return cached {@link JmsQuota} instance. 49 | */ 50 | @Nonnull 51 | @Override 52 | public CloseableIterator iterator(int key) { 53 | return pool.computeIfAbsent(key, 54 | value -> new JmsQuota<>(key, function, factory, queue, user, password, tx, acknowledgeMode)); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/egetman/source/package-info.java: -------------------------------------------------------------------------------- 1 | @ParametersAreNullableByDefault 2 | 3 | package com.github.egetman.source; 4 | 5 | import javax.annotation.ParametersAreNullableByDefault; -------------------------------------------------------------------------------- /src/test/java/com/github/egetman/BalancingSubscriberBlackBoxTest.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman; 2 | 3 | import com.github.egetman.barrier.OpenBarrier; 4 | 5 | import org.reactivestreams.Subscriber; 6 | import org.reactivestreams.tck.SubscriberBlackboxVerification; 7 | import org.reactivestreams.tck.TestEnvironment; 8 | import org.testng.annotations.AfterMethod; 9 | 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | /** 13 | * TCK {@link Subscriber} black box verification. 14 | */ 15 | @Slf4j 16 | public class BalancingSubscriberBlackBoxTest extends SubscriberBlackboxVerification { 17 | 18 | private static final int BATCH_SIZE = 100; 19 | private static final int POLL_INTERVAL = 1; 20 | private static final int DEFAULT_TIMEOUT_MILLIS = 300; 21 | 22 | private BalancingSubscriber subscriber; 23 | 24 | public BalancingSubscriberBlackBoxTest() { 25 | super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS, true)); 26 | } 27 | 28 | @Override 29 | public Subscriber createSubscriber() { 30 | subscriber = new BalancingSubscriber<>(i -> log.info("{}", i), new OpenBarrier(), BATCH_SIZE, POLL_INTERVAL); 31 | return subscriber; 32 | } 33 | 34 | @Override 35 | public Integer createElement(int element) { 36 | return element; 37 | } 38 | 39 | @AfterMethod 40 | private void shutdown() { 41 | if (subscriber != null) { 42 | log.debug("Closing {}", subscriber); 43 | subscriber.close(); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/egetman/BalancingSubscriberWhiteBoxTest.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman; 2 | 3 | import com.github.egetman.barrier.OpenBarrier; 4 | 5 | import org.reactivestreams.Subscriber; 6 | import org.reactivestreams.Subscription; 7 | import org.reactivestreams.tck.SubscriberWhiteboxVerification; 8 | import org.reactivestreams.tck.TestEnvironment; 9 | import org.testng.annotations.AfterMethod; 10 | 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | /** 14 | * TCK {@link Subscriber} white box verification. 15 | */ 16 | @Slf4j 17 | public class BalancingSubscriberWhiteBoxTest extends SubscriberWhiteboxVerification { 18 | 19 | private static final int BATCH_SIZE = 100; 20 | private static final int POLL_INTERVAL = 1; 21 | private static final int DEFAULT_TIMEOUT_MILLIS = 300; 22 | 23 | private BalancingSubscriber subscriber; 24 | 25 | public BalancingSubscriberWhiteBoxTest() { 26 | super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS, true)); 27 | } 28 | 29 | @Override 30 | public Subscriber createSubscriber(WhiteboxSubscriberProbe probe) { 31 | subscriber = new BalancingSubscriber(i -> log.info("{}", i), new OpenBarrier(), BATCH_SIZE, POLL_INTERVAL) { 32 | 33 | @Override 34 | public void onSubscribe(Subscription subscription) { 35 | super.onSubscribe(subscription); 36 | probe.registerOnSubscribe(new SubscriberPuppet() { 37 | 38 | @Override 39 | public void triggerRequest(long elements) { 40 | subscription.request(elements); 41 | } 42 | 43 | @Override 44 | public void signalCancel() { 45 | subscription.cancel(); 46 | } 47 | }); 48 | } 49 | 50 | @Override 51 | public void onNext(Integer next) { 52 | super.onNext(next); 53 | probe.registerOnNext(next); 54 | } 55 | 56 | @Override 57 | public void onError(Throwable throwable) { 58 | super.onError(throwable); 59 | probe.registerOnError(throwable); 60 | } 61 | 62 | @Override 63 | public void onComplete() { 64 | super.onComplete(); 65 | probe.registerOnComplete(); 66 | } 67 | }; 68 | return subscriber; 69 | } 70 | 71 | @Override 72 | public Integer createElement(int element) { 73 | return element; 74 | } 75 | 76 | @AfterMethod 77 | private void shutdown() { 78 | if (subscriber != null) { 79 | log.debug("Closing {}", subscriber); 80 | subscriber.close(); 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/github/egetman/UnicastJmsQueueAutoAcknowledgePublisherTest.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman; 2 | 3 | import javax.jms.Session; 4 | 5 | public class UnicastJmsQueueAutoAcknowledgePublisherTest extends UnicastJmsQueueColdPublisherTest { 6 | 7 | public UnicastJmsQueueAutoAcknowledgePublisherTest() { 8 | super(Session.AUTO_ACKNOWLEDGE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/egetman/UnicastJmsQueueClientAcknowledgePublisherTest.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman; 2 | 3 | import javax.jms.Session; 4 | 5 | public class UnicastJmsQueueClientAcknowledgePublisherTest extends UnicastJmsQueueColdPublisherTest { 6 | 7 | public UnicastJmsQueueClientAcknowledgePublisherTest() { 8 | super(Session.CLIENT_ACKNOWLEDGE); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/egetman/UnicastJmsQueueColdPublisherTest.java: -------------------------------------------------------------------------------- 1 | package com.github.egetman; 2 | 3 | import java.util.function.Function; 4 | import java.util.stream.IntStream; 5 | import javax.jms.Connection; 6 | import javax.jms.JMSException; 7 | import javax.jms.Message; 8 | import javax.jms.MessageProducer; 9 | import javax.jms.Queue; 10 | import javax.jms.Session; 11 | import javax.jms.TextMessage; 12 | 13 | import com.github.egetman.source.Source; 14 | import com.github.egetman.source.UnicastJmsQueueSource; 15 | 16 | import org.apache.activemq.ActiveMQConnectionFactory; 17 | import org.reactivestreams.Publisher; 18 | import org.reactivestreams.tck.PublisherVerification; 19 | import org.reactivestreams.tck.TestEnvironment; 20 | import org.testng.annotations.AfterMethod; 21 | 22 | import lombok.extern.slf4j.Slf4j; 23 | 24 | import static java.lang.String.*; 25 | 26 | /** 27 | * TCK {@link Publisher} verification. 28 | */ 29 | @Slf4j 30 | public abstract class UnicastJmsQueueColdPublisherTest extends PublisherVerification { 31 | 32 | private static final int MESSAGES_IN_QUEUE_SIZE = 10; 33 | private static final int DEFAULT_TIMEOUT_MILLIS = 300; 34 | 35 | private static final String QUEUE = "queue"; 36 | private static final String BROKER_URL = "vm://localhost?broker.persistent=false"; 37 | private static final Function MESSAGE_TO_STRING = message -> { 38 | try { 39 | return ((TextMessage) message).getText(); 40 | } catch (Exception e) { 41 | log.error("Exception during message transformation", e); 42 | throw new IllegalStateException(e); 43 | } 44 | }; 45 | 46 | private final int acknowledgeMode; 47 | private ActiveMQConnectionFactory factory; 48 | private ColdPublisher coldPublisher; 49 | 50 | @AfterMethod 51 | public void shutdown() { 52 | if (coldPublisher != null) { 53 | log.debug("Closing {}", coldPublisher); 54 | coldPublisher.close(); 55 | } 56 | } 57 | 58 | UnicastJmsQueueColdPublisherTest(int acknowledgeMode) { 59 | super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS, true)); 60 | this.acknowledgeMode = acknowledgeMode; 61 | // set prefetch to 1, so every consumer can receive some messages. 62 | factory = new ActiveMQConnectionFactory(BROKER_URL); 63 | factory.getPrefetchPolicy().setAll(1); 64 | } 65 | 66 | @Override 67 | public Publisher createPublisher(long elements) { 68 | log.debug("Requested {} elements", elements); 69 | try (final Connection connection = factory.createConnection()) { 70 | connection.start(); 71 | try (final Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE)) { 72 | final Queue queue = session.createQueue(QUEUE); 73 | try (final MessageProducer producer = session.createProducer(queue)) { 74 | // add 10 messages into queue for each test 75 | IntStream.range(0, MESSAGES_IN_QUEUE_SIZE).forEach(element -> { 76 | try { 77 | producer.send(session.createTextMessage(valueOf(element))); 78 | } catch (JMSException e) { 79 | log.error("Failed to send {} element into {}", element, QUEUE); 80 | } 81 | }); 82 | } 83 | log.debug("{} mesages sent into {}", elements, QUEUE); 84 | 85 | } 86 | } catch (Exception e) { 87 | log.error("Exception on publisher creation", e); 88 | } 89 | final Source source = new UnicastJmsQueueSource<>(factory, MESSAGE_TO_STRING, QUEUE, "u1", null, true, 90 | acknowledgeMode); 91 | coldPublisher = new ColdPublisher<>(source); 92 | return coldPublisher; 93 | } 94 | 95 | @Override 96 | public Publisher createFailedPublisher() { 97 | return null; 98 | } 99 | 100 | @Override 101 | public long maxElementsFromPublisher() { 102 | // unbounded one 103 | return Long.MAX_VALUE; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{yyyy-MM-dd HH:mm:ss} [%thread] %highlight(%-5level) %logger{10} - %msg%n 7 | 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | --------------------------------------------------------------------------------