Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3c79c9c

Browse files
committedJun 18, 2024
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

Lines changed: 27 additions & 18 deletions
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

Lines changed: 25 additions & 26 deletions
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

Lines changed: 18 additions & 5 deletions
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

Lines changed: 39 additions & 17 deletions
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
}

‎retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
import java.util.Collections;
3030
import java.util.List;
3131
import java.util.Random;
32+
import java.util.concurrent.*;
3233
import java.util.concurrent.atomic.AtomicInteger;
3334
import java.util.function.Predicate;
3435

3536
/**
36-
* Decorates {@link BusinessOperation business operation} with "retry" capabilities.
37+
* Decorates {@link BusinessOperation business operation} with "retry"
38+
* capabilities.
3739
*
3840
* @param <T> the remote op's return type
3941
*/
@@ -45,28 +47,31 @@ public final class RetryExponentialBackoff<T> implements BusinessOperation<T> {
4547
private final AtomicInteger attempts;
4648
private final Predicate<Exception> test;
4749
private final List<Exception> errors;
50+
private final ScheduledExecutorService scheduler;
4851

4952
/**
5053
* Ctor.
5154
*
5255
* @param op the {@link BusinessOperation} to retry
5356
* @param maxAttempts number of times to retry
54-
* @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions
57+
* @param maxDelay maximum delay between retries (in milliseconds)
58+
* @param ignoreTests tests to check whether the remote exception can be
59+
* ignored. No exceptions
5560
* will be ignored if no tests are given
5661
*/
5762
@SafeVarargs
5863
public RetryExponentialBackoff(
5964
BusinessOperation<T> op,
6065
int maxAttempts,
6166
long maxDelay,
62-
Predicate<Exception>... ignoreTests
63-
) {
67+
Predicate<Exception>... ignoreTests) {
6468
this.op = op;
6569
this.maxAttempts = maxAttempts;
6670
this.maxDelay = maxDelay;
6771
this.attempts = new AtomicInteger();
6872
this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false);
6973
this.errors = new ArrayList<>();
74+
this.scheduler = Executors.newScheduledThreadPool(1); // Create a single-threaded scheduled executor
7075
}
7176

7277
/**
@@ -89,6 +94,23 @@ public int attempts() {
8994

9095
@Override
9196
public T perform() throws BusinessException {
97+
try {
98+
return executeWithRetry();
99+
} finally {
100+
scheduler.shutdown(); // Shutdown the scheduler when no longer needed
101+
}
102+
}
103+
104+
private T executeWithRetry() throws BusinessException {
105+
ScheduledFuture<T> future = scheduler.schedule(this::retryOperation, 0, TimeUnit.MILLISECONDS);
106+
try {
107+
return future.get(); // Wait for the operation to complete
108+
} catch (InterruptedException | ExecutionException e) {
109+
throw new BusinessException("Retry operation failed");
110+
}
111+
}
112+
113+
private T retryOperation() throws BusinessException {
92114
do {
93115
try {
94116
return this.op.perform();
@@ -100,11 +122,12 @@ public T perform() throws BusinessException {
100122
}
101123

102124
try {
103-
var testDelay = (long) Math.pow(2, this.attempts()) * 1000 + RANDOM.nextInt(1000);
104-
var delay = Math.min(testDelay, this.maxDelay);
125+
long testDelay = (long) Math.pow(2, this.attempts()) * 1000 + RANDOM.nextInt(1000);
126+
long delay = Math.min(testDelay, this.maxDelay);
105127
Thread.sleep(delay);
106128
} catch (InterruptedException f) {
107-
//ignore
129+
Thread.currentThread().interrupt(); // Reset interrupt status
130+
throw new BusinessException("Thread interrupted while retrying operation");
108131
}
109132
}
110133
} while (true);

‎server-session/src/main/java/com/iluwatar/sessionserver/App.java

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,38 @@
3131
import java.util.HashMap;
3232
import java.util.Iterator;
3333
import java.util.Map;
34+
import java.util.concurrent.Executors;
35+
import java.util.concurrent.ScheduledExecutorService;
36+
import java.util.concurrent.TimeUnit;
3437
import lombok.extern.slf4j.Slf4j;
3538

3639
/**
37-
* The server session pattern is a behavioral design pattern concerned with assigning the responsibility
38-
* of storing session data on the server side. Within the context of stateless protocols like HTTP all
39-
* requests are isolated events independent of previous requests. In order to create sessions during
40-
* user-access for a particular web application various methods can be used, such as cookies. Cookies
41-
* are a small piece of data that can be sent between client and server on every request and response
42-
* so that the server can "remember" the previous requests. In general cookies can either store the session
43-
* data or the cookie can store a session identifier and be used to access appropriate data from a persistent
44-
* storage. In the latter case the session data is stored on the server-side and appropriate data is
40+
* The server session pattern is a behavioral design pattern concerned with
41+
* assigning the responsibility
42+
* of storing session data on the server side. Within the context of stateless
43+
* protocols like HTTP all
44+
* requests are isolated events independent of previous requests. In order to
45+
* create sessions during
46+
* user-access for a particular web application various methods can be used,
47+
* such as cookies. Cookies
48+
* are a small piece of data that can be sent between client and server on every
49+
* request and response
50+
* so that the server can "remember" the previous requests. In general cookies
51+
* can either store the session
52+
* data or the cookie can store a session identifier and be used to access
53+
* appropriate data from a persistent
54+
* storage. In the latter case the session data is stored on the server-side and
55+
* appropriate data is
4556
* identified by the cookie sent from a client's request.
4657
* This project demonstrates the latter case.
47-
* In the following example the ({@link App}) class starts a server and assigns ({@link LoginHandler})
48-
* class to handle login request. When a user logs in a session identifier is created and stored for future
49-
* requests in a list. When a user logs out the session identifier is deleted from the list along with
50-
* the appropriate user session data, which is handle by the ({@link LogoutHandler}) class.
58+
* In the following example the ({@link App}) class starts a server and assigns
59+
* ({@link LoginHandler})
60+
* class to handle login request. When a user logs in a session identifier is
61+
* created and stored for future
62+
* requests in a list. When a user logs out the session identifier is deleted
63+
* from the list along with
64+
* the appropriate user session data, which is handle by the
65+
* ({@link LogoutHandler}) class.
5166
*/
5267

