From be1c0b81433ab4965be95f76c072c49908ca1378 Mon Sep 17 00:00:00 2001 From: Zhang WH Date: Fri, 27 Mar 2020 03:14:44 +0800 Subject: [PATCH] Fix issue #179: Leader Followers Pattern (#1189) * add leader followers pattern * use var and streams instead in App::execute * use logger instead of printing to system output stream --- leader-followers/README.md | 32 ++++++ leader-followers/etc/leader-followers.png | Bin 0 -> 49302 bytes .../etc/leader-followers.urm.puml | 54 ++++++++++ leader-followers/pom.xml | 46 +++++++++ .../com.iluwatar.leaderfollowers/App.java | 93 +++++++++++++++++ .../com.iluwatar.leaderfollowers/Task.java | 51 ++++++++++ .../TaskHandler.java | 46 +++++++++ .../com.iluwatar.leaderfollowers/TaskSet.java | 47 +++++++++ .../WorkCenter.java | 76 ++++++++++++++ .../com.iluwatar.leaderfollowers/Worker.java | 96 ++++++++++++++++++ .../com.iluwatar.leaderfollowers/AppTest.java | 41 ++++++++ .../TaskHandlerTest.java | 42 ++++++++ .../TaskSetTest.java | 50 +++++++++ .../WorkCenterTest.java | 62 +++++++++++ pom.xml | 1 + 15 files changed, 737 insertions(+) create mode 100644 leader-followers/README.md create mode 100644 leader-followers/etc/leader-followers.png create mode 100644 leader-followers/etc/leader-followers.urm.puml create mode 100644 leader-followers/pom.xml create mode 100644 leader-followers/src/main/java/com.iluwatar.leaderfollowers/App.java create mode 100644 leader-followers/src/main/java/com.iluwatar.leaderfollowers/Task.java create mode 100644 leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskHandler.java create mode 100644 leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskSet.java create mode 100644 leader-followers/src/main/java/com.iluwatar.leaderfollowers/WorkCenter.java create mode 100644 leader-followers/src/main/java/com.iluwatar.leaderfollowers/Worker.java create mode 100644 leader-followers/src/test/java/com.iluwatar.leaderfollowers/AppTest.java create mode 100644 leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskHandlerTest.java create mode 100644 leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskSetTest.java create mode 100644 leader-followers/src/test/java/com.iluwatar.leaderfollowers/WorkCenterTest.java diff --git a/leader-followers/README.md b/leader-followers/README.md new file mode 100644 index 000000000..fa355644a --- /dev/null +++ b/leader-followers/README.md @@ -0,0 +1,32 @@ +--- +layout: pattern +title: Leader/Followers +folder: leader-followers +permalink: /patterns/leader-followers/ +categories: Concurrency +tags: + - Performance +--- + +## Intent +The Leader/Followers pattern provides a concurrency model where multiple +threads can efficiently de-multiplex events and dispatch event handlers +that process I/O handles shared by the threads. + +## Class diagram +![Leader/Followers class diagram](./etc/leader-followers.png) + +## Applicability +Use Leader-Followers pattern when + +* multiple threads take turns sharing a set of event sources in order to detect, de-multiplex, dispatch and process service requests that occur on the event sources. + +## Real world examples + +* [ACE Thread Pool Reactor framework](https://www.dre.vanderbilt.edu/~schmidt/PDF/HPL.pdf) +* [JAWS](http://www.dre.vanderbilt.edu/~schmidt/PDF/PDCP.pdf) +* [Real-time CORBA](http://www.dre.vanderbilt.edu/~schmidt/PDF/RTS.pdf) + +## Credits + +* [Douglas C. Schmidt and Carlos O’Ryan - Leader/Followers](http://www.kircher-schwanninger.de/michael/publications/lf.pdf) diff --git a/leader-followers/etc/leader-followers.png b/leader-followers/etc/leader-followers.png new file mode 100644 index 0000000000000000000000000000000000000000..de8efd9f0b6bfb6a897bfc70f8be53450dfa428c GIT binary patch literal 49302 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 literal 0 HcmV?d00001 diff --git a/leader-followers/etc/leader-followers.urm.puml b/leader-followers/etc/leader-followers.urm.puml new file mode 100644 index 000000000..fc674a518 --- /dev/null +++ b/leader-followers/etc/leader-followers.urm.puml @@ -0,0 +1,54 @@ +@startuml +package com.iluwatar.leaderfollowers { + class App { + + App() + - addTasks(taskSet : TaskSet) {static} + - execute(workCenter : WorkCenter, taskSet : TaskSet) {static} + + main(args : String[]) {static} + } + class Task { + - finished : boolean + - time : int + + Task(time : int) + + getTime() : int + + isFinished() : boolean + + setFinished() + } + class TaskHandler { + + TaskHandler() + + handleTask(task : Task) + } + class TaskSet { + - queue : BlockingQueue + + TaskSet() + + addTask(task : Task) + + getSize() : int + + getTask() : Task + } + class WorkCenter { + - leader : Worker + - workers : List + + WorkCenter() + + addWorker(worker : Worker) + + createWorkers(numberOfWorkers : int, taskSet : TaskSet, taskHandler : TaskHandler) + + getLeader() : Worker + + getWorkers() : List + + promoteLeader() + + removeWorker(worker : Worker) + } + class Worker { + - id : long + - taskHandler : TaskHandler + - taskSet : TaskSet + - workCenter : WorkCenter + + Worker(id : long, workCenter : WorkCenter, taskSet : TaskSet, taskHandler : TaskHandler) + + equals(o : Object) : boolean + + hashCode() : int + + run() + } +} +Worker --> "-taskSet" TaskSet +Worker --> "-taskHandler" TaskHandler +TaskSet --> "-queue" Task +Worker --> "-workCenter" WorkCenter +@enduml \ No newline at end of file diff --git a/leader-followers/pom.xml b/leader-followers/pom.xml new file mode 100644 index 000000000..4efdbf3ac --- /dev/null +++ b/leader-followers/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + com.iluwatar + java-design-patterns + 1.23.0-SNAPSHOT + + leader-followers + + + junit + junit + test + + + org.mockito + mockito-core + test + + + \ No newline at end of file diff --git a/leader-followers/src/main/java/com.iluwatar.leaderfollowers/App.java b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/App.java new file mode 100644 index 000000000..2a4d716ef --- /dev/null +++ b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/App.java @@ -0,0 +1,93 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import java.util.Random; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +/** + * Leader/Followers pattern is a concurrency pattern. This pattern behaves like a taxi stand where + * one of the threads acts as leader thread which listens for event from event sources, + * de-multiplexes, dispatches and handles the event. It promotes the follower to be the new leader. + * When processing completes the thread joins the followers queue, if there are no followers then it + * becomes the leader and cycle repeats again. + * + *

