Skip to content

Commit

Permalink
Add Nacos Support (#5409)
Browse files Browse the repository at this point in the history
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.

---------

Co-authored-by: jrhee17 <[email protected]>
Co-authored-by: Ikhun Um <[email protected]>
Co-authored-by: minwoox <[email protected]>
  • Loading branch information
4 people authored Nov 7, 2024
1 parent 011741d commit 0b36df3
Show file tree
Hide file tree
Showing 22 changed files with 1,998 additions and 0 deletions.
4 changes: 4 additions & 0 deletions nacos/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dependencies {
implementation(libs.caffeine)
testImplementation(libs.testcontainers.junit.jupiter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2024 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.client.nacos;

import static java.util.Objects.requireNonNull;

import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.MoreObjects;

import com.linecorp.armeria.client.Endpoint;
import com.linecorp.armeria.client.endpoint.DynamicEndpointGroup;
import com.linecorp.armeria.client.endpoint.EndpointGroup;
import com.linecorp.armeria.client.endpoint.EndpointSelectionStrategy;
import com.linecorp.armeria.common.CommonPools;
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.annotation.UnstableApi;
import com.linecorp.armeria.internal.nacos.NacosClient;

import io.netty.util.concurrent.EventExecutor;

/**
* A Nacos-based {@link EndpointGroup} implementation that retrieves the list of {@link Endpoint} from Nacos
* 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.
*/
@UnstableApi
public final class NacosEndpointGroup extends DynamicEndpointGroup {

private static final Logger logger = LoggerFactory.getLogger(NacosEndpointGroup.class);

/**
* Returns a {@link NacosEndpointGroup} with the specified {@code serviceName}.
*/
public static NacosEndpointGroup of(URI nacosUri, String serviceName) {
return builder(nacosUri, serviceName).build();
}

/**
* Returns a newly-created {@link NacosEndpointGroupBuilder} with the specified {@code nacosUri}
* and {@code serviceName} to build {@link NacosEndpointGroupBuilder}.
*
* @param nacosUri the URI of Nacos API service, including the path up to but not including API version.
* (example: http://localhost:8848/nacos)
*/
public static NacosEndpointGroupBuilder builder(URI nacosUri, String serviceName) {
return new NacosEndpointGroupBuilder(nacosUri, serviceName);
}

private final NacosClient nacosClient;

private final long registryFetchIntervalMillis;

private final EventExecutor eventLoop;

@Nullable
private ScheduledFuture<?> scheduledFuture;

NacosEndpointGroup(EndpointSelectionStrategy selectionStrategy, boolean allowEmptyEndpoints,
long selectionTimeoutMillis, NacosClient nacosClient,
long registryFetchIntervalMillis) {
super(selectionStrategy, allowEmptyEndpoints, selectionTimeoutMillis);
this.nacosClient = requireNonNull(nacosClient, "nacosClient");
this.registryFetchIntervalMillis = registryFetchIntervalMillis;
eventLoop = CommonPools.workerGroup().next();

update();
}

private void update() {
if (isClosing()) {
return;
}

nacosClient.endpoints()
.handleAsync((endpoints, cause) -> {
if (isClosing()) {
return null;
}

if (cause != null) {
logger.warn("Unexpected exception while fetching the registry from: {}",
nacosClient.uri(), cause);
} else {
setEndpoints(endpoints);
}

scheduledFuture = eventLoop.schedule(this::update, registryFetchIntervalMillis,
TimeUnit.MILLISECONDS);
return null;
}, eventLoop);
}

@Override
protected void doCloseAsync(CompletableFuture<?> future) {
if (!eventLoop.inEventLoop()) {
eventLoop.execute(() -> doCloseAsync(future));
return;
}

if (scheduledFuture != null) {
scheduledFuture.cancel(true);
}

future.complete(null);
}

@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("registryFetchIntervalMillis", registryFetchIntervalMillis)
.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright 2024 LY Corporation
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.client.nacos;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import java.net.URI;
import java.time.Duration;

import com.linecorp.armeria.client.endpoint.AbstractDynamicEndpointGroupBuilder;
import com.linecorp.armeria.client.endpoint.EndpointSelectionStrategy;
import com.linecorp.armeria.common.Flags;
import com.linecorp.armeria.common.annotation.UnstableApi;
import com.linecorp.armeria.common.nacos.NacosConfigSetters;
import com.linecorp.armeria.internal.nacos.NacosClient;
import com.linecorp.armeria.internal.nacos.NacosClientBuilder;

/**
* A builder class for {@link NacosEndpointGroup}.
* <h2>Examples</h2>
* <pre>{@code
* NacosEndpointGroup endpointGroup = NacosEndpointGroup.builder(nacosUri, "myService")
* .build();
* WebClient client = WebClient.of(SessionProtocol.HTTPS, endpointGroup);
* }</pre>
*/
@UnstableApi
public final class NacosEndpointGroupBuilder
extends AbstractDynamicEndpointGroupBuilder<NacosEndpointGroupBuilder>
implements NacosConfigSetters<NacosEndpointGroupBuilder> {

private static final long DEFAULT_CHECK_INTERVAL_MILLIS = 10_000;

private final NacosClientBuilder nacosClientBuilder;
private EndpointSelectionStrategy selectionStrategy = EndpointSelectionStrategy.weightedRoundRobin();
private long registryFetchIntervalMillis = DEFAULT_CHECK_INTERVAL_MILLIS;

NacosEndpointGroupBuilder(URI nacosUri, String serviceName) {
super(Flags.defaultResponseTimeoutMillis());
nacosClientBuilder = NacosClient.builder(nacosUri, requireNonNull(serviceName, "serviceName"));
}

/**
* Sets the {@link EndpointSelectionStrategy} of the {@link NacosEndpointGroup}.
*/
public NacosEndpointGroupBuilder selectionStrategy(EndpointSelectionStrategy selectionStrategy) {
this.selectionStrategy = requireNonNull(selectionStrategy, "selectionStrategy");
return this;
}

@Override
public NacosEndpointGroupBuilder namespaceId(String namespaceId) {
nacosClientBuilder.namespaceId(namespaceId);
return this;
}

@Override
public NacosEndpointGroupBuilder groupName(String groupName) {
nacosClientBuilder.groupName(groupName);
return this;
}

@Override
public NacosEndpointGroupBuilder clusterName(String clusterName) {
nacosClientBuilder.clusterName(clusterName);
return this;
}

@Override
public NacosEndpointGroupBuilder app(String app) {
nacosClientBuilder.app(app);
return this;
}

@Override
public NacosEndpointGroupBuilder nacosApiVersion(String nacosApiVersion) {
nacosClientBuilder.nacosApiVersion(nacosApiVersion);
return this;
}

@Override
public NacosEndpointGroupBuilder authorization(String username, String password) {
nacosClientBuilder.authorization(username, password);
return this;
}

/**
* Sets the healthy to retrieve only healthy instances from Nacos.
* Make sure that your target endpoints are health-checked by Nacos before enabling this feature.
* If not set, false is used by default.
*/
public NacosEndpointGroupBuilder useHealthyEndpoints(boolean useHealthyEndpoints) {
nacosClientBuilder.healthyOnly(useHealthyEndpoints);
return this;
}

/**
* Sets the interval between fetching registry requests.
* If not set, {@value #DEFAULT_CHECK_INTERVAL_MILLIS} milliseconds is used by default.
*/
public NacosEndpointGroupBuilder registryFetchInterval(Duration registryFetchInterval) {
requireNonNull(registryFetchInterval, "registryFetchInterval");
return registryFetchIntervalMillis(registryFetchInterval.toMillis());
}

/**
* Sets the interval between fetching registry requests.
* If not set, {@value #DEFAULT_CHECK_INTERVAL_MILLIS} milliseconds is used by default.
*/
public NacosEndpointGroupBuilder registryFetchIntervalMillis(long registryFetchIntervalMillis) {
checkArgument(registryFetchIntervalMillis > 0, "registryFetchIntervalMillis: %s (expected: > 0)",
registryFetchIntervalMillis);
this.registryFetchIntervalMillis = registryFetchIntervalMillis;
return this;
}

/**
* Returns a newly-created {@link NacosEndpointGroup}.
*/
public NacosEndpointGroup build() {
return new NacosEndpointGroup(selectionStrategy, shouldAllowEmptyEndpoints(), selectionTimeoutMillis(),
nacosClientBuilder.build(), registryFetchIntervalMillis);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2024 LY Corporation
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

/**
* Nacos-based {@link com.linecorp.armeria.client.endpoint.EndpointGroup} implementation.
*/
@NonNullByDefault
@UnstableApi
package com.linecorp.armeria.client.nacos;

import com.linecorp.armeria.common.annotation.NonNullByDefault;
import com.linecorp.armeria.common.annotation.UnstableApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2024 LY Corporation
*
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.linecorp.armeria.common.nacos;

import com.linecorp.armeria.common.annotation.UnstableApi;
import com.linecorp.armeria.internal.nacos.NacosClientBuilder;

/**
* Sets properties for building a Nacos client.
*/
@UnstableApi
public interface NacosConfigSetters<SELF extends NacosConfigSetters<SELF>> {

/**
* Sets the namespace ID to query or register instances.
*/
SELF namespaceId(String namespaceId);

/**
* Sets the group name to query or register instances.
*/
SELF groupName(String groupName);

/**
* Sets the cluster name to query or register instances.
*/
SELF clusterName(String clusterName);

/**
* Sets the app name to query or register instances.
*/
SELF app(String app);

/**
* Sets the specified Nacos's API version.
* @param nacosApiVersion the version of Nacos API service, default: {@value
* NacosClientBuilder#DEFAULT_NACOS_API_VERSION}
*/
SELF nacosApiVersion(String nacosApiVersion);

/**
* Sets the username and password pair for Nacos's API.
* Please refer to the
* <a href=https://nacos.io/en-us/docs/v2/guide/user/auth.html>Nacos Authentication Document</a>
* for more details.
*
* @param username the username for access Nacos API, default: {@code null}
* @param password the password for access Nacos API, default: {@code null}
*/
SELF authorization(String username, String password);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2024 LY Corporation
* LY Corporation licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

/**
* Various classes used internally. Anything in this package can be changed or removed at any time.
*/
@NonNullByDefault
@UnstableApi
package com.linecorp.armeria.common.nacos;

import com.linecorp.armeria.common.annotation.NonNullByDefault;
import com.linecorp.armeria.common.annotation.UnstableApi;
Loading

0 comments on commit 0b36df3

Please sign in to comment.