5368
@Slf4j
@@ -60,11 +75,12 @@ public class App {
6075

6176
/**
6277
* Main entry point.
78+
*
6379
* @param args arguments
6480
* @throws IOException ex
6581
*/
6682
public static void main(String[] args) throws IOException {
67-
// Create HTTP server listening on port 8000
83+
// Create HTTP server listening on port 8080
6884
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
6985

7086
// Set up session management endpoints
@@ -74,38 +90,33 @@ public static void main(String[] args) throws IOException {
7490
// Start the server
7591
server.start();
7692

77-
// Start background task to check for expired sessions
93+
// Start scheduled task to check for expired sessions
7894
sessionExpirationTask();
7995

8096
LOGGER.info("Server started. Listening on port 8080...");
8197
}
8298

8399
private static void sessionExpirationTask() {
84-
new Thread(() -> {
85-
while (true) {
86-
try {
87-
LOGGER.info("Session expiration checker started...");
88-
Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time
89-
Instant currentTime = Instant.now();
90-
synchronized (sessions) {
91-
synchronized (sessionCreationTimes) {
92-
Iterator<Map.Entry<String, Instant>> iterator =
93-
sessionCreationTimes.entrySet().iterator();
94-
while (iterator.hasNext()) {
95-
Map.Entry<String, Instant> entry = iterator.next();
96-
if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) {
97-
sessions.remove(entry.getKey());
98-
iterator.remove();
99-
}
100-
}
100+
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
101+
102+
Runnable task = () -> {
103+
LOGGER.info("Session expiration checker started...");
104+
Instant currentTime = Instant.now();
105+
synchronized (sessions) {
106+
synchronized (sessionCreationTimes) {
107+
Iterator<Map.Entry<String, Instant>> iterator = sessionCreationTimes.entrySet().iterator();
108+
while (iterator.hasNext()) {
109+
Map.Entry<String, Instant> entry = iterator.next();
110+
if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) {
111+
sessions.remove(entry.getKey());
112+
iterator.remove();
101113
}
102114
}
103-
LOGGER.info("Session expiration checker finished!");
104-
} catch (InterruptedException e) {
105-
LOGGER.error("An error occurred: ", e);
106-
Thread.currentThread().interrupt();
107115
}
108116
}
109-
}).start();
117+
LOGGER.info("Session expiration checker finished!");
118+
};
119+
120+
scheduler.scheduleAtFixedRate(task, 0, SESSION_EXPIRATION_TIME, TimeUnit.MILLISECONDS);
110121
}
111-
}
122+
}

‎server-session/src/test/java/com.iluwatar.sessionserver/LoginHandlerTest.java

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -43,40 +43,40 @@
4343
*/
4444
public class LoginHandlerTest {
4545

46-
private LoginHandler loginHandler;
47-
//private Headers headers;
48-
private Map<String, Integer> sessions;
49-
private Map<String, Instant> sessionCreationTimes;
46+
private LoginHandler loginHandler;
47+
// private Headers headers;
48+
private Map<String, Integer> sessions;
49+
private Map<String, Instant> sessionCreationTimes;
5050

51-
@Mock
52-
private HttpExchange exchange;
51+
@Mock
52+
private HttpExchange exchange;
5353

54-
/**
55-
* Setup tests.
56-
*/
57-
@BeforeEach
58-
public void setUp() {
59-
MockitoAnnotations.initMocks(this);
60-
sessions = new HashMap<>();
61-
sessionCreationTimes = new HashMap<>();
62-
loginHandler = new LoginHandler(sessions, sessionCreationTimes);
63-
}
54+
/**
55+
* Setup tests.
56+
*/
57+
@BeforeEach
58+
public void setUp() {
59+
MockitoAnnotations.initMocks(this);
60+
sessions = new HashMap<>();
61+
sessionCreationTimes = new HashMap<>();
62+
loginHandler = new LoginHandler(sessions, sessionCreationTimes);
63+
}
6464

65-
@Test
66-
public void testHandle() {
65+
@Test
66+
public void testHandle() {
6767

68-
//assemble
69-
ByteArrayOutputStream outputStream =
70-
new ByteArrayOutputStream(); //Exchange object is mocked so OutputStream must be manually created
71-
when(exchange.getResponseHeaders()).thenReturn(
72-
new Headers()); //Exchange object is mocked so Header object must be manually created
73-
when(exchange.getResponseBody()).thenReturn(outputStream);
68+
// assemble
69+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); // Exchange object is mocked so OutputStream
70+
// must be manually created
71+
when(exchange.getResponseHeaders()).thenReturn(new Headers()); // Exchange object is mocked so Header object
72+
// must be manually created
73+
when(exchange.getResponseBody()).thenReturn(outputStream);
7474

75-
//act
76-
loginHandler.handle(exchange);
75+
// act
76+
loginHandler.handle(exchange);
7777

78-
//assert
79-
String[] response = outputStream.toString().split("Session ID: ");
80-
assertEquals(sessions.entrySet().toArray()[0].toString().split("=1")[0], response[1]);
81-
}
78+
// assert
79+
String[] response = outputStream.toString().split("Session ID: ");
80+
assertEquals(sessions.entrySet().toArray()[0].toString().split("=1")[0], response[1]);
81+
}
8282
}

‎twin/src/main/java/com/iluwatar/twin/BallThread.java

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@
2828
import lombok.extern.slf4j.Slf4j;
2929

3030
/**
31-
* This class is a UI thread for drawing the {@link BallItem}, and provide the method for suspend
32-
* and resume. It holds the reference of {@link BallItem} to delegate the draw task.
31+
* This class is a UI thread for drawing the {@link BallItem}, and provide the
32+
* method for suspend
33+
* and resume. It holds the reference of {@link BallItem} to delegate the draw
34+
* task.
3335
*/
3436

3537
@Slf4j
@@ -46,33 +48,45 @@ public class BallThread extends Thread {
4648
* Run the thread.
4749
*/
4850
public void run() {
49-
50-
while (isRunning) {
51-
if (!isSuspended) {
51+
synchronized (this) {
52+
while (isRunning) {
53+
while (isSuspended) {
54+
try {
55+
wait();
56+
} catch (InterruptedException e) {
57+
Thread.currentThread().interrupt();
58+
LOGGER.error("Thread was interrupted", e);
59+
return;
60+
}
61+
}
5262
twin.draw();
5363
twin.move();
54-
}
55-
try {
56-
Thread.sleep(250);
57-
} catch (InterruptedException e) {
58-
throw new RuntimeException(e);
64+
try {
65+
Thread.sleep(250);
66+
} catch (InterruptedException e) {
67+
Thread.currentThread().interrupt();
68+
LOGGER.error("Thread was interrupted during sleep", e);
69+
return;
70+
}
5971
}
6072
}
6173
}
6274

63-
public void suspendMe() {
75+
public synchronized void suspendMe() {
6476
isSuspended = true;
65-
LOGGER.info("Begin to suspend BallThread");
77+
LOGGER.info("Suspending BallThread");
6678
}
6779

68-
public void resumeMe() {
80+
public synchronized void resumeMe() {
6981
isSuspended = false;
70-
LOGGER.info("Begin to resume BallThread");
82+
notify();
83+
LOGGER.info("Resuming BallThread");
7184
}
7285

73-
public void stopMe() {
74-
this.isRunning = false;
75-
this.isSuspended = true;
86+
public synchronized void stopMe() {
87+
isRunning = false;
88+
isSuspended = false;
89+
notify();
90+
LOGGER.info("Stopping BallThread");
7691
}
7792
}
78-

0 commit comments

Comments
 (0)
Please sign in to comment.