In this example, one of the workers becomes Leader and listens on the {@link TaskSet} for + * work. {@link TaskSet} basically acts as the source of input events for the {@link Worker}, who + * are spawned and controlled by the {@link WorkCenter} . When {@link Task} arrives then the leader + * takes the work and calls the {@link TaskHandler}. It also calls the {@link WorkCenter} to + * promotes one of the followers to be the new leader, who can then process the next work and so + * on. + * + *

The pros for this pattern are: + * It enhances CPU cache affinity and eliminates unbound allocation and data buffer sharing between + * threads by reading the request into buffer space allocated on the stack of the leader or by using + * the Thread-Specific Storage pattern [22] to allocate memory. It minimizes locking overhead by not + * exchanging data between threads, thereby reducing thread synchronization. In bound handle/thread + * associations, the leader thread dispatches the event based on the I/O handle. It can minimize + * priority inversion because no extra queuing is introduced in the server. It does not require a + * context switch to handle each event, reducing the event dispatching latency. Note that promoting + * a follower thread to fulfill the leader role requires a context switch. Programming simplicity: + * The Leader/Followers pattern simplifies the programming of concurrency models where multiple + * threads can receive requests, process responses, and de-multiplex connections using a shared + * handle set. + */ +public class App { + + /** + * The main method for the leader followers pattern. + */ + public static void main(String[] args) throws InterruptedException { + var taskSet = new TaskSet(); + var taskHandler = new TaskHandler(); + var workCenter = new WorkCenter(); + workCenter.createWorkers(4, taskSet, taskHandler); + execute(workCenter, taskSet); + } + + /** + * Start the work, dispatch tasks and stop the thread pool at last. + */ + private static void execute(WorkCenter workCenter, TaskSet taskSet) throws InterruptedException { + var workers = workCenter.getWorkers(); + var exec = Executors.newFixedThreadPool(workers.size()); + workers.forEach(exec::submit); + Thread.sleep(1000); + addTasks(taskSet); + exec.awaitTermination(2, TimeUnit.SECONDS); + exec.shutdownNow(); + } + + /** + * Add tasks. + */ + private static void addTasks(TaskSet taskSet) throws InterruptedException { + var rand = new Random(); + for (var i = 0; i < 5; i++) { + var time = Math.abs(rand.nextInt(1000)); + taskSet.addTask(new Task(time)); + } + } +} diff --git a/leader-followers/src/main/java/com.iluwatar.leaderfollowers/Task.java b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/Task.java new file mode 100644 index 000000000..7d91e40cc --- /dev/null +++ b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/Task.java @@ -0,0 +1,51 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +/** + * A unit of work to be processed by the Workers. + */ +public class Task { + + private final int time; + + private boolean finished; + + public Task(int time) { + this.time = time; + } + + public int getTime() { + return time; + } + + public void setFinished() { + this.finished = true; + } + + public boolean isFinished() { + return this.finished; + } + +} diff --git a/leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskHandler.java b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskHandler.java new file mode 100644 index 000000000..5dfd67d69 --- /dev/null +++ b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskHandler.java @@ -0,0 +1,46 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The TaskHandler is used by the {@link Worker} to process the newly arrived task. + */ +public class TaskHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(TaskHandler.class); + + /** + * This interface handles one task at a time. + */ + public void handleTask(Task task) throws InterruptedException { + var time = task.getTime(); + Thread.sleep(time); + LOGGER.info("It takes " + time + " milliseconds to finish the task"); + task.setFinished(); + } + +} diff --git a/leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskSet.java b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskSet.java new file mode 100644 index 000000000..3138427a3 --- /dev/null +++ b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/TaskSet.java @@ -0,0 +1,47 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +/** + * A TaskSet is a collection of the tasks, the leader receives task from here. + */ +public class TaskSet { + + private BlockingQueue queue = new ArrayBlockingQueue<>(100); + + public void addTask(Task task) throws InterruptedException { + queue.put(task); + } + + public Task getTask() throws InterruptedException { + return queue.take(); + } + + public int getSize() { + return queue.size(); + } +} diff --git a/leader-followers/src/main/java/com.iluwatar.leaderfollowers/WorkCenter.java b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/WorkCenter.java new file mode 100644 index 000000000..7c63d95d2 --- /dev/null +++ b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/WorkCenter.java @@ -0,0 +1,76 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A WorkCenter contains a leader and a list of idle workers. The leader is responsible for + * receiving work when it arrives. This class also provides a mechanism to promote a new leader. A + * worker once he completes his task will add himself back to the center. + */ +public class WorkCenter { + + private Worker leader; + private List workers = new CopyOnWriteArrayList<>(); + + /** + * Create workers and set leader. + */ + public void createWorkers(int numberOfWorkers, TaskSet taskSet, TaskHandler taskHandler) { + for (var id = 1; id <= numberOfWorkers; id++) { + var worker = new Worker(id, this, taskSet, taskHandler); + workers.add(worker); + } + promoteLeader(); + } + + public void addWorker(Worker worker) { + workers.add(worker); + } + + public void removeWorker(Worker worker) { + workers.remove(worker); + } + + public Worker getLeader() { + return leader; + } + + /** + * Promote a leader. + */ + public void promoteLeader() { + Worker leader = null; + if (workers.size() > 0) { + leader = workers.get(0); + } + this.leader = leader; + } + + public List getWorkers() { + return workers; + } +} diff --git a/leader-followers/src/main/java/com.iluwatar.leaderfollowers/Worker.java b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/Worker.java new file mode 100644 index 000000000..6912af89b --- /dev/null +++ b/leader-followers/src/main/java/com.iluwatar.leaderfollowers/Worker.java @@ -0,0 +1,96 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Worker implements Runnable { + + private static final Logger LOGGER = LoggerFactory.getLogger(Worker.class); + + private final long id; + private final WorkCenter workCenter; + private final TaskSet taskSet; + private final TaskHandler taskHandler; + + /** + * Constructor to create a worker which will take work from the work center. + */ + public Worker(long id, WorkCenter workCenter, TaskSet taskSet, TaskHandler taskHandler) { + super(); + this.id = id; + this.workCenter = workCenter; + this.taskSet = taskSet; + this.taskHandler = taskHandler; + } + + /** + * The leader thread listens for task. When task arrives, it promotes one of the followers to be + * the new leader. Then it handles the task and add himself back to work center. + */ + @Override + public void run() { + while (!Thread.interrupted()) { + try { + if (workCenter.getLeader() != null && !workCenter.getLeader().equals(this)) { + synchronized (workCenter) { + workCenter.wait(); + } + continue; + } + final Task task = taskSet.getTask(); + synchronized (workCenter) { + workCenter.removeWorker(this); + workCenter.promoteLeader(); + workCenter.notifyAll(); + } + taskHandler.handleTask(task); + LOGGER.info("The Worker with the ID " + id + " completed the task"); + workCenter.addWorker(this); + } catch (InterruptedException e) { + LOGGER.warn("Worker interrupted"); + return; + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Worker)) { + return false; + } + var worker = (Worker) o; + return id == worker.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/leader-followers/src/test/java/com.iluwatar.leaderfollowers/AppTest.java b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/AppTest.java new file mode 100644 index 000000000..00fbfe4d2 --- /dev/null +++ b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/AppTest.java @@ -0,0 +1,41 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import org.junit.Test; + +/** + * + * Application test + * + */ +public class AppTest { + + @Test + public void test() throws InterruptedException { + String[] args = {}; + App.main(args); + } + +} diff --git a/leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskHandlerTest.java b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskHandlerTest.java new file mode 100644 index 000000000..9fb39c922 --- /dev/null +++ b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskHandlerTest.java @@ -0,0 +1,42 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for TaskHandler + */ +public class TaskHandlerTest { + + @Test + public void testHandleTask() throws InterruptedException { + var taskHandler = new TaskHandler(); + var handle = new Task(100); + taskHandler.handleTask(handle); + Assert.assertTrue(handle.isFinished()); + } + +} diff --git a/leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskSetTest.java b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskSetTest.java new file mode 100644 index 000000000..53cb31deb --- /dev/null +++ b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/TaskSetTest.java @@ -0,0 +1,50 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for TaskSet + */ +public class TaskSetTest { + + @Test + public void testAddTask() throws InterruptedException { + var taskSet = new TaskSet(); + taskSet.addTask(new Task(10)); + Assert.assertTrue(taskSet.getSize() == 1); + } + + @Test + public void testGetTask() throws InterruptedException { + var taskSet = new TaskSet(); + taskSet.addTask(new Task(100)); + Task task = taskSet.getTask(); + Assert.assertTrue(task.getTime() == 100); + Assert.assertTrue(taskSet.getSize() == 0); + } + +} diff --git a/leader-followers/src/test/java/com.iluwatar.leaderfollowers/WorkCenterTest.java b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/WorkCenterTest.java new file mode 100644 index 000000000..045d8c3dc --- /dev/null +++ b/leader-followers/src/test/java/com.iluwatar.leaderfollowers/WorkCenterTest.java @@ -0,0 +1,62 @@ +/* + * The MIT License + * Copyright © 2014-2019 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.leaderfollowers; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Tests for WorkCenter + */ +public class WorkCenterTest { + + @Test + public void testCreateWorkers() { + var taskSet = new TaskSet(); + var taskHandler = new TaskHandler(); + var workCenter = new WorkCenter(); + workCenter.createWorkers(5, taskSet, taskHandler); + Assert.assertEquals(workCenter.getWorkers().size(), 5); + Assert.assertEquals(workCenter.getWorkers().get(0), workCenter.getLeader()); + } + + @Test + public void testNullLeader() { + var workCenter = new WorkCenter(); + workCenter.promoteLeader(); + Assert.assertNull(workCenter.getLeader()); + } + + @Test + public void testPromoteLeader() { + var taskSet = new TaskSet(); + var taskHandler = new TaskHandler(); + var workCenter = new WorkCenter(); + workCenter.createWorkers(5, taskSet, taskHandler); + workCenter.removeWorker(workCenter.getLeader()); + workCenter.promoteLeader(); + Assert.assertEquals(workCenter.getWorkers().size(), 4); + Assert.assertEquals(workCenter.getWorkers().get(0), workCenter.getLeader()); + } +} diff --git a/pom.xml b/pom.xml index d035cfe40..b81cfe8d4 100644 --- a/pom.xml +++ b/pom.xml @@ -183,6 +183,7 @@ game-loop combinator update-method + leader-followers