Skip to content

Commit b118079

Browse files
committed
Move call decorator to interceptors
Instead of a separate CallDecorator abstraction, unify call interception into ClientInterceptors using a new interceptCall hook and interceptCalls boolean method. If interceptCalls returns true, then the interceptors is used in a chain of middleware-style interceptors that can completely intercept control flow of the call. This can be used for things like caching, hedging, etc. A tradeoff of making this an interceptor hook vs a dedicated abstraction is that the Client, if needed, is not generic and instead stashed in Context. The cost for making calls is essentially the same when nothing intercepts. The cost of actually intercepting is essentially the same (one pre-resolves the chain in a nested call stack, this is iterative). The advantages are that there's a single abstraction and composition is the same as before.
1 parent 19fc4cf commit b118079

10 files changed

Lines changed: 194 additions & 318 deletions

File tree

client/client-core/src/main/java/software/amazon/smithy/java/client/core/CallDecorator.java

Lines changed: 0 additions & 109 deletions
This file was deleted.

client/client-core/src/main/java/software/amazon/smithy/java/client/core/Client.java

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import software.amazon.smithy.java.client.core.auth.scheme.AuthSchemeResolver;
1818
import software.amazon.smithy.java.client.core.interceptors.CallHook;
1919
import software.amazon.smithy.java.client.core.interceptors.ClientInterceptor;
20+
import software.amazon.smithy.java.client.core.interceptors.InputHook;
2021
import software.amazon.smithy.java.client.core.plugins.AutoPlugin;
2122
import software.amazon.smithy.java.context.Context;
2223
import software.amazon.smithy.java.core.schema.ApiOperation;
@@ -41,7 +42,6 @@ public abstract class Client implements Closeable {
4142
private final ClientInterceptor interceptor;
4243
private final IdentityResolvers identityResolvers;
4344
private final RetryStrategy retryStrategy;
44-
private static final CallDecorator.Invoker CALL_INVOKER = Client::sendCall;
4545

4646
protected Client(Builder<?, ?> builder) {
4747
ClientConfig.Builder configBuilder = builder.configBuilder();
@@ -72,7 +72,6 @@ protected Client(Builder<?, ?> builder) {
7272
* @param <O> Output shape.
7373
* @return Returns the deserialized output.
7474
*/
75-
@SuppressWarnings("unchecked")
7675
protected <I extends SerializableStruct, O extends SerializableStruct> O call(
7776
I input,
7877
ApiOperation<I, O> operation,
@@ -115,15 +114,21 @@ protected <I extends SerializableStruct, O extends SerializableStruct> O call(
115114
TypeRegistry.compose(operation.errorRegistry(), typeRegistry),
116115
retryStrategy);
117116

118-
// Resolve the decorator from the (possibly modifyBeforeCall-mutated) config so an
119-
// interceptor can install or replace it.
120-
var decorator = (CallDecorator<Client>) callConfig.callDecorator();
121-
return decorator != null ? decorator.apply(this, call, CALL_INVOKER) : sendCall(call);
122-
}
117+
// Make the running client available to interceptors that need to re-enter.
118+
call.context.put(ClientContext.CLIENT, this);
119+
120+
if (!callInterceptor.interceptCalls()) {
121+
return callPipeline.send(call);
122+
}
123123

124-
private static <I extends SerializableStruct, O extends SerializableStruct> O sendCall(ClientCallView<I, O> call) {
125-
ClientCall<I, O> impl = (ClientCall<I, O>) call;
126-
return impl.pipeline.send(impl);
124+
// Build an InputHook for interceptCall and a terminal Invoker that sends the
125+
// (possibly input-substituted) call through the pipeline.
126+
InputHook<I, O> hook = new InputHook<>(operation, call.context, input);
127+
return callInterceptor.interceptCall(hook, h -> {
128+
return h.input() == call.input
129+
? call.pipeline.send(call)
130+
: call.pipeline.send(new ClientCall<>(call, h.input()));
131+
});
127132
}
128133

129134
/**
@@ -422,19 +427,6 @@ public boolean test(ClientPlugin plugin) {
422427
}.and(configBuilder.pluginPredicate()));
423428
}
424429

425-
/**
426-
* Adds a decorator that wraps client call execution. Multiple decorators compose: the first
427-
* added is the outermost wrapper.
428-
*
429-
* @param callDecorator the call decorator to add.
430-
* @return the builder.
431-
*/
432-
@SuppressWarnings("unchecked")
433-
public B addCallDecorator(CallDecorator<I> callDecorator) {
434-
configBuilder.addCallDecorator(callDecorator);
435-
return (B) this;
436-
}
437-
438430
/**
439431
* Creates the client.
440432
*

client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientCall.java

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
* @param <I> Input to send.
3333
* @param <O> Output to return.
3434
*/
35-
final class ClientCall<I extends SerializableStruct, O extends SerializableStruct> implements ClientCallView<I, O> {
35+
final class ClientCall<I extends SerializableStruct, O extends SerializableStruct> {
3636

3737
final I input;
3838
final ApiOperation<I, O> operation;
@@ -91,7 +91,7 @@ final class ClientCall<I extends SerializableStruct, O extends SerializableStruc
9191
* resolved configuration. Retry state is shared too — decorators run before the retry loop starts,
9292
* so {@code attemptCount}/{@code retryToken} are at their initial values either way.
9393
*/
94-
private ClientCall(ClientCall<I, O> source, I newInput) {
94+
ClientCall(ClientCall<I, O> source, I newInput) {
9595
this.input = Objects.requireNonNull(newInput, "input is null");
9696
this.operation = source.operation;
9797
this.context = source.context;
@@ -109,51 +109,6 @@ private ClientCall(ClientCall<I, O> source, I newInput) {
109109
: null;
110110
}
111111

112-
@Override
113-
public I input() {
114-
return input;
115-
}
116-
117-
@Override
118-
public ApiOperation<I, O> operation() {
119-
return operation;
120-
}
121-
122-
@Override
123-
public Context context() {
124-
return context;
125-
}
126-
127-
@Override
128-
public EndpointResolver endpointResolver() {
129-
return endpointResolver;
130-
}
131-
132-
@Override
133-
public TypeRegistry typeRegistry() {
134-
return typeRegistry;
135-
}
136-
137-
@Override
138-
public AuthSchemeResolver authSchemeResolver() {
139-
return authSchemeResolver;
140-
}
141-
142-
@Override
143-
public Map<ShapeId, AuthScheme<?, ?>> supportedAuthSchemes() {
144-
return supportedAuthSchemes;
145-
}
146-
147-
@Override
148-
public IdentityResolvers identityResolvers() {
149-
return identityResolvers;
150-
}
151-
152-
@Override
153-
public ClientCallView<I, O> withInput(I newInput) {
154-
return new ClientCall<>(this, newInput);
155-
}
156-
157112
/**
158113
* Check if a retry is disallowed for this call.
159114
*

client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientCallView.java

Lines changed: 0 additions & 80 deletions
This file was deleted.

client/client-core/src/main/java/software/amazon/smithy/java/client/core/ClientConfig.java

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public final class ClientConfig {
5353
private final RetryStrategy retryStrategy;
5454
private final String retryScope;
5555
private final Set<Class<? extends ClientPlugin>> appliedPluginClasses;
56-
private final CallDecorator<?> callDecorator;
5756

5857
private ClientConfig(Builder builder) {
5958
// Collect and apply plugins, updating builder.appliedPluginClasses as we go
@@ -96,7 +95,6 @@ private ClientConfig(Builder builder) {
9695

9796
this.context = Context.unmodifiableCopy(builder.context);
9897
this.service = Objects.requireNonNull(builder.service, "Missing required service schema");
99-
this.callDecorator = builder.callDecorator;
10098
}
10199

102100
private static List<ClientPlugin> collectPlugins(
@@ -225,10 +223,6 @@ String retryScope() {
225223
return retryScope;
226224
}
227225

228-
CallDecorator<?> callDecorator() {
229-
return callDecorator;
230-
}
231-
232226
/**
233227
* Create a new builder to build {@link ClientConfig}.
234228
*
@@ -328,7 +322,6 @@ public static final class Builder {
328322
private final Map<Class<? extends ClientPlugin>, ClientPlugin> plugins = new LinkedHashMap<>();
329323
// Mutable set that tracks which plugin classes have been applied to this builder
330324
private final Set<Class<? extends ClientPlugin>> appliedPluginClasses = new HashSet<>();
331-
private CallDecorator<?> callDecorator;
332325

333326
public Builder() {
334327
plugins.put(DefaultPlugin.class, DefaultPlugin.INSTANCE);
@@ -350,7 +343,6 @@ private Builder copyBuilder() {
350343
builder.plugins.putAll(plugins);
351344
builder.pluginPredicate = pluginPredicate;
352345
builder.appliedPluginClasses.addAll(appliedPluginClasses);
353-
builder.callDecorator = callDecorator;
354346
return builder;
355347
}
356348

@@ -667,20 +659,6 @@ public Builder addPluginPredicate(Predicate<ClientPlugin> pluginPredicate) {
667659
return pluginPredicate(this.pluginPredicate.and(pluginPredicate));
668660
}
669661

670-
/**
671-
* Adds a decorator that wraps client call execution. Multiple decorators compose: the first
672-
* added is the outermost wrapper.
673-
*
674-
* @param callDecorator the call decorator to add.
675-
* @return the builder.
676-
*/
677-
@SuppressWarnings({"unchecked", "rawtypes"})
678-
public Builder addCallDecorator(CallDecorator<?> callDecorator) {
679-
this.callDecorator = CallDecorator.chain((CallDecorator) this.callDecorator,
680-
(CallDecorator) callDecorator);
681-
return this;
682-
}
683-
684662
/**
685663
* Get the plugin predicate of the builder.
686664
*

0 commit comments

Comments
 (0)