-
Notifications
You must be signed in to change notification settings - Fork 181
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
loadbalancer-experimental: add provider for enabling DefaultLoadBalan…
…cer (#2900) Motivation: We want to make it easy for users to enable DefaultLoadBalancer for specific clients and then manipulate it's behavior via system properties so they don't require rebuilding apps to test. Modifications: Add a new package that includes a SingleAddressHttpClientBuilderProvider which enables users to enable DefaultLoadBalancer for clients based on the address used, or all clients if desired.
- Loading branch information
1 parent
8f096da
commit 53f9f35
Showing
9 changed files
with
569 additions
and
1 deletion.
There are no files selected for viewing
55 changes: 55 additions & 0 deletions
55
servicetalk-loadbalancer-experimental-provider/README.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
= DefaultLoadBalancer Providers | ||
|
||
This package provides providers for enabling the DefaultLoadBalancer via system properties to allow for easy | ||
experimentation that doesn't require a recompilation of the application. | ||
|
||
> WARNING: this package is only for experimentation and will be removed in the future. | ||
|
||
|
||
== Enabling DefaultLoadBalancer via System Properties | ||
|
||
=== Dynamically Loading the DefaultHttpLoadBalancerProvider | ||
|
||
This package uses the standard providers pattern. To enable the provider you need to both include this package as | ||
part of your application bundle and also include a file in the resources as follows: | ||
``` | ||
resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider | ||
``` | ||
|
||
The contents of this must contain the line | ||
|
||
``` | ||
io.servicetalk.loadbalancer.experimental.DefaultHttpLoadBalancerProvider | ||
``` | ||
|
||
=== Targeting Clients for Which to Enable DefaultLoadBalancer | ||
|
||
The `DefaultHttpLoadBalancerProvider` supports enabling the load balancer either for all clients or only a set of | ||
specific clients. Enabling the load balancer for all clients can be done by setting the following system property: | ||
|
||
``` | ||
io.servicetalk.loadbalancer.experimental.clientsEnabledFor=all | ||
``` | ||
|
||
The experimental load balancer can also be enabled for only a subset of clients. This can be done via setting the | ||
system property to a comma separated list: | ||
|
||
``` | ||
io.servicetalk.loadbalancer.experimental.clientsEnabledFor=service1,service2 | ||
``` | ||
|
||
The specific names will depend on how the client is built. If the client is built using a `HostAndPort`, the names are | ||
only the host component. If the client is built using some other unresolved address form then the string representation | ||
of that is used. | ||
|
||
=== Customizing Name Extraction | ||
|
||
The provider depends on the service name for selecting which client to use. If you're using a custom naming system | ||
the default implementation may not be able to decode the unresolved address type to the appropriate name. Custom naming | ||
schemes can be supported by extending the `DefaultHttpLoadBalancerProvider` and overriding the `clientNameFromAddress` | ||
method. Then this custom provider can be added to the service load list as described in the section above. | ||
|
||
=== All Supported Properties | ||
|
||
All system properties contain the prefix "io.servicetalk.loadbalancer.experimental.". A comprehensive list of the | ||
supported properties can be found in the `DefaultLoadBalancerProviderConfig` class for reference. |
34 changes: 34 additions & 0 deletions
34
servicetalk-loadbalancer-experimental-provider/build.gradle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors | ||
* | ||
* Licensed 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 | ||
* | ||
* http://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. | ||
*/ | ||
|
||
apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library" | ||
|
||
dependencies { | ||
implementation platform(project(":servicetalk-dependencies")) | ||
testImplementation enforcedPlatform("org.junit:junit-bom:$junit5Version") | ||
|
||
api project(":servicetalk-client-api") | ||
api project(":servicetalk-concurrent-api") | ||
|
||
implementation project(":servicetalk-annotations") | ||
implementation project(":servicetalk-loadbalancer") | ||
implementation project(":servicetalk-loadbalancer-experimental") | ||
implementation project(":servicetalk-http-api") | ||
implementation project(":servicetalk-http-netty") | ||
implementation project(":servicetalk-utils-internal") | ||
implementation "com.google.code.findbugs:jsr305" | ||
implementation "org.slf4j:slf4j-api" | ||
} |
119 changes: 119 additions & 0 deletions
119
...c/main/java/io/servicetalk/loadbalancer/experimental/DefaultHttpLoadBalancerProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
/* | ||
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors | ||
* | ||
* Licensed 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 | ||
* | ||
* http://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 io.servicetalk.loadbalancer.experimental; | ||
|
||
import io.servicetalk.client.api.LoadBalancerFactory; | ||
import io.servicetalk.http.api.DelegatingSingleAddressHttpClientBuilder; | ||
import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection; | ||
import io.servicetalk.http.api.HttpLoadBalancerFactory; | ||
import io.servicetalk.http.api.HttpProviders; | ||
import io.servicetalk.http.api.SingleAddressHttpClientBuilder; | ||
import io.servicetalk.http.netty.DefaultHttpLoadBalancerFactory; | ||
import io.servicetalk.loadbalancer.LoadBalancers; | ||
import io.servicetalk.transport.api.HostAndPort; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
/** | ||
* A client builder provider that supports enabling the new `DefaultLoadBalancer` in applications via property flags. | ||
* See the packages README.md for more details. | ||
*/ | ||
public class DefaultHttpLoadBalancerProvider implements HttpProviders.SingleAddressHttpClientBuilderProvider { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpLoadBalancerProvider.class); | ||
|
||
private final DefaultLoadBalancerProviderConfig config; | ||
|
||
public DefaultHttpLoadBalancerProvider() { | ||
this(DefaultLoadBalancerProviderConfig.INSTANCE); | ||
} | ||
|
||
// exposed for testing | ||
DefaultHttpLoadBalancerProvider(final DefaultLoadBalancerProviderConfig config) { | ||
this.config = requireNonNull(config, "config"); | ||
} | ||
|
||
@Override | ||
public final <U, R> SingleAddressHttpClientBuilder<U, R> newBuilder(U address, | ||
SingleAddressHttpClientBuilder<U, R> builder) { | ||
final String serviceName = clientNameFromAddress(address); | ||
if (config.enabledForServiceName(serviceName)) { | ||
try { | ||
HttpLoadBalancerFactory<R> loadBalancerFactory = DefaultHttpLoadBalancerFactory.Builder.<R>from( | ||
defaultLoadBalancer(serviceName)).build(); | ||
builder = builder.loadBalancerFactory(loadBalancerFactory); | ||
return new LoadBalancerIgnoringBuilder(builder, serviceName); | ||
} catch (Throwable ex) { | ||
LOGGER.warn("Failed to enabled DefaultLoadBalancer for client to address {}.", address, ex); | ||
} | ||
} | ||
return builder; | ||
} | ||
|
||
private <R> LoadBalancerFactory<R, FilterableStreamingHttpLoadBalancedConnection> defaultLoadBalancer( | ||
String serviceName) { | ||
return LoadBalancers.<R, FilterableStreamingHttpLoadBalancedConnection> | ||
builder("experimental-load-balancer") | ||
.loadBalancerObserver(new DefaultLoadBalancerObserver(serviceName)) | ||
// set up the new features. | ||
.outlierDetectorConfig(config.outlierDetectorConfig()) | ||
.loadBalancingPolicy(config.getLoadBalancingPolicy()) | ||
.build(); | ||
} | ||
|
||
/** | ||
* Extract the service name from the address object. | ||
* Note: this is a protected method to allow overriding for custom address types. | ||
* @param <U> the unresolved type of the address. | ||
* @param address the address from which to extract the service name. | ||
* @return the String representation of the provided address. | ||
*/ | ||
protected <U> String clientNameFromAddress(U address) { | ||
String serviceName; | ||
if (address instanceof HostAndPort) { | ||
serviceName = ((HostAndPort) address).hostName(); | ||
} else if (address instanceof String) { | ||
serviceName = (String) address; | ||
} else { | ||
LOGGER.warn("Unknown service address type={} was provided, " | ||
+ "default 'toString()' will be used as serviceName", address.getClass()); | ||
serviceName = address.toString(); | ||
} | ||
return serviceName; | ||
} | ||
|
||
private static final class LoadBalancerIgnoringBuilder<U, R> | ||
extends DelegatingSingleAddressHttpClientBuilder<U, R> { | ||
|
||
private final String serviceName; | ||
|
||
LoadBalancerIgnoringBuilder(final SingleAddressHttpClientBuilder<U, R> delegate, final String serviceName) { | ||
super(delegate); | ||
this.serviceName = serviceName; | ||
} | ||
|
||
@Override | ||
public SingleAddressHttpClientBuilder<U, R> loadBalancerFactory( | ||
HttpLoadBalancerFactory<R> loadBalancerFactory) { | ||
LOGGER.info("Ignoring http load balancer factory of type {} for client to {} which has " + | ||
"DefaultLoadBalancer enabled.", loadBalancerFactory.getClass(), serviceName); | ||
return this; | ||
} | ||
} | ||
} |
104 changes: 104 additions & 0 deletions
104
...r/src/main/java/io/servicetalk/loadbalancer/experimental/DefaultLoadBalancerObserver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
/* | ||
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors | ||
* | ||
* Licensed 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 | ||
* | ||
* http://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 io.servicetalk.loadbalancer.experimental; | ||
|
||
import io.servicetalk.client.api.NoActiveHostException; | ||
import io.servicetalk.client.api.ServiceDiscovererEvent; | ||
import io.servicetalk.loadbalancer.LoadBalancerObserver; | ||
|
||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.util.Collection; | ||
import javax.annotation.Nullable; | ||
|
||
import static java.util.Objects.requireNonNull; | ||
|
||
final class DefaultLoadBalancerObserver implements LoadBalancerObserver { | ||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultLoadBalancerObserver.class); | ||
|
||
private final String clientName; | ||
|
||
DefaultLoadBalancerObserver(final String clientName) { | ||
this.clientName = requireNonNull(clientName, "clientName"); | ||
} | ||
|
||
@Override | ||
public HostObserver hostObserver(Object resolvedAddress) { | ||
return new HostObserverImpl(resolvedAddress); | ||
} | ||
|
||
@Override | ||
public void onNoHostsAvailable() { | ||
LOGGER.debug("{}- onNoHostsAvailable()", clientName); | ||
} | ||
|
||
@Override | ||
public void onServiceDiscoveryEvent(Collection<? extends ServiceDiscovererEvent<?>> events, int oldHostSetSize, | ||
int newHostSetSize) { | ||
LOGGER.debug("{}- onServiceDiscoveryEvent(events: {}, oldHostSetSize: {}, newHostSetSize: {})", | ||
clientName, events, oldHostSetSize, newHostSetSize); | ||
} | ||
|
||
@Override | ||
public void onNoActiveHostsAvailable(int hostSetSize, NoActiveHostException exception) { | ||
LOGGER.debug("{}- No active hosts available. Host set size: {}.", clientName, hostSetSize, exception); | ||
} | ||
|
||
private final class HostObserverImpl implements HostObserver { | ||
|
||
private final Object resolvedAddress; | ||
|
||
HostObserverImpl(final Object resolvedAddress) { | ||
this.resolvedAddress = resolvedAddress; | ||
} | ||
|
||
@Override | ||
public void onHostMarkedExpired(int connectionCount) { | ||
LOGGER.debug("{}:{}- onHostMarkedExpired(connectionCount: {})", | ||
clientName, resolvedAddress, connectionCount); | ||
} | ||
|
||
@Override | ||
public void onActiveHostRemoved(int connectionCount) { | ||
LOGGER.debug("{}:{}- onActiveHostRemoved(connectionCount: {})", | ||
clientName, resolvedAddress, connectionCount); | ||
} | ||
|
||
@Override | ||
public void onExpiredHostRevived(int connectionCount) { | ||
LOGGER.debug("{}:{}- onExpiredHostRevived(connectionCount: {})", | ||
clientName, resolvedAddress, connectionCount); | ||
} | ||
|
||
@Override | ||
public void onExpiredHostRemoved(int connectionCount) { | ||
LOGGER.debug("{}:{}- onExpiredHostRemoved(connectionCount: {})", | ||
clientName, resolvedAddress, connectionCount); | ||
} | ||
|
||
@Override | ||
public void onHostMarkedUnhealthy(@Nullable Throwable cause) { | ||
LOGGER.debug("{}:{}- onHostMarkedUnhealthy(ex)", clientName, resolvedAddress, cause); | ||
} | ||
|
||
@Override | ||
public void onHostRevived() { | ||
LOGGER.debug("{}:{}- onHostRevived()", clientName, resolvedAddress); | ||
} | ||
} | ||
} |
Oops, something went wrong.