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

Add Nacos Support #5409

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

Conversation

KonaEspresso94
Copy link

@KonaEspresso94 KonaEspresso94 commented Jan 25, 2024

Motivation:

Nacos is a service discovery application extensively used. By providing native support for Nacos, it will be beneficial to Armeria users.

#5360 #5365

Modification:

  • Add a nacos module.
  • Add NacosClient.
    Its internal implementation includes LoginClient for obtaining an accessToken through basic Nacos authentication, QueryInstancesClient for querying service instances, and RegisterInstanceClient for handling the registration and deregistration of instances.
  • Implement NacosEndpointGroup and NacosUpdatingListener.
  • Configure unit tests using Testcontainers and the Nacos image to facilitate actual call testing.

Result:

Closes #5365

Nacos discovery and instance registration functionalities are now integrated into Armeria.


In writing this code, I was strongly influenced by the implementation on the consul and eureka modules.
I appreciate your review and feedback. Thank you.

@CLAassistant
Copy link

CLAassistant commented Jan 25, 2024

CLA assistant check
All committers have signed the CLA.

KonaEspresso94 and others added 2 commits January 27, 2024 16:59
…stead of QueryParams, making it easier to append additional parameters (e.g., accessToken).
Copy link
Contributor

@ikhoon ikhoon left a comment

Choose a reason for hiding this comment

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

The overall direction looks great.

Comment on lines 105 to 110
String accessToken;

Integer tokenTtl;

@Nullable
Boolean globalAdmin;
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's make them immutable.

Suggested change
String accessToken;
Integer tokenTtl;
@Nullable
Boolean globalAdmin;
private final String accessToken;
private final Integer tokenTtl;
@Nullable
private final Boolean globalAdmin;

* A Nacos client that is responsible for
* <a href="https://nacos.io/en-us/docs/auth.html">Nacos Authentication</a>.
*/
final class LoginClient {
Copy link
Contributor

Choose a reason for hiding this comment

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

The decorator pattern may hide the details of authentication.
How about making LoginClient as a decorator to decorate WebClient?


private static final String NACOS_ACCESS_TOKEN_CACHE_KEY = "NACOS_ACCESS_TOKEN_CACHE_KEY";
private final AsyncLoadingCache<String, LoginResult> tokenCache = Caffeine.newBuilder()
.maximumSize(1)
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional) It seems overkill to use Caffeine only for 1 element.
What do you think of creating a function that triggers loginInternal() if access tokens are expired while invoking login()?

If you find it more difficult than Caffeine approach, I'm okay to keep this as is.

final StringBuilder path = new StringBuilder("/").append(nacosApiVersion).append("/ns/instance/list?");
final QueryParamsBuilder paramsBuilder = NacosClientUtil
.queryParamsBuilder(namespaceId, groupName, serviceName, clusterName, healthyOnly, app, null,
null, null);
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be inefficient to create QueryParams for the same parameters for all requests.
How about creating and caching the QueryParams as a member field when QueryInstancesClient is created?

