Skip to content

Commit

Permalink
Implemented ShadowingInterceptor (#1258)
Browse files Browse the repository at this point in the history
* 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
christiangoerdes authored Sep 4, 2024
1 parent 77b793f commit 1bd5a25
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 0 deletions.
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;
}
}
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);
}

}
41 changes: 41 additions & 0 deletions distribution/examples/shadowing/README.md
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>
```
35 changes: 35 additions & 0 deletions distribution/examples/shadowing/proxies.xml
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>
18 changes: 18 additions & 0 deletions distribution/examples/shadowing/service-proxy.bat
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
35 changes: 35 additions & 0 deletions distribution/examples/shadowing/service-proxy.sh
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

0 comments on commit 1bd5a25

Please sign in to comment.