Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update plugins-scenarios.md to Styx 1.0 API #320

Merged
merged 15 commits into from
Oct 29, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 83 additions & 119 deletions docs/developer-guide/plugins-scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Transforming a request object synchronously is trivial. By "synchronously" we mean in the
same thread, in non-blocking fashion.

Just call `request.newBuilder()` to create a new `HttpRequest.Builder` object.
Just call `request.newBuilder` to create a new `HttpRequest.Builder` object.
It has already been initialised with the copy of the original `request`. Modify
the builder as desired, construct a new version, and pass it to the `chain.proceed()`,
as the example demonstrates:
Expand All @@ -22,7 +22,7 @@ import static com.hotels.styx.api.HttpHeaderNames.USER_AGENT;

public class SyncRequestPlugin implements Plugin {
@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, Chain chain) {
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return chain.proceed(
request.newBuilder()
.header(USER_AGENT, "Styx/1.0 just testing plugins")
Expand All @@ -35,7 +35,7 @@ public class SyncRequestPlugin implements Plugin {
### Synchronously transforming response

This example demonstrates how to synchronously transform a HTTP response. We will
use a `StyxObservable.map` method to add an "X-Foo" header to the response.
call `Eventual.map` to add an "X-Foo" header to the response.

```java
import com.hotels.styx.api.HttpInterceptor;
Expand All @@ -46,7 +46,7 @@ import com.hotels.styx.api.plugins.spi.Plugin;

public class SyncResponsePlugin implements Plugin {
@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, HttpInterceptor.Chain chain) {
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, HttpInterceptor.Chain chain) {
return chain.proceed(request)
.map(response -> response.newBuilder()
.header("X-Foo", "bar")
Expand Down Expand Up @@ -75,7 +75,7 @@ import java.util.concurrent.CompletableFuture;
public class AsyncRequestInterceptor implements Plugin {

@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, HttpInterceptor.Chain chain) {
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, HttpInterceptor.Chain chain) {

return asyncUrlReplacement(request.url()) // (1)
.map(newUrl -> request.newBuilder() // (4)
Expand All @@ -84,8 +84,8 @@ public class AsyncRequestInterceptor implements Plugin {
.flatMap(chain::proceed); // (5)
}

private static StyxObservable<Url> asyncUrlReplacement(Url url) {
return StyxObservable.from(pathReplacementService(url.path())) // (3)
private static Eventual<Url> asyncUrlReplacement(Url url) {
return Eventual.from(pathReplacementService(url.path())) // (3)
.map(newPath -> new Url.Builder(url)
.path(newPath)
.build());
Expand All @@ -98,25 +98,25 @@ public class AsyncRequestInterceptor implements Plugin {
}
```

Step 1. We call the `asyncUrlReplacement`, which returns a `StyxObservable<Url>`.
Step 1. We call the `asyncUrlReplacement`, which returns an `Eventual<Url>`.
The `asyncUrlReplacement` wraps a call to the remote service and converts
the outcome into a `StyxObservable`, which is the basis for our response observable.
the outcome into an `Eventual`.

Step 2. A call to `pathReplacementService` makes a non-blocking call to the remote key/value store.
Well, at least we pretend to call the key value store, but in this example we'll just return a
completed future of a constant value.

Step 3. `CompletableFuture` is converted to `StyxObservable`, so that other asynchronous
Step 3. `CompletableFuture` is converted to an `Eventual`, so that other asynchronous
operations like `chain.proceed` can be bound to it later on.

Step 4. The eventual outcome from the `asyncUrlReplacement` yields a new, modified URL instance.
We'll transform the `request` by substituting the URL path with the new one.
This is a quick synchronous operation so we'll do it in a `map` operator.

Step 5. Finally, we will bind the outcome from `chain.proceed` into the response observable.
Remember that `chain.proceed` returns an `Observable<HttpResponse>` it is therefore
interface compatible and can be `flatMap`'d to the response observable. The resulting
response observable chain is returned from the `intercept`.
Step 5. Finally, we will bind the outcome from `chain.proceed` into the response `Eventual`.
Remember that `chain.proceed` returns an `Eventual<LiveHttpResponse>`. It is, therefore,
interface compatible and can be `flatMap`'d to the response `Eventual`. The resulting
response `Eventual` chain is returned from the `intercept`.


### Asynchronously transform response object
Expand All @@ -140,11 +140,10 @@ public class AsyncResponseInterceptor implements Plugin {
private static final String X_MY_HEADER = "X-My-Header";

@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, HttpInterceptor.Chain chain) {
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, HttpInterceptor.Chain chain) {
return chain.proceed(request) // (1)
.flatMap(response -> // (3)
StyxObservable
.from(thirdPartyHeaderService(response.header(X_MY_HEADER).orElse("default"))) // (1)
Eventual.from(thirdPartyHeaderService(response.header(X_MY_HEADER).orElse("default"))) // (2)
.map(value -> // (4)
response.newBuilder()
.header(X_MY_HEADER, value)
Expand All @@ -159,24 +158,24 @@ public class AsyncResponseInterceptor implements Plugin {
}
```

Step 1. We start by calling `chain.proceed(request)` to obtain a response observable.
Step 1. We start by calling `chain.proceed(request)` to obtain a response `Eventual`.

Step 2. A `callTo3rdParty` returns a CompletableFuture. We use `StyxObservable.from` to convert it
into a `StyxObservable` so that it can be bound to the response observable.
Step 2. A `callTo3rdParty` returns a `CompletableFuture`. It is converted to an `Eventual` with
`Eventual.from` and bound to the response `Eventual`.

Step 3. The `flatMap` operator binds `callToThirdParty` into the response observable.
Step 3. The `flatMap` operator binds `callToThirdParty` into the response `Eventual`.

Step 4. We will transform the HTTP response by inserting an outcome of `callToThirdParty`, or `value`,
into the response headers.


## HTTP Content Transformations

Styx exposes HTTP messages to interceptors as streaming `HttpRequest` and `HttpResponse` messages.
Styx exposes HTTP messages to interceptors as streaming `LiveHttpRequest` and `LiveHttpResponse` messages.
In this form, the interceptors can process the content in a streaming fashion. That is, they they
can look into, and modify the content as it streams through.

Alternatively, streaming messages can be aggregated into a `FullHttpRequest` or `FullHttpResponse`
Alternatively, live messages can be aggregated to `HttpRequest` or `HttpResponse`
messages. The full HTTP message body is then available for the interceptor to use. Note that content
aggregation is always an asynchronous operation. This is because the streaming HTTP message is
exposing the content, in byte buffers, as it arrives from the network, and Styx must wait until
Expand All @@ -195,7 +194,7 @@ public class RequestAggregationPlugin implements Plugin {
private static final int MAX_CONTENT_LENGTH = 100000;

@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, Chain chain) {
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return request.toFullRequest(MAX_CONTENT_LENGTH)
.map(fullRequest -> fullRequest.newBuilder()
.body(new byte[0], true)
Expand All @@ -206,7 +205,7 @@ public class RequestAggregationPlugin implements Plugin {
```

`maxContentBytes` is a safety valve that prevents Styx from accumulating too much content
and possibly running out of memory. Styx only accumulates up to `maxContentBytes` of content.
and running out of memory. Styx only accumulates up to `maxContentBytes` of content.
`toFullRequest` fails when the content stream exceeds this amount, and Styx emits a
`ContentOverflowException`,

Expand All @@ -216,15 +215,14 @@ and possibly running out of memory. Styx only accumulates up to `maxContentBytes
Streaming HTTP body content can be transformed both synchronously and asynchronously.
However there are some pitfalls you need to know:

- Reference counting. Styx exposes the content stream as an observable of reference counted
Netty `ByteBuf` objects. Ensure the reference counts are correctly decremented when
buffers are transformed.

- Continuity (or discontinuity) of Styx content observable. Each content tranformation with
`map` or `flatMap` is a composition of some source observable. So is each content transformation
linked to some source observable, an ultimate source being the Styx server core.
- **Reference counting:** Styx exposes the content stream as a `ByteStream` of reference counted
`Buffer` objects.

- **Continuity (or discontinuity) of Styx content `ByteStream`:** each content transformation with
`map` or `flatMap` is a composition of another `ByteStream`. So is each content transformation
linked to some source `ByteStream`, an ultimate source being the Styx server core.
It is the consumer's responsibility to ensure this link never gets broken. That is, you
are not allowed to just substitute the content observable with another one, unless it composes
are not allowed to just replace the content stream with another one, unless it composes
to the previous content source.


Expand All @@ -233,115 +231,81 @@ However there are some pitfalls you need to know:
In this rather contrived example we will transform HTTP response content to
upper case letters.

Streaming `HttpRequest` content is a byte buffer stream. The stream can be accessed
with a `request.body()` method and its data type is `StyxObservable<ByteBuf>`.

Because `toUpperCase` is non-blocking transformation we can compose it to the original
byte stream using a `map` operator.

The mapping function decodes the original byte buffer `buf` to an UTF-8 encoded
Java `String`, and copies its upper case representation into a newly created byte buffer.
The old byte buffer `buf` is discarded.

Because `buf` is a Netty reference counted `ByteBuf`, we must take care to decrement its
reference count by calling `buf.release()`.

```java
public class MyPlugin extends Plugin {
@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, Chain chain) {
StyxObservable<ByteBuf> toUpperCase = request.body()
.map(buf -> {
buf.release();
return copiedBuffer(buf.toString(UTF_8).toUpperCase(), UTF_8);
});

public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return chain.proceed(
request.newBuilder()
.body(toUpperCase)
.body(byteStream -> // 1
byteStream.map(buf -> { // 2
String upperCase = new String(buf.content(), UTF_8).toUpperCase(); // 3
return new Buffer(upperCase, UTF_8);
}))
.build()
);
}
}
```

### Asynchronously transforming streaming request content

Asynchronous content stream transformation is very similar to the synchronous transformation.
The only difference is that asynchronous transformation must be composed with a `flatMap`
instead of `map`. Otherwise the same discussion apply. As always it is important to take care
of the reference counting.
A call to `request.newBuilder` opens up a new request builder that allows the request
to be transformed.

```java
public class AsyncRequestContentTransformation implements Plugin {

@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, Chain chain) {
StyxObservable<ByteBuf> contentMapping = request.body()
.flatMap(buf -> {
String content = buf.toString(UTF_8);
buf.release();
return sendToRemoteServer(content)
.map(value -> copiedBuffer(value, UTF_8));
});
The body transformation involves two lambda methods:

return chain.proceed(
request.newBuilder()
.body(contentMapping)
.build()
);
}

StyxObservable<String> sendToRemoteServer(String buf) {
return StyxObservable.of("modified 3rd party content");
}
}
```
1. `.body(Function<ByteStream, ByteStream>)` accepts a lambda that modifies the request
byte stream. Here we provide a lambda that accepts a `ByteStream` and returns another
by applying a synchronous `map` operator on the stream.

2. The `ByteStream` is modified by applying a `map` operator that synchronously modifies
each `Buffer` that streams through. The `map` involves another lambda that accepts a
`Buffer` and returns a modified buffer.

3. In this example, the content of the buffer is interpreted as `String` and
converted to upper case.


### Synchronously transforming streaming response content

This example demonstrates how HTTP response content can be transformed synchronously.

```java
public class AsyncResponseContentStreamTransformation implements Plugin {
@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, Chain chain) {
public Eventual<LiveHttpResponse> intercept(LiveHttpRequest request, Chain chain) {
return chain.proceed(request)
.map(response -> {
StyxObservable<ByteBuf> contentMapping = response.body()
.map(buf -> {
buf.release();
return copiedBuffer(buf.toString(UTF_8).toUpperCase(), UTF_8);
});
return response.newBuilder()
.body(contentMapping)
.build();
});
.map(response -> // 1
response.newBuilder()
.body(byteStream -> // 2
byteStream.map(buf -> { // 3
String upperCase = new String(buf.content(), UTF_8).toUpperCase(); // 4
return new Buffer(upperCase, UTF_8);
}))
.build());
}
}
```

### Asynchronously transforming streaming response content
This is quite similar to the request transformation, but it involves an additional lambda
expression to capture the HTTP response from its `Eventual` envelope:

```java
public class AsyncResponseContentStreamTransformation implements Plugin {
@Override
public StyxObservable<HttpResponse> intercept(HttpRequest request, Chain chain) {
return chain.proceed(request)
.map(response -> {
StyxObservable<ByteBuf> contentMapping = response.body()
.flatMap(buf -> {
String content = buf.toString(UTF_8);
buf.release();
return sendToRemoteServer(content)
.map(value -> copiedBuffer(value, UTF_8));
});
return response.newBuilder()
.body(contentMapping)
.build();
});
}
1. `chain.proceed` returns an `Eventual` HTTP response. We apply an `Eventual.map` operator
to tap into this response. The `map` operator accepts a lambda expression that handles the
response when it is available.

After that, we will transform the response just like we did in the previous example for
response content transformations.

StyxObservable<String> sendToRemoteServer(String buf) {
return StyxObservable.of("modified 3rd party content");
}
}
```


### Asynchronously transforming streaming request content

Not supported at the moment. Please raise a feature request and the Styx team can implement this.





### Asynchronously transforming streaming response content

Not supported at the moment. Please raise a feature request and the Styx team can implement this.