diff --git a/thread-pool/README.md b/thread-pool/README.md index d6cb11c1f..248c3a5d4 100644 --- a/thread-pool/README.md +++ b/thread-pool/README.md @@ -18,11 +18,11 @@ threads and eliminating the latency of creating new threads. ## Explanation -Real world example +Real-world example > We have a large number of relatively short tasks at hand. We need to peel huge amounts of potatoes -> and serve mighty amount of coffee cups. Creating a new thread for each task would be a waste so we -> establish a thread pool. +> and serve a mighty amount of coffee cups. Creating a new thread for each task would be a waste so +> we establish a thread pool. In plain words @@ -99,7 +99,7 @@ public class PotatoPeelingTask extends Task { } ``` -Next we present a runnable `Worker` class that the thread pool will utilize to handle all the potato +Next, we present a runnable `Worker` class that the thread pool will utilize to handle all the potato peeling and coffee making. ```java diff --git a/throttling/README.md b/throttling/README.md index 66c02a1c0..333b714f1 100644 --- a/throttling/README.md +++ b/throttling/README.md @@ -16,10 +16,12 @@ Ensure that a given client is not able to access service resources more than the ## Explanation -Real world example +Real-world example -> A large multinational corporation offers API to its customers. The API is rate-limited and each -> customer can only make certain amount of calls per second. +> A young human and an old dwarf walk into a bar. They start ordering beers from the bartender. +> The bartender immediately sees that the young human shouldn't consume too many drinks too fast +> and refuses to serve if enough time has not passed. For the old dwarf, the serving rate can +> be higher. In plain words @@ -33,30 +35,25 @@ In plain words **Programmatic Example** -Tenant class presents the clients of the API. CallsCount tracks the number of API calls per tenant. +`BarCustomer` class presents the clients of the `Bartender` API. `CallsCount` tracks the number of +calls per `BarCustomer`. ```java -public class Tenant { +public class BarCustomer { - private final String name; - private final int allowedCallsPerSecond; + @Getter + private final String name; + @Getter + private final int allowedCallsPerSecond; - public Tenant(String name, int allowedCallsPerSecond, CallsCount callsCount) { - if (allowedCallsPerSecond < 0) { - throw new InvalidParameterException("Number of calls less than 0 not allowed"); + public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) { + if (allowedCallsPerSecond < 0) { + throw new InvalidParameterException("Number of calls less than 0 not allowed"); + } + this.name = name; + this.allowedCallsPerSecond = allowedCallsPerSecond; + callsCount.addTenant(name); } - this.name = name; - this.allowedCallsPerSecond = allowedCallsPerSecond; - callsCount.addTenant(name); - } - - public String getName() { - return name; - } - - public int getAllowedCallsPerSecond() { - return allowedCallsPerSecond; - } } @Slf4j @@ -76,14 +73,14 @@ public final class CallsCount { } public void reset() { - LOGGER.debug("Resetting the map."); tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0)); + LOGGER.info("reset counters"); } } ``` -Next we introduce the service that the tenants are calling. To track the call count we use the -throttler timer. +Next, the service that the tenants are calling is introduced. To track the call count, a throttler +timer is used. ```java public interface Throttler { @@ -111,71 +108,103 @@ public class ThrottleTimerImpl implements Throttler { }, 0, throttlePeriod); } } +``` -class B2BService { +`Bartender` offers the `orderDrink` service to the `BarCustomer`s. The customers probably don't +know that the beer serving rate is limited by their appearances. - private static final Logger LOGGER = LoggerFactory.getLogger(B2BService.class); - private final CallsCount callsCount; +```java +class Bartender { - public B2BService(Throttler timer, CallsCount callsCount) { - this.callsCount = callsCount; - timer.start(); - } + private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class); + private final CallsCount callsCount; - public int dummyCustomerApi(Tenant tenant) { - var tenantName = tenant.getName(); - var count = callsCount.getCount(tenantName); - LOGGER.debug("Counter for {} : {} ", tenant.getName(), count); - if (count >= tenant.getAllowedCallsPerSecond()) { - LOGGER.error("API access per second limit reached for: {}", tenantName); - return -1; + public Bartender(Throttler timer, CallsCount callsCount) { + this.callsCount = callsCount; + timer.start(); } - callsCount.incrementCount(tenantName); - return getRandomCustomerId(); - } - private int getRandomCustomerId() { - return ThreadLocalRandom.current().nextInt(1, 10000); - } + public int orderDrink(BarCustomer barCustomer) { + var tenantName = barCustomer.getName(); + var count = callsCount.getCount(tenantName); + if (count >= barCustomer.getAllowedCallsPerSecond()) { + LOGGER.error("I'm sorry {}, you've had enough for today!", tenantName); + return -1; + } + callsCount.incrementCount(tenantName); + LOGGER.debug("Serving beer to {} : [{} consumed] ", barCustomer.getName(), count+1); + return getRandomCustomerId(); + } + + private int getRandomCustomerId() { + return ThreadLocalRandom.current().nextInt(1, 10000); + } } ``` -Now we are ready to see the full example in action. Tenant Adidas is rate-limited to 5 calls per -second and Nike to 6. +Now it is possible to see the full example in action. `BarCustomer` young human is rate-limited to 2 +calls per second and the old dwarf to 4. ```java - public static void main(String[] args) { +public static void main(String[] args) { var callsCount = new CallsCount(); - var adidas = new Tenant("Adidas", 5, callsCount); - var nike = new Tenant("Nike", 6, callsCount); + var human = new BarCustomer("young human", 2, callsCount); + var dwarf = new BarCustomer("dwarf soldier", 4, callsCount); var executorService = Executors.newFixedThreadPool(2); - executorService.execute(() -> makeServiceCalls(adidas, callsCount)); - executorService.execute(() -> makeServiceCalls(nike, callsCount)); - executorService.shutdown(); - - try { - executorService.awaitTermination(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - LOGGER.error("Executor Service terminated: {}", e.getMessage()); - } - } - private static void makeServiceCalls(Tenant tenant, CallsCount callsCount) { - var timer = new ThrottleTimerImpl(10, callsCount); - var service = new B2BService(timer, callsCount); + executorService.execute(() -> makeServiceCalls(human, callsCount)); + executorService.execute(() -> makeServiceCalls(dwarf, callsCount)); + + executorService.shutdown(); + try { + executorService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + LOGGER.error("Executor service terminated: {}", e.getMessage()); + } +} + +private static void makeServiceCalls(BarCustomer barCustomer, CallsCount callsCount) { + var timer = new ThrottleTimerImpl(1000, callsCount); + var service = new Bartender(timer, callsCount); // Sleep is introduced to keep the output in check and easy to view and analyze the results. - IntStream.range(0, 20).forEach(i -> { - service.dummyCustomerApi(tenant); - try { - Thread.sleep(1); - } catch (InterruptedException e) { - LOGGER.error("Thread interrupted: {}", e.getMessage()); - } + IntStream.range(0, 50).forEach(i -> { + service.orderDrink(barCustomer); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + LOGGER.error("Thread interrupted: {}", e.getMessage()); + } }); - } +} ``` +An excerpt from the example's console output: + +``` +18:46:36.218 [Timer-0] INFO com.iluwatar.throttling.CallsCount - reset counters +18:46:36.218 [Timer-1] INFO com.iluwatar.throttling.CallsCount - reset counters +18:46:36.242 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [1 consumed] +18:46:36.242 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [1 consumed] +18:46:36.342 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [2 consumed] +18:46:36.342 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [2 consumed] +18:46:36.443 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:36.443 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [3 consumed] +18:46:36.544 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:36.544 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [4 consumed] +18:46:36.645 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today! +18:46:36.645 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:36.745 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:36.745 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today! +18:46:36.846 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:36.846 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today! +18:46:36.947 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today! +18:46:36.947 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:37.048 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today! +18:46:37.048 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:37.148 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today! +18:46:37.148 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today! +``` ## Class diagram @@ -185,7 +214,7 @@ second and Nike to 6. The Throttling pattern should be used: -* When a service access needs to be restricted to not have high impacts on the performance of the service. +* When service access needs to be restricted not to have high impact on the performance of the service. * When multiple clients are consuming the same service resources and restriction has to be made according to the usage per client. ## Credits diff --git a/throttling/src/main/java/com/iluwatar/throttling/App.java b/throttling/src/main/java/com/iluwatar/throttling/App.java index ab8aa8601..1db6ecf93 100644 --- a/throttling/src/main/java/com/iluwatar/throttling/App.java +++ b/throttling/src/main/java/com/iluwatar/throttling/App.java @@ -34,11 +34,11 @@ import lombok.extern.slf4j.Slf4j; * complete service by users or a particular tenant. This can allow systems to continue to function * and meet service level agreements, even when an increase in demand places load on resources. *

