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

Introduce Preprocessor based clients #6060

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jrhee17
Copy link
Contributor

@jrhee17 jrhee17 commented Jan 9, 2025

Motivation:

#6057 has described preprocessors, which allow users to dynamically modify how to connect to a target endpoint dynamically. However, there is a limitation where an EndpointGroup and SessionProtocol must be provided even though the Preprocessor will solely determine how to connect to a remote.

For example, specifying the protocol and endpoint when building the client is not necessary since the preprocessor will fill in all of the necessary information.

HttpPreprocessor preprocessor = (delegate, ctx, req) -> {
  ctx.setEndpoint(Endpoint.of(...));
  ctx.setSessionProtocol(HTTPS);
  return delegate.execute(ctx, req);
}
GrpcClients.of(HTTP, Endpoint.of(...)).preprocessor(preprocessor)

I propose that users are able to build a client solely based on Preprocessor. By doing so, we may later introduce a XdsPreprocessor which may dynamically determine how to connect to a remote based on resources.

GrpcClients.of(XdsPreprocessor.of(...));

In order to support the above, I propose the following:

  • If a client is built based on Preprocessor, the SessionProtocol must be UNDEFINED.
    • For instance, GrpcClients.of(HttpPreprocessor).params().scheme().sessionProtocol() will be UNDEFINED
  • If a client is built with SessionProtocol.UNDEFINED, at least one Preprocessor must be present in ClientOptions.
    • For instance, a client built via (WebClient.of("undefined://google.com")) must also supply a preprocessor.
    • An exception to this rule is WebClient.of(). Because it is easy to supply a target EndpointGroup, SessionProtocol from its API, a preprocessor is not necessarily for creating a default WebClient.

Modifications:

  • User facing APIs have been added which allow clients to be initialized solely based on a Preprocessor.
    • Users may specify Preprocessor, and optionally a SerializationFormat and prefix path.

With the addition preprocessor-based clients, it is important to validate whether a client was created incorrectly. In order to validate client parameters from a unified location, it is necessary to know how a client was created. For this reason, following changes have been made to ClientBuilderParams:

  • If a client is not created based on a URI, create an internal URI based on a rule so that we can check how a client was created. e.g. endpointGroup based clients will have prefix armeria-group-, preprocessor based clients will have prefix armeria-preprocessor-, etc...
    • ClientBuilderParamsUtil has utility methods which determines how a client has been created.
  • Introduce a ClientBuilderParamsBuilder which introduces a fluent way to create a new ClientBuilderParams based on an existing ClientBuilderParams. This will help preserve the URI authority which embeds how a client was created.

Result:

  • Users can create clients solely based on Preprocessor

Miscellaneous

Just because an exception has been thrown from preprocessing does not necessarily mean that the request has not been sent. I have removed the UnprocessedRequestException wrapping behavior.
e.g.

val preprocessor = (delegate, ctx, req) -> {
  val res = delegate.execute(ctx, req);
  throw new RuntimeException();
}

@jrhee17 jrhee17 mentioned this pull request Jan 9, 2025
@jrhee17 jrhee17 force-pushed the feat/xds-filter-v2 branch 2 times, most recently from 64371bc to e111262 Compare January 15, 2025 07:36
@jrhee17 jrhee17 force-pushed the feat/xds-filter-v2 branch from e111262 to 23a2685 Compare January 15, 2025 07:38
@jrhee17 jrhee17 marked this pull request as ready for review January 15, 2025 08:21
@jrhee17 jrhee17 added this to the 1.32.0 milestone Jan 15, 2025
@jrhee17
Copy link
Contributor Author

jrhee17 commented Jan 15, 2025

ready for review

PROXY("proxy", false, false, 0);
PROXY("proxy", false, false, 0),

UNDEFINED("undefined", false, false, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) What do you think of using HTTP as the default session protocol instead? If a scheme is missing in curl or a browser, HTTP protocol is used by default.

$ curl google.com -v
* Host google.com:80 was resolved.
...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within Armeria, HTTP is currently not the default session protocol.

e.g. the following API exists

WebClient of(SessionProtocol protocol, EndpointGroup endpointGroup)

but the following API doesn't

WebClient of(EndpointGroup endpointGroup)

I'm fine with setting HTTP as the default session protocol, but for consistency I prefer that all APIs reflect this default session protocol behavior.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebClient of(EndpointGroup endpointGroup)

It would also be a good idea to add this API in the future.

SessionProtocol.UNDEFINED is unlikely to be used by users, and additional validation is required in places where UNDEFINED is not allowed.

Personally, I doubt that SessionProtocol.PROXY was a good decision. Because the usage of SessionProtocol.PROXY is limited only when specifying a port for creating a server. The rest of our codebase has a lot of validation added to check if it is HTTP or HTTPS.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be a good idea to add this API in the future.

If the others agree with this direction, let me also add the relevant APIs in this PR then.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also prefer not using SessionProtocol.UNDEFINED if we can avoid it. 😉

/**
* Allows creation of a new {@link ClientBuilderParams} based on a previous {@link ClientBuilderParams}.
*/
public final class ClientBuilderParamsBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) Could we use this builder to create DefaultClientBuilderParams in ClientBuilderParams.of()?

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.UnmodifiableFuture;

