Skip to content

Commit 4963447

Browse files
committed
+ added feature: builder configuration of task rejection handling policy
+ added blocking retry policy + ConseqExecutor shutdown now blocks on work pool shutdown before shutting down admin pool
1 parent bb601f1 commit 4963447

File tree

4 files changed

+129
-53
lines changed

4 files changed

+129
-53
lines changed

README.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,22 +262,36 @@ Notes:
262262
```jshelllanguage
263263
ConseqExecutor.newInstance()
264264
```
265+
or
266+
```jshelllanguage
267+
new ConseqExecutor.Builder().build();
268+
```
265269

266270
The concurrency can be customized:
267271
```jshelllanguage
268272
ConseqExecutor.newInstance(10)
269273
```
274+
or
275+
```jshelllanguage
276+
new ConseqExecutor.Builder().concurrency(10).build();
277+
```
278+
270279
- The default work queue capacity for any executor is unlimited, which ensures the asynchronous semantics to the caller.
271-
However, in the case of `ConseqExecutor` (API Style 2), the work queue capacity can be customized:
280+
However, in the case of `ConseqExecutor` (API Style 2), the work queue capacity can be customized, e.g.:
272281
```jshelllanguage
273282
ConseqExecutor conseqExecutor = new ConseqExecutor.builder().workQueueCapacity(256).build(); // default concurrency with work queue capacity of 256
274283
```
275284
```jshelllanguage
276285
ConseqExecutor conseqExecutor = new ConseqExecutor.builder().workQueueCapacity(256).concurrency(10).build();
277286
```
278-
When the work queue is full at capacity, the caller thread will be blocked until more room becomes available. This
279-
temporarily alters the asynchronous semantics and imposes "back pressure" to caller thread, which may be a desired
280-
behavior in some cases.
287+
- When the executor work queue is full at capacity, the async task will be rejected. The default handling policy of
288+
rejection is the JDK `AbortPolicy` which raises the `RejectedExecutionException`. However, in the case
289+
of `ConseqExecutor` (API Style 2), the policy can be customized with any `RejectedExecutionHandler` policy, e.g.:
290+
```jshelllanguage
291+
new ConseqExecutor.builder().rejectedPolicy(conseq4j.util.MorePolicies.blockingRetryPolicy()).build();
292+
```
293+
where the caller thread will block and retry until the task is put in the work queue. This temporarily alters the
294+
asynchronous semantics and imposes "back pressure" to caller thread, which may be a desired behavior in some cases.
281295

282296
## Full Disclosure - Asynchronous Conundrum
283297

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<modelVersion>4.0.0</modelVersion>
2828
<groupId>io.github.q3769</groupId>
2929
<artifactId>conseq4j</artifactId>
30-
<version>20230128.20230506.0</version>
30+
<version>20230128.20230510.0</version>
3131
<packaging>jar</packaging>
3232
<name>conseq4j</name>
3333
<description>A Java concurrent API to sequence related tasks while concurring unrelated ones</description>

src/main/java/conseq4j/execute/ConseqExecutor.java

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import javax.annotation.Nonnull;
3131
import javax.annotation.concurrent.ThreadSafe;
32+
import java.util.Map;
3233
import java.util.concurrent.*;
3334

3435
/**
@@ -41,36 +42,36 @@
4142
public final class ConseqExecutor implements SequentialExecutor {
4243
private static final int DEFAULT_CONCURRENCY = Math.max(16, Runtime.getRuntime().availableProcessors());
4344
private static final int DEFAULT_WORK_QUEUE_CAPACITY = Integer.MAX_VALUE;
44-
private final ConcurrentMap<Object, CompletableFuture<?>> sequentialExecutors;
45+
private static final ThreadPoolExecutor.AbortPolicy DEFAULT_REJECTED_HANDLER = new ThreadPoolExecutor.AbortPolicy();
46+
private final Map<Object, CompletableFuture<?>> sequentialExecutors = new ConcurrentHashMap<>();
47+
private final ExecutorService adminThreadPool = Executors.newCachedThreadPool();
4548
/**
4649
* The worker thread pool facilitates the overall async execution, independent of the submitted tasks. Any thread
4750
* from the pool can be used to execute any task, regardless of sequence keys. The pool capacity decides the overall
4851
* max parallelism of task execution.
4952
*/
5053
private final ExecutorService workerThreadPool;
51-
private final ExecutorService adminThreadPool = Executors.newCachedThreadPool();
5254

53-
private ConseqExecutor(int concurrency, int workQueueCapacity) {
54-
this.workerThreadPool =
55-
workQueueCapacity == DEFAULT_WORK_QUEUE_CAPACITY ? Executors.newFixedThreadPool(concurrency) :
56-
new ThreadPoolExecutor(concurrency,
57-
concurrency,
58-
0,
59-
TimeUnit.MILLISECONDS,
60-
new ArrayBlockingQueue<>(workQueueCapacity),
61-
new BlockingRetryHandler());
62-
sequentialExecutors = new ConcurrentHashMap<>(concurrency);
55+
private ConseqExecutor(@Nonnull Builder builder) {
56+
this(new ThreadPoolExecutor(builder.concurrency,
57+
builder.concurrency,
58+
0,
59+
TimeUnit.MILLISECONDS,
60+
builder.workQueueCapacity == DEFAULT_WORK_QUEUE_CAPACITY ? new LinkedBlockingQueue<>() :
61+
new ArrayBlockingQueue<>(builder.workQueueCapacity),
62+
Executors.defaultThreadFactory(),
63+
builder.rejectedPolicy));
6364
}
6465

65-
private ConseqExecutor(@Nonnull Builder builder) {
66-
this(builder.concurrency, builder.workQueueCapacity);
66+
private ConseqExecutor(ThreadPoolExecutor workerThreadPool) {
67+
this.workerThreadPool = workerThreadPool;
6768
}
6869

6970
/**
7071
* @return conseq executor with default concurrency
7172
*/
7273
public static @Nonnull ConseqExecutor newInstance() {
73-
return new ConseqExecutor(DEFAULT_CONCURRENCY, DEFAULT_WORK_QUEUE_CAPACITY);
74+
return new Builder().build();
7475
}
7576

7677
/**
@@ -80,7 +81,16 @@ private ConseqExecutor(@Nonnull Builder builder) {
8081
* @return conseq executor with given concurrency
8182
*/
8283
public static @Nonnull ConseqExecutor newInstance(int concurrency) {
83-
return new ConseqExecutor(concurrency, DEFAULT_WORK_QUEUE_CAPACITY);
84+
return new Builder().concurrency(concurrency).build();
85+
}
86+
87+
/**
88+
* @param workerThreadPool
89+
* the worker thread pool that facilitates the overall async execution, independent of the submitted tasks.
90+
* @return new ConseqExecutor instance backed by the specified workerThreadPool
91+
*/
92+
public static ConseqExecutor from(ThreadPoolExecutor workerThreadPool) {
93+
return new ConseqExecutor(workerThreadPool);
8494
}
8595

8696
private static <T> T call(Callable<T> task) {
@@ -155,6 +165,15 @@ public <T> CompletableFuture<T> submit(@NonNull Callable<T> task, @NonNull Objec
155165
@Override
156166
public void shutdown() {
157167
this.workerThreadPool.shutdown();
168+
while (true) {
169+
try {
170+
if (this.workerThreadPool.awaitTermination(100, TimeUnit.MILLISECONDS)) {
171+
break;
172+
}
173+
} catch (InterruptedException e) {
174+
Thread.currentThread().interrupt();
175+
}
176+
}
158177
this.adminThreadPool.shutdown();
159178
}
160179

@@ -167,51 +186,21 @@ int estimateActiveExecutorCount() {
167186
return this.sequentialExecutors.size();
168187
}
169188

170-
static class BlockingRetryHandler implements RejectedExecutionHandler {
171-
private static void blockingRetry(Runnable r, @NonNull ThreadPoolExecutor executor) {
172-
if (executor.isTerminated()) {
173-
return;
174-
}
175-
BlockingQueue<Runnable> workQueue = executor.getQueue();
176-
if (workQueue.offer(r)) {
177-
return;
178-
}
179-
boolean interrupted = false;
180-
try {
181-
while (true) {
182-
try {
183-
workQueue.put(r);
184-
break;
185-
} catch (InterruptedException e) {
186-
interrupted = true;
187-
}
188-
}
189-
} finally {
190-
if (interrupted) {
191-
Thread.currentThread().interrupt();
192-
}
193-
}
194-
}
195-
196-
@Override
197-
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
198-
blockingRetry(r, executor);
199-
}
200-
}
201-
202189
/**
203190
* {@code ConseqExecutor} builder static inner class.
204191
*/
205192
public static final class Builder {
206193
private int concurrency;
207194
private int workQueueCapacity;
195+
private RejectedExecutionHandler rejectedPolicy;
208196

209197
/**
210198
*
211199
*/
212200
public Builder() {
213201
concurrency = DEFAULT_CONCURRENCY;
214202
workQueueCapacity = DEFAULT_WORK_QUEUE_CAPACITY;
203+
rejectedPolicy = DEFAULT_REJECTED_HANDLER;
215204
}
216205

217206
/**
@@ -238,6 +227,17 @@ public Builder workQueueCapacity(int workQueueCapacity) {
238227
return this;
239228
}
240229

230+
/**
231+
* @param rejectedPolicy
232+
* handler executed by the caller thread if a task is rejected by the conseq executor, maybe e.g.
233+
* because of work queue is full. Default is {@link ThreadPoolExecutor.AbortPolicy}
234+
* @return a reference to this Builder
235+
*/
236+
public Builder rejectedPolicy(RejectedExecutionHandler rejectedPolicy) {
237+
this.rejectedPolicy = rejectedPolicy;
238+
return this;
239+
}
240+
241241
/**
242242
* Returns a {@code ConseqExecutor} built from the parameters previously set.
243243
*
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2021 Qingtian Wang
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
package conseq4j.util;
26+
27+
import java.util.concurrent.RejectedExecutionHandler;
28+
import java.util.concurrent.ThreadPoolExecutor;
29+
30+
/**
31+
*
32+
*/
33+
public class MorePolicies {
34+
private MorePolicies() {
35+
}
36+
37+
/**
38+
* @return a {@link RejectedExecutionHandler} that uses/blocks the caller thread to retry the rejected task, or drop
39+
* the task if the executor has been terminated.
40+
*/
41+
public static RejectedExecutionHandler blockingRetryPolicy() {
42+
return new BlockingRetryPolicy();
43+
}
44+
45+
/**
46+
*
47+
*/
48+
private static class BlockingRetryPolicy implements RejectedExecutionHandler {
49+
50+
@Override
51+
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
52+
if (executor.isTerminated()) {
53+
return;
54+
}
55+
try {
56+
executor.getQueue().put(r);
57+
} catch (InterruptedException e) {
58+
Thread.currentThread().interrupt();
59+
}
60+
}
61+
}
62+
}

0 commit comments

Comments
 (0)