- * In this example we have ({@link App}) as the initiating point of the service. This is a time + * In this example there is a {@link Bartender} serving beer to {@link BarCustomer}s. This is a time * based throttling, i.e. only a certain number of calls are allowed per second. *

- * ({@link Tenant}) is the Tenant POJO class with which many tenants can be created ({@link - * B2BService}) is the service which is consumed by the tenants and is throttled. + * ({@link BarCustomer}) is the service tenant class having a name and the number of calls allowed. + * ({@link Bartender}) is the service which is consumed by the tenants and is throttled. */ @Slf4j public class App { @@ -50,33 +50,35 @@ public class App { */ public static void main(String[] args) { var callsCount = new CallsCount(); - var adidas = new Tenant("Adidas", 5, callsCount); - var nike = new Tenant("Nike", 6, callsCount); + var human = new BarCustomer("young human", 2, callsCount); + var dwarf = new BarCustomer("dwarf soldier", 4, callsCount); var executorService = Executors.newFixedThreadPool(2); - executorService.execute(() -> makeServiceCalls(adidas, callsCount)); - executorService.execute(() -> makeServiceCalls(nike, callsCount)); + executorService.execute(() -> makeServiceCalls(human, callsCount)); + executorService.execute(() -> makeServiceCalls(dwarf, callsCount)); executorService.shutdown(); try { - executorService.awaitTermination(10, TimeUnit.SECONDS); + if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } } catch (InterruptedException e) { - LOGGER.error("Executor Service terminated: {}", e.getMessage()); + executorService.shutdownNow(); } } /** - * Make calls to the B2BService dummy API. + * Make calls to the bartender. */ - private static void makeServiceCalls(Tenant tenant, CallsCount callsCount) { - var timer = new ThrottleTimerImpl(10, callsCount); - var service = new B2BService(timer, callsCount); + private static void makeServiceCalls(BarCustomer barCustomer, CallsCount callsCount) { + var timer = new ThrottleTimerImpl(1000, callsCount); + var service = new Bartender(timer, callsCount); // Sleep is introduced to keep the output in check and easy to view and analyze the results. - IntStream.range(0, 20).forEach(i -> { - service.dummyCustomerApi(tenant); + IntStream.range(0, 50).forEach(i -> { + service.orderDrink(barCustomer); try { - Thread.sleep(1); + Thread.sleep(100); } catch (InterruptedException e) { LOGGER.error("Thread interrupted: {}", e.getMessage()); } diff --git a/throttling/src/main/java/com/iluwatar/throttling/Tenant.java b/throttling/src/main/java/com/iluwatar/throttling/BarCustomer.java similarity index 81% rename from throttling/src/main/java/com/iluwatar/throttling/Tenant.java rename to throttling/src/main/java/com/iluwatar/throttling/BarCustomer.java index 60a08704f..b46670861 100644 --- a/throttling/src/main/java/com/iluwatar/throttling/Tenant.java +++ b/throttling/src/main/java/com/iluwatar/throttling/BarCustomer.java @@ -25,22 +25,26 @@ package com.iluwatar.throttling; import java.security.InvalidParameterException; -/** - * A Pojo class to create a basic Tenant with the allowed calls per second. - */ -public class Tenant { +import lombok.Getter; +/** + * BarCustomer is a tenant with a name and a number of allowed calls per second. + */ +public class BarCustomer { + + @Getter private final String name; + @Getter private final int allowedCallsPerSecond; /** * Constructor. * - * @param name Name of the tenant - * @param allowedCallsPerSecond The number of calls allowed for a particular tenant. + * @param name Name of the BarCustomer + * @param allowedCallsPerSecond The number of calls allowed for this particular tenant. * @throws InvalidParameterException If number of calls is less than 0, throws exception. */ - public Tenant(String name, int allowedCallsPerSecond, CallsCount callsCount) { + public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) { if (allowedCallsPerSecond < 0) { throw new InvalidParameterException("Number of calls less than 0 not allowed"); } @@ -48,12 +52,4 @@ public class Tenant { this.allowedCallsPerSecond = allowedCallsPerSecond; callsCount.addTenant(name); } - - public String getName() { - return name; - } - - public int getAllowedCallsPerSecond() { - return allowedCallsPerSecond; - } } diff --git a/throttling/src/main/java/com/iluwatar/throttling/B2BService.java b/throttling/src/main/java/com/iluwatar/throttling/Bartender.java similarity index 75% rename from throttling/src/main/java/com/iluwatar/throttling/B2BService.java rename to throttling/src/main/java/com/iluwatar/throttling/Bartender.java index 70657afd8..e81d770d7 100644 --- a/throttling/src/main/java/com/iluwatar/throttling/B2BService.java +++ b/throttling/src/main/java/com/iluwatar/throttling/Bartender.java @@ -29,33 +29,32 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * A service which accepts a tenant and throttles the resource based on the time given to the - * tenant. + * Bartender is a service which accepts a BarCustomer (tenant) and throttles + * the resource based on the time given to the tenant. */ -class B2BService { +class Bartender { - private static final Logger LOGGER = LoggerFactory.getLogger(B2BService.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class); private final CallsCount callsCount; - public B2BService(Throttler timer, CallsCount callsCount) { + public Bartender(Throttler timer, CallsCount callsCount) { this.callsCount = callsCount; timer.start(); } /** - * Calls dummy customer api. - * + * Orders a drink from the bartender. * @return customer id which is randomly generated */ - public int dummyCustomerApi(Tenant tenant) { - var tenantName = tenant.getName(); + public int orderDrink(BarCustomer barCustomer) { + var tenantName = barCustomer.getName(); var count = callsCount.getCount(tenantName); - LOGGER.debug("Counter for {} : {} ", tenant.getName(), count); - if (count >= tenant.getAllowedCallsPerSecond()) { - LOGGER.error("API access per second limit reached for: {}", tenantName); + if (count >= barCustomer.getAllowedCallsPerSecond()) { + LOGGER.error("I'm sorry {}, you've had enough for today!", tenantName); return -1; } callsCount.incrementCount(tenantName); + LOGGER.debug("Serving beer to {} : [{} consumed] ", barCustomer.getName(), count + 1); return getRandomCustomerId(); } diff --git a/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java b/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java index 88a80d481..4196e10dd 100644 --- a/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java +++ b/throttling/src/main/java/com/iluwatar/throttling/CallsCount.java @@ -69,7 +69,7 @@ public final class CallsCount { * Resets the count of all the tenants in the map. */ public void reset() { - LOGGER.debug("Resetting the map."); tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0)); + LOGGER.info("reset counters"); } } diff --git a/throttling/src/test/java/com/iluwatar/throttling/TenantTest.java b/throttling/src/test/java/com/iluwatar/throttling/BarCustomerTest.java similarity index 94% rename from throttling/src/test/java/com/iluwatar/throttling/TenantTest.java rename to throttling/src/test/java/com/iluwatar/throttling/BarCustomerTest.java index 2ea33ec3d..9818fdf6b 100644 --- a/throttling/src/test/java/com/iluwatar/throttling/TenantTest.java +++ b/throttling/src/test/java/com/iluwatar/throttling/BarCustomerTest.java @@ -23,20 +23,21 @@ package com.iluwatar.throttling; -import static org.junit.jupiter.api.Assertions.assertThrows; +import org.junit.jupiter.api.Test; import java.security.InvalidParameterException; -import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; /** * TenantTest to test the creation of Tenant with valid parameters. */ -public class TenantTest { +public class BarCustomerTest { @Test void constructorTest() { assertThrows(InvalidParameterException.class, () -> { - new Tenant("FailTenant", -1, new CallsCount()); + new BarCustomer("sirBrave", -1, new CallsCount()); }); } } diff --git a/throttling/src/test/java/com/iluwatar/throttling/B2BServiceTest.java b/throttling/src/test/java/com/iluwatar/throttling/BartenderTest.java similarity index 89% rename from throttling/src/test/java/com/iluwatar/throttling/B2BServiceTest.java rename to throttling/src/test/java/com/iluwatar/throttling/BartenderTest.java index 93cf3efaa..b2d80fbd4 100644 --- a/throttling/src/test/java/com/iluwatar/throttling/B2BServiceTest.java +++ b/throttling/src/test/java/com/iluwatar/throttling/BartenderTest.java @@ -32,19 +32,18 @@ import org.junit.jupiter.api.Test; /** * B2BServiceTest class to test the B2BService */ -public class B2BServiceTest { +public class BartenderTest { private final CallsCount callsCount = new CallsCount(); @Test void dummyCustomerApiTest() { - var tenant = new Tenant("testTenant", 2, callsCount); + var tenant = new BarCustomer("pirate", 2, callsCount); // In order to assure that throttling limits will not be reset, we use an empty throttling implementation - var timer = (Throttler) () -> { - }; - var service = new B2BService(timer, callsCount); + var timer = (Throttler) () -> {}; + var service = new Bartender(timer, callsCount); - IntStream.range(0, 5).mapToObj(i -> tenant).forEach(service::dummyCustomerApi); + IntStream.range(0, 5).mapToObj(i -> tenant).forEach(service::orderDrink); var counter = callsCount.getCount(tenant.getName()); assertEquals(2, counter, "Counter limit must be reached"); }