public final class FailingEndpointGroup implements EndpointGroup {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional) How about exposing FailingEndpointGroup as a public API or its singleton instance? It may be useful for a user to check whether the current EndpointGroup is initialized in Preprocessor to conditionally create an EndpointGroup.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UninitializedEndpointGroup or UndefinedEndpointGroup?
How about raising the exception for all method calls?

Comment on lines +37 to +39
private static final String PREPROCESSOR_PREFIX = "armeria-preprocessor-";
public static final String ENDPOINTGROUP_PREFIX = "armeria-group-";
public static final String UNDEFINED_URI_AUTHORITY = "armeria-undefined:1";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question) What do you think about also using UNDEFINED_URI_AUTHORITY as a placeholder for Preprocessor to indicate that the endpoint needs initializing?

If SerializationFormat is needed, I wonder if it would also be possible to express it like this:

gproto+http://armeria-undefined:1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understood the question. Are you suggesting that we treat UNDEFINED_URI_AUTHORITY and Preprocessor equivalently?

Copy link
Contributor

@ikhoon ikhoon Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if it was possible to reuse the existing undefined URI, UNDEFINED_URI_AUTHORITY rather than creating a new one.

* Returns a {@link ClientBuilderParamsBuilder} which allows creation of a new
* {@link ClientBuilderParams} based on the current properties.
*/
default ClientBuilderParamsBuilder paramsBuilder() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global comment: @UnstableApi

* Sets the {@link SerializationFormat} for this {@link ClientBuilderParams}.
*/
public ClientBuilderParamsBuilder serializationFormat(SerializationFormat serializationFormat) {
this.serializationFormat = serializationFormat;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rnn for all methods?

* the specified {@link SerializationFormat} or
* {@link ClientPreprocessors}
*/
public static <T> T newClient(ClientPreprocessors preprocessors, Class<T> clientType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public static <T> T newClient(ClientPreprocessors preprocessors, Class<T> clientType) {
public static <T> T newClient(ClientPreprocessors preprocessors, Class<T> clientType) {

* @param clientType the type of the new client
*
* @throws IllegalArgumentException if the specified {@code clientType} is unsupported for
* the specified {@link SerializationFormat} or
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SerializationFormat is not specfied

Copy link
Contributor

@minwoox minwoox left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good all in all. 👍

return uri == UNDEFINED_URI;
return (uri == ClientUtil.UNDEFINED_URI ||
ClientBuilderParamsUtil.UNDEFINED_URI_AUTHORITY.equals(uri.getAuthority())) &&
uri.getPort() == 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably remove uri.getPort() == 1 because the above conditions already imply it.

import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.UnmodifiableFuture;

public final class FailingEndpointGroup implements EndpointGroup {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UninitializedEndpointGroup or UndefinedEndpointGroup?
How about raising the exception for all method calls?

@@ -89,7 +94,7 @@ final class DefaultClientBuilderParams implements ClientBuilderParams {
normalizedAbsolutePathRef);
} else {
// Create a valid URI which will never succeed.
uri = URI.create(schemeStr + "://armeria-group-" +
uri = URI.create(schemeStr + "://" + ClientBuilderParamsUtil.ENDPOINTGROUP_PREFIX +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ENDPOINT_GROUP_PREFIX

"Scheme and authority must be specified in \":path\" or " +
"in \":scheme\" and \":authority\". :path=" +
originalPath + ", :scheme=" + req.scheme() + ", :authority=" + req.authority()));
"Failed to parse a scheme: " + reqTarget.scheme(), e));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Failed to parse a scheme: " + reqTarget.scheme(), e));
"Failed to parse a scheme: " + scheme, e));

Comment on lines +94 to +98
if (scheme.serializationFormat() == SerializationFormat.NONE) {
schemeStr = scheme.sessionProtocol().uriText();
} else {
schemeStr = scheme.uriText();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (scheme.serializationFormat() == SerializationFormat.NONE) {
schemeStr = scheme.sessionProtocol().uriText();
} else {
schemeStr = scheme.uriText();
}
if (scheme.serializationFormat() == SerializationFormat.NONE) {
schemeStr = scheme.sessionProtocol().uriText();
} else {
schemeStr = scheme.uriText();
}

I've seen this logic in multiple places. How about extracting it into a method? e.g. scheme.effectiveUriText()

PROXY("proxy", false, false, 0);
PROXY("proxy", false, false, 0),

UNDEFINED("undefined", false, false, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also prefer not using SessionProtocol.UNDEFINED if we can avoid it. 😉

@UnstableApi
public static GrpcClientBuilder builder(HttpPreprocessor httpPreprocessor) {
requireNonNull(httpPreprocessor, "httpPreprocessor");
return builder(SerializationFormat.NONE, httpPreprocessor);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be GrpcSerializationFormats.PROTO?

*/
@UnstableApi
public static ThriftClientBuilder builder(RpcPreprocessor rpcPreprocessor) {
return new ThriftClientBuilder(SerializationFormat.NONE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isnt' this ThriftSerializationFormats.BINARY?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants