jizhuozhi
2 years ago
committed by
GitHub
6 changed files with 448 additions and 1 deletions
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or 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 |
||||
* |
||||
* 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 org.springframework.cloud.loadbalancer.core; |
||||
|
||||
import org.springframework.cloud.client.ServiceInstance; |
||||
|
||||
/** |
||||
* Represents a function that calculate the weight of the given service instance. |
||||
* |
||||
* <p> |
||||
* This is a functional interface whose functional method is |
||||
* {@link #apply(ServiceInstance)}. |
||||
* |
||||
* @author Zhuozhi Ji |
||||
* @see java.util.function.ToIntFunction |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface WeightFunction { |
||||
|
||||
/** |
||||
* Applies this function to the given service instance. |
||||
* @param instance the service instance |
||||
* @return the weight of service instance |
||||
*/ |
||||
int apply(ServiceInstance instance); |
||||
|
||||
} |
@ -0,0 +1,141 @@
@@ -0,0 +1,141 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or 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 |
||||
* |
||||
* 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 org.springframework.cloud.loadbalancer.core; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import org.apache.commons.logging.Log; |
||||
import org.apache.commons.logging.LogFactory; |
||||
import reactor.core.publisher.Flux; |
||||
|
||||
import org.springframework.cloud.client.ServiceInstance; |
||||
|
||||
/** |
||||
* A {@link ServiceInstanceListSupplier} implementation that uses weights to expand the |
||||
* instances provided by delegate. |
||||
* |
||||
* @author Zhuozhi Ji |
||||
*/ |
||||
public class WeightedServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { |
||||
|
||||
private static final Log LOG = LogFactory.getLog(WeightedServiceInstanceListSupplier.class); |
||||
|
||||
static final String METADATA_WEIGHT_KEY = "weight"; |
||||
|
||||
static final int DEFAULT_WEIGHT = 1; |
||||
|
||||
private final WeightFunction weightFunction; |
||||
|
||||
public WeightedServiceInstanceListSupplier(ServiceInstanceListSupplier delegate) { |
||||
super(delegate); |
||||
this.weightFunction = WeightedServiceInstanceListSupplier::metadataWeightFunction; |
||||
} |
||||
|
||||
public WeightedServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, WeightFunction weightFunction) { |
||||
super(delegate); |
||||
this.weightFunction = weightFunction; |
||||
} |
||||
|
||||
@Override |
||||
public Flux<List<ServiceInstance>> get() { |
||||
return delegate.get().map(this::expandByWeight); |
||||
} |
||||
|
||||
private List<ServiceInstance> expandByWeight(List<ServiceInstance> instances) { |
||||
if (instances.size() == 0) { |
||||
return instances; |
||||
} |
||||
|
||||
int[] weights = instances.stream().mapToInt(instance -> { |
||||
try { |
||||
int weight = weightFunction.apply(instance); |
||||
if (weight <= 0) { |
||||
if (LOG.isDebugEnabled()) { |
||||
LOG.debug(String.format( |
||||
"The weight of the instance %s should be a positive integer, but it got %d, using %d as default", |
||||
instance.getInstanceId(), weight, DEFAULT_WEIGHT)); |
||||
} |
||||
return DEFAULT_WEIGHT; |
||||
} |
||||
return weight; |
||||
} |
||||
catch (Exception e) { |
||||
if (LOG.isDebugEnabled()) { |
||||
LOG.debug(String.format( |
||||
"Exception occurred during apply weight function to instance %s, using %d as default", |
||||
instance.getInstanceId(), DEFAULT_WEIGHT), e); |
||||
} |
||||
return DEFAULT_WEIGHT; |
||||
} |
||||
}).toArray(); |
||||
|
||||
// Calculate the greatest common divisor (GCD) of weights and the total number of
|
||||
// elements after expansion.
|
||||
int gcd = 0; |
||||
int total = 0; |
||||
for (int weight : weights) { |
||||
gcd = greatestCommonDivisor(gcd, weight); |
||||
total += weight; |
||||
} |
||||
|
||||
// Because scaling by the gcd does not affect the distribution,
|
||||
// we can reduce memory usage by this way.
|
||||
List<ServiceInstance> newInstances = new ArrayList<>(total / gcd); |
||||
|
||||
// use iterator for some implementation of the List that not supports
|
||||
// RandomAccess, but `weights` is supported, so use a local variable `i`
|
||||
// to get the current position.
|
||||
int i = 0; |
||||
for (ServiceInstance instance : instances) { |
||||
int weight = weights[i] / gcd; |
||||
for (int j = 0; j < weight; j++) { |
||||
newInstances.add(instance); |
||||
} |
||||
i++; |
||||
} |
||||
|
||||
Collections.shuffle(newInstances); |
||||
return newInstances; |
||||
} |
||||
|
||||
static int metadataWeightFunction(ServiceInstance serviceInstance) { |
||||
Map<String, String> metadata = serviceInstance.getMetadata(); |
||||
if (metadata != null) { |
||||
String weightValue = metadata.get(METADATA_WEIGHT_KEY); |
||||
if (weightValue != null) { |
||||
return Integer.parseInt(weightValue); |
||||
} |
||||
} |
||||
// using default weight when metadata is missing or
|
||||
// weight is not specified
|
||||
return DEFAULT_WEIGHT; |
||||
} |
||||
|
||||
static int greatestCommonDivisor(int a, int b) { |
||||
int r; |
||||
while (b != 0) { |
||||
r = a % b; |
||||
a = b; |
||||
b = r; |
||||
} |
||||
return a; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
/* |
||||
* Copyright 2012-2022 the original author or 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 |
||||
* |
||||
* 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 org.springframework.cloud.loadbalancer.core; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Objects; |
||||
import java.util.stream.Collectors; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import reactor.core.publisher.Flux; |
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance; |
||||
import org.springframework.cloud.client.ServiceInstance; |
||||
|
||||
import static java.util.stream.Collectors.summingInt; |
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
import static org.springframework.cloud.loadbalancer.core.WeightedServiceInstanceListSupplier.DEFAULT_WEIGHT; |
||||
|
||||
/** |
||||
* Tests for {@link WeightedServiceInstanceListSupplier}. |
||||
* |
||||
* @author Zhuozhi Ji |
||||
*/ |
||||
class WeightedServiceInstanceListSupplierTests { |
||||
|
||||
private final DiscoveryClientServiceInstanceListSupplier delegate = mock( |
||||
DiscoveryClientServiceInstanceListSupplier.class); |
||||
|
||||
@Test |
||||
void shouldReturnEmptyWhenDelegateReturnedEmpty() { |
||||
when(delegate.get()).thenReturn(Flux.just(Collections.emptyList())); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
assertThat(serviceInstances).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void shouldBeSameAsWeightsRatioWhenGcdOfWeightsIs1() { |
||||
ServiceInstance one = serviceInstance("test-1", buildWeightMetadata(1)); |
||||
ServiceInstance two = serviceInstance("test-2", buildWeightMetadata(2)); |
||||
ServiceInstance three = serviceInstance("test-3", buildWeightMetadata(3)); |
||||
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(one, two, three))); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
Map<String, Integer> counter = serviceInstances.stream() |
||||
.collect(Collectors.groupingBy(ServiceInstance::getInstanceId, summingInt(e -> 1))); |
||||
assertThat(counter).containsEntry("test-1", 1); |
||||
assertThat(counter).containsEntry("test-2", 2); |
||||
assertThat(counter).containsEntry("test-3", 3); |
||||
} |
||||
|
||||
@Test |
||||
void shouldBeSameAsWeightsRatioWhenGcdOfWeightsIs10() { |
||||
ServiceInstance one = serviceInstance("test-1", buildWeightMetadata(10)); |
||||
ServiceInstance two = serviceInstance("test-2", buildWeightMetadata(20)); |
||||
ServiceInstance three = serviceInstance("test-3", buildWeightMetadata(30)); |
||||
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(one, two, three))); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
Map<String, Integer> counter = serviceInstances.stream() |
||||
.collect(Collectors.groupingBy(ServiceInstance::getInstanceId, summingInt(e -> 1))); |
||||
assertThat(counter).containsEntry("test-1", 1); |
||||
assertThat(counter).containsEntry("test-2", 2); |
||||
assertThat(counter).containsEntry("test-3", 3); |
||||
} |
||||
|
||||
@Test |
||||
void shouldUseDefaultWeightWhenWeightNotSpecified() { |
||||
ServiceInstance one = serviceInstance("test-1", Collections.emptyMap()); |
||||
ServiceInstance two = serviceInstance("test-2", Collections.emptyMap()); |
||||
ServiceInstance three = serviceInstance("test-3", buildWeightMetadata(3)); |
||||
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(one, two, three))); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
Map<String, Integer> counter = serviceInstances.stream() |
||||
.collect(Collectors.groupingBy(ServiceInstance::getInstanceId, summingInt(e -> 1))); |
||||
assertThat(counter).containsEntry("test-1", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-2", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-3", 3); |
||||
} |
||||
|
||||
@Test |
||||
void shouldUseDefaultWeightWhenWeightIsNotNumber() { |
||||
ServiceInstance one = serviceInstance("test-1", buildWeightMetadata("Foo")); |
||||
ServiceInstance two = serviceInstance("test-2", buildWeightMetadata("Bar")); |
||||
ServiceInstance three = serviceInstance("test-3", buildWeightMetadata("Baz")); |
||||
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(one, two, three))); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
Map<String, Integer> counter = serviceInstances.stream() |
||||
.collect(Collectors.groupingBy(ServiceInstance::getInstanceId, summingInt(e -> 1))); |
||||
assertThat(counter).containsEntry("test-1", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-2", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-3", DEFAULT_WEIGHT); |
||||
} |
||||
|
||||
@Test |
||||
void shouldUseDefaultWeightWhenWeightedFunctionReturnedZero() { |
||||
ServiceInstance one = serviceInstance("test-1", Collections.emptyMap()); |
||||
ServiceInstance two = serviceInstance("test-2", Collections.emptyMap()); |
||||
ServiceInstance three = serviceInstance("test-3", Collections.emptyMap()); |
||||
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(one, two, three))); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate, instance -> 0); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
Map<String, Integer> counter = serviceInstances.stream() |
||||
.collect(Collectors.groupingBy(ServiceInstance::getInstanceId, summingInt(e -> 1))); |
||||
assertThat(counter).containsEntry("test-1", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-2", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-3", DEFAULT_WEIGHT); |
||||
} |
||||
|
||||
@Test |
||||
void shouldUseDefaultWeightWhenWeightedFunctionReturnedNegative() { |
||||
ServiceInstance one = serviceInstance("test-1", Collections.emptyMap()); |
||||
ServiceInstance two = serviceInstance("test-2", Collections.emptyMap()); |
||||
ServiceInstance three = serviceInstance("test-3", Collections.emptyMap()); |
||||
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(one, two, three))); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate, |
||||
instance -> -1); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
Map<String, Integer> counter = serviceInstances.stream() |
||||
.collect(Collectors.groupingBy(ServiceInstance::getInstanceId, summingInt(e -> 1))); |
||||
assertThat(counter).containsEntry("test-1", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-2", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-3", DEFAULT_WEIGHT); |
||||
} |
||||
|
||||
@Test |
||||
void shouldUseDefaultWeightWhenWeightedFunctionThrownException() { |
||||
ServiceInstance one = serviceInstance("test-1", Collections.emptyMap()); |
||||
ServiceInstance two = serviceInstance("test-2", Collections.emptyMap()); |
||||
ServiceInstance three = serviceInstance("test-3", Collections.emptyMap()); |
||||
|
||||
when(delegate.get()).thenReturn(Flux.just(Arrays.asList(one, two, three))); |
||||
WeightedServiceInstanceListSupplier supplier = new WeightedServiceInstanceListSupplier(delegate, instance -> { |
||||
throw new RuntimeException(); |
||||
}); |
||||
|
||||
List<ServiceInstance> serviceInstances = Objects.requireNonNull(supplier.get().blockFirst()); |
||||
Map<String, Integer> counter = serviceInstances.stream() |
||||
.collect(Collectors.groupingBy(ServiceInstance::getInstanceId, summingInt(e -> 1))); |
||||
assertThat(counter).containsEntry("test-1", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-2", DEFAULT_WEIGHT); |
||||
assertThat(counter).containsEntry("test-3", DEFAULT_WEIGHT); |
||||
} |
||||
|
||||
private ServiceInstance serviceInstance(String instanceId, Map<String, String> metadata) { |
||||
return new DefaultServiceInstance(instanceId, "test", "localhost", 8080, false, metadata); |
||||
} |
||||
|
||||
private Map<String, String> buildWeightMetadata(Object weight) { |
||||
Map<String, String> metadata = new HashMap<>(); |
||||
metadata.put("weight", weight.toString()); |
||||
return metadata; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue