Compare commits
27 Commits
all-contri
...
all-contri
Author | SHA1 | Date | |
---|---|---|---|
|
4b18e223cd | ||
|
13c6de036f | ||
|
6f979d0cb2 | ||
|
f084f8bf41 | ||
|
6c95868b8d | ||
|
4c7f1b7822 | ||
|
0c44b53909 | ||
|
9ead3adf73 | ||
|
97e3a3debc | ||
|
43ed09015d | ||
|
7eee546208 | ||
|
dc31960710 | ||
|
7c0fdad5a2 | ||
|
5a8933ea17 | ||
|
d02233f0b7 | ||
|
d42bcab9fc | ||
|
77b2ff2150 | ||
|
cff072d1ca | ||
|
2332520d67 | ||
|
af1b611136 | ||
|
4ff196ce35 | ||
|
242ae6a412 | ||
|
b689fe0a26 | ||
|
7aea765dd1 | ||
|
ea49cbfe94 | ||
|
4f62070eb2 | ||
|
b29bd66369 |
@@ -1240,6 +1240,33 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "akashchandwani",
|
||||
"name": "Akash Chandwani",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/3483277?v=4",
|
||||
"profile": "https://github.com/akashchandwani",
|
||||
"contributions": [
|
||||
"review"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "manannikov",
|
||||
"name": "Pavlo Manannikov",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/7019769?v=4",
|
||||
"profile": "http://www.linkedin.com/in/manannikov",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "eimanip",
|
||||
"name": "Eiman",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/20307301?v=4",
|
||||
"profile": "https://github.com/eimanip",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 4,
|
||||
|
@@ -10,7 +10,7 @@
|
||||
[](https://sonarcloud.io/dashboard?id=iluwatar_java-design-patterns)
|
||||
[](https://gitter.im/iluwatar/java-design-patterns?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
# Introduction
|
||||
@@ -273,6 +273,11 @@ This project is licensed under the terms of the MIT license.
|
||||
<td align="center"><a href="https://github.com/Ascenio"><img src="https://avatars1.githubusercontent.com/u/7662016?v=4" width="100px;" alt=""/><br /><sub><b>Ascênio</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/pulls?q=is%3Apr+reviewed-by%3AAscenio" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center"><a href="https://www.linkedin.com/in/domenico-sibilio/"><img src="https://avatars2.githubusercontent.com/u/24280982?v=4" width="100px;" alt=""/><br /><sub><b>Domenico Sibilio</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=dsibilio" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/akashchandwani"><img src="https://avatars2.githubusercontent.com/u/3483277?v=4" width="100px;" alt=""/><br /><sub><b>Akash Chandwani</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/pulls?q=is%3Apr+reviewed-by%3Aakashchandwani" title="Reviewed Pull Requests">👀</a></td>
|
||||
<td align="center"><a href="http://www.linkedin.com/in/manannikov"><img src="https://avatars2.githubusercontent.com/u/7019769?v=4" width="100px;" alt=""/><br /><sub><b>Pavlo Manannikov</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=manannikov" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/eimanip"><img src="https://avatars0.githubusercontent.com/u/20307301?v=4" width="100px;" alt=""/><br /><sub><b>Eiman</b></sub></a><br /><a href="https://github.com/iluwatar/java-design-patterns/commits?author=eimanip" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-enable -->
|
||||
|
@@ -19,12 +19,15 @@ cannot bring the whole application down, and we can reconnect to the service as
|
||||
|
||||
Real world example
|
||||
|
||||
> Imagine a web application that has both local files/images and remote database entries to serve.
|
||||
> The database might not be responding due to a variety of reasons, so if the application keeps
|
||||
> trying to read from the database using multiple threads/processes, soon all of them will hang
|
||||
> causing our entire web application will crash. We should be able to detect this situation and show
|
||||
> the user an appropriate message so that he/she can explore other parts of the app unaffected by
|
||||
> the database failure.
|
||||
> Imagine a web application that has both local files/images and remote services that are used for
|
||||
> fetching data. These remote services may be either healthy and responsive at times, or may become
|
||||
> slow and unresponsive at some point of time due to variety of reasons. So if one of the remote
|
||||
> services is slow or not responding successfully, our application will try to fetch response from
|
||||
> the remote service using multiple threads/processes, soon all of them will hang (also called
|
||||
> [thread starvation](https://en.wikipedia.org/wiki/Starvation_(computer_science))) causing our entire web application to crash. We should be able to detect
|
||||
> this situation and show the user an appropriate message so that he/she can explore other parts of
|
||||
> the app unaffected by the remote service failure. Meanwhile, the other services that are working
|
||||
> normally, should keep functioning unaffected by this failure.
|
||||
|
||||
In plain words
|
||||
|
||||
@@ -52,40 +55,104 @@ In terms of code, the end user application is:
|
||||
|
||||
```java
|
||||
public class App {
|
||||
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
||||
|
||||
|
||||
/**
|
||||
* Program entry point.
|
||||
*
|
||||
* @param args command line args
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
var obj = new MonitoringService();
|
||||
var circuitBreaker = new CircuitBreaker(3000, 1, 2000 * 1000 * 1000);
|
||||
|
||||
var serverStartTime = System.nanoTime();
|
||||
while (true) {
|
||||
LOGGER.info(obj.localResourceResponse());
|
||||
LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime));
|
||||
LOGGER.info(circuitBreaker.getState());
|
||||
try {
|
||||
Thread.sleep(5 * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.error(e.getMessage());
|
||||
}
|
||||
|
||||
var delayedService = new DelayedRemoteService(serverStartTime, 5);
|
||||
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, 2,
|
||||
2000 * 1000 * 1000);
|
||||
|
||||
var quickService = new QuickRemoteService();
|
||||
var quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, 2,
|
||||
2000 * 1000 * 1000);
|
||||
|
||||
//Create an object of monitoring service which makes both local and remote calls
|
||||
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
|
||||
quickServiceCircuitBreaker);
|
||||
|
||||
//Fetch response from local resource
|
||||
LOGGER.info(monitoringService.localResourceResponse());
|
||||
|
||||
//Fetch response from delayed service 2 times, to meet the failure threshold
|
||||
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||
|
||||
//Fetch current state of delayed service circuit breaker after crossing failure threshold limit
|
||||
//which is OPEN now
|
||||
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||
|
||||
//Meanwhile, the delayed service is down, fetch response from the healthy quick service
|
||||
LOGGER.info(monitoringService.quickServiceResponse());
|
||||
LOGGER.info(quickServiceCircuitBreaker.getState());
|
||||
|
||||
//Wait for the delayed service to become responsive
|
||||
try {
|
||||
LOGGER.info("Waiting for delayed service to become responsive");
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//Check the state of delayed circuit breaker, should be HALF_OPEN
|
||||
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||
|
||||
//Fetch response from delayed service, which should be healthy by now
|
||||
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||
//As successful response is fetched, it should be CLOSED again.
|
||||
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The monitoring service:
|
||||
|
||||
``` java
|
||||
```java
|
||||
public class MonitoringService {
|
||||
|
||||
private final CircuitBreaker delayedService;
|
||||
|
||||
private final CircuitBreaker quickService;
|
||||
|
||||
public MonitoringService(CircuitBreaker delayedService, CircuitBreaker quickService) {
|
||||
this.delayedService = delayedService;
|
||||
this.quickService = quickService;
|
||||
}
|
||||
|
||||
//Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic
|
||||
public String localResourceResponse() {
|
||||
return "Local Service is working";
|
||||
}
|
||||
|
||||
public String remoteResourceResponse(CircuitBreaker circuitBreaker, long serverStartTime) {
|
||||
/**
|
||||
* Fetch response from the delayed service (with some simulated startup time).
|
||||
*
|
||||
* @return response string
|
||||
*/
|
||||
public String delayedServiceResponse() {
|
||||
try {
|
||||
return circuitBreaker.call("delayedService", serverStartTime);
|
||||
} catch (Exception e) {
|
||||
return this.delayedService.attemptRequest();
|
||||
} catch (RemoteServiceException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches response from a healthy service without any failure.
|
||||
*
|
||||
* @return response string
|
||||
*/
|
||||
public String quickServiceResponse() {
|
||||
try {
|
||||
return this.quickService.attemptRequest();
|
||||
} catch (RemoteServiceException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
@@ -95,72 +162,127 @@ As it can be seen, it does the call to get local resources directly, but it wrap
|
||||
remote (costly) service in a circuit breaker object, which prevents faults as follows:
|
||||
|
||||
```java
|
||||
public class CircuitBreaker {
|
||||
public class DefaultCircuitBreaker implements CircuitBreaker {
|
||||
|
||||
private final long timeout;
|
||||
private final long retryTimePeriod;
|
||||
private final RemoteService service;
|
||||
long lastFailureTime;
|
||||
private String lastFailureResponse;
|
||||
int failureCount;
|
||||
private final int failureThreshold;
|
||||
private State state;
|
||||
private final long futureTime = 1000 * 1000 * 1000 * 1000;
|
||||
|
||||
CircuitBreaker(long timeout, int failureThreshold, long retryTimePeriod) {
|
||||
/**
|
||||
* Constructor to create an instance of Circuit Breaker.
|
||||
*
|
||||
* @param timeout Timeout for the API request. Not necessary for this simple example
|
||||
* @param failureThreshold Number of failures we receive from the depended service before changing
|
||||
* state to 'OPEN'
|
||||
* @param retryTimePeriod Time period after which a new request is made to remote service for
|
||||
* status check.
|
||||
*/
|
||||
DefaultCircuitBreaker(RemoteService serviceToCall, long timeout, int failureThreshold,
|
||||
long retryTimePeriod) {
|
||||
this.service = serviceToCall;
|
||||
// We start in a closed state hoping that everything is fine
|
||||
this.state = State.CLOSED;
|
||||
this.failureThreshold = failureThreshold;
|
||||
// Timeout for the API request.
|
||||
// Used to break the calls made to remote resource if it exceeds the limit
|
||||
this.timeout = timeout;
|
||||
this.retryTimePeriod = retryTimePeriod;
|
||||
//An absurd amount of time in future which basically indicates the last failure never happened
|
||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||
this.failureCount = 0;
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
|
||||
// Reset everything to defaults
|
||||
@Override
|
||||
public void recordSuccess() {
|
||||
this.failureCount = 0;
|
||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||
this.state = State.CLOSED;
|
||||
}
|
||||
|
||||
private void recordFailure() {
|
||||
@Override
|
||||
public void recordFailure(String response) {
|
||||
failureCount = failureCount + 1;
|
||||
this.lastFailureTime = System.nanoTime();
|
||||
// Cache the failure response for returning on open state
|
||||
this.lastFailureResponse = response;
|
||||
}
|
||||
|
||||
protected void setState() {
|
||||
if (failureCount > failureThreshold) {
|
||||
|
||||
// Evaluate the current state based on failureThreshold, failureCount and lastFailureTime.
|
||||
protected void evaluateState() {
|
||||
if (failureCount >= failureThreshold) { //Then something is wrong with remote service
|
||||
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
|
||||
//We have waited long enough and should try checking if service is up
|
||||
state = State.HALF_OPEN;
|
||||
} else {
|
||||
//Service would still probably be down
|
||||
state = State.OPEN;
|
||||
}
|
||||
} else {
|
||||
//Everything is working fine
|
||||
state = State.CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
evaluateState();
|
||||
return state.name();
|
||||
}
|
||||
|
||||
public void setStateForBypass(State state) {
|
||||
|
||||
/**
|
||||
* Break the circuit beforehand if it is known service is down Or connect the circuit manually if
|
||||
* service comes online before expected.
|
||||
*
|
||||
* @param state State at which circuit is in
|
||||
*/
|
||||
@Override
|
||||
public void setState(State state) {
|
||||
this.state = state;
|
||||
switch (state) {
|
||||
case OPEN:
|
||||
this.failureCount = failureThreshold;
|
||||
this.lastFailureTime = System.nanoTime();
|
||||
break;
|
||||
case HALF_OPEN:
|
||||
this.failureCount = failureThreshold;
|
||||
this.lastFailureTime = System.nanoTime() - retryTimePeriod;
|
||||
break;
|
||||
default:
|
||||
this.failureCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public String call(String serviceToCall, long serverStartTime) throws Exception {
|
||||
setState();
|
||||
|
||||
/**
|
||||
* Executes service call.
|
||||
*
|
||||
* @return Value from the remote resource, stale response or a custom exception
|
||||
*/
|
||||
@Override
|
||||
public String attemptRequest() throws RemoteServiceException {
|
||||
evaluateState();
|
||||
if (state == State.OPEN) {
|
||||
return "This is stale response from API";
|
||||
// return cached response if the circuit is in OPEN state
|
||||
return this.lastFailureResponse;
|
||||
} else {
|
||||
if (serviceToCall.equals("delayedService")) {
|
||||
var delayedService = new DelayedService(20);
|
||||
var response = delayedService.response(serverStartTime);
|
||||
if (response.split(" ")[3].equals("working")) {
|
||||
reset();
|
||||
return response;
|
||||
} else {
|
||||
recordFailure();
|
||||
throw new Exception("Remote service not responding");
|
||||
}
|
||||
} else {
|
||||
throw new Exception("Unknown Service Name");
|
||||
// Make the API request if the circuit is not OPEN
|
||||
try {
|
||||
//In a real application, this would be run in a thread and the timeout
|
||||
//parameter of the circuit breaker would be utilized to know if service
|
||||
//is working. Here, we simulate that based on server response itself
|
||||
var response = service.call();
|
||||
// Yay!! the API responded fine. Let's reset everything.
|
||||
recordSuccess();
|
||||
return response;
|
||||
} catch (RemoteServiceException ex) {
|
||||
recordFailure(ex.getMessage());
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 79 KiB |
@@ -5,32 +5,52 @@ package com.iluwatar.circuitbreaker {
|
||||
+ App()
|
||||
+ main(args : String[]) {static}
|
||||
}
|
||||
class CircuitBreaker {
|
||||
interface CircuitBreaker {
|
||||
+ attemptRequest() : String {abstract}
|
||||
+ getState() : String {abstract}
|
||||
+ recordFailure(String) {abstract}
|
||||
+ recordSuccess() {abstract}
|
||||
+ setState(State) {abstract}
|
||||
}
|
||||
class DefaultCircuitBreaker {
|
||||
~ failureCount : int
|
||||
- failureThreshold : int
|
||||
- futureTime : long
|
||||
- lastFailureResponse : String
|
||||
~ lastFailureTime : long
|
||||
- retryTimePeriod : long
|
||||
- service : RemoteService
|
||||
- state : State
|
||||
- timeout : long
|
||||
~ CircuitBreaker(timeout : long, failureThreshold : int, retryTimePeriod : long)
|
||||
+ call(serviceToCall : String, serverStartTime : long) : String
|
||||
~ DefaultCircuitBreaker(serviceToCall : RemoteService, timeout : long, failureThreshold : int, retryTimePeriod : long)
|
||||
+ attemptRequest() : String
|
||||
# evaluateState()
|
||||
+ getState() : String
|
||||
- recordFailure()
|
||||
- reset()
|
||||
# setState()
|
||||
+ setStateForBypass(state : State)
|
||||
+ recordFailure(response : String)
|
||||
+ recordSuccess()
|
||||
+ setState(state : State)
|
||||
}
|
||||
class DelayedService {
|
||||
class DelayedRemoteService {
|
||||
- delay : int
|
||||
+ DelayedService()
|
||||
+ DelayedService(delay : int)
|
||||
+ response(serverStartTime : long) : String
|
||||
- serverStartTime : long
|
||||
+ DelayedRemoteService()
|
||||
+ DelayedRemoteService(serverStartTime : long, delay : int)
|
||||
+ call() : String
|
||||
}
|
||||
class MonitoringService {
|
||||
+ MonitoringService()
|
||||
- delayedService : CircuitBreaker
|
||||
- quickService : CircuitBreaker
|
||||
+ MonitoringService(delayedService : CircuitBreaker, quickService : CircuitBreaker)
|
||||
+ delayedServiceResponse() : String
|
||||
+ localResourceResponse() : String
|
||||
+ remoteResourceResponse(circuitBreaker : CircuitBreaker, serverStartTime : long) : String
|
||||
+ quickServiceResponse() : String
|
||||
}
|
||||
class QuickRemoteService {
|
||||
+ QuickRemoteService()
|
||||
+ call() : String
|
||||
}
|
||||
interface RemoteService {
|
||||
+ call() : String {abstract}
|
||||
}
|
||||
enum State {
|
||||
+ CLOSED {static}
|
||||
@@ -40,5 +60,10 @@ package com.iluwatar.circuitbreaker {
|
||||
+ values() : State[] {static}
|
||||
}
|
||||
}
|
||||
CircuitBreaker --> "-state" State
|
||||
DefaultCircuitBreaker --> "-state" State
|
||||
MonitoringService --> "-delayedService" CircuitBreaker
|
||||
DefaultCircuitBreaker --> "-service" RemoteService
|
||||
DefaultCircuitBreaker ..|> CircuitBreaker
|
||||
DelayedRemoteService ..|> RemoteService
|
||||
QuickRemoteService ..|> RemoteService
|
||||
@enduml
|
@@ -36,17 +36,18 @@ import org.slf4j.LoggerFactory;
|
||||
* operational again, so that we can use it
|
||||
* </p>
|
||||
* <p>
|
||||
* In this example, the circuit breaker pattern is demonstrated by using two services: {@link
|
||||
* MonitoringService} and {@link DelayedService}. The monitoring service is responsible for calling
|
||||
* two services: a local service and a remote service {@link DelayedService} , and by using the
|
||||
* circuit breaker construction we ensure that if the call to remote service is going to fail, we
|
||||
* are going to save our resources and not make the function call at all, by wrapping our call to
|
||||
* the remote service in the circuit breaker object.
|
||||
* In this example, the circuit breaker pattern is demonstrated by using three services: {@link
|
||||
* DelayedRemoteService}, {@link QuickRemoteService} and {@link MonitoringService}. The monitoring
|
||||
* service is responsible for calling three services: a local service, a quick remove service
|
||||
* {@link QuickRemoteService} and a delayed remote service {@link DelayedRemoteService} , and by
|
||||
* using the circuit breaker construction we ensure that if the call to remote service is going to
|
||||
* fail, we are going to save our resources and not make the function call at all, by wrapping our
|
||||
* call to the remote services in the {@link DefaultCircuitBreaker} implementation object.
|
||||
* </p>
|
||||
* <p>
|
||||
* This works as follows: The {@link CircuitBreaker} object can be in one of three states:
|
||||
* <b>Open</b>, <b>Closed</b> and <b>Half-Open</b>, which represents the real world circuits. If the
|
||||
* state is closed (initial), we assume everything is alright and perform the function call.
|
||||
* This works as follows: The {@link DefaultCircuitBreaker} object can be in one of three states:
|
||||
* <b>Open</b>, <b>Closed</b> and <b>Half-Open</b>, which represents the real world circuits. If
|
||||
* the state is closed (initial), we assume everything is alright and perform the function call.
|
||||
* However, every time the call fails, we note it and once it crosses a threshold, we set the state
|
||||
* to Open, preventing any further calls to the remote server. Then, after a certain retry period
|
||||
* (during which we expect thee service to recover), we make another call to the remote server and
|
||||
@@ -63,22 +64,50 @@ public class App {
|
||||
*
|
||||
* @param args command line args
|
||||
*/
|
||||
@SuppressWarnings("squid:S2189")
|
||||
public static void main(String[] args) {
|
||||
//Create an object of monitoring service which makes both local and remote calls
|
||||
var obj = new MonitoringService();
|
||||
//Set the circuit Breaker parameters
|
||||
var circuitBreaker = new CircuitBreaker(3000, 1, 2000 * 1000 * 1000);
|
||||
|
||||
var serverStartTime = System.nanoTime();
|
||||
while (true) {
|
||||
LOGGER.info(obj.localResourceResponse());
|
||||
LOGGER.info(obj.remoteResourceResponse(circuitBreaker, serverStartTime));
|
||||
LOGGER.info(circuitBreaker.getState());
|
||||
try {
|
||||
Thread.sleep(5 * 1000);
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.error(e.getMessage());
|
||||
}
|
||||
|
||||
var delayedService = new DelayedRemoteService(serverStartTime, 5);
|
||||
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000, 2,
|
||||
2000 * 1000 * 1000);
|
||||
|
||||
var quickService = new QuickRemoteService();
|
||||
var quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, 2,
|
||||
2000 * 1000 * 1000);
|
||||
|
||||
//Create an object of monitoring service which makes both local and remote calls
|
||||
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
|
||||
quickServiceCircuitBreaker);
|
||||
|
||||
//Fetch response from local resource
|
||||
LOGGER.info(monitoringService.localResourceResponse());
|
||||
|
||||
//Fetch response from delayed service 2 times, to meet the failure threshold
|
||||
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||
|
||||
//Fetch current state of delayed service circuit breaker after crossing failure threshold limit
|
||||
//which is OPEN now
|
||||
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||
|
||||
//Meanwhile, the delayed service is down, fetch response from the healthy quick service
|
||||
LOGGER.info(monitoringService.quickServiceResponse());
|
||||
LOGGER.info(quickServiceCircuitBreaker.getState());
|
||||
|
||||
//Wait for the delayed service to become responsive
|
||||
try {
|
||||
LOGGER.info("Waiting for delayed service to become responsive");
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//Check the state of delayed circuit breaker, should be HALF_OPEN
|
||||
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||
|
||||
//Fetch response from delayed service, which should be healthy by now
|
||||
LOGGER.info(monitoringService.delayedServiceResponse());
|
||||
//As successful response is fetched, it should be CLOSED again.
|
||||
LOGGER.info(delayedServiceCircuitBreaker.getState());
|
||||
}
|
||||
}
|
||||
|
@@ -24,114 +24,22 @@
|
||||
package com.iluwatar.circuitbreaker;
|
||||
|
||||
/**
|
||||
* The circuit breaker class with all configurations.
|
||||
* The Circuit breaker interface.
|
||||
*/
|
||||
public class CircuitBreaker {
|
||||
private final long timeout;
|
||||
private final long retryTimePeriod;
|
||||
long lastFailureTime;
|
||||
int failureCount;
|
||||
private final int failureThreshold;
|
||||
private State state;
|
||||
private final long futureTime = 1000 * 1000 * 1000 * 1000;
|
||||
public interface CircuitBreaker {
|
||||
|
||||
/**
|
||||
* Constructor to create an instance of Circuit Breaker.
|
||||
*
|
||||
* @param timeout Timeout for the API request. Not necessary for this simple example
|
||||
* @param failureThreshold Number of failures we receive from the depended service before changing
|
||||
* state to 'OPEN'
|
||||
* @param retryTimePeriod Time period after which a new request is made to remote service for
|
||||
* status check.
|
||||
*/
|
||||
CircuitBreaker(long timeout, int failureThreshold, long retryTimePeriod) {
|
||||
// We start in a closed state hoping that everything is fine
|
||||
this.state = State.CLOSED;
|
||||
this.failureThreshold = failureThreshold;
|
||||
// Timeout for the API request.
|
||||
// Used to break the calls made to remote resource if it exceeds the limit
|
||||
this.timeout = timeout;
|
||||
this.retryTimePeriod = retryTimePeriod;
|
||||
//An absurd amount of time in future which basically indicates the last failure never happened
|
||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||
this.failureCount = 0;
|
||||
}
|
||||
// Success response. Reset everything to defaults
|
||||
void recordSuccess();
|
||||
|
||||
//Reset everything to defaults
|
||||
private void reset() {
|
||||
this.failureCount = 0;
|
||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||
this.state = State.CLOSED;
|
||||
}
|
||||
// Failure response. Handle accordingly with response and change state if required.
|
||||
void recordFailure(String response);
|
||||
|
||||
private void recordFailure() {
|
||||
failureCount = failureCount + 1;
|
||||
this.lastFailureTime = System.nanoTime();
|
||||
}
|
||||
// Get the current state of circuit breaker
|
||||
String getState();
|
||||
|
||||
protected void setState() {
|
||||
if (failureCount > failureThreshold) { //Then something is wrong with remote service
|
||||
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
|
||||
//We have waited long enough and should try checking if service is up
|
||||
state = State.HALF_OPEN;
|
||||
} else {
|
||||
//Service would still probably be down
|
||||
state = State.OPEN;
|
||||
}
|
||||
} else {
|
||||
//Everything is working fine
|
||||
state = State.CLOSED;
|
||||
}
|
||||
}
|
||||
// Set the specific state manually.
|
||||
void setState(State state);
|
||||
|
||||
public String getState() {
|
||||
return state.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Break the circuit beforehand if it is known service is down Or connect the circuit manually if
|
||||
* service comes online before expected.
|
||||
*
|
||||
* @param state State at which circuit is in
|
||||
*/
|
||||
public void setStateForBypass(State state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes service call.
|
||||
*
|
||||
* @param serviceToCall The name of the service in String. Can be changed to data URLs in case
|
||||
* of web applications
|
||||
* @param serverStartTime Time at which actual server was started which makes calls to this
|
||||
* service
|
||||
* @return Value from the remote resource, stale response or a custom exception
|
||||
*/
|
||||
public String call(String serviceToCall, long serverStartTime) throws Exception {
|
||||
setState();
|
||||
if (state == State.OPEN) {
|
||||
// return cached response if no the circuit is in OPEN state
|
||||
return "This is stale response from API";
|
||||
} else {
|
||||
// Make the API request if the circuit is not OPEN
|
||||
if (serviceToCall.equals("delayedService")) {
|
||||
var delayedService = new DelayedService(20);
|
||||
var response = delayedService.response(serverStartTime);
|
||||
//In a real application, this would be run in a thread and the timeout
|
||||
//parameter of the circuit breaker would be utilized to know if service
|
||||
//is working. Here, we simulate that based on server response itself
|
||||
if (response.split(" ")[3].equals("working")) {
|
||||
// Yay!! the API responded fine. Let's reset everything.
|
||||
reset();
|
||||
return response;
|
||||
} else {
|
||||
// Uh-oh!! the call still failed. Let's update that in our records.
|
||||
recordFailure();
|
||||
throw new Exception("Remote service not responding");
|
||||
}
|
||||
} else {
|
||||
throw new Exception("Unknown Service Name");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Attempt to fetch response from the remote service.
|
||||
String attemptRequest() throws RemoteServiceException;
|
||||
}
|
||||
|
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
/**
|
||||
* The delay based Circuit breaker implementation that works in a
|
||||
* CLOSED->OPEN-(retry_time_period)->HALF_OPEN->CLOSED flow with some retry time period for failed
|
||||
* services and a failure threshold for service to open circuit.
|
||||
*/
|
||||
public class DefaultCircuitBreaker implements CircuitBreaker {
|
||||
|
||||
private final long timeout;
|
||||
private final long retryTimePeriod;
|
||||
private final RemoteService service;
|
||||
long lastFailureTime;
|
||||
private String lastFailureResponse;
|
||||
int failureCount;
|
||||
private final int failureThreshold;
|
||||
private State state;
|
||||
private final long futureTime = 1000 * 1000 * 1000 * 1000;
|
||||
|
||||
/**
|
||||
* Constructor to create an instance of Circuit Breaker.
|
||||
*
|
||||
* @param timeout Timeout for the API request. Not necessary for this simple example
|
||||
* @param failureThreshold Number of failures we receive from the depended service before changing
|
||||
* state to 'OPEN'
|
||||
* @param retryTimePeriod Time period after which a new request is made to remote service for
|
||||
* status check.
|
||||
*/
|
||||
DefaultCircuitBreaker(RemoteService serviceToCall, long timeout, int failureThreshold,
|
||||
long retryTimePeriod) {
|
||||
this.service = serviceToCall;
|
||||
// We start in a closed state hoping that everything is fine
|
||||
this.state = State.CLOSED;
|
||||
this.failureThreshold = failureThreshold;
|
||||
// Timeout for the API request.
|
||||
// Used to break the calls made to remote resource if it exceeds the limit
|
||||
this.timeout = timeout;
|
||||
this.retryTimePeriod = retryTimePeriod;
|
||||
//An absurd amount of time in future which basically indicates the last failure never happened
|
||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||
this.failureCount = 0;
|
||||
}
|
||||
|
||||
// Reset everything to defaults
|
||||
@Override
|
||||
public void recordSuccess() {
|
||||
this.failureCount = 0;
|
||||
this.lastFailureTime = System.nanoTime() + futureTime;
|
||||
this.state = State.CLOSED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordFailure(String response) {
|
||||
failureCount = failureCount + 1;
|
||||
this.lastFailureTime = System.nanoTime();
|
||||
// Cache the failure response for returning on open state
|
||||
this.lastFailureResponse = response;
|
||||
}
|
||||
|
||||
// Evaluate the current state based on failureThreshold, failureCount and lastFailureTime.
|
||||
protected void evaluateState() {
|
||||
if (failureCount >= failureThreshold) { //Then something is wrong with remote service
|
||||
if ((System.nanoTime() - lastFailureTime) > retryTimePeriod) {
|
||||
//We have waited long enough and should try checking if service is up
|
||||
state = State.HALF_OPEN;
|
||||
} else {
|
||||
//Service would still probably be down
|
||||
state = State.OPEN;
|
||||
}
|
||||
} else {
|
||||
//Everything is working fine
|
||||
state = State.CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
evaluateState();
|
||||
return state.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Break the circuit beforehand if it is known service is down Or connect the circuit manually if
|
||||
* service comes online before expected.
|
||||
*
|
||||
* @param state State at which circuit is in
|
||||
*/
|
||||
@Override
|
||||
public void setState(State state) {
|
||||
this.state = state;
|
||||
switch (state) {
|
||||
case OPEN:
|
||||
this.failureCount = failureThreshold;
|
||||
this.lastFailureTime = System.nanoTime();
|
||||
break;
|
||||
case HALF_OPEN:
|
||||
this.failureCount = failureThreshold;
|
||||
this.lastFailureTime = System.nanoTime() - retryTimePeriod;
|
||||
break;
|
||||
default:
|
||||
this.failureCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes service call.
|
||||
*
|
||||
* @return Value from the remote resource, stale response or a custom exception
|
||||
*/
|
||||
@Override
|
||||
public String attemptRequest() throws RemoteServiceException {
|
||||
evaluateState();
|
||||
if (state == State.OPEN) {
|
||||
// return cached response if the circuit is in OPEN state
|
||||
return this.lastFailureResponse;
|
||||
} else {
|
||||
// Make the API request if the circuit is not OPEN
|
||||
try {
|
||||
//In a real application, this would be run in a thread and the timeout
|
||||
//parameter of the circuit breaker would be utilized to know if service
|
||||
//is working. Here, we simulate that based on server response itself
|
||||
var response = service.call();
|
||||
// Yay!! the API responded fine. Let's reset everything.
|
||||
recordSuccess();
|
||||
return response;
|
||||
} catch (RemoteServiceException ex) {
|
||||
recordFailure(ex.getMessage());
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -27,7 +27,9 @@ package com.iluwatar.circuitbreaker;
|
||||
* This simulates the remote service It responds only after a certain timeout period (default set to
|
||||
* 20 seconds).
|
||||
*/
|
||||
public class DelayedService {
|
||||
public class DelayedRemoteService implements RemoteService {
|
||||
|
||||
private final long serverStartTime;
|
||||
private final int delay;
|
||||
|
||||
/**
|
||||
@@ -35,22 +37,23 @@ public class DelayedService {
|
||||
*
|
||||
* @param delay the delay after which service would behave properly, in seconds
|
||||
*/
|
||||
public DelayedService(int delay) {
|
||||
public DelayedRemoteService(long serverStartTime, int delay) {
|
||||
this.serverStartTime = serverStartTime;
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
public DelayedService() {
|
||||
this.delay = 60;
|
||||
public DelayedRemoteService() {
|
||||
this.serverStartTime = System.nanoTime();
|
||||
this.delay = 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds based on delay, current time and server start time if the service is down / working.
|
||||
*
|
||||
* @param serverStartTime Time at which actual server was started which makes calls to this
|
||||
* service
|
||||
* @return The state of the service
|
||||
*/
|
||||
public String response(long serverStartTime) {
|
||||
@Override
|
||||
public String call() throws RemoteServiceException {
|
||||
var currentTime = System.nanoTime();
|
||||
//Since currentTime and serverStartTime are both in nanoseconds, we convert it to
|
||||
//seconds by diving by 10e9 and ensure floating point division by multiplying it
|
||||
@@ -58,9 +61,8 @@ public class DelayedService {
|
||||
//send the reply
|
||||
if ((currentTime - serverStartTime) * 1.0 / (1000 * 1000 * 1000) < delay) {
|
||||
//Can use Thread.sleep() here to block and simulate a hung server
|
||||
return "Delayed service is down";
|
||||
} else {
|
||||
return "Delayed service is working";
|
||||
throw new RemoteServiceException("Delayed service is down");
|
||||
}
|
||||
return "Delayed service is working";
|
||||
}
|
||||
}
|
@@ -24,28 +24,47 @@
|
||||
package com.iluwatar.circuitbreaker;
|
||||
|
||||
/**
|
||||
* The service class which makes local and remote calls Uses {@link CircuitBreaker} object to ensure
|
||||
* remote calls don't use up resources.
|
||||
* The service class which makes local and remote calls Uses {@link DefaultCircuitBreaker} object to
|
||||
* ensure remote calls don't use up resources.
|
||||
*/
|
||||
public class MonitoringService {
|
||||
|
||||
private final CircuitBreaker delayedService;
|
||||
|
||||
private final CircuitBreaker quickService;
|
||||
|
||||
public MonitoringService(CircuitBreaker delayedService, CircuitBreaker quickService) {
|
||||
this.delayedService = delayedService;
|
||||
this.quickService = quickService;
|
||||
}
|
||||
|
||||
//Assumption: Local service won't fail, no need to wrap it in a circuit breaker logic
|
||||
public String localResourceResponse() {
|
||||
return "Local Service is working";
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get result from remote server.
|
||||
* Fetch response from the delayed service (with some simulated startup time).
|
||||
*
|
||||
* @param circuitBreaker The circuitBreaker object with all parameters
|
||||
* @param serverStartTime Time at which actual server was started which makes calls to this
|
||||
* service
|
||||
* @return result from the remote response or exception raised by it.
|
||||
* @return response string
|
||||
*/
|
||||
public String remoteResourceResponse(CircuitBreaker circuitBreaker, long serverStartTime) {
|
||||
public String delayedServiceResponse() {
|
||||
try {
|
||||
return circuitBreaker.call("delayedService", serverStartTime);
|
||||
} catch (Exception e) {
|
||||
return this.delayedService.attemptRequest();
|
||||
} catch (RemoteServiceException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches response from a healthy service without any failure.
|
||||
*
|
||||
* @return response string
|
||||
*/
|
||||
public String quickServiceResponse() {
|
||||
try {
|
||||
return this.quickService.attemptRequest();
|
||||
} catch (RemoteServiceException e) {
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
|
@@ -23,19 +23,13 @@
|
||||
|
||||
package com.iluwatar.circuitbreaker;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Monitoring Service test
|
||||
* A quick response remote service, that responds healthy without any delay or failure.
|
||||
*/
|
||||
public class DelayedServiceTest {
|
||||
public class QuickRemoteService implements RemoteService {
|
||||
|
||||
//Improves code coverage
|
||||
@Test
|
||||
public void testDefaultConstructor() {
|
||||
var obj = new DelayedService();
|
||||
assertEquals(obj.response(System.nanoTime()), "Delayed service is down");
|
||||
@Override
|
||||
public String call() throws RemoteServiceException {
|
||||
return "Quick Service is working";
|
||||
}
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
/**
|
||||
* The Remote service interface, used by {@link CircuitBreaker} for fetching response from remote
|
||||
* services.
|
||||
*/
|
||||
public interface RemoteService {
|
||||
|
||||
//Fetch response from remote service.
|
||||
String call() throws RemoteServiceException;
|
||||
}
|
@@ -21,39 +21,14 @@
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.iluwatar.command;
|
||||
package com.iluwatar.circuitbreaker;
|
||||
|
||||
/**
|
||||
* ShrinkSpell is a concrete command.
|
||||
* Exception thrown when {@link RemoteService} does not respond successfully.
|
||||
*/
|
||||
public class ShrinkSpell implements Command {
|
||||
public class RemoteServiceException extends Exception {
|
||||
|
||||
private Size oldSize;
|
||||
private Target target;
|
||||
|
||||
@Override
|
||||
public void execute(Target target) {
|
||||
oldSize = target.getSize();
|
||||
target.setSize(Size.SMALL);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undo() {
|
||||
if (oldSize != null && target != null) {
|
||||
var temp = target.getSize();
|
||||
target.setSize(oldSize);
|
||||
oldSize = temp;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void redo() {
|
||||
undo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Shrink spell";
|
||||
public RemoteServiceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* App Test showing usage of circuit breaker.
|
||||
*/
|
||||
public class AppTest {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AppTest.class);
|
||||
|
||||
//Startup delay for delayed service (in seconds)
|
||||
private static final int STARTUP_DELAY = 4;
|
||||
|
||||
//Number of failed requests for circuit breaker to open
|
||||
private static final int FAILURE_THRESHOLD = 1;
|
||||
|
||||
//Time period in seconds for circuit breaker to retry service
|
||||
private static final int RETRY_PERIOD = 2;
|
||||
|
||||
private MonitoringService monitoringService;
|
||||
|
||||
private CircuitBreaker delayedServiceCircuitBreaker;
|
||||
|
||||
private CircuitBreaker quickServiceCircuitBreaker;
|
||||
|
||||
/**
|
||||
* Setup the circuit breakers and services, where {@link DelayedRemoteService} will be start with
|
||||
* a delay of 4 seconds and a {@link QuickRemoteService} responding healthy. Both services are
|
||||
* wrapped in a {@link DefaultCircuitBreaker} implementation with failure threshold of 1 failure
|
||||
* and retry time period of 2 seconds.
|
||||
*/
|
||||
@BeforeEach
|
||||
public void setupCircuitBreakers() {
|
||||
var delayedService = new DelayedRemoteService(System.nanoTime(), STARTUP_DELAY);
|
||||
//Set the circuit Breaker parameters
|
||||
delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||
FAILURE_THRESHOLD,
|
||||
RETRY_PERIOD * 1000 * 1000 * 1000);
|
||||
|
||||
var quickService = new QuickRemoteService();
|
||||
//Set the circuit Breaker parameters
|
||||
quickServiceCircuitBreaker = new DefaultCircuitBreaker(quickService, 3000, FAILURE_THRESHOLD,
|
||||
RETRY_PERIOD * 1000 * 1000 * 1000);
|
||||
|
||||
monitoringService = new MonitoringService(delayedServiceCircuitBreaker,
|
||||
quickServiceCircuitBreaker);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_OpenStateTransition() {
|
||||
//Calling delayed service, which will be unhealthy till 4 seconds
|
||||
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||
//As failure threshold is "1", the circuit breaker is changed to OPEN
|
||||
assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
|
||||
//As circuit state is OPEN, we expect a quick fallback response from circuit breaker.
|
||||
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||
|
||||
//Meanwhile, the quick service is responding and the circuit state is CLOSED
|
||||
assertEquals("Quick Service is working", monitoringService.quickServiceResponse());
|
||||
assertEquals("CLOSED", quickServiceCircuitBreaker.getState());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_HalfOpenStateTransition() {
|
||||
//Calling delayed service, which will be unhealthy till 4 seconds
|
||||
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||
//As failure threshold is "1", the circuit breaker is changed to OPEN
|
||||
assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
|
||||
|
||||
//Waiting for recovery period of 2 seconds for circuit breaker to retry service.
|
||||
try {
|
||||
LOGGER.info("Waiting 2s for delayed service to become responsive");
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//After 2 seconds, the circuit breaker should move to "HALF_OPEN" state and retry fetching response from service again
|
||||
assertEquals("HALF_OPEN", delayedServiceCircuitBreaker.getState());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRecovery_ClosedStateTransition() {
|
||||
//Calling delayed service, which will be unhealthy till 4 seconds
|
||||
assertEquals("Delayed service is down", monitoringService.delayedServiceResponse());
|
||||
//As failure threshold is "1", the circuit breaker is changed to OPEN
|
||||
assertEquals("OPEN", delayedServiceCircuitBreaker.getState());
|
||||
|
||||
//Waiting for 4 seconds, which is enough for DelayedService to become healthy and respond successfully.
|
||||
try {
|
||||
LOGGER.info("Waiting 4s for delayed service to become responsive");
|
||||
Thread.sleep(4000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
//As retry period is 2 seconds (<4 seconds of wait), hence the circuit breaker should be back in HALF_OPEN state.
|
||||
assertEquals("HALF_OPEN", delayedServiceCircuitBreaker.getState());
|
||||
//Check the success response from delayed service.
|
||||
assertEquals("Delayed service is working", monitoringService.delayedServiceResponse());
|
||||
//As the response is success, the state should be CLOSED
|
||||
assertEquals("CLOSED", delayedServiceCircuitBreaker.getState());
|
||||
}
|
||||
|
||||
}
|
@@ -25,56 +25,60 @@ package com.iluwatar.circuitbreaker;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.rmi.Remote;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Circuit Breaker test
|
||||
*/
|
||||
public class CircuitBreakerTest {
|
||||
public class DefaultCircuitBreakerTest {
|
||||
|
||||
//long timeout, int failureThreshold, long retryTimePeriod
|
||||
@Test
|
||||
public void testSetState() {
|
||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
||||
public void testEvaluateState() {
|
||||
var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 100);
|
||||
//Right now, failureCount<failureThreshold, so state should be closed
|
||||
assertEquals(circuitBreaker.getState(), "CLOSED");
|
||||
circuitBreaker.failureCount = 4;
|
||||
circuitBreaker.lastFailureTime = System.nanoTime();
|
||||
circuitBreaker.setState();
|
||||
circuitBreaker.evaluateState();
|
||||
//Since failureCount>failureThreshold, and lastFailureTime is nearly equal to current time,
|
||||
//state should be half-open
|
||||
assertEquals(circuitBreaker.getState(), "HALF_OPEN");
|
||||
//Since failureCount>failureThreshold, and lastFailureTime is much lesser current time,
|
||||
//state should be open
|
||||
circuitBreaker.lastFailureTime = System.nanoTime() - 1000 * 1000 * 1000 * 1000;
|
||||
circuitBreaker.setState();
|
||||
circuitBreaker.evaluateState();
|
||||
assertEquals(circuitBreaker.getState(), "OPEN");
|
||||
//Now set it back again to closed to test idempotency
|
||||
circuitBreaker.failureCount = 0;
|
||||
circuitBreaker.setState();
|
||||
circuitBreaker.evaluateState();
|
||||
assertEquals(circuitBreaker.getState(), "CLOSED");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetStateForBypass() {
|
||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
||||
var circuitBreaker = new DefaultCircuitBreaker(null, 1, 1, 2000 * 1000 * 1000);
|
||||
//Right now, failureCount<failureThreshold, so state should be closed
|
||||
//Bypass it and set it to open
|
||||
circuitBreaker.setStateForBypass(State.OPEN);
|
||||
circuitBreaker.setState(State.OPEN);
|
||||
assertEquals(circuitBreaker.getState(), "OPEN");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testApiResponses() {
|
||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
||||
try {
|
||||
//Call with the paramater start_time set to huge amount of time in past so that service
|
||||
//replies with "Ok". Also, state is CLOSED in start
|
||||
var serviceStartTime = System.nanoTime() - 60 * 1000 * 1000 * 1000;
|
||||
var response = circuitBreaker.call("delayedService", serviceStartTime);
|
||||
assertEquals(response, "Delayed service is working");
|
||||
} catch (Exception e) {
|
||||
System.out.println(e.getMessage());
|
||||
}
|
||||
public void testApiResponses() throws RemoteServiceException {
|
||||
RemoteService mockService = new RemoteService() {
|
||||
@Override
|
||||
public String call() throws RemoteServiceException {
|
||||
return "Remote Success";
|
||||
}
|
||||
};
|
||||
var circuitBreaker = new DefaultCircuitBreaker(mockService, 1, 1, 100);
|
||||
//Call with the paramater start_time set to huge amount of time in past so that service
|
||||
//replies with "Ok". Also, state is CLOSED in start
|
||||
var serviceStartTime = System.nanoTime() - 60 * 1000 * 1000 * 1000;
|
||||
var response = circuitBreaker.attemptRequest();
|
||||
assertEquals(response, "Remote Success");
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Monitoring Service test
|
||||
*/
|
||||
public class DelayedRemoteServiceTest {
|
||||
|
||||
/**
|
||||
* Testing immediate response of the delayed service.
|
||||
*
|
||||
* @throws RemoteServiceException
|
||||
*/
|
||||
@Test
|
||||
public void testDefaultConstructor() throws RemoteServiceException {
|
||||
Assertions.assertThrows(RemoteServiceException.class, () -> {
|
||||
var obj = new DelayedRemoteService();
|
||||
obj.call();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing server started in past (2 seconds ago) and with a simulated delay of 1 second.
|
||||
*
|
||||
* @throws RemoteServiceException
|
||||
*/
|
||||
@Test
|
||||
public void testParameterizedConstructor() throws RemoteServiceException {
|
||||
var obj = new DelayedRemoteService(System.nanoTime()-2000*1000*1000,1);
|
||||
assertEquals("Delayed service is working",obj.call());
|
||||
}
|
||||
}
|
@@ -35,28 +35,45 @@ public class MonitoringServiceTest {
|
||||
//long timeout, int failureThreshold, long retryTimePeriod
|
||||
@Test
|
||||
public void testLocalResponse() {
|
||||
var monitoringService = new MonitoringService();
|
||||
var monitoringService = new MonitoringService(null,null);
|
||||
var response = monitoringService.localResourceResponse();
|
||||
assertEquals(response, "Local Service is working");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoteResponse() {
|
||||
var monitoringService = new MonitoringService();
|
||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
||||
public void testDelayedRemoteResponseSuccess() {
|
||||
var delayedService = new DelayedRemoteService(System.nanoTime()-2*1000*1000*1000, 2);
|
||||
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||
1,
|
||||
2 * 1000 * 1000 * 1000);
|
||||
|
||||
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null);
|
||||
//Set time in past to make the server work
|
||||
var serverStartTime = System.nanoTime() / 10;
|
||||
var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime);
|
||||
var response = monitoringService.delayedServiceResponse();
|
||||
assertEquals(response, "Delayed service is working");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoteResponse2() {
|
||||
var monitoringService = new MonitoringService();
|
||||
var circuitBreaker = new CircuitBreaker(1, 1, 100);
|
||||
public void testDelayedRemoteResponseFailure() {
|
||||
var delayedService = new DelayedRemoteService(System.nanoTime(), 2);
|
||||
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||
1,
|
||||
2 * 1000 * 1000 * 1000);
|
||||
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null);
|
||||
//Set time as current time as initially server fails
|
||||
var serverStartTime = System.nanoTime();
|
||||
var response = monitoringService.remoteResourceResponse(circuitBreaker, serverStartTime);
|
||||
assertEquals(response, "Remote service not responding");
|
||||
var response = monitoringService.delayedServiceResponse();
|
||||
assertEquals(response, "Delayed service is down");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuickRemoteServiceResponse() {
|
||||
var delayedService = new QuickRemoteService();
|
||||
var delayedServiceCircuitBreaker = new DefaultCircuitBreaker(delayedService, 3000,
|
||||
1,
|
||||
2 * 1000 * 1000 * 1000);
|
||||
var monitoringService = new MonitoringService(delayedServiceCircuitBreaker,null);
|
||||
//Set time as current time as initially server fails
|
||||
var response = monitoringService.delayedServiceResponse();
|
||||
assertEquals(response, "Quick Service is working");
|
||||
}
|
||||
}
|
||||
|
@@ -14,14 +14,14 @@ Action, Transaction
|
||||
|
||||
## Intent
|
||||
|
||||
Encapsulate a request as an object, thereby letting you parameterize clients with different
|
||||
Encapsulate a request as an object, thereby letting you parameterize clients with different
|
||||
requests, queue or log requests, and support undoable operations.
|
||||
|
||||
## Explanation
|
||||
Real world example
|
||||
|
||||
> There is a wizard casting spells on a goblin. The spells are executed on the goblin one by one.
|
||||
> The first spell shrinks the goblin and the second makes him invisible. Then the wizard reverses
|
||||
> There is a wizard casting spells on a goblin. The spells are executed on the goblin one by one.
|
||||
> The first spell shrinks the goblin and the second makes him invisible. Then the wizard reverses
|
||||
> the spells one by one. Each spell here is a command object that can be undone.
|
||||
|
||||
In plain words
|
||||
@@ -30,8 +30,8 @@ In plain words
|
||||
|
||||
Wikipedia says
|
||||
|
||||
> In object-oriented programming, the command pattern is a behavioral design pattern in which an
|
||||
> object is used to encapsulate all information needed to perform an action or trigger an event at
|
||||
> In object-oriented programming, the command pattern is a behavioral design pattern in which an
|
||||
> object is used to encapsulate all information needed to perform an action or trigger an event at
|
||||
> a later time.
|
||||
|
||||
**Programmatic Example**
|
||||
@@ -41,25 +41,21 @@ Here's the sample code with wizard and goblin. Let's start from the `Wizard` cla
|
||||
```java
|
||||
public class Wizard {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Wizard.class);
|
||||
|
||||
private final Deque<Command> undoStack = new LinkedList<>();
|
||||
private final Deque<Command> redoStack = new LinkedList<>();
|
||||
|
||||
public Wizard() {}
|
||||
|
||||
public void castSpell(Command command, Target target) {
|
||||
LOGGER.info("{} casts {} at {}", this, command, target);
|
||||
command.execute(target);
|
||||
undoStack.offerLast(command);
|
||||
public void castSpell(Runnable runnable) {
|
||||
runnable.run();
|
||||
undoStack.offerLast(runnable);
|
||||
}
|
||||
|
||||
public void undoLastSpell() {
|
||||
if (!undoStack.isEmpty()) {
|
||||
var previousSpell = undoStack.pollLast();
|
||||
redoStack.offerLast(previousSpell);
|
||||
LOGGER.info("{} undoes {}", this, previousSpell);
|
||||
previousSpell.undo();
|
||||
previousSpell.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +63,7 @@ public class Wizard {
|
||||
if (!redoStack.isEmpty()) {
|
||||
var previousSpell = redoStack.pollLast();
|
||||
undoStack.offerLast(previousSpell);
|
||||
LOGGER.info("{} redoes {}", this, previousSpell);
|
||||
previousSpell.redo();
|
||||
previousSpell.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,84 +74,7 @@ public class Wizard {
|
||||
}
|
||||
```
|
||||
|
||||
Next we present the spell hierarchy.
|
||||
|
||||
```java
|
||||
public interface Command {
|
||||
|
||||
void execute(Target target);
|
||||
|
||||
void undo();
|
||||
|
||||
void redo();
|
||||
|
||||
String toString();
|
||||
}
|
||||
|
||||
public class InvisibilitySpell implements Command {
|
||||
|
||||
private Target target;
|
||||
|
||||
@Override
|
||||
public void execute(Target target) {
|
||||
target.setVisibility(Visibility.INVISIBLE);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undo() {
|
||||
if (target != null) {
|
||||
target.setVisibility(Visibility.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void redo() {
|
||||
if (target != null) {
|
||||
target.setVisibility(Visibility.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Invisibility spell";
|
||||
}
|
||||
}
|
||||
|
||||
public class ShrinkSpell implements Command {
|
||||
|
||||
private Size oldSize;
|
||||
private Target target;
|
||||
|
||||
@Override
|
||||
public void execute(Target target) {
|
||||
oldSize = target.getSize();
|
||||
target.setSize(Size.SMALL);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undo() {
|
||||
if (oldSize != null && target != null) {
|
||||
var temp = target.getSize();
|
||||
target.setSize(oldSize);
|
||||
oldSize = temp;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void redo() {
|
||||
undo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Shrink spell";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, we have the goblin who's the target of the spells.
|
||||
Next, we have the goblin who's the target of the spells.
|
||||
|
||||
```java
|
||||
public abstract class Target {
|
||||
@@ -203,33 +121,73 @@ public class Goblin extends Target {
|
||||
return "Goblin";
|
||||
}
|
||||
|
||||
public void changeSize() {
|
||||
var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL;
|
||||
setSize(oldSize);
|
||||
}
|
||||
|
||||
public void changeVisibility() {
|
||||
var visible = getVisibility() == Visibility.INVISIBLE
|
||||
? Visibility.VISIBLE : Visibility.INVISIBLE;
|
||||
setVisibility(visible);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally we have the wizard in main function who casts spell
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
var wizard = new Wizard();
|
||||
var goblin = new Goblin();
|
||||
|
||||
// casts shrink/unshrink spell
|
||||
wizard.castSpell(goblin::changeSize);
|
||||
|
||||
// casts visible/invisible spell
|
||||
wizard.castSpell(goblin::changeVisibility);
|
||||
|
||||
// undo and redo casts
|
||||
wizard.undoLastSpell();
|
||||
wizard.redoLastSpell();
|
||||
```
|
||||
|
||||
Here's the whole example in action.
|
||||
|
||||
```java
|
||||
var wizard = new Wizard();
|
||||
var goblin = new Goblin();
|
||||
|
||||
goblin.printStatus();
|
||||
wizard.castSpell(new ShrinkSpell(), goblin);
|
||||
wizard.castSpell(goblin::changeSize);
|
||||
goblin.printStatus();
|
||||
wizard.castSpell(new InvisibilitySpell(), goblin);
|
||||
|
||||
wizard.castSpell(goblin::changeVisibility);
|
||||
goblin.printStatus();
|
||||
|
||||
wizard.undoLastSpell();
|
||||
goblin.printStatus();
|
||||
|
||||
wizard.undoLastSpell();
|
||||
goblin.printStatus();
|
||||
|
||||
wizard.redoLastSpell();
|
||||
goblin.printStatus();
|
||||
|
||||
wizard.redoLastSpell();
|
||||
goblin.printStatus();
|
||||
```
|
||||
|
||||
Here's the program output:
|
||||
|
||||
```java
|
||||
// Goblin, [size=normal] [visibility=visible]
|
||||
// Wizard casts Shrink spell at Goblin
|
||||
// Goblin, [size=small] [visibility=visible]
|
||||
// Wizard casts Invisibility spell at Goblin
|
||||
// Goblin, [size=small] [visibility=invisible]
|
||||
// Wizard undoes Invisibility spell
|
||||
// Goblin, [size=small] [visibility=visible]
|
||||
Goblin, [size=normal] [visibility=visible]
|
||||
Goblin, [size=small] [visibility=visible]
|
||||
Goblin, [size=small] [visibility=invisible]
|
||||
Goblin, [size=small] [visibility=visible]
|
||||
Goblin, [size=normal] [visibility=visible]
|
||||
Goblin, [size=small] [visibility=visible]
|
||||
Goblin, [size=small] [visibility=invisible]
|
||||
```
|
||||
|
||||
## Class diagram
|
||||
@@ -240,26 +198,26 @@ Here's the program output:
|
||||
|
||||
Use the Command pattern when you want to:
|
||||
|
||||
* Parameterize objects by an action to perform. You can express such parameterization in a
|
||||
procedural language with a callback function, that is, a function that's registered somewhere to be
|
||||
* Parameterize objects by an action to perform. You can express such parameterization in a
|
||||
procedural language with a callback function, that is, a function that's registered somewhere to be
|
||||
called at a later point. Commands are an object-oriented replacement for callbacks.
|
||||
* Specify, queue, and execute requests at different times. A Command object can have a lifetime
|
||||
independent of the original request. If the receiver of a request can be represented in an address
|
||||
space-independent way, then you can transfer a command object for the request to a different process
|
||||
* Specify, queue, and execute requests at different times. A Command object can have a lifetime
|
||||
independent of the original request. If the receiver of a request can be represented in an address
|
||||
space-independent way, then you can transfer a command object for the request to a different process
|
||||
and fulfill the request there.
|
||||
* Support undo. The Command's execute operation can store state for reversing its effects in the
|
||||
command itself. The Command interface must have an added un-execute operation that reverses the
|
||||
effects of a previous call to execute. The executed commands are stored in a history list.
|
||||
Unlimited-level undo and redo is achieved by traversing this list backwards and forwards calling
|
||||
* Support undo. The Command's execute operation can store state for reversing its effects in the
|
||||
command itself. The Command interface must have an added un-execute operation that reverses the
|
||||
effects of a previous call to execute. The executed commands are stored in a history list.
|
||||
Unlimited-level undo and redo is achieved by traversing this list backwards and forwards calling
|
||||
un-execute and execute, respectively.
|
||||
* Support logging changes so that they can be reapplied in case of a system crash. By augmenting the
|
||||
Command interface with load and store operations, you can keep a persistent log of changes.
|
||||
Recovering from a crash involves reloading logged commands from disk and re-executing them with
|
||||
* Support logging changes so that they can be reapplied in case of a system crash. By augmenting the
|
||||
Command interface with load and store operations, you can keep a persistent log of changes.
|
||||
Recovering from a crash involves reloading logged commands from disk and re-executing them with
|
||||
the execute operation.
|
||||
* Structure a system around high-level operations build on primitive operations. Such a structure is
|
||||
common in information systems that support transactions. A transaction encapsulates a set of changes
|
||||
to data. The Command pattern offers a way to model transactions. Commands have a common interface,
|
||||
letting you invoke all transactions the same way. The pattern also makes it easy to extend the
|
||||
* Structure a system around high-level operations build on primitive operations. Such a structure is
|
||||
common in information systems that support transactions. A transaction encapsulates a set of changes
|
||||
to data. The Command pattern offers a way to model transactions. Commands have a common interface,
|
||||
letting you invoke all transactions the same way. The pattern also makes it easy to extend the
|
||||
system with new transactions.
|
||||
|
||||
## Typical Use Case
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 50 KiB |
@@ -1,116 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<class-diagram version="1.1.8" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true"
|
||||
realizations="true" associations="true" dependencies="false" nesting-relationships="true">
|
||||
<class id="1" language="java" name="com.iluwatar.command.ShrinkSpell" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/ShrinkSpell.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="178" width="141" x="-30" y="681"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<class id="2" language="java" name="com.iluwatar.command.Goblin" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/Goblin.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="-1" width="-1" x="129" y="1223"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<class id="3" language="java" name="com.iluwatar.command.Wizard" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="-1" width="-1" x="129" y="362"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<class id="4" language="java" name="com.iluwatar.command.Command" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/Command.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="-1" width="-1" x="129" y="561"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<class id="5" language="java" name="com.iluwatar.command.InvisibilitySpell" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/InvisibilitySpell.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="160" width="141" x="151" y="681"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<class id="6" language="java" name="com.iluwatar.command.Target" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/Target.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="-1" width="-1" x="129" y="1014"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<association id="7">
|
||||
<end type="SOURCE" refId="3" navigable="false">
|
||||
<attribute id="8" name="redoStack">
|
||||
<position height="20" width="67" x="140" y="451"/>
|
||||
</attribute>
|
||||
<multiplicity id="9" minimum="0" maximum="2147483647">
|
||||
<position height="18" width="25" x="221" y="452"/>
|
||||
</multiplicity>
|
||||
</end>
|
||||
<end type="TARGET" refId="4" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<generalization id="10">
|
||||
<end type="SOURCE" refId="2"/>
|
||||
<end type="TARGET" refId="6"/>
|
||||
</generalization>
|
||||
<association id="11">
|
||||
<end type="SOURCE" refId="1" navigable="false">
|
||||
<attribute id="12" name="target"/>
|
||||
<multiplicity id="13" minimum="0" maximum="1"/>
|
||||
</end>
|
||||
<end type="TARGET" refId="6" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<generalization id="14">
|
||||
<end type="SOURCE" refId="1"/>
|
||||
<end type="TARGET" refId="4"/>
|
||||
</generalization>
|
||||
<association id="15">
|
||||
<end type="SOURCE" refId="3" navigable="false">
|
||||
<attribute id="16" name="undoStack">
|
||||
<position height="20" width="70" x="-17" y="451"/>
|
||||
</attribute>
|
||||
<multiplicity id="17" minimum="0" maximum="2147483647">
|
||||
<position height="18" width="25" x="60" y="452"/>
|
||||
</multiplicity>
|
||||
</end>
|
||||
<end type="TARGET" refId="4" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<generalization id="18">
|
||||
<end type="SOURCE" refId="5"/>
|
||||
<end type="TARGET" refId="4"/>
|
||||
</generalization>
|
||||
<association id="19">
|
||||
<end type="SOURCE" refId="5" navigable="false">
|
||||
<attribute id="20" name="target"/>
|
||||
<multiplicity id="21" minimum="0" maximum="1"/>
|
||||
</end>
|
||||
<end type="TARGET" refId="6" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</classifier-display>
|
||||
<class-diagram version="1.1.8" icons="true" automaticImage="PNG" always-add-relationships="false" generalizations="true"
|
||||
realizations="true" associations="true" dependencies="false" nesting-relationships="true">
|
||||
<class id="2" language="java" name="com.iluwatar.command.Goblin" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/Goblin.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="-1" width="-1" x="129" y="1223"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<class id="3" language="java" name="com.iluwatar.command.Wizard" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/Wizard.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="-1" width="-1" x="129" y="362"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<class id="6" language="java" name="com.iluwatar.command.Target" project="command"
|
||||
file="/command/src/main/java/com/iluwatar/command/Target.java" binary="false" corner="BOTTOM_RIGHT">
|
||||
<position height="-1" width="-1" x="129" y="1014"/>
|
||||
<display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</display>
|
||||
</class>
|
||||
<association id="7">
|
||||
<end type="SOURCE" refId="3" navigable="false">
|
||||
<attribute id="8" name="redoStack">
|
||||
<position height="20" width="67" x="140" y="451"/>
|
||||
</attribute>
|
||||
<multiplicity id="9" minimum="0" maximum="2147483647">
|
||||
<position height="18" width="25" x="221" y="452"/>
|
||||
</multiplicity>
|
||||
</end>
|
||||
<end type="TARGET" refId="4" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<generalization id="10">
|
||||
<end type="SOURCE" refId="2"/>
|
||||
<end type="TARGET" refId="6"/>
|
||||
</generalization>
|
||||
<association id="11">
|
||||
<end type="SOURCE" refId="1" navigable="false">
|
||||
<attribute id="12" name="target"/>
|
||||
<multiplicity id="13" minimum="0" maximum="1"/>
|
||||
</end>
|
||||
<end type="TARGET" refId="6" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<generalization id="14">
|
||||
<end type="SOURCE" refId="1"/>
|
||||
<end type="TARGET" refId="4"/>
|
||||
</generalization>
|
||||
<association id="15">
|
||||
<end type="SOURCE" refId="3" navigable="false">
|
||||
<attribute id="16" name="undoStack">
|
||||
<position height="20" width="70" x="-17" y="451"/>
|
||||
</attribute>
|
||||
<multiplicity id="17" minimum="0" maximum="2147483647">
|
||||
<position height="18" width="25" x="60" y="452"/>
|
||||
</multiplicity>
|
||||
</end>
|
||||
<end type="TARGET" refId="4" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<generalization id="18">
|
||||
<end type="SOURCE" refId="5"/>
|
||||
<end type="TARGET" refId="4"/>
|
||||
</generalization>
|
||||
<association id="19">
|
||||
<end type="SOURCE" refId="5" navigable="false">
|
||||
<attribute id="20" name="target"/>
|
||||
<multiplicity id="21" minimum="0" maximum="1"/>
|
||||
</end>
|
||||
<end type="TARGET" refId="6" navigable="true"/>
|
||||
<display labels="true" multiplicity="true"/>
|
||||
</association>
|
||||
<classifier-display autosize="true" stereotype="true" package="true" initial-value="false" signature="true"
|
||||
sort-features="false" accessors="true" visibility="true">
|
||||
<attributes public="true" package="true" protected="true" private="true" static="true"/>
|
||||
<operations public="true" package="true" protected="true" private="true" static="true"/>
|
||||
</classifier-display>
|
||||
<association-display labels="true" multiplicity="true"/>
|
||||
</class-diagram>
|
||||
</class-diagram>
|
||||
|
@@ -4,33 +4,11 @@ package com.iluwatar.command {
|
||||
+ App()
|
||||
+ main(args : String[]) {static}
|
||||
}
|
||||
interface Command {
|
||||
+ Command()
|
||||
+ execute(Target) {abstract}
|
||||
+ redo() {abstract}
|
||||
+ toString() : String {abstract}
|
||||
+ undo() {abstract}
|
||||
}
|
||||
class Goblin {
|
||||
+ Goblin()
|
||||
+ toString() : String
|
||||
}
|
||||
class InvisibilitySpell {
|
||||
- target : Target
|
||||
+ InvisibilitySpell()
|
||||
+ execute(target : Target)
|
||||
+ redo()
|
||||
+ toString() : String
|
||||
+ undo()
|
||||
}
|
||||
class ShrinkSpell {
|
||||
- oldSize : Size
|
||||
- target : Target
|
||||
+ ShrinkSpell()
|
||||
+ execute(target : Target)
|
||||
+ redo()
|
||||
+ toString() : String
|
||||
+ undo()
|
||||
+ changeSize()
|
||||
+ changeVisibility()
|
||||
}
|
||||
enum Size {
|
||||
+ NORMAL {static}
|
||||
@@ -62,22 +40,19 @@ package com.iluwatar.command {
|
||||
}
|
||||
class Wizard {
|
||||
- LOGGER : Logger {static}
|
||||
- redoStack : Deque<Command>
|
||||
- undoStack : Deque<Command>
|
||||
- redoStack : Deque<Runnable>
|
||||
- undoStack : Deque<Runnable>
|
||||
+ Wizard()
|
||||
+ castSpell(command : Command, target : Target)
|
||||
+ castSpell(Runnable : runnable)
|
||||
+ redoLastSpell()
|
||||
+ toString() : String
|
||||
+ undoLastSpell()
|
||||
}
|
||||
}
|
||||
Target --> "-size" Size
|
||||
Wizard --> "-undoStack" Command
|
||||
ShrinkSpell --> "-oldSize" Size
|
||||
InvisibilitySpell --> "-target" Target
|
||||
ShrinkSpell --> "-target" Target
|
||||
Wizard --> "-changeSize" Goblin
|
||||
Wizard --> "-changeVisibility" Goblin
|
||||
Target --> "-visibility" Visibility
|
||||
Goblin --|> Target
|
||||
InvisibilitySpell ..|> Command
|
||||
ShrinkSpell ..|> Command
|
||||
App --> "castSpell" Wizard
|
||||
@enduml
|
||||
|
@@ -30,12 +30,10 @@ package com.iluwatar.command;
|
||||
*
|
||||
* <p>Four terms always associated with the command pattern are command, receiver, invoker and
|
||||
* client. A command object (spell) knows about the receiver (target) and invokes a method of the
|
||||
* receiver. Values for parameters of the receiver method are stored in the command. The receiver
|
||||
* then does the work. An invoker object (wizard) knows how to execute a command, and optionally
|
||||
* does bookkeeping about the command execution. The invoker does not know anything about a concrete
|
||||
* command, it knows only about command interface. Both an invoker object and several command
|
||||
* objects are held by a client object (app). The client decides which commands to execute at which
|
||||
* points. To execute a command, it passes the command object to the invoker object.
|
||||
* receiver. An invoker object (wizard) receives a reference to the command to be executed and
|
||||
* optionally does bookkeeping about the command execution. The invoker does not know anything
|
||||
* about how the command is executed. The client decides which commands to execute at which
|
||||
* points. To execute a command, it passes a reference of the function to the invoker object.
|
||||
*
|
||||
* <p>In other words, in this example the wizard casts spells on the goblin. The wizard keeps track
|
||||
* of the previous spells cast, so it is easy to undo them. In addition, the wizard keeps track of
|
||||
@@ -54,10 +52,10 @@ public class App {
|
||||
|
||||
goblin.printStatus();
|
||||
|
||||
wizard.castSpell(new ShrinkSpell(), goblin);
|
||||
wizard.castSpell(goblin::changeSize);
|
||||
goblin.printStatus();
|
||||
|
||||
wizard.castSpell(new InvisibilitySpell(), goblin);
|
||||
wizard.castSpell(goblin::changeVisibility);
|
||||
goblin.printStatus();
|
||||
|
||||
wizard.undoLastSpell();
|
||||
|
@@ -37,5 +37,4 @@ public class Goblin extends Target {
|
||||
public String toString() {
|
||||
return "Goblin";
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -62,4 +62,21 @@ public abstract class Target {
|
||||
public void printStatus() {
|
||||
LOGGER.info("{}, [size={}] [visibility={}]", this, getSize(), getVisibility());
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the size of the target.
|
||||
*/
|
||||
public void changeSize() {
|
||||
var oldSize = getSize() == Size.NORMAL ? Size.SMALL : Size.NORMAL;
|
||||
setSize(oldSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the visibility of the target.
|
||||
*/
|
||||
public void changeVisibility() {
|
||||
var visible = getVisibility() == Visibility.INVISIBLE
|
||||
? Visibility.VISIBLE : Visibility.INVISIBLE;
|
||||
setVisibility(visible);
|
||||
}
|
||||
}
|
||||
|
@@ -25,30 +25,24 @@ package com.iluwatar.command;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Wizard is the invoker of the commands.
|
||||
*/
|
||||
public class Wizard {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(Wizard.class);
|
||||
|
||||
private final Deque<Command> undoStack = new LinkedList<>();
|
||||
private final Deque<Command> redoStack = new LinkedList<>();
|
||||
private final Deque<Runnable> undoStack = new LinkedList<>();
|
||||
private final Deque<Runnable> redoStack = new LinkedList<>();
|
||||
|
||||
public Wizard() {
|
||||
// comment to ignore sonar issue: LEVEL critical
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast spell.
|
||||
*/
|
||||
public void castSpell(Command command, Target target) {
|
||||
LOGGER.info("{} casts {} at {}", this, command, target);
|
||||
command.execute(target);
|
||||
undoStack.offerLast(command);
|
||||
public void castSpell(Runnable runnable) {
|
||||
runnable.run();
|
||||
undoStack.offerLast(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,8 +52,7 @@ public class Wizard {
|
||||
if (!undoStack.isEmpty()) {
|
||||
var previousSpell = undoStack.pollLast();
|
||||
redoStack.offerLast(previousSpell);
|
||||
LOGGER.info("{} undoes {}", this, previousSpell);
|
||||
previousSpell.undo();
|
||||
previousSpell.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +63,7 @@ public class Wizard {
|
||||
if (!redoStack.isEmpty()) {
|
||||
var previousSpell = redoStack.pollLast();
|
||||
undoStack.offerLast(previousSpell);
|
||||
LOGGER.info("{} redoes {}", this, previousSpell);
|
||||
previousSpell.redo();
|
||||
previousSpell.run();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -56,10 +56,10 @@ public class CommandTest {
|
||||
var wizard = new Wizard();
|
||||
var goblin = new Goblin();
|
||||
|
||||
wizard.castSpell(new ShrinkSpell(), goblin);
|
||||
wizard.castSpell(goblin::changeSize);
|
||||
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.VISIBLE);
|
||||
|
||||
wizard.castSpell(new InvisibilitySpell(), goblin);
|
||||
wizard.castSpell(goblin::changeVisibility);
|
||||
verifyGoblin(goblin, GOBLIN, Size.SMALL, Visibility.INVISIBLE);
|
||||
|
||||
wizard.undoLastSpell();
|
||||
|
1
pom.xml
@@ -89,6 +89,7 @@
|
||||
<module>state</module>
|
||||
<module>strategy</module>
|
||||
<module>template-method</module>
|
||||
<module>version-number</module>
|
||||
<module>visitor</module>
|
||||
<module>double-checked-locking</module>
|
||||
<module>servant</module>
|
||||
|
169
version-number/README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
layout: pattern
|
||||
title: Version Number
|
||||
folder: versionnumber
|
||||
permalink: /patterns/versionnumber/
|
||||
description: Entity versioning with version number
|
||||
|
||||
categories:
|
||||
- Concurrency
|
||||
|
||||
tags:
|
||||
- Data access
|
||||
- Microservices
|
||||
---
|
||||
|
||||
## Name / classification
|
||||
|
||||
Version Number.
|
||||
|
||||
## Also known as
|
||||
|
||||
Entity Versioning, Optimistic Locking.
|
||||
|
||||
## Intent
|
||||
|
||||
Resolve concurrency conflicts when multiple clients are trying to update same entity simultaneously.
|
||||
|
||||
## Explanation
|
||||
|
||||
Real world example
|
||||
|
||||
> Alice and Bob are working on the book, which stored in the database. Our heroes are making
|
||||
> changes simultaneously, and we need some mechanism to prevent them from overwriting each other.
|
||||
|
||||
In plain words
|
||||
|
||||
> Version Number pattern grants protection against concurrent updates to same entity.
|
||||
|
||||
Wikipedia says
|
||||
|
||||
> Optimistic concurrency control assumes that multiple transactions can frequently complete
|
||||
> without interfering with each other. While running, transactions use data resources without
|
||||
> acquiring locks on those resources. Before committing, each transaction verifies that no other
|
||||
> transaction has modified the data it has read. If the check reveals conflicting modifications,
|
||||
> the committing transaction rolls back and can be restarted.
|
||||
|
||||
**Programmatic Example**
|
||||
|
||||
We have a `Book` entity, which is versioned, and has a copy-constructor:
|
||||
|
||||
```java
|
||||
public class Book {
|
||||
private long id;
|
||||
private String title = "";
|
||||
private String author = "";
|
||||
|
||||
private long version = 0; // version number
|
||||
|
||||
public Book(Book book) {
|
||||
this.id = book.id;
|
||||
this.title = book.title;
|
||||
this.author = book.author;
|
||||
this.version = book.version;
|
||||
}
|
||||
|
||||
// getters and setters are omitted here
|
||||
}
|
||||
```
|
||||
|
||||
We also have `BookRepository`, which implements concurrency control:
|
||||
|
||||
```java
|
||||
public class BookRepository {
|
||||
private final Map<Long, Book> collection = new HashMap<>();
|
||||
|
||||
public void update(Book book) throws BookNotFoundException, VersionMismatchException {
|
||||
if (!collection.containsKey(book.getId())) {
|
||||
throw new BookNotFoundException("Not found book with id: " + book.getId());
|
||||
}
|
||||
|
||||
var latestBook = collection.get(book.getId());
|
||||
if (book.getVersion() != latestBook.getVersion()) {
|
||||
throw new VersionMismatchException(
|
||||
"Tried to update stale version " + book.getVersion()
|
||||
+ " while actual version is " + latestBook.getVersion()
|
||||
);
|
||||
}
|
||||
|
||||
// update version, including client representation - modify by reference here
|
||||
book.setVersion(book.getVersion() + 1);
|
||||
|
||||
// save book copy to repository
|
||||
collection.put(book.getId(), new Book(book));
|
||||
}
|
||||
|
||||
public Book get(long bookId) throws BookNotFoundException {
|
||||
if (!collection.containsKey(bookId)) {
|
||||
throw new BookNotFoundException("Not found book with id: " + bookId);
|
||||
}
|
||||
|
||||
// return copy of the book
|
||||
return new Book(collection.get(bookId));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here's the concurrency control in action:
|
||||
|
||||
```java
|
||||
var bookId = 1;
|
||||
// Alice and Bob took the book concurrently
|
||||
final var aliceBook = bookRepository.get(bookId);
|
||||
final var bobBook = bookRepository.get(bookId);
|
||||
|
||||
aliceBook.setTitle("Kama Sutra"); // Alice has updated book title
|
||||
bookRepository.update(aliceBook); // and successfully saved book in database
|
||||
LOGGER.info("Alice updates the book with new version {}", aliceBook.getVersion());
|
||||
|
||||
// now Bob has the stale version of the book with empty title and version = 0
|
||||
// while actual book in database has filled title and version = 1
|
||||
bobBook.setAuthor("Vatsyayana Mallanaga"); // Bob updates the author
|
||||
try {
|
||||
LOGGER.info("Bob tries to update the book with his version {}", bobBook.getVersion());
|
||||
bookRepository.update(bobBook); // Bob tries to save his book to database
|
||||
} catch (VersionMismatchException e) {
|
||||
// Bob update fails, and book in repository remained untouchable
|
||||
LOGGER.info("Exception: {}", e.getMessage());
|
||||
// Now Bob should reread actual book from repository, do his changes again and save again
|
||||
}
|
||||
```
|
||||
|
||||
Program output:
|
||||
|
||||
```java
|
||||
Alice updates the book with new version 1
|
||||
Bob tries to update the book with his version 0
|
||||
Exception: Tried to update stale version 0 while actual version is 1
|
||||
```
|
||||
|
||||
## Class diagram
|
||||
|
||||

|
||||
|
||||
## Applicability
|
||||
|
||||
Use Version Number for:
|
||||
|
||||
* resolving concurrent write-access to the data
|
||||
* strong data consistency
|
||||
|
||||
## Tutorials
|
||||
* [Version Number Pattern Tutorial](http://www.java2s.com/Tutorial/Java/0355__JPA/VersioningEntity.htm)
|
||||
|
||||
## Known uses
|
||||
* [Hibernate](https://vladmihalcea.com/jpa-entity-version-property-hibernate/)
|
||||
* [Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html#index-versioning)
|
||||
* [Apache Solr](https://lucene.apache.org/solr/guide/6_6/updating-parts-of-documents.html)
|
||||
|
||||
## Consequences
|
||||
Version Number pattern allows to implement a concurrency control, which is usually done
|
||||
via Optimistic Offline Lock pattern.
|
||||
|
||||
## Related patterns
|
||||
* [Optimistic Offline Lock](https://martinfowler.com/eaaCatalog/optimisticOfflineLock.html)
|
||||
|
||||
## Credits
|
||||
* [Optimistic Locking in JPA](https://www.baeldung.com/jpa-optimistic-locking)
|
||||
* [JPA entity versioning](https://www.byteslounge.com/tutorials/jpa-entity-versioning-version-and-optimistic-locking)
|
||||
* [J2EE Design Patterns](http://ommolketab.ir/aaf-lib/axkwht7wxrhvgs2aqkxse8hihyu9zv.pdf)
|
BIN
version-number/etc/version-number.urm.png
Normal file
After Width: | Height: | Size: 22 KiB |
32
version-number/etc/version-number.urm.puml
Normal file
@@ -0,0 +1,32 @@
|
||||
@startuml
|
||||
package com.iluwatar.versionnumber {
|
||||
class App {
|
||||
- LOGGER : Logger {static}
|
||||
+ App()
|
||||
+ main(args : String[]) {static}
|
||||
}
|
||||
class Book {
|
||||
- author : String
|
||||
- id : long
|
||||
- title : String
|
||||
- version : long
|
||||
+ Book()
|
||||
+ Book(book : Book)
|
||||
+ getAuthor() : String
|
||||
+ getId() : long
|
||||
+ getTitle() : String
|
||||
+ getVersion() : long
|
||||
+ setAuthor(author : String)
|
||||
+ setId(id : long)
|
||||
+ setTitle(title : String)
|
||||
+ setVersion(version : long)
|
||||
}
|
||||
class BookRepository {
|
||||
- collection : Map<Long, Book>
|
||||
+ BookRepository()
|
||||
+ add(book : Book)
|
||||
+ get(bookId : long) : Book
|
||||
+ update(book : Book)
|
||||
}
|
||||
}
|
||||
@enduml
|
66
version-number/pom.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.24.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>version-number</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.iluwatar.versionnumber.App</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.versionnumber;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The Version Number pattern helps to resolve concurrency conflicts in applications.
|
||||
* Usually these conflicts arise in database operations, when multiple clients are trying
|
||||
* to update the same record simultaneously.
|
||||
* Resolving such conflicts requires determining whether an object has changed.
|
||||
* For this reason we need a version number that is incremented with each change
|
||||
* to the underlying data, e.g. database. The version number can be used by repositories
|
||||
* to check for external changes and to report concurrency issues to the users.
|
||||
*
|
||||
* <p>In this example we show how Alice and Bob will try to update the {@link Book}
|
||||
* and save it simultaneously to {@link BookRepository}, which represents a typical database.
|
||||
*
|
||||
* <p>As in real databases, each client operates with copy of the data instead of original data
|
||||
* passed by reference, that's why we are using {@link Book} copy-constructor here.
|
||||
*/
|
||||
public class App {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
|
||||
|
||||
/**
|
||||
* Program entry point.
|
||||
*
|
||||
* @param args command line args
|
||||
*/
|
||||
public static void main(String[] args) throws
|
||||
BookDuplicateException,
|
||||
BookNotFoundException,
|
||||
VersionMismatchException {
|
||||
var bookId = 1;
|
||||
|
||||
var bookRepository = new BookRepository();
|
||||
var book = new Book();
|
||||
book.setId(bookId);
|
||||
bookRepository.add(book); // adding a book with empty title and author
|
||||
LOGGER.info("An empty book with version {} was added to repository", book.getVersion());
|
||||
|
||||
// Alice and Bob took the book concurrently
|
||||
final var aliceBook = bookRepository.get(bookId);
|
||||
final var bobBook = bookRepository.get(bookId);
|
||||
|
||||
aliceBook.setTitle("Kama Sutra"); // Alice has updated book title
|
||||
bookRepository.update(aliceBook); // and successfully saved book in database
|
||||
LOGGER.info("Alice updates the book with new version {}", aliceBook.getVersion());
|
||||
|
||||
// now Bob has the stale version of the book with empty title and version = 0
|
||||
// while actual book in database has filled title and version = 1
|
||||
bobBook.setAuthor("Vatsyayana Mallanaga"); // Bob updates the author
|
||||
try {
|
||||
LOGGER.info("Bob tries to update the book with his version {}", bobBook.getVersion());
|
||||
bookRepository.update(bobBook); // Bob tries to save his book to database
|
||||
} catch (VersionMismatchException e) {
|
||||
// Bob update fails, and book in repository remained untouchable
|
||||
LOGGER.info("Exception: {}", e.getMessage());
|
||||
// Now Bob should reread actual book from repository, do his changes again and save again
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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.versionnumber;
|
||||
|
||||
public class Book {
|
||||
private long id;
|
||||
private String title = "";
|
||||
private String author = "";
|
||||
|
||||
private long version = 0; // version number
|
||||
|
||||
public Book() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* We need this copy constructor to copy book representation in {@link BookRepository}.
|
||||
*/
|
||||
public Book(Book book) {
|
||||
this.id = book.id;
|
||||
this.title = book.title;
|
||||
this.author = book.author;
|
||||
this.version = book.version;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(long version) {
|
||||
this.version = version;
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.versionnumber;
|
||||
|
||||
/**
|
||||
* When someone has tried to add a book which repository already have.
|
||||
*/
|
||||
public class BookDuplicateException extends Exception {
|
||||
public BookDuplicateException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.versionnumber;
|
||||
|
||||
/**
|
||||
* Client has tried to make an operation with book which repository does not have.
|
||||
*/
|
||||
public class BookNotFoundException extends Exception {
|
||||
public BookNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* 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.versionnumber;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* This repository represents simplified database.
|
||||
* As a typical database do, repository operates with copies of object.
|
||||
* So client and repo has different copies of book, which can lead to concurrency conflicts
|
||||
* as much as in real databases.
|
||||
*/
|
||||
public class BookRepository {
|
||||
private final Map<Long, Book> collection = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Adds book to collection.
|
||||
* Actually we are putting copy of book (saving a book by value, not by reference);
|
||||
*/
|
||||
public void add(Book book) throws BookDuplicateException {
|
||||
if (collection.containsKey(book.getId())) {
|
||||
throw new BookDuplicateException("Duplicated book with id: " + book.getId());
|
||||
}
|
||||
|
||||
// add copy of the book
|
||||
collection.put(book.getId(), new Book(book));
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates book in collection only if client has modified the latest version of the book.
|
||||
*/
|
||||
public void update(Book book) throws BookNotFoundException, VersionMismatchException {
|
||||
if (!collection.containsKey(book.getId())) {
|
||||
throw new BookNotFoundException("Not found book with id: " + book.getId());
|
||||
}
|
||||
|
||||
var latestBook = collection.get(book.getId());
|
||||
if (book.getVersion() != latestBook.getVersion()) {
|
||||
throw new VersionMismatchException(
|
||||
"Tried to update stale version " + book.getVersion()
|
||||
+ " while actual version is " + latestBook.getVersion()
|
||||
);
|
||||
}
|
||||
|
||||
// update version, including client representation - modify by reference here
|
||||
book.setVersion(book.getVersion() + 1);
|
||||
|
||||
// save book copy to repository
|
||||
collection.put(book.getId(), new Book(book));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns book representation to the client.
|
||||
* Representation means we are returning copy of the book.
|
||||
*/
|
||||
public Book get(long bookId) throws BookNotFoundException {
|
||||
if (!collection.containsKey(bookId)) {
|
||||
throw new BookNotFoundException("Not found book with id: " + bookId);
|
||||
}
|
||||
|
||||
// return copy of the book
|
||||
return new Book(collection.get(bookId));
|
||||
}
|
||||
}
|
@@ -1,37 +1,33 @@
|
||||
/*
|
||||
* 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.command;
|
||||
|
||||
/**
|
||||
* Interface for Commands.
|
||||
*/
|
||||
public interface Command {
|
||||
void execute(Target target);
|
||||
|
||||
void undo();
|
||||
|
||||
void redo();
|
||||
|
||||
String toString();
|
||||
}
|
||||
/*
|
||||
* 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.versionnumber;
|
||||
|
||||
/**
|
||||
* Client has tried to update a stale version of the book.
|
||||
*/
|
||||
public class VersionMismatchException extends Exception {
|
||||
public VersionMismatchException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@@ -21,37 +21,26 @@
|
||||
* THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package com.iluwatar.command;
|
||||
package com.iluwatar.versionnumber;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
|
||||
/**
|
||||
* InvisibilitySpell is a concrete command.
|
||||
* Application test
|
||||
*/
|
||||
public class InvisibilitySpell implements Command {
|
||||
class AppTest {
|
||||
|
||||
private Target target;
|
||||
/**
|
||||
* Issue: Add at least one assertion to this test case.
|
||||
*
|
||||
* Solution: Inserted assertion to check whether the execution of the main method in {@link App#main(String[])}
|
||||
* throws an exception.
|
||||
*/
|
||||
|
||||
@Override
|
||||
public void execute(Target target) {
|
||||
target.setVisibility(Visibility.INVISIBLE);
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void undo() {
|
||||
if (target != null) {
|
||||
target.setVisibility(Visibility.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void redo() {
|
||||
if (target != null) {
|
||||
target.setVisibility(Visibility.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Invisibility spell";
|
||||
@Test
|
||||
void shouldExecuteApplicationWithoutException() {
|
||||
assertDoesNotThrow(() -> App.main(new String[]{}));
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.versionnumber;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link BookRepository}
|
||||
*/
|
||||
class BookRepositoryTest {
|
||||
private final long bookId = 1;
|
||||
private final BookRepository bookRepository = new BookRepository();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws BookDuplicateException {
|
||||
var book = new Book();
|
||||
book.setId(bookId);
|
||||
bookRepository.add(book);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDefaultVersionRemainsZeroAfterAdd() throws BookNotFoundException {
|
||||
var book = bookRepository.get(bookId);
|
||||
assertEquals(0, book.getVersion());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAliceAndBobHaveDifferentVersionsAfterAliceUpdate() throws BookNotFoundException, VersionMismatchException {
|
||||
final var aliceBook = bookRepository.get(bookId);
|
||||
final var bobBook = bookRepository.get(bookId);
|
||||
|
||||
aliceBook.setTitle("Kama Sutra");
|
||||
bookRepository.update(aliceBook);
|
||||
|
||||
assertEquals(1, aliceBook.getVersion());
|
||||
assertEquals(0, bobBook.getVersion());
|
||||
var actualBook = bookRepository.get(bookId);
|
||||
assertEquals(aliceBook.getVersion(), actualBook.getVersion());
|
||||
assertEquals(aliceBook.getTitle(), actualBook.getTitle());
|
||||
assertNotEquals(aliceBook.getTitle(), bobBook.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShouldThrowVersionMismatchExceptionOnStaleUpdate() throws BookNotFoundException, VersionMismatchException {
|
||||
final var aliceBook = bookRepository.get(bookId);
|
||||
final var bobBook = bookRepository.get(bookId);
|
||||
|
||||
aliceBook.setTitle("Kama Sutra");
|
||||
bookRepository.update(aliceBook);
|
||||
|
||||
bobBook.setAuthor("Vatsyayana Mallanaga");
|
||||
try {
|
||||
bookRepository.update(bobBook);
|
||||
} catch (VersionMismatchException e) {
|
||||
assertEquals(0, bobBook.getVersion());
|
||||
var actualBook = bookRepository.get(bookId);
|
||||
assertEquals(1, actualBook.getVersion());
|
||||
assertEquals(aliceBook.getVersion(), actualBook.getVersion());
|
||||
assertEquals("", bobBook.getTitle());
|
||||
assertNotEquals(aliceBook.getAuthor(), bobBook.getAuthor());
|
||||
}
|
||||
}
|
||||
}
|