From 30cb9af12b910e44f686f79cb4aae2b85d16d800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ilkka=20Sepp=C3=A4l=C3=A4?= Date: Mon, 6 May 2024 18:55:50 +0300 Subject: [PATCH] docs: improve leader/followers --- leader-followers/README.md | 204 ++++++++++++------ .../com/iluwatar/leaderfollowers/Task.java | 19 +- .../iluwatar/leaderfollowers/TaskHandler.java | 3 +- .../iluwatar/leaderfollowers/WorkCenter.java | 8 +- 4 files changed, 146 insertions(+), 88 deletions(-) diff --git a/leader-followers/README.md b/leader-followers/README.md index 519809788..f593c9e80 100644 --- a/leader-followers/README.md +++ b/leader-followers/README.md @@ -3,103 +3,171 @@ title: Leader/Followers category: Concurrency language: en tag: -- Performance + - Decoupling + - Performance + - Synchronization + - Thread management --- ## Intent -The Leader/Followers design pattern is a pattern used to coordinate a selection of 'workers'. It allows tasks to execute concurrently -with the Leader delegating tasks to the Follower threads for execution. It is a very common design pattern used in multithreaded -situations such as servers, and works to help prevent ambiguity around delegation of tasks. + +To manage a set of worker threads efficiently, where multiple threads take turns sharing a set of event sources in order to utilize fewer resources than one-thread-per-source. ## Explanation + Real-world Example -> The best real world example of Leader/Followers is a web server. Web servers have to be able to handle a multitude of incoming -> connections all at once. In a web server, the Leader/Followers pattern works by using the Leader to listen to incoming requests -> and accept connections. Once a connection is made to a client the Leader can then find a Follower thread to delegate the task -> to for execution and return to the client. This means that the Leader does not have to wait to finish execution before it can -> accept another incoming connection, and can focus on delegating tasks. This pattern is created to aid in concurrency of applicaitons, -> allowing for many connections to work simultaneously. + +> Imagine managing a busy restaurant with multiple servers and a single host. The host acts as the "leader" and is responsible for greeting guests, managing the waitlist, and seating guests. Once the guests are seated, the host returns to the entrance to manage new arrivals. The servers, or "followers," wait for the host to assign them tables. This assignment is done on a rotational basis where the next available server takes the next group of guests. This system ensures that the host efficiently handles the incoming flow of guests while servers focus on providing service, similar to how the Leader and Followers pattern manages threads and tasks in a software system. This approach optimizes resource utilization (in this case, staff) and ensures smooth operations during peak times, much like it optimizes thread usage in computing environments. In plain words -> You can picture the Leader as a traffic controller that has to direct traffic from one lane into 25 lanes. As a car comes in, -> the Leader sends it down a road that isn't full or busy. This car can then have its request filled, or reach its destination. -> If the Leader had to drive each car down the lane itself, the line would pile up and progress would be slow. But as the Leader -> has Followers that can also drive the cars, the Leader can focus on making the line move quickly and ensuring traffic doesn't -> back up. -Wikipedia says -> A concurrency pattern are those types of design patterns that deal with the multi-threaded programming paradigm. +> The Leader and Followers design pattern utilizes a single leader thread to distribute work among multiple follower threads, effectively managing task delegation and thread synchronization to maximize resource efficiency. + +[martinfowler.com](https://martinfowler.com/articles/patterns-of-distributed-systems/leader-follower.html) says + +> Select one server in the cluster as a leader. The leader is responsible for taking decisions on behalf of the entire cluster and propagating the decisions to all the other servers. ## Programmatic Example -This example shows Java code that sets up a Leader that listens for client requests on port 8080. Once a request is sent, -the leader will accept it and delegate it to a new Follower to execute. This means that the Leader can keep delegating, -and ensure requests are fulfilled timely. This is only pseudocode and the working code would require a more concrete implementation -of Leader and Followers. + +The Leader/Followers pattern is a concurrency pattern where one thread (the leader) waits for work to arrive, de-multiplexes, dispatches, and processes the work. Once the leader finishes processing the work, it promotes one of the follower threads to be the new leader. This pattern is useful for enhancing CPU cache affinity, minimizing locking overhead, and reducing event dispatching latency. + +In the provided code, we have a `WorkCenter` class that manages a group of `Worker` threads. One of these workers is designated as the leader and is responsible for receiving and processing tasks. Once a task is processed, the leader promotes a new leader from the remaining workers. + ```java -public class LeaderFollowerWebServer { +// WorkCenter class +public class WorkCenter { - public static void main(String[] args) throws IOException { - int port = 8080; // the port that clients can reach the leader on - int numFollowers = 5; // the amount of followers we can delegate tasks to + @Getter + private Worker leader; + private final List workers = new CopyOnWriteArrayList<>(); - ServerSocket serverSocket = new ServerSocket(port); // pseudocode for creating a socket to the server - ExecutorService executorService = Executors.newFixedThreadPool(numFollowers); // pseudocode to start execution for Followers - - System.out.println("Web server started. Listening on port " + port); - - while (true) { - Socket clientSocket = serverSocket.accept(); - // Accept a new connection and assign it to a follower thread for processing - executorService.execute(new Follower(clientSocket)); - } + // Method to create workers and set the initial 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(); + } -class Follower implements Runnable { - private final Socket clientSocket; - - public Follower(Socket clientSocket) { - this.clientSocket = clientSocket; - } - - @Override - public void run() { - try { - // handle the client request, e.g., read and write data - // this is where you would implement your request processing logic. - // we will just close the socket - clientSocket.close(); - } catch (IOException e) { - e.printStackTrace(); - } + // Method to promote a new leader + public void promoteLeader() { + Worker leader = null; + if (!workers.isEmpty()) { + leader = workers.get(0); } + this.leader = leader; + } } ``` + +In the `Worker` class, each worker is a thread that waits for tasks to process. If the worker is the leader, it processes the task and then promotes a new leader. + +```java +// Worker class +public class Worker implements Runnable { + + private final long id; + private final WorkCenter workCenter; + private final TaskSet taskSet; + private final TaskHandler taskHandler; + + @Override + public void run() { + while (!Thread.interrupted()) { + try { + if (workCenter.getLeader() != null && !workCenter.getLeader().equals(this)) { + synchronized (workCenter) { + if (workCenter.getLeader() != null && !workCenter.getLeader().equals(this)) { + workCenter.wait(); + continue; + } + } + } + final Task task = taskSet.getTask(); + synchronized (workCenter) { + workCenter.removeWorker(this); + workCenter.promoteLeader(); + workCenter.notifyAll(); + } + taskHandler.handleTask(task); + workCenter.addWorker(this); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } +} +``` + +In the `App` class, we create a `WorkCenter`, add tasks to a `TaskSet`, and then start the workers. The leader worker will start processing the tasks, and once it finishes a task, it will promote a new leader. + +```java +// App class +public class App { + + 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); + addTasks(taskSet); + startWorkers(workCenter); + } + + private static void addTasks(TaskSet taskSet) throws InterruptedException { + var rand = new SecureRandom(); + for (var i = 0; i < 5; i++) { + var time = Math.abs(rand.nextInt(1000)); + taskSet.addTask(new Task(time)); + } + } + + private static void startWorkers(WorkCenter workCenter) throws InterruptedException { + var workers = workCenter.getWorkers(); + var exec = Executors.newFixedThreadPool(workers.size()); + workers.forEach(exec::submit); + exec.awaitTermination(2, TimeUnit.SECONDS); + exec.shutdownNow(); + } +} +``` + +This is a basic example of the Leader/Followers pattern. The leader worker processes tasks and promotes a new leader once it finishes a task. The new leader then starts processing the next task, and the cycle continues. + ## Class diagram + ![Leader/Followers class diagram](./etc/leader-followers.png) ## Applicability -Use Leader-Followers pattern when -* You want to establish a concurrent application -* You want faster response times on heavy load -* You want an easily scalable program -* You want to load balance a program +* Useful in scenarios where programs need to handle multiple services on a single thread to avoid resource thrashing and to improve scalability. +* Applicable in server environments where multiple client requests must be handled concurrently with minimal resource consumption. + +## Known Uses + +* Network servers handling multiple incoming connections. +* Event-driven applications that manage a large number of input/output sources. ## Consequences -Consequences involved with using the Leader/Followers pattern -* Implementing this pattern will increase complexity of the code -* If the leader is too slow at delegating processes, some Followers may not get to execute tasks leading to a waste of resources -* There is overhead with organising and maintaining a thread pool -* Debugging is more complex +Benefits: -## Real world examples +* Reduces the number of threads and context switching, leading to better performance and lower resource utilization. +* Improves system scalability and responsiveness. -* ACE Thread Pool Reactor framework -* JAWS -* Real-time CORBA +Trade-offs: + +* Increased complexity in managing the synchronization between leader and followers. +* Potential for underutilization of resources if not correctly implemented. + +## Related Patterns + +* [Half-Sync/Half-Async](https://java-design-patterns.com/patterns/half-sync-half-async/): Leader and Followers can be seen as a variation where the synchronization aspect is divided between the leader (synchronous handling) and followers (waiting asynchronously). +* [Thread Pool](https://java-design-patterns.com/patterns/thread-pool/): Both patterns manage a pool of worker threads, but Thread Pool assigns tasks to any available thread rather than using a leader to distribute work. ## Credits -* Douglas C. Schmidt and Carlos O’Ryan - Leader/Followers \ No newline at end of file +* [Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects](https://amzn.to/3UgC24V) +* [Java Concurrency in Practice](https://amzn.to/4aRMruW) diff --git a/leader-followers/src/main/java/com/iluwatar/leaderfollowers/Task.java b/leader-followers/src/main/java/com/iluwatar/leaderfollowers/Task.java index 58b7a2ba3..e5a405f43 100644 --- a/leader-followers/src/main/java/com/iluwatar/leaderfollowers/Task.java +++ b/leader-followers/src/main/java/com/iluwatar/leaderfollowers/Task.java @@ -24,29 +24,22 @@ */ package com.iluwatar.leaderfollowers; +import lombok.Getter; +import lombok.Setter; + /** * A unit of work to be processed by the Workers. */ public class Task { + @Getter private final int time; + @Getter + @Setter 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 index 9b38b971b..60c102b65 100644 --- a/leader-followers/src/main/java/com/iluwatar/leaderfollowers/TaskHandler.java +++ b/leader-followers/src/main/java/com/iluwatar/leaderfollowers/TaskHandler.java @@ -39,7 +39,6 @@ public class TaskHandler { var time = task.getTime(); Thread.sleep(time); LOGGER.info("It takes " + time + " milliseconds to finish the task"); - task.setFinished(); + task.setFinished(true); } - } diff --git a/leader-followers/src/main/java/com/iluwatar/leaderfollowers/WorkCenter.java b/leader-followers/src/main/java/com/iluwatar/leaderfollowers/WorkCenter.java index 5976487fa..bc5796e30 100644 --- a/leader-followers/src/main/java/com/iluwatar/leaderfollowers/WorkCenter.java +++ b/leader-followers/src/main/java/com/iluwatar/leaderfollowers/WorkCenter.java @@ -26,6 +26,7 @@ package com.iluwatar.leaderfollowers; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import lombok.Getter; /** * A WorkCenter contains a leader and a list of idle workers. The leader is responsible for @@ -34,6 +35,7 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class WorkCenter { + @Getter private Worker leader; private final List workers = new CopyOnWriteArrayList<>(); @@ -56,16 +58,12 @@ public class WorkCenter { workers.remove(worker); } - public Worker getLeader() { - return leader; - } - /** * Promote a leader. */ public void promoteLeader() { Worker leader = null; - if (workers.size() > 0) { + if (!workers.isEmpty()) { leader = workers.get(0); } this.leader = leader;