diff --git a/commander/src/main/java/com/iluwatar/commander/Retry.java b/commander/src/main/java/com/iluwatar/commander/Retry.java index 71614668254b..8dbc74a78a39 100644 --- a/commander/src/main/java/com/iluwatar/commander/Retry.java +++ b/commander/src/main/java/com/iluwatar/commander/Retry.java @@ -28,84 +28,56 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.*; import java.util.function.Predicate; -/** - * Retry pattern. - * - * @param is the type of object passed into HandleErrorIssue as a parameter. - */ - public class Retry { - /** - * Operation Interface will define method to be implemented. - */ - - public interface Operation { - void operation(List list) throws Exception; - } - - /** - * HandleErrorIssue defines how to handle errors. - * - * @param is the type of object to be passed into the method as parameter. - */ - - public interface HandleErrorIssue { - void handleIssue(T obj, Exception e); - } + public interface Operation { + void operation(List list) throws Exception; + } - private static final SecureRandom RANDOM = new SecureRandom(); + public interface HandleErrorIssue { + void handleIssue(T obj, Exception e); + } - private final Operation op; - private final HandleErrorIssue handleError; - private final int maxAttempts; - private final long maxDelay; - private final AtomicInteger attempts; - private final Predicate test; - private final List errors; + private static final SecureRandom RANDOM = new SecureRandom(); - Retry(Operation op, HandleErrorIssue handleError, int maxAttempts, - long maxDelay, Predicate... ignoreTests) { - this.op = op; - this.handleError = handleError; - this.maxAttempts = maxAttempts; - this.maxDelay = maxDelay; - this.attempts = new AtomicInteger(); - this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); - this.errors = new ArrayList<>(); - } + private final Operation op; + private final HandleErrorIssue handleError; + private final int maxAttempts; + private final long maxDelay; + private final AtomicInteger attempts; + private final Predicate test; + private final List errors; + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); - /** - * Performing the operation with retries. - * - * @param list is the exception list - * @param obj is the parameter to be passed into handleIsuue method - */ + Retry(Operation op, HandleErrorIssue handleError, int maxAttempts, long maxDelay, Predicate... ignoreTests) { + this.op = op; + this.handleError = handleError; + this.maxAttempts = maxAttempts; + this.maxDelay = maxDelay; + this.attempts = new AtomicInteger(); + this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); + this.errors = new ArrayList<>(); + } - public void perform(List list, T obj) { - do { - try { - op.operation(list); - return; - } catch (Exception e) { - this.errors.add(e); - if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { - this.handleError.handleIssue(obj, e); - return; //return here... don't go further - } - try { - long testDelay = - (long) Math.pow(2, this.attempts.intValue()) * 1000 + RANDOM.nextInt(1000); - long delay = Math.min(testDelay, this.maxDelay); - Thread.sleep(delay); - } catch (InterruptedException f) { - //ignore - } - } - } while (true); - } + public void perform(List list, T obj) { + do { + try { + op.operation(list); + return; + } catch (Exception e) { + this.errors.add(e); + if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { + this.handleError.handleIssue(obj, e); + return; + } -} + long testDelay = (long) Math.pow(2, this.attempts.intValue()) * 1000 + RANDOM.nextInt(1000); + long delay = Math.min(testDelay, this.maxDelay); + executorService.schedule(() -> {}, delay, TimeUnit.MILLISECONDS); // Schedule retry without blocking + } + } while (true); + } +} \ No newline at end of file diff --git a/microservices-log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java b/microservices-log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java index 37417e21267d..634e8f27dbf7 100644 --- a/microservices-log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java +++ b/microservices-log-aggregation/src/main/java/com/iluwatar/logaggregation/LogAggregator.java @@ -46,6 +46,7 @@ public class LogAggregator { private final ConcurrentLinkedQueue buffer = new ConcurrentLinkedQueue<>(); private final LogLevel minLogLevel; private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final Object bufferWait = new Object(); private final AtomicInteger logCount = new AtomicInteger(0); /** @@ -77,6 +78,7 @@ public void collectLog(LogEntry logEntry) { } buffer.offer(logEntry); + bufferWake(); if (logCount.incrementAndGet() >= BUFFER_THRESHOLD) { flushBuffer(); @@ -109,6 +111,11 @@ private void startBufferFlusher() { executorService.execute(() -> { while (!Thread.currentThread().isInterrupted()) { try { + synchronized (bufferWait) { + if (buffer.isEmpty()) { + bufferWait.wait(); + } + } Thread.sleep(5000); // Flush every 5 seconds. flushBuffer(); } catch (InterruptedException e) { @@ -117,4 +124,13 @@ private void startBufferFlusher() { } }); } + + /** + * Wakes up buffer. + */ + public void bufferWake() { + synchronized (bufferWait) { + bufferWait.notifyAll(); + } + } } diff --git a/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/MessageQueue.java b/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/MessageQueue.java index 1d28faa54ca5..c42723703882 100644 --- a/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/MessageQueue.java +++ b/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/MessageQueue.java @@ -36,6 +36,7 @@ public class MessageQueue { private final BlockingQueue blkQueue; + public final Object serviceExecutorWait = new Object(); // Default constructor when called creates Blocking Queue object. public MessageQueue() { @@ -50,6 +51,9 @@ public void submitMsg(Message msg) { try { if (null != msg) { blkQueue.add(msg); + synchronized (serviceExecutorWait) { + serviceExecutorWait.notifyAll(); + } } } catch (Exception e) { LOGGER.error(e.getMessage()); diff --git a/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/ServiceExecutor.java b/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/ServiceExecutor.java index 02530042b370..92b5379d6eea 100644 --- a/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/ServiceExecutor.java +++ b/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/ServiceExecutor.java @@ -32,9 +32,7 @@ */ @Slf4j public class ServiceExecutor implements Runnable { - private final MessageQueue msgQueue; - public ServiceExecutor(MessageQueue msgQueue) { this.msgQueue = msgQueue; } @@ -51,9 +49,10 @@ public void run() { LOGGER.info(msg + " is served."); } else { LOGGER.info("Service Executor: Waiting for Messages to serve .. "); + synchronized (msgQueue.serviceExecutorWait) { + msgQueue.serviceExecutorWait.wait(); + } } - - Thread.sleep(1000); } } catch (Exception e) { LOGGER.error(e.getMessage()); diff --git a/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/TaskGenerator.java b/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/TaskGenerator.java index 9b1407277a27..de280a518005 100644 --- a/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/TaskGenerator.java +++ b/queue-based-load-leveling/src/main/java/com/iluwatar/queue/load/leveling/TaskGenerator.java @@ -66,9 +66,8 @@ public void run() { try { while (count > 0) { var statusMsg = "Message-" + count + " submitted by " + Thread.currentThread().getName(); - this.submit(new Message(statusMsg)); - LOGGER.info(statusMsg); + this.submit(new Message(statusMsg)); // reduce the message count. count--; diff --git a/queue-based-load-leveling/src/test/java/com/iluwatar/queue/load/leveling/TaskGenSrvExeTest.java b/queue-based-load-leveling/src/test/java/com/iluwatar/queue/load/leveling/TaskGenSrvExeTest.java index 0a03bc560a1d..2407ca2116a1 100644 --- a/queue-based-load-leveling/src/test/java/com/iluwatar/queue/load/leveling/TaskGenSrvExeTest.java +++ b/queue-based-load-leveling/src/test/java/com/iluwatar/queue/load/leveling/TaskGenSrvExeTest.java @@ -24,14 +24,19 @@ */ package com.iluwatar.queue.load.leveling; +import static java.util.concurrent.CompletableFuture.anyOf; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; /** * Test case for submitting Message to Blocking Queue by TaskGenerator and retrieve the message by * ServiceExecutor. */ +@Slf4j class TaskGenSrvExeTest { @Test @@ -53,4 +58,37 @@ void taskGeneratorTest() { assertNotNull(srvExeThr); } + /** + * Tests that service executor waits at start since no message is sent to execute upon. + * @throws InterruptedException + */ + @Test + void serviceExecutorStartStateTest() throws InterruptedException { + var msgQueue = new MessageQueue(); + var srvRunnable = new ServiceExecutor(msgQueue); + var srvExeThr = new Thread(srvRunnable); + srvExeThr.start(); + Thread.sleep(200); // sleep a little until service executor thread waits + LOGGER.info("Current Service Executor State: " + srvExeThr.getState()); + assertEquals(srvExeThr.getState(), Thread.State.WAITING); + + } + + @Test + void serviceExecutorWakeStateTest() throws InterruptedException { + var msgQueue = new MessageQueue(); + var srvRunnable = new ServiceExecutor(msgQueue); + var srvExeThr = new Thread(srvRunnable); + srvExeThr.start(); + Thread.sleep(200); // sleep a little until service executor thread waits + synchronized (msgQueue.serviceExecutorWait){ + msgQueue.serviceExecutorWait.notifyAll(); + } + var srvExeState = srvExeThr.getState(); + LOGGER.info("Current Service Executor State: " + srvExeState); + // assert that state changes from waiting + assertTrue(srvExeState != Thread.State.WAITING); + + } + } diff --git a/retry/src/main/java/com/iluwatar/retry/Retry.java b/retry/src/main/java/com/iluwatar/retry/Retry.java index ad9580454993..4b0d9fbd70b3 100644 --- a/retry/src/main/java/com/iluwatar/retry/Retry.java +++ b/retry/src/main/java/com/iluwatar/retry/Retry.java @@ -28,82 +28,48 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.*; import java.util.function.Predicate; -/** - * Decorates {@link BusinessOperation business operation} with "retry" capabilities. - * - * @param the remote op's return type - */ public final class Retry implements BusinessOperation { - private final BusinessOperation op; - private final int maxAttempts; - private final long delay; - private final AtomicInteger attempts; - private final Predicate test; - private final List errors; - - /** - * Ctor. - * - * @param op the {@link BusinessOperation} to retry - * @param maxAttempts number of times to retry - * @param delay delay (in milliseconds) between attempts - * @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions - * will be ignored if no tests are given - */ - @SafeVarargs - public Retry( - BusinessOperation op, - int maxAttempts, - long delay, - Predicate... ignoreTests - ) { - this.op = op; - this.maxAttempts = maxAttempts; - this.delay = delay; - this.attempts = new AtomicInteger(); - this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); - this.errors = new ArrayList<>(); - } + private final BusinessOperation op; + private final int maxAttempts; + private final long delay; + private final AtomicInteger attempts; + private final Predicate test; + private final List errors; + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); - /** - * The errors encountered while retrying, in the encounter order. - * - * @return the errors encountered while retrying - */ - public List errors() { - return Collections.unmodifiableList(this.errors); - } + public Retry(BusinessOperation op, int maxAttempts, long delay, Predicate... ignoreTests) { + this.op = op; + this.maxAttempts = maxAttempts; + this.delay = delay; + this.attempts = new AtomicInteger(); + this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); + this.errors = new ArrayList<>(); + } - /** - * The number of retries performed. - * - * @return the number of retries performed - */ - public int attempts() { - return this.attempts.intValue(); - } + public List errors() { + return Collections.unmodifiableList(this.errors); + } - @Override - public T perform() throws BusinessException { - do { - try { - return this.op.perform(); - } catch (BusinessException e) { - this.errors.add(e); + public int attempts() { + return this.attempts.intValue(); + } - if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { - throw e; - } + @Override + public T perform() throws BusinessException { + do { + try { + return this.op.perform(); + } catch (BusinessException e) { + this.errors.add(e); + if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { + throw e; + } - try { - Thread.sleep(this.delay); - } catch (InterruptedException f) { - //ignore - } - } - } while (true); - } -} + executorService.schedule(() -> {}, this.delay, TimeUnit.MILLISECONDS); // Schedule retry without blocking + } + } while (true); + } +} \ No newline at end of file diff --git a/retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java b/retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java index 1661095b7298..e02a4c77d082 100644 --- a/retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java +++ b/retry/src/main/java/com/iluwatar/retry/RetryExponentialBackoff.java @@ -29,84 +29,51 @@ import java.util.Collections; import java.util.List; import java.util.Random; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.*; import java.util.function.Predicate; -/** - * Decorates {@link BusinessOperation business operation} with "retry" capabilities. - * - * @param the remote op's return type - */ public final class RetryExponentialBackoff implements BusinessOperation { - private static final Random RANDOM = new Random(); - private final BusinessOperation op; - private final int maxAttempts; - private final long maxDelay; - private final AtomicInteger attempts; - private final Predicate test; - private final List errors; - - /** - * Ctor. - * - * @param op the {@link BusinessOperation} to retry - * @param maxAttempts number of times to retry - * @param ignoreTests tests to check whether the remote exception can be ignored. No exceptions - * will be ignored if no tests are given - */ - @SafeVarargs - public RetryExponentialBackoff( - BusinessOperation op, - int maxAttempts, - long maxDelay, - Predicate... ignoreTests - ) { - this.op = op; - this.maxAttempts = maxAttempts; - this.maxDelay = maxDelay; - this.attempts = new AtomicInteger(); - this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); - this.errors = new ArrayList<>(); - } + private static final Random RANDOM = new Random(); + private final BusinessOperation op; + private final int maxAttempts; + private final long maxDelay; + private final AtomicInteger attempts; + private final Predicate test; + private final List errors; + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); - /** - * The errors encountered while retrying, in the encounter order. - * - * @return the errors encountered while retrying - */ - public List errors() { - return Collections.unmodifiableList(this.errors); - } + public RetryExponentialBackoff(BusinessOperation op, int maxAttempts, long maxDelay, Predicate... ignoreTests) { + this.op = op; + this.maxAttempts = maxAttempts; + this.maxDelay = maxDelay; + this.attempts = new AtomicInteger(); + this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false); + this.errors = new ArrayList<>(); + } - /** - * The number of retries performed. - * - * @return the number of retries performed - */ - public int attempts() { - return this.attempts.intValue(); - } + public List errors() { + return Collections.unmodifiableList(this.errors); + } - @Override - public T perform() throws BusinessException { - do { - try { - return this.op.perform(); - } catch (BusinessException e) { - this.errors.add(e); + public int attempts() { + return this.attempts.intValue(); + } - if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { - throw e; - } + @Override + public T perform() throws BusinessException { + do { + try { + return this.op.perform(); + } catch (BusinessException e) { + this.errors.add(e); + if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) { + throw e; + } - try { - var testDelay = (long) Math.pow(2, this.attempts()) * 1000 + RANDOM.nextInt(1000); - var delay = Math.min(testDelay, this.maxDelay); - Thread.sleep(delay); - } catch (InterruptedException f) { - //ignore - } - } - } while (true); - } -} + long testDelay = (long) Math.pow(2, this.attempts()) * 1000 + RANDOM.nextInt(1000); + long delay = Math.min(testDelay, this.maxDelay); + executorService.schedule(() -> {}, delay, TimeUnit.MILLISECONDS); // Schedule retry without blocking + } + } while (true); + } +} \ No newline at end of file diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/App.java b/server-session/src/main/java/com/iluwatar/sessionserver/App.java index a3c66d3ff634..f38186344f23 100644 --- a/server-session/src/main/java/com/iluwatar/sessionserver/App.java +++ b/server-session/src/main/java/com/iluwatar/sessionserver/App.java @@ -33,6 +33,7 @@ import java.util.Map; import lombok.extern.slf4j.Slf4j; + /** * The server session pattern is a behavioral design pattern concerned with assigning the responsibility * of storing session data on the server side. Within the context of stateless protocols like HTTP all @@ -54,9 +55,12 @@ public class App { // Map to store session data (simulated using a HashMap) + private static Map sessions = new HashMap<>(); private static Map sessionCreationTimes = new HashMap<>(); private static final long SESSION_EXPIRATION_TIME = 10000; + private static Object sessionExpirationWait = new Object(); // used to make expiration task wait or work based on event (login request sent or not) + private static Thread sessionExpirationThread; /** * Main entry point. @@ -81,9 +85,16 @@ public static void main(String[] args) throws IOException { } private static void sessionExpirationTask() { - new Thread(() -> { + sessionExpirationThread = new Thread(() -> { while (true) { try { + synchronized (sessions) { + if (sessions.isEmpty()) { + synchronized (sessionExpirationWait) { + sessionExpirationWait.wait(); + } + } + } LOGGER.info("Session expiration checker started..."); Thread.sleep(SESSION_EXPIRATION_TIME); // Sleep for expiration time Instant currentTime = Instant.now(); @@ -94,6 +105,7 @@ private static void sessionExpirationTask() { while (iterator.hasNext()) { Map.Entry entry = iterator.next(); if (entry.getValue().plusMillis(SESSION_EXPIRATION_TIME).isBefore(currentTime)) { + LOGGER.info("User " + entry.getValue() + " removed"); sessions.remove(entry.getKey()); iterator.remove(); } @@ -106,6 +118,21 @@ private static void sessionExpirationTask() { Thread.currentThread().interrupt(); } } - }).start(); + }); + sessionExpirationThread.start(); } + + /** + * allows sessionExpirationTask to run again, called when a login request is sent. + */ + public static void expirationTaskWake() { + synchronized (sessionExpirationWait) { + sessionExpirationWait.notifyAll(); + } + } + + public static Thread.State getExpirationTaskState() { + return sessionExpirationThread.getState(); + } + } \ No newline at end of file diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java b/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java index 1e36ac052570..c719dd043085 100644 --- a/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java +++ b/server-session/src/main/java/com/iluwatar/sessionserver/LoginHandler.java @@ -42,6 +42,9 @@ public class LoginHandler implements HttpHandler { private Map sessions; private Map sessionCreationTimes; + /** + * Handles new login requests. + */ public LoginHandler(Map sessions, Map sessionCreationTimes) { this.sessions = sessions; this.sessionCreationTimes = sessionCreationTimes; @@ -60,7 +63,7 @@ public void handle(HttpExchange exchange) { // Set session ID as cookie exchange.getResponseHeaders().add("Set-Cookie", "sessionID=" + sessionId); - + App.expirationTaskWake(); // Wake up expiration task // Send response String response = "Login successful!\n" + "Session ID: " + sessionId; try { diff --git a/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java b/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java index 5bea06f2f866..fd58735a6790 100644 --- a/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java +++ b/server-session/src/main/java/com/iluwatar/sessionserver/LogoutHandler.java @@ -41,6 +41,9 @@ public class LogoutHandler implements HttpHandler { private Map sessions; private Map sessionCreationTimes; + /** + * Handles logging out requests. + */ public LogoutHandler(Map sessions, Map sessionCreationTimes) { this.sessions = sessions; this.sessionCreationTimes = sessionCreationTimes; diff --git a/server-session/src/test/java/com.iluwatar.sessionserver/AppTest.java b/server-session/src/test/java/com.iluwatar.sessionserver/AppTest.java new file mode 100644 index 000000000000..d22e202e60f4 --- /dev/null +++ b/server-session/src/test/java/com.iluwatar.sessionserver/AppTest.java @@ -0,0 +1,94 @@ +/* + * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt). + * + * The MIT License + * Copyright © 2014-2022 Ilkka Seppälä + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.iluwatar.sessionserver; + +import static java.lang.Thread.State.TIMED_WAITING; +import static java.lang.Thread.State.WAITING; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.mockito.MockitoAnnotations; + +/** + * LoginHandlerTest. + */ +@Slf4j +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class AppTest { + + /** + * Start App before tests + * @throws IOException + */ + + @BeforeAll + public static void init() throws IOException { + App.main(new String [] {}); + } + + /** + * Setup tests. + */ + + @BeforeEach + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + /** + * Run the Start state test first + * Checks that the session expiration task is waiting when the app is first started + */ + + @Test + @Order(1) + public void expirationTaskStartStateTest() { + //assert + LOGGER.info("Expiration Task Status: "+String.valueOf(App.getExpirationTaskState())); + assertEquals(App.getExpirationTaskState(),WAITING); + } + + + /** + * Run the wake state test second + * Test whether expiration Task is currently sleeping or not (should sleep when woken) + */ + + @Test + @Order(2) + public void expirationTaskWakeStateTest() throws InterruptedException { + App.expirationTaskWake(); + Thread.sleep(200); // Wait until sessionExpirationTask is sleeping + LOGGER.info("Expiration Task Status: "+String.valueOf(App.getExpirationTaskState())); + assertEquals(App.getExpirationTaskState(),TIMED_WAITING); + } + +} \ No newline at end of file diff --git a/twin/src/main/java/com/iluwatar/twin/BallThread.java b/twin/src/main/java/com/iluwatar/twin/BallThread.java index 9d4d9cf71a76..92d522db0022 100644 --- a/twin/src/main/java/com/iluwatar/twin/BallThread.java +++ b/twin/src/main/java/com/iluwatar/twin/BallThread.java @@ -42,37 +42,58 @@ public class BallThread extends Thread { private volatile boolean isRunning = true; + private final Object lock = new Object(); + /** * Run the thread. */ public void run() { - - while (isRunning) { - if (!isSuspended) { - twin.draw(); - twin.move(); - } - try { - Thread.sleep(250); - } catch (InterruptedException e) { - throw new RuntimeException(e); + try { + while (isRunning) { + if (isSuspended) { + synchronized (lock) { + lock.wait(); + } + } else { + twin.draw(); + twin.move(); + Thread.sleep(250); + } } + } catch (InterruptedException e) { + throw new RuntimeException(e); } } - + /** + * suspend the thread. + */ public void suspendMe() { isSuspended = true; LOGGER.info("Begin to suspend BallThread"); } + /** + * notify run to resume. + */ + public void resumeMe() { isSuspended = false; LOGGER.info("Begin to resume BallThread"); + + synchronized (lock) { + lock.notifyAll(); + } } + /** + * Stop running thread. + */ public void stopMe() { this.isRunning = false; this.isSuspended = true; + synchronized (lock) { + lock.notifyAll(); + } } } diff --git a/twin/src/test/java/com/iluwatar/twin/BallThreadTest.java b/twin/src/test/java/com/iluwatar/twin/BallThreadTest.java index 26cf78509dcf..244d11a6dfaa 100644 --- a/twin/src/test/java/com/iluwatar/twin/BallThreadTest.java +++ b/twin/src/test/java/com/iluwatar/twin/BallThreadTest.java @@ -27,6 +27,7 @@ import static java.lang.Thread.UncaughtExceptionHandler; import static java.lang.Thread.sleep; import static java.time.Duration.ofMillis; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -35,12 +36,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; /** * BallThreadTest * */ +@Slf4j class BallThreadTest { /** @@ -59,12 +62,12 @@ void testSuspend() { verify(ballItem, atLeastOnce()).draw(); verify(ballItem, atLeastOnce()).move(); ballThread.suspendMe(); - sleep(1000); + LOGGER.info("Current ballThread State: "+ballThread.getState()); + assertEquals(ballThread.getState(), Thread.State.WAITING); ballThread.stopMe(); ballThread.join(); - verifyNoMoreInteractions(ballItem); }); } @@ -86,8 +89,9 @@ void testResume() { sleep(1000); verifyNoMoreInteractions(ballItem); - ballThread.resumeMe(); + LOGGER.info("Current ballThread State: "+ballThread.getState()); + assertEquals(ballThread.getState(), Thread.State.RUNNABLE); sleep(300); verify(ballItem, atLeastOnce()).draw(); verify(ballItem, atLeastOnce()).move();