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 super T> 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 super T> subscriber;
122 | private final AtomicLong requested = new AtomicLong();
123 |
124 | private Demand(@Nonnull Subscriber super T> 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 |
--------------------------------------------------------------------------------