Skip to content

Commit 3c79c9c

Browse files
committed
Fix busy-waiting loops iluwatar#2977
1 parent 5dce3d8 commit 3c79c9c

File tree

8 files changed

+249
-158
lines changed

8 files changed

+249
-158
lines changed

commander/src/main/java/com/iluwatar/commander/Retry.java

+27-18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.ArrayList;
2929
import java.util.Arrays;
3030
import java.util.List;
31+
import java.util.concurrent.*;
3132
import java.util.concurrent.atomic.AtomicInteger;
3233
import java.util.function.Predicate;
3334

@@ -66,46 +67,54 @@ public interface HandleErrorIssue<T> {
6667
private final AtomicInteger attempts;
6768
private final Predicate<Exception> test;
6869
private final List<Exception> errors;
70+
private final ScheduledExecutorService scheduler;
6971

7072
Retry(Operation op, HandleErrorIssue<T> handleError, int maxAttempts,
71-
long maxDelay, Predicate<Exception>... ignoreTests) {
73+
long maxDelay, Predicate<Exception>... ignoreTests) {
7274
this.op = op;
7375
this.handleError = handleError;
7476
this.maxAttempts = maxAttempts;
7577
this.maxDelay = maxDelay;
7678
this.attempts = new AtomicInteger();
7779
this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false);
7880
this.errors = new ArrayList<>();
81+
this.scheduler = Executors.newScheduledThreadPool(1);
7982
}
8083

8184
/**
8285
* Performing the operation with retries.
8386
*
8487
* @param list is the exception list
85-
* @param obj is the parameter to be passed into handleIsuue method
88+
* @param obj is the parameter to be passed into handleIssue method
8689
*/
87-
8890
public void perform(List<Exception> list, T obj) {
89-
do {
91+
attempts.set(0); // reset attempts before starting
92+
executeWithRetry(list, obj);
93+
}
94+
95+
private void executeWithRetry(List<Exception> list, T obj) {
96+
scheduler.schedule(() -> {
9097
try {
9198
op.operation(list);
92-
return;
9399
} catch (Exception e) {
94-
this.errors.add(e);
95-
if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) {
96-
this.handleError.handleIssue(obj, e);
97-
return; //return here... don't go further
98-
}
99-
try {
100-
long testDelay =
101-
(long) Math.pow(2, this.attempts.intValue()) * 1000 + RANDOM.nextInt(1000);
102-
long delay = Math.min(testDelay, this.maxDelay);
103-
Thread.sleep(delay);
104-
} catch (InterruptedException f) {
105-
//ignore
100+
errors.add(e);
101+
if (attempts.incrementAndGet() >= maxAttempts || !test.test(e)) {
102+
handleError.handleIssue(obj, e);
103+
scheduler.shutdown();
104+
} else {
105+
long testDelay = (long) Math.pow(2, attempts.intValue()) * 1000 + RANDOM.nextInt(1000);
106+
long delay = Math.min(testDelay, maxDelay);
107+
executeWithRetry(list, obj);
106108
}
107109
}
108-
} while (true);
110+
}, calculateDelay(), TimeUnit.MILLISECONDS);
109111
}
110112

113+
private long calculateDelay() {
114+
if (attempts.get() == 0) {
115+
return 0;
116+
}
117+
long testDelay = (long) Math.pow(2, attempts.intValue()) * 1000 + RANDOM.nextInt(1000);
118+
return Math.min(testDelay, maxDelay);
119+
}
111120
}

microservices-log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java

+25-26
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,18 @@
2424
*/
2525
package com.iluwatar.logaggregation;
2626

27-
import java.util.concurrent.ConcurrentLinkedQueue;
28-
import java.util.concurrent.ExecutorService;
29-
import java.util.concurrent.Executors;
30-
import java.util.concurrent.TimeUnit;
31-
import java.util.concurrent.atomic.AtomicInteger;
3227
import lombok.extern.slf4j.Slf4j;
3328

