-
Notifications
You must be signed in to change notification settings - Fork 138
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented ShadowingInterceptor (#1258)
* Implement demo shadowing interceptor with body cloning functionality * Implement Shadowing Interceptor for concurrent request handling - Move `ShadowingInterceptor` to a new package `shadowing` - Refactor request handling to log status codes from shadow targets - Update example configurations to demonstrate shadow requests - Enhance README with instructions and configurations for running the example * Improve shadowing behavior and documentation in ShadowingInterceptor - Simplify request sending logic by removing conditional check for "shadow-request-send". - Add a short description for the interceptor to clarify its purpose. - Enhance Javadoc for `setTargets` method to clearly explain the function and expected targets. * Add unit tests for ShadowingInterceptor and refactor method visibility - Implement initial unit tests for `ShadowingInterceptor` in `ShadowingInterceptorTest`. - Mock `Target` class to verify behavior of `getDestFromTarget` method with and without URL. - Change visibility of `getDestFromTarget` and `performCall` methods to public for test accessibility. - Refactor asynchronous request handling in `cloneRequestAndSend` method to improve error logging.
- Loading branch information
1 parent
77b793f
commit 1bd5a25
Showing
6 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
121 changes: 121 additions & 0 deletions
121
core/src/main/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package com.predic8.membrane.core.interceptor.shadowing; | ||
|
||
import com.predic8.membrane.annot.MCChildElement; | ||
import com.predic8.membrane.annot.MCElement; | ||
import com.predic8.membrane.core.exchange.Exchange; | ||
import com.predic8.membrane.core.http.AbstractBody; | ||
import com.predic8.membrane.core.http.Chunk; | ||
import com.predic8.membrane.core.http.MessageObserver; | ||
import com.predic8.membrane.core.http.Request; | ||
import com.predic8.membrane.core.interceptor.AbstractInterceptor; | ||
import com.predic8.membrane.core.interceptor.Outcome; | ||
import com.predic8.membrane.core.rules.AbstractServiceProxy.Target; | ||
import com.predic8.membrane.core.transport.http.HttpClient; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.concurrent.ExecutorService; | ||
|
||
import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; | ||
import static java.util.concurrent.Executors.newCachedThreadPool; | ||
|
||
@MCElement(name="shadowing") | ||
public class ShadowingInterceptor extends AbstractInterceptor { | ||
|
||
private static final HttpClient client = new HttpClient(); | ||
private static final Logger log = LoggerFactory.getLogger(ShadowingInterceptor.class); | ||
|
||
private List<Target> targets = new ArrayList<>(); | ||
|
||
@Override | ||
public Outcome handleRequest(Exchange exc) throws Exception { | ||
exc.getRequest().getBody().getObservers().add(new MessageObserver() { | ||
@Override | ||
public void bodyRequested(AbstractBody body) {} | ||
@Override | ||
public void bodyChunk(Chunk chunk) {} | ||
@Override | ||
public void bodyChunk(byte[] buffer, int offset, int length) {} | ||
|
||
@Override | ||
public void bodyComplete(AbstractBody body) { | ||
cloneRequestAndSend(body); | ||
} | ||
}); | ||
return CONTINUE; | ||
} | ||
|
||
@Override | ||
public String getShortDescription() { | ||
return "Sends requests to shadow hosts (processed in the background)."; | ||
} | ||
|
||
public void cloneRequestAndSend(AbstractBody body) { | ||
ExecutorService executor = newCachedThreadPool(); | ||
for (Target target : targets) { | ||
Exchange exc; | ||
try { | ||
exc = new Request.Builder() | ||
.body(body.getContent()) | ||
.get(getDestFromTarget(target, router.getParentProxy(this).getKey().getPath())) | ||
.buildExchange(); | ||
} catch (Exception e) { | ||
log.error("Error creating request for target {}", target, e); | ||
continue; | ||
} | ||
|
||
executor.submit(() -> { | ||
try { | ||
Exchange res = performCall(exc); | ||
if (res.getResponse().getStatusCode() >= 500) | ||
log.info("{} returned StatusCode {}", res.getDestinations().get(0), res.getResponse().getStatusCode()); | ||
} catch (Exception e) { | ||
log.error("Error performing call for target {}", target, e); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
|
||
static String getDestFromTarget(Target t, String path) { | ||
return (t.getUrl() != null) ? t.getUrl() : extracted(t, path); | ||
} | ||
|
||
@SuppressWarnings("HttpUrlsUsage") | ||
private static String extracted(Target t, String path) { | ||
return ((t.getSslParser() != null) ? "https://" : "http://") + | ||
t.getHost() + | ||
":" + | ||
t.getPort() + | ||
(path != null ? path : ""); | ||
} | ||
|
||
static Exchange performCall(Exchange exchange) { | ||
try { | ||
return client.call(exchange); | ||
} catch (Exception e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
/** | ||
* Sets the list of shadow hosts to which requests will be cloned and sent. | ||
* <p> | ||
* Each target in the list represents a shadow host where the request will be forwarded. | ||
* These shadow hosts are processed in the background, and if a response from any shadow host | ||
* contains a 5XX status code, it will be logged. | ||
* </p> | ||
* | ||
* @param targets a list of {@link Target} objects representing the shadow hosts. | ||
*/ | ||
@MCChildElement | ||
public void setTargets(List<Target> targets) { | ||
this.targets = targets; | ||
} | ||
|
||
public List<Target> getTargets() { | ||
return targets; | ||
} | ||
} |
37 changes: 37 additions & 0 deletions
37
...c/test/java/com/predic8/membrane/core/interceptor/shadowing/ShadowingInterceptorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package com.predic8.membrane.core.interceptor.shadowing; | ||
|
||
import com.predic8.membrane.core.rules.AbstractServiceProxy.Target; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.Mock; | ||
import org.mockito.MockitoAnnotations; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.mockito.Mockito.when; | ||
|
||
class ShadowingInterceptorTest { | ||
|
||
@Mock | ||
private Target mockTarget; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
MockitoAnnotations.openMocks(this); | ||
} | ||
|
||
@Test | ||
void testGetDestFromTarget_WithUrl() { | ||
when(mockTarget.getUrl()).thenReturn("http://example.com"); | ||
String result = ShadowingInterceptor.getDestFromTarget(mockTarget, "/path"); | ||
assertEquals("http://example.com", result); | ||
} | ||
|
||
@Test | ||
void testGetDestFromTarget_WithoutUrl() { | ||
when(mockTarget.getHost()).thenReturn("localhost"); | ||
when(mockTarget.getPort()).thenReturn(8080); | ||
String result = ShadowingInterceptor.getDestFromTarget(mockTarget, "/path"); | ||
assertEquals("http://localhost:8080/path", result); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# Shadowing Interceptor | ||
|
||
This example demonstrates how to send requests to multiple shadow hosts. A request is sent to the primary target, with additional requests concurrently sent to shadow hosts. The response from the primary target is returned immediately, while shadow requests are processed in the background. | ||
|
||
## Running the Example | ||
|
||
1. Run `service-proxy.bat` or `service-proxy.sh` | ||
2. Open [localhost:2000](http://localhost:2000) in your browser or use `curl`: | ||
|
||
``` | ||
curl -v http://localhost:2000 | ||
``` | ||
The output should look like this: | ||
```json | ||
{ | ||
"apis": [ | ||
{ | ||
"name": "Shop API Showcase", | ||
"description":"API for REST exploration, test and demonstration. Feel free to manipulate the resources using the POST, PUT and DELETE methods. This API acts as a showcase for REST API design.", | ||
"url":"/shop/v2/" | ||
} | ||
] | ||
} | ||
``` | ||
|
||
## Configuration | ||
The targets specified in `shadowing` are shadow hosts. Responses from these hosts are ignored; however, if the returned status code is a 5XX, the endpoint that generated this response is logged. | ||
```xml | ||
<api port="2000"> | ||
<shadowing> | ||
<target host="localhost" port="3000" /> | ||
<target host="localhost" port="3001" /> | ||
<target host="localhost" port="3002" /> | ||
</shadowing> | ||
<target host="api.predic8.de" port="443"> | ||
<ssl/> | ||
</target> | ||
</api> | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
<spring:beans xmlns="http://membrane-soa.org/proxies/1/" | ||
xmlns:spring="http://www.springframework.org/schema/beans" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd | ||
http://membrane-soa.org/proxies/1/ http://membrane-soa.org/schemas/proxies-1.xsd"> | ||
|
||
<router> | ||
|
||
<api port="2000"> | ||
<shadowing> | ||
<target host="localhost" port="3000" /> | ||
<target host="localhost" port="3001" /> | ||
<target host="localhost" port="3002" /> | ||
</shadowing> | ||
<target host="api.predic8.de" port="443"> | ||
<ssl/> | ||
</target> | ||
</api> | ||
|
||
<api port="3000"> | ||
<log /> | ||
<return statusCode="200"/> | ||
</api> | ||
<api port="3001"> | ||
<log /> | ||
<return statusCode="201"/> | ||
</api> | ||
<api port="3002"> | ||
<log /> | ||
<return statusCode="202"/> | ||
</api> | ||
|
||
</router> | ||
|
||
</spring:beans> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
@echo off | ||
if not "%MEMBRANE_HOME%" == "" goto homeSet | ||
set "MEMBRANE_HOME=%cd%\..\.." | ||
echo "%MEMBRANE_HOME%" | ||
if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk | ||
|
||
:homeSet | ||
if exist "%MEMBRANE_HOME%\service-proxy.bat" goto homeOk | ||
echo Please set the MEMBRANE_HOME environment variable to point to | ||
echo the directory where you have extracted the Membrane software. | ||
exit | ||
|
||
:homeOk | ||
set "CLASSPATH=%MEMBRANE_HOME%" | ||
set "CLASSPATH=%MEMBRANE_HOME%/conf" | ||
set "CLASSPATH=%CLASSPATH%;%MEMBRANE_HOME%/starter.jar" | ||
echo Membrane Router running... | ||
java -classpath "%CLASSPATH%" com.predic8.membrane.core.Starter -c proxies.xml |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
#!/bin/bash | ||
homeSet() { | ||
echo "MEMBRANE_HOME variable is now set" | ||
CLASSPATH="$MEMBRANE_HOME/conf" | ||
CLASSPATH="$CLASSPATH:$MEMBRANE_HOME/starter.jar" | ||
export CLASSPATH | ||
echo Membrane Router running... | ||
java -classpath "$CLASSPATH" com.predic8.membrane.core.Starter -c proxies.xml | ||
|
||
} | ||
|
||
terminate() { | ||
echo "Starting of Membrane Router failed." | ||
echo "Please execute this script from the appropriate subfolder of MEMBRANE_HOME/examples/" | ||
|
||
} | ||
|
||
homeNotSet() { | ||
echo "MEMBRANE_HOME variable is not set" | ||
|
||
if [ -f "`pwd`/../../starter.jar" ] | ||
then | ||
export MEMBRANE_HOME="`pwd`/../.." | ||
homeSet | ||
else | ||
terminate | ||
fi | ||
} | ||
|
||
|
||
if [ "$MEMBRANE_HOME" ] | ||
then homeSet | ||
else homeNotSet | ||
fi | ||
|