.as(HttpEntity::content)
.execute();
} else {
return loginClient.login().thenCompose(accessToken ->
Copy link
Contributor

Choose a reason for hiding this comment

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

As I mentioned above, would you make loginClient as a decorator?

@JsonIgnoreProperties(ignoreUnknown = true)
private static final class Host {
@Nullable
String instanceId;
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto. Let's make them immutable.

@KonaEspresso94 KonaEspresso94 marked this pull request as draft February 9, 2024 13:58
@KonaEspresso94
Copy link
Author

@ikhoon

Thank you for your review comments. I've applied your suggestions into the code.

  • LoginClient now implements SimpleDecoratingHttpClient. Contexts related to 'nacos basic authentication' have been hidden as much as possible from other clients.
  • Caching access token by own implementation instead of Caffeine cache.
  • Changed the query string that was being created each time in LoginClient and QueryInstanceClient to be reused efficiently.
  • NacosUpdatingListener and RegisterInstanceClient are not guaranteed to always have the same endpoint used as parameter. Eventually, we will need to create a StringBuilder or QueryParamsBuilder. However, I have minimized the parameter of the API by turning immutable parameters into properties, such as in the above two clients.
  • Fixed some missing final.

The implementation to replace the Caffein, I made it as simple as possible. So now the login api can be called concurrently. Considering the usability of NacosUpdatingListener and NacosEndpointGroup, I think it's unlikely that concurrent calls will actually happen, and if they do, it won't be an issue. But I'm not sure. I'd be interested in your thoughts.

Thank you!

@KonaEspresso94 KonaEspresso94 marked this pull request as ready for review February 11, 2024 11:13
return UnmodifiableFuture.completedFuture(cacheEntry.loginResult.accessToken);
}

return login().thenApply(loginResult -> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This may result in sending multiple login requests that are not what we really want.
If login() is being performed, other requests can piggyback on the returned future.

By the way, I think an AsyncLoader loading a value when it is expired or periodically would be useful in many places.
Created #5506

Copy link
Member

@trustin trustin Jun 6, 2024

Choose a reason for hiding this comment

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

Maybe we shouldn't let @KonaEspresso94 wait for too long for the PR to be merged but just use Caffeine internally and later replace the Caffeine usage with AsyncLoader?

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't know it would take this long. 😅 Let's leave that part as is. I will continue to review this PR tomorrow.

Copy link
Author

@KonaEspresso94 KonaEspresso94 Jun 16, 2024

Choose a reason for hiding this comment

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

Thank you for your consideration. Following your comments, I have reverted the commit to use Caffeine instead of my implementation.

@github-actions github-actions bot added the Stale label Apr 15, 2024
@trustin
Copy link
Member

trustin commented May 8, 2024

@KonaEspresso94 Gentle ping - looking forward to having this feature merged 🙇

@github-actions github-actions bot removed the Stale label May 9, 2024
@jrhee17 jrhee17 added this to the 1.30.0 milestone Jun 17, 2024
Copy link
Contributor

@jrhee17 jrhee17 left a comment

Choose a reason for hiding this comment

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

Direction looks good to me 👍 Left some minor comments

* using <a href="https://nacos.io/en-us/docs/v2/guide/user/open-api.html">Nacos's HTTP Open API</a>
* and updates the {@link Endpoint}s periodically.
*/
public class NacosEndpointGroup extends DynamicEndpointGroup {
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Can NacosEndpointGroup be package-private?

final class NacosEndpointGroup {
  ...
  public static EndpointGroup of(URI
}

private final long registryFetchIntervalMillis;

@Nullable
private volatile ScheduledFuture<?> scheduledFuture;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit; I don't think we need to worry about race conditions when closing the EndpointGroup if we pre-choose an EventExecutor

private final EventExecutor eventLoop;
...
  NacosEndpointGroup(...) {
    eventLoop = CommonPools.workerGroup().next();
  }
...
protected void doCloseAsync(CompletableFuture<?> future) {
  if (!eventExecutor.inEventLoop()) {
    eventExecutor.execute(() -> doCloseAsync(future));
  }
...

* sb.serverListener(listener);
* }</pre>
*/
public class NacosEndpointGroupBuilder extends AbstractDynamicEndpointGroupBuilder {
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 class NacosEndpointGroupBuilder extends AbstractDynamicEndpointGroupBuilder {
public final class NacosEndpointGroupBuilder extends AbstractDynamicEndpointGroupBuilder<NacosEndpointGroupBuilder> {

* Sets properties for building a Nacos client.
*/
@UnstableApi
public interface NacosConfigSetters {
Copy link
Contributor

Choose a reason for hiding this comment

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

optional; What do you think of generalizing this interface and adding the related methods?

e.g.

public interface NacosConfigSetters<SELF extends NacosConfigSetters<SELF>> {
  SELF nacosApiVersion(...)
}

Comment on lines 74 to 76
.map(QueryInstancesClient::toEndpoint)
.filter(Objects::nonNull)
.collect(toImmutableList()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you mind running an auto-indent for files in this PR in general one more time?


CompletableFuture<List<Endpoint>> endpoints() {
return queryInstances()
.thenApply(response -> response.data.hosts.stream()
Copy link
Contributor

Choose a reason for hiding this comment

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

Question) Just to make sure, even if code != 0 (which I assume means successful) will data and hosts always exist?
I'm asking because there doesn't seem to defence against null check

return nacosUri;
}

protected static int[] unusedPorts(int numPorts) {
Copy link
Contributor

Choose a reason for hiding this comment

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

To other reviewers) I guess the ideal approach (binding to port 0) can be done for all similar tests at once. I think this code is fine if the CI passes reliably at least for now.

Comment on lines 159 to 182
@Override
public NacosEndpointGroupBuilder allowEmptyEndpoints(boolean allowEmptyEndpoints) {
return (NacosEndpointGroupBuilder) super.allowEmptyEndpoints(allowEmptyEndpoints);
}

/**
* Sets the timeout to wait until a successful {@link Endpoint} selection.
* {@link Duration#ZERO} disables the timeout.
* If unspecified, {@link Flags#defaultResponseTimeoutMillis()} is used by default.
*/
@Override
public NacosEndpointGroupBuilder selectionTimeout(Duration selectionTimeout) {
return (NacosEndpointGroupBuilder) super.selectionTimeout(selectionTimeout);
}

/**
* Sets the timeout to wait until a successful {@link Endpoint} selection.
* {@code 0} disables the timeout.
* If unspecified, {@link Flags#defaultResponseTimeoutMillis()} is used by default.
*/
@Override
public NacosEndpointGroupBuilder selectionTimeoutMillis(long selectionTimeoutMillis) {
return (NacosEndpointGroupBuilder) super.selectionTimeoutMillis(selectionTimeoutMillis);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

This is unrelated to your PR, but we can probably remove these lines now

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.

Service discovery with Nacos
5 participants