Skip to content

Commit 53f9f35

Browse files
loadbalancer-experimental: add provider for enabling DefaultLoadBalancer (#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.
1 parent 8f096da commit 53f9f35

File tree

9 files changed

+569
-1
lines changed

9 files changed

+569
-1
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
= DefaultLoadBalancer Providers
2+
3+
This package provides providers for enabling the DefaultLoadBalancer via system properties to allow for easy
4+
experimentation that doesn't require a recompilation of the application.
5+
6+
> WARNING: this package is only for experimentation and will be removed in the future.
7+
8+
9+
== Enabling DefaultLoadBalancer via System Properties
10+
11+
=== Dynamically Loading the DefaultHttpLoadBalancerProvider
12+
13+
This package uses the standard providers pattern. To enable the provider you need to both include this package as
14+
part of your application bundle and also include a file in the resources as follows:
15+
```
16+
resources/META-INF/services/io.servicetalk.http.api.HttpProviders$SingleAddressHttpClientBuilderProvider
17+
```
18+
19+
The contents of this must contain the line
20+
21+
```
22+
io.servicetalk.loadbalancer.experimental.DefaultHttpLoadBalancerProvider
23+
```
24+
25+
=== Targeting Clients for Which to Enable DefaultLoadBalancer
26+
27+
The `DefaultHttpLoadBalancerProvider` supports enabling the load balancer either for all clients or only a set of
28+
specific clients. Enabling the load balancer for all clients can be done by setting the following system property:
29+
30+
```
31+
io.servicetalk.loadbalancer.experimental.clientsEnabledFor=all
32+
```
33+
34+
The experimental load balancer can also be enabled for only a subset of clients. This can be done via setting the
35+
system property to a comma separated list:
36+
37+
```
38+
io.servicetalk.loadbalancer.experimental.clientsEnabledFor=service1,service2
39+
```
40+
41+
The specific names will depend on how the client is built. If the client is built using a `HostAndPort`, the names are
42+
only the host component. If the client is built using some other unresolved address form then the string representation
43+
of that is used.
44+
45+
=== Customizing Name Extraction
46+
47+
The provider depends on the service name for selecting which client to use. If you're using a custom naming system
48+
the default implementation may not be able to decode the unresolved address type to the appropriate name. Custom naming
49+
schemes can be supported by extending the `DefaultHttpLoadBalancerProvider` and overriding the `clientNameFromAddress`
50+
method. Then this custom provider can be added to the service load list as described in the section above.
51+
52+
=== All Supported Properties
53+
54+
All system properties contain the prefix "io.servicetalk.loadbalancer.experimental.". A comprehensive list of the
55+
supported properties can be found in the `DefaultLoadBalancerProviderConfig` class for reference.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
apply plugin: "io.servicetalk.servicetalk-gradle-plugin-internal-library"
18+
19+
dependencies {
20+
implementation platform(project(":servicetalk-dependencies"))
21+
testImplementation enforcedPlatform("org.junit:junit-bom:$junit5Version")
22+
23+
api project(":servicetalk-client-api")
24+
api project(":servicetalk-concurrent-api")
25+
26+
implementation project(":servicetalk-annotations")
27+
implementation project(":servicetalk-loadbalancer")
28+
implementation project(":servicetalk-loadbalancer-experimental")
29+
implementation project(":servicetalk-http-api")
30+
implementation project(":servicetalk-http-netty")
31+
implementation project(":servicetalk-utils-internal")
32+
implementation "com.google.code.findbugs:jsr305"
33+
implementation "org.slf4j:slf4j-api"
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.servicetalk.loadbalancer.experimental;
17+
18+
import io.servicetalk.client.api.LoadBalancerFactory;
19+
import io.servicetalk.http.api.DelegatingSingleAddressHttpClientBuilder;
20+
import io.servicetalk.http.api.FilterableStreamingHttpLoadBalancedConnection;
21+
import io.servicetalk.http.api.HttpLoadBalancerFactory;
22+
import io.servicetalk.http.api.HttpProviders;
23+
import io.servicetalk.http.api.SingleAddressHttpClientBuilder;
24+
import io.servicetalk.http.netty.DefaultHttpLoadBalancerFactory;
25+
import io.servicetalk.loadbalancer.LoadBalancers;
26+
import io.servicetalk.transport.api.HostAndPort;
27+
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
31+
import static java.util.Objects.requireNonNull;
32+
33+
/**
34+
* A client builder provider that supports enabling the new `DefaultLoadBalancer` in applications via property flags.
35+
* See the packages README.md for more details.
36+
*/
37+
public class DefaultHttpLoadBalancerProvider implements HttpProviders.SingleAddressHttpClientBuilderProvider {
38+
39+
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpLoadBalancerProvider.class);
40+
41+
private final DefaultLoadBalancerProviderConfig config;
42+
43+
public DefaultHttpLoadBalancerProvider() {
44+
this(DefaultLoadBalancerProviderConfig.INSTANCE);
45+
}
46+
47+
// exposed for testing
48+
DefaultHttpLoadBalancerProvider(final DefaultLoadBalancerProviderConfig config) {
49+
this.config = requireNonNull(config, "config");
50+
}
51+
52+
@Override
53+
public final <U, R> SingleAddressHttpClientBuilder<U, R> newBuilder(U address,
54+
SingleAddressHttpClientBuilder<U, R> builder) {
55+
final String serviceName = clientNameFromAddress(address);
56+
if (config.enabledForServiceName(serviceName)) {
57+
try {
58+
HttpLoadBalancerFactory<R> loadBalancerFactory = DefaultHttpLoadBalancerFactory.Builder.<R>from(
59+
defaultLoadBalancer(serviceName)).build();
60+
builder = builder.loadBalancerFactory(loadBalancerFactory);
61+
return new LoadBalancerIgnoringBuilder(builder, serviceName);
62+
} catch (Throwable ex) {
63+
LOGGER.warn("Failed to enabled DefaultLoadBalancer for client to address {}.", address, ex);
64+
}
65+
}
66+
return builder;
67+
}
68+
69+
private <R> LoadBalancerFactory<R, FilterableStreamingHttpLoadBalancedConnection> defaultLoadBalancer(
70+
String serviceName) {
71+
return LoadBalancers.<R, FilterableStreamingHttpLoadBalancedConnection>
72+
builder("experimental-load-balancer")
73+
.loadBalancerObserver(new DefaultLoadBalancerObserver(serviceName))
74+
// set up the new features.
75+
.outlierDetectorConfig(config.outlierDetectorConfig())
76+
.loadBalancingPolicy(config.getLoadBalancingPolicy())
77+
.build();
78+
}
79+
80+
/**
81+
* Extract the service name from the address object.
82+
* Note: this is a protected method to allow overriding for custom address types.
83+
* @param <U> the unresolved type of the address.
84+
* @param address the address from which to extract the service name.
85+
* @return the String representation of the provided address.
86+
*/
87+
protected <U> String clientNameFromAddress(U address) {
88+
String serviceName;
89+
if (address instanceof HostAndPort) {
90+
serviceName = ((HostAndPort) address).hostName();
91+
} else if (address instanceof String) {
92+
serviceName = (String) address;
93+
} else {
94+
LOGGER.warn("Unknown service address type={} was provided, "
95+
+ "default 'toString()' will be used as serviceName", address.getClass());
96+
serviceName = address.toString();
97+
}
98+
return serviceName;
99+
}
100+
101+
private static final class LoadBalancerIgnoringBuilder<U, R>
102+
extends DelegatingSingleAddressHttpClientBuilder<U, R> {
103+
104+
private final String serviceName;
105+
106+
LoadBalancerIgnoringBuilder(final SingleAddressHttpClientBuilder<U, R> delegate, final String serviceName) {
107+
super(delegate);
108+
this.serviceName = serviceName;
109+
}
110+
111+
@Override
112+
public SingleAddressHttpClientBuilder<U, R> loadBalancerFactory(
113+
HttpLoadBalancerFactory<R> loadBalancerFactory) {
114+
LOGGER.info("Ignoring http load balancer factory of type {} for client to {} which has " +
115+
"DefaultLoadBalancer enabled.", loadBalancerFactory.getClass(), serviceName);
116+
return this;
117+
}
118+
}
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright © 2024 Apple Inc. and the ServiceTalk project authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.servicetalk.loadbalancer.experimental;
17+
18+
import io.servicetalk.client.api.NoActiveHostException;
19+
import io.servicetalk.client.api.ServiceDiscovererEvent;
20+
import io.servicetalk.loadbalancer.LoadBalancerObserver;
21+
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import java.util.Collection;
26+
import javax.annotation.Nullable;
27+
28+
import static java.util.Objects.requireNonNull;
29+
30+
final class DefaultLoadBalancerObserver implements LoadBalancerObserver {
31+
32+
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultLoadBalancerObserver.class);
33+
34+
private final String clientName;
35+
36+
DefaultLoadBalancerObserver(final String clientName) {
37+
this.clientName = requireNonNull(clientName, "clientName");
38+
}
39+
40+
@Override
41+
public HostObserver hostObserver(Object resolvedAddress) {
42+
return new HostObserverImpl(resolvedAddress);
43+
}
44+
45+
@Override
46+
public void onNoHostsAvailable() {
47+
LOGGER.debug("{}- onNoHostsAvailable()", clientName);
48+
}
49+
50+
@Override
51+
public void onServiceDiscoveryEvent(Collection<? extends ServiceDiscovererEvent<?>> events, int oldHostSetSize,
52+
int newHostSetSize) {
53+
LOGGER.debug("{}- onServiceDiscoveryEvent(events: {}, oldHostSetSize: {}, newHostSetSize: {})",
54+
clientName, events, oldHostSetSize, newHostSetSize);
55+
}
56+
57+
@Override
58+
public void onNoActiveHostsAvailable(int hostSetSize, NoActiveHostException exception) {
59+
LOGGER.debug("{}- No active hosts available. Host set size: {}.", clientName, hostSetSize, exception);
60+
}
61+
62+
private final class HostObserverImpl implements HostObserver {
63+
64+
private final Object resolvedAddress;
65+
66+
HostObserverImpl(final Object resolvedAddress) {
67+
this.resolvedAddress = resolvedAddress;
68+
}
69+
70+
@Override
71+
public void onHostMarkedExpired(int connectionCount) {
72+
LOGGER.debug("{}:{}- onHostMarkedExpired(connectionCount: {})",
73+
clientName, resolvedAddress, connectionCount);
74+
}
75+
76+
@Override
77+
public void onActiveHostRemoved(int connectionCount) {
78+
LOGGER.debug("{}:{}- onActiveHostRemoved(connectionCount: {})",
79+
clientName, resolvedAddress, connectionCount);
80+
}
81+
82+
@Override
83+
public void onExpiredHostRevived(int connectionCount) {
84+
LOGGER.debug("{}:{}- onExpiredHostRevived(connectionCount: {})",
85+
clientName, resolvedAddress, connectionCount);
86+
}
87+
88+
@Override
89+
public void onExpiredHostRemoved(int connectionCount) {
90+
LOGGER.debug("{}:{}- onExpiredHostRemoved(connectionCount: {})",
91+
clientName, resolvedAddress, connectionCount);
92+
}
93+
94+
@Override
95+
public void onHostMarkedUnhealthy(@Nullable Throwable cause) {
96+
LOGGER.debug("{}:{}- onHostMarkedUnhealthy(ex)", clientName, resolvedAddress, cause);
97+
}
98+
99+
@Override
100+
public void onHostRevived() {
101+
LOGGER.debug("{}:{}- onHostRevived()", clientName, resolvedAddress);
102+
}
103+
}
104+
}

0 commit comments

Comments
 (0)