Compare commits

..

2 Commits

Author SHA1 Message Date
allcontributors[bot]
850027c035 docs: update .all-contributorsrc [skip ci] 2021-12-23 18:44:43 +00:00
allcontributors[bot]
a9a82efc0e docs: update README.md [skip ci] 2021-12-23 18:44:42 +00:00
8 changed files with 124 additions and 150 deletions

View File

@@ -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 a mighty amount of coffee cups. Creating a new thread for each task would be a waste so
> we establish a thread pool.
> and serve 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

View File

@@ -16,12 +16,10 @@ Ensure that a given client is not able to access service resources more than the
## Explanation
Real-world example
Real world example
> 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.
> 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.
In plain words
@@ -35,25 +33,30 @@ In plain words
**Programmatic Example**
`BarCustomer` class presents the clients of the `Bartender` API. `CallsCount` tracks the number of
calls per `BarCustomer`.
Tenant class presents the clients of the API. CallsCount tracks the number of API calls per tenant.
```java
public class BarCustomer {
public class Tenant {
@Getter
private final String name;
@Getter
private final int allowedCallsPerSecond;
private final String name;
private final int allowedCallsPerSecond;
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);
public Tenant(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);
}
public String getName() {
return name;
}
public int getAllowedCallsPerSecond() {
return allowedCallsPerSecond;
}
}
@Slf4j
@@ -73,14 +76,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, the service that the tenants are calling is introduced. To track the call count, a throttler
timer is used.
Next we introduce the service that the tenants are calling. To track the call count we use the
throttler timer.
```java
public interface Throttler {
@@ -108,103 +111,71 @@ public class ThrottleTimerImpl implements Throttler {
}, 0, throttlePeriod);
}
}
```
`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.
class B2BService {
```java
class Bartender {
private static final Logger LOGGER = LoggerFactory.getLogger(B2BService.class);
private final CallsCount callsCount;
private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class);
private final CallsCount callsCount;
public B2BService(Throttler timer, CallsCount callsCount) {
this.callsCount = callsCount;
timer.start();
}
public Bartender(Throttler timer, CallsCount callsCount) {
this.callsCount = callsCount;
timer.start();
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;
}
callsCount.incrementCount(tenantName);
return getRandomCustomerId();
}
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);
}
private int getRandomCustomerId() {
return ThreadLocalRandom.current().nextInt(1, 10000);
}
}
```
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.
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.
```java
public static void main(String[] args) {
public static void main(String[] args) {
var callsCount = new CallsCount();
var human = new BarCustomer("young human", 2, callsCount);
var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
var adidas = new Tenant("Adidas", 5, callsCount);
var nike = new Tenant("Nike", 6, callsCount);
var executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> makeServiceCalls(human, callsCount));
executorService.execute(() -> makeServiceCalls(dwarf, callsCount));
executorService.execute(() -> makeServiceCalls(adidas, callsCount));
executorService.execute(() -> makeServiceCalls(nike, callsCount));
executorService.shutdown();
try {
executorService.awaitTermination(10, TimeUnit.SECONDS);
executorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
LOGGER.error("Executor service terminated: {}", e.getMessage());
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);
private static void makeServiceCalls(Tenant tenant, CallsCount callsCount) {
var timer = new ThrottleTimerImpl(10, callsCount);
var service = new B2BService(timer, callsCount);
// Sleep is introduced to keep the output in check and easy to view and analyze the results.
IntStream.range(0, 50).forEach(i -> {
service.orderDrink(barCustomer);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
LOGGER.error("Thread interrupted: {}", e.getMessage());
}
IntStream.range(0, 20).forEach(i -> {
service.dummyCustomerApi(tenant);
try {
Thread.sleep(1);
} 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
@@ -214,7 +185,7 @@ An excerpt from the example's console output:
The Throttling pattern should be used:
* When service access needs to be restricted not to have high impact on the performance of the service.
* When a service access needs to be restricted to not have high impacts 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

View File

@@ -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.
* <p>
* In this example there is a {@link Bartender} serving beer to {@link BarCustomer}s. This is a time
* In this example we have ({@link App}) as the initiating point of the service. This is a time
* based throttling, i.e. only a certain number of calls are allowed per second.
* </p>
* ({@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.
* ({@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.
*/
@Slf4j
public class App {
@@ -50,35 +50,33 @@ public class App {
*/
public static void main(String[] args) {
var callsCount = new CallsCount();
var human = new BarCustomer("young human", 2, callsCount);
var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
var adidas = new Tenant("Adidas", 5, callsCount);
var nike = new Tenant("Nike", 6, callsCount);
var executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> makeServiceCalls(human, callsCount));
executorService.execute(() -> makeServiceCalls(dwarf, callsCount));
executorService.execute(() -> makeServiceCalls(adidas, callsCount));
executorService.execute(() -> makeServiceCalls(nike, callsCount));
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
executorService.awaitTermination(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
executorService.shutdownNow();
LOGGER.error("Executor Service terminated: {}", e.getMessage());
}
}
/**
* Make calls to the bartender.
* Make calls to the B2BService dummy API.
*/
private static void makeServiceCalls(BarCustomer barCustomer, CallsCount callsCount) {
var timer = new ThrottleTimerImpl(1000, callsCount);
var service = new Bartender(timer, callsCount);
private static void makeServiceCalls(Tenant tenant, CallsCount callsCount) {
var timer = new ThrottleTimerImpl(10, callsCount);
var service = new B2BService(timer, callsCount);
// Sleep is introduced to keep the output in check and easy to view and analyze the results.
IntStream.range(0, 50).forEach(i -> {
service.orderDrink(barCustomer);
IntStream.range(0, 20).forEach(i -> {
service.dummyCustomerApi(tenant);
try {
Thread.sleep(100);
Thread.sleep(1);
} catch (InterruptedException e) {
LOGGER.error("Thread interrupted: {}", e.getMessage());
}

View File

@@ -29,32 +29,33 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Bartender is a service which accepts a BarCustomer (tenant) and throttles
* the resource based on the time given to the tenant.
* A service which accepts a tenant and throttles the resource based on the time given to the
* tenant.
*/
class Bartender {
class B2BService {
private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class);
private static final Logger LOGGER = LoggerFactory.getLogger(B2BService.class);
private final CallsCount callsCount;
public Bartender(Throttler timer, CallsCount callsCount) {
public B2BService(Throttler timer, CallsCount callsCount) {
this.callsCount = callsCount;
timer.start();
}
/**
* Orders a drink from the bartender.
* Calls dummy customer api.
*
* @return customer id which is randomly generated
*/
public int orderDrink(BarCustomer barCustomer) {
var tenantName = barCustomer.getName();
public int dummyCustomerApi(Tenant tenant) {
var tenantName = tenant.getName();
var count = callsCount.getCount(tenantName);
if (count >= barCustomer.getAllowedCallsPerSecond()) {
LOGGER.error("I'm sorry {}, you've had enough for today!", tenantName);
LOGGER.debug("Counter for {} : {} ", tenant.getName(), count);
if (count >= tenant.getAllowedCallsPerSecond()) {
LOGGER.error("API access per second limit reached for: {}", tenantName);
return -1;
}
callsCount.incrementCount(tenantName);
LOGGER.debug("Serving beer to {} : [{} consumed] ", barCustomer.getName(), count + 1);
return getRandomCustomerId();
}

View File

@@ -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");
}
}

View File

@@ -25,26 +25,22 @@ package com.iluwatar.throttling;
import java.security.InvalidParameterException;
import lombok.Getter;
/**
* BarCustomer is a tenant with a name and a number of allowed calls per second.
* A Pojo class to create a basic Tenant with the allowed calls per second.
*/
public class BarCustomer {
public class Tenant {
@Getter
private final String name;
@Getter
private final int allowedCallsPerSecond;
/**
* Constructor.
*
* @param name Name of the BarCustomer
* @param allowedCallsPerSecond The number of calls allowed for this particular tenant.
* @param name Name of the tenant
* @param allowedCallsPerSecond The number of calls allowed for a particular tenant.
* @throws InvalidParameterException If number of calls is less than 0, throws exception.
*/
public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) {
public Tenant(String name, int allowedCallsPerSecond, CallsCount callsCount) {
if (allowedCallsPerSecond < 0) {
throw new InvalidParameterException("Number of calls less than 0 not allowed");
}
@@ -52,4 +48,12 @@ public class BarCustomer {
this.allowedCallsPerSecond = allowedCallsPerSecond;
callsCount.addTenant(name);
}
public String getName() {
return name;
}
public int getAllowedCallsPerSecond() {
return allowedCallsPerSecond;
}
}

View File

@@ -32,18 +32,19 @@ import org.junit.jupiter.api.Test;
/**
* B2BServiceTest class to test the B2BService
*/
public class BartenderTest {
public class B2BServiceTest {
private final CallsCount callsCount = new CallsCount();
@Test
void dummyCustomerApiTest() {
var tenant = new BarCustomer("pirate", 2, callsCount);
var tenant = new Tenant("testTenant", 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 Bartender(timer, callsCount);
var timer = (Throttler) () -> {
};
var service = new B2BService(timer, callsCount);
IntStream.range(0, 5).mapToObj(i -> tenant).forEach(service::orderDrink);
IntStream.range(0, 5).mapToObj(i -> tenant).forEach(service::dummyCustomerApi);
var counter = callsCount.getCount(tenant.getName());
assertEquals(2, counter, "Counter limit must be reached");
}

View File

@@ -23,21 +23,20 @@
package com.iluwatar.throttling;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.security.InvalidParameterException;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
/**
* TenantTest to test the creation of Tenant with valid parameters.
*/
public class BarCustomerTest {
public class TenantTest {
@Test
void constructorTest() {
assertThrows(InvalidParameterException.class, () -> {
new BarCustomer("sirBrave", -1, new CallsCount());
new Tenant("FailTenant", -1, new CallsCount());
});
}
}