29+
import java.util.concurrent.*;
30+
import java.util.concurrent.atomic.AtomicInteger;
31+
3432
/**
3533
* Responsible for collecting and buffering logs from different services.
3634
* Once the logs reach a certain threshold or after a certain time interval,
37-
* they are flushed to the central log store. This class ensures logs are collected
38-
* and processed asynchronously and efficiently, providing both an immediate collection
35+
* they are flushed to the central log store. This class ensures logs are
36+
* collected
37+
* and processed asynchronously and efficiently, providing both an immediate
38+
* collection
3939
* and periodic flushing.
4040
*/
4141
@Slf4j
@@ -45,14 +45,14 @@ public class LogAggregator {
4545
private final CentralLogStore centralLogStore;
4646
private final ConcurrentLinkedQueue<LogEntry> buffer = new ConcurrentLinkedQueue<>();
4747
private final LogLevel minLogLevel;
48-
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
48+
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
4949
private final AtomicInteger logCount = new AtomicInteger(0);
5050

5151
/**
5252
* constructor of LogAggregator.
5353
*
5454
* @param centralLogStore central log store implement
55-
* @param minLogLevel min log level to store log
55+
* @param minLogLevel min log level to store log
5656
*/
5757
public LogAggregator(CentralLogStore centralLogStore, LogLevel minLogLevel) {
5858
this.centralLogStore = centralLogStore;
@@ -86,15 +86,23 @@ public void collectLog(LogEntry logEntry) {
8686
/**
8787
* Stops the log aggregator service and flushes any remaining logs to
8888
* the central log store.
89-
*
90-
* @throws InterruptedException If any thread has interrupted the current thread.
9189
*/
92-
public void stop() throws InterruptedException {
93-
executorService.shutdownNow();
94-
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
95-
LOGGER.error("Log aggregator did not terminate.");
90+
public void stop() {
91+
scheduler.shutdown();
92+
try {
93+
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
94+
scheduler.shutdownNow();
95+
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
96+
LOGGER.error("Log aggregator did not terminate.");
97+
}
98+
}
99+
} catch (InterruptedException e) {
100+
scheduler.shutdownNow();
101+
Thread.currentThread().interrupt();
102+
LOGGER.error("Log aggregator thread interrupted.", e);
103+
} finally {
104+
flushBuffer();
96105
}
97-
flushBuffer();
98106
}
99107

100108
private void flushBuffer() {
@@ -106,15 +114,6 @@ private void flushBuffer() {
106114
}
107115

108116
private void startBufferFlusher() {
109-
executorService.execute(() -> {
110-
while (!Thread.currentThread().isInterrupted()) {
111-
try {
112-
Thread.sleep(5000); // Flush every 5 seconds.
113-
flushBuffer();
114-
} catch (InterruptedException e) {
115-
Thread.currentThread().interrupt();
116-
}
117-
}
118-
});
117+
scheduler.scheduleAtFixedRate(this::flushBuffer, 5, 5, TimeUnit.SECONDS);
119118
}
120119
}

queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/ServiceExecutor.java

+18-5
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,21 @@
2222
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
* THE SOFTWARE.
2424
*/
25+
2526
package com.iluwatar.queue.load.leveling;
2627

2728
import lombok.extern.slf4j.Slf4j;
2829

2930
/**
30-
* ServiceExecuotr class. This class will pick up Messages one by one from the Blocking Queue and
31+
* ServiceExecutor class. This class will pick up Messages one by one from the
32+
* Blocking Queue and
3133
* process them.
3234
*/
3335
@Slf4j
3436
public class ServiceExecutor implements Runnable {
3537

3638
private final MessageQueue msgQueue;
39+
private volatile boolean isRunning = true;
3740

3841
public ServiceExecutor(MessageQueue msgQueue) {
3942
this.msgQueue = msgQueue;
@@ -42,21 +45,31 @@ public ServiceExecutor(MessageQueue msgQueue) {
4245
/**
4346
* The ServiceExecutor thread will retrieve each message and process it.
4447
*/
48+
@Override
4549
public void run() {
4650
try {
47-
while (!Thread.currentThread().isInterrupted()) {
51+
while (isRunning) {
4852
var msg = msgQueue.retrieveMsg();
4953

50-
if (null != msg) {
54+
if (msg != null) {
5155
LOGGER.info(msg + " is served.");
5256
} else {
5357
LOGGER.info("Service Executor: Waiting for Messages to serve .. ");
5458
}
5559

60+
// Simulating processing time
5661
Thread.sleep(1000);
5762
}
58-
} catch (Exception e) {
59-
LOGGER.error(e.getMessage());
63+
} catch (InterruptedException e) {
64+
Thread.currentThread().interrupt(); // Reset interrupt status
65+
LOGGER.error("ServiceExecutor thread interrupted", e);
6066
}
6167
}
68+
69+
/**
70+
* Stops the execution of the ServiceExecutor thread.
71+
*/
72+
public void stop() {
73+
isRunning = false;
74+
}
6275
}

retry/src/main/java/com/iluwatar/retry/Retry.java

+39-17
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,16 @@
2828
import java.util.Arrays;
2929
import java.util.Collections;
3030
import java.util.List;
31+
import java.util.concurrent.CompletableFuture;
32+
import java.util.concurrent.Executors;
33+
import java.util.concurrent.ScheduledExecutorService;
34+
import java.util.concurrent.TimeUnit;
3135
import java.util.concurrent.atomic.AtomicInteger;
3236
import java.util.function.Predicate;
3337

3438
/**
35-
* Decorates {@link BusinessOperation business operation} with "retry" capabilities.
39+
* Decorates {@link BusinessOperation business operation} with "retry"
40+
* capabilities.
3641
*
3742
* @param <T> the remote op's return type
3843
*/
@@ -43,29 +48,31 @@ public final class Retry<T> implements BusinessOperation<T> {
4348
private final AtomicInteger attempts;
4449
private final Predicate<Exception> test;
4550
private final List<Exception> errors;
51+
private final ScheduledExecutorService scheduler;
4652

4753
/**
4854
* Ctor.
4955
*
5056
* @param op the {@link BusinessOperation} to retry
5157
* @param maxAttempts number of times to retry
5258
* @param delay delay (in milliseconds) between attempts
53-
* @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions
59+
* @param ignoreTests tests to check whether the remote exception can be
60+
* ignored. No exceptions
5461
* will be ignored if no tests are given
5562
*/
5663
@SafeVarargs
5764
public Retry(
5865
BusinessOperation<T> op,
5966
int maxAttempts,
6067
long delay,
61-
Predicate<Exception>... ignoreTests
62-
) {
68+
Predicate<Exception>... ignoreTests) {
6369
this.op = op;
6470
this.maxAttempts = maxAttempts;
6571
this.delay = delay;
6672
this.attempts = new AtomicInteger();
6773
this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false);
6874
this.errors = new ArrayList<>();
75+
this.scheduler = Executors.newScheduledThreadPool(1);
6976
}
7077

7178
/**
@@ -88,22 +95,37 @@ public int attempts() {
8895

8996
@Override
9097
public T perform() throws BusinessException {
91-
do {
92-
try {
93-
return this.op.perform();
94-
} catch (BusinessException e) {
95-
this.errors.add(e);
98+
CompletableFuture<T> future = new CompletableFuture<>();
99+
performWithRetry(future);
100+
try {
101+
return future.get();
102+
} catch (Exception e) {
103+
throw new BusinessException("Operation failed after retries");
104+
} finally {
105+
scheduler.shutdown();
106+
}
107+
}
96108

109+
private void performWithRetry(CompletableFuture<T> future) {
110+
scheduler.schedule(() -> {
111+
try {
112+
future.complete(this.op.perform());
113+
} catch (Exception e) {
114+
this.errors.add((Exception) e);
97115
if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) {
98-
throw e;
99-
}
100-
101-
try {
102-
Thread.sleep(this.delay);
103-
} catch (InterruptedException f) {
104-
//ignore
116+
future.completeExceptionally(e);
117+
scheduler.shutdown();
118+
} else {
119+
performWithRetry(future);
105120
}
106121
}
107-
} while (true);
122+
}, calculateDelay(), TimeUnit.MILLISECONDS);
123+
}
124+
125+
private long calculateDelay() {
126+
if (attempts.get() == 0) {
127+
return 0;
128+
}
129+
return delay;
108130
}
109131
}

0 commit comments

Comments
 (0)