-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #274 from michalszynkiewicz/sticky-load-balancer
`sticky` LoadBalancer implementation
- Loading branch information
Showing
8 changed files
with
543 additions
and
0 deletions.
There are no files selected for viewing
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,30 @@ | ||
# Sticky Load Balancing | ||
|
||
The sticky load balancer selects a single service instance and keeps using it until it fails. | ||
Then, it selects another one. | ||
|
||
It is possible to configure a backoff time to specify for how long a failing service instance should not be retried. | ||
|
||
Precisely, the load balancer works as follows: | ||
|
||
* if no service instance has been selected so far, select the first instance from the collection; | ||
* else if the previously selected service instance has not failed, and is still available, return it; | ||
* else return the first available service instance that has no recorded failure, if one exists; | ||
* else, find the available instance for which the time since the last failure is the longest, and | ||
* if the backoff time since the failure passed, return it; | ||
* or, throw an `NoAcceptableServiceInstanceFoundException` as no acceptable instances are available. | ||
|
||
## Configuration | ||
|
||
To use the `sticky` load service selection strategy, set the load balancer type to `sticky`: | ||
|
||
```properties | ||
stork.my-service.service-discovery=... | ||
stork.my-service.service-discovery...=... | ||
stork.my-service.load-balancer.type=sticky | ||
``` | ||
|
||
|
||
The following attributes are supported: | ||
|
||
--8<-- "load-balancer/sticky/target/classes/META-INF/stork-docs/sticky-lb-attributes.txt" |
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,77 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<parent> | ||
<groupId>io.smallrye.stork</groupId> | ||
<artifactId>stork-parent</artifactId> | ||
<version>1.1.1-SNAPSHOT</version> | ||
<relativePath>../../pom.xml</relativePath> | ||
</parent> | ||
|
||
<artifactId>stork-load-balancer-sticky</artifactId> | ||
|
||
<name>SmallRye Stork Load Balancer : Sticky</name> | ||
<description> | ||
SmallRye Stork Load Balancer that returns a single service instance until it fails, and then switching | ||
to a different one | ||
</description> | ||
|
||
<properties> | ||
<revapi.skip>false</revapi.skip> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>io.smallrye.stork</groupId> | ||
<artifactId>stork-api</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.smallrye.stork</groupId> | ||
<artifactId>stork-test-utils</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.smallrye.stork</groupId> | ||
<artifactId>stork-service-discovery-static-list</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.smallrye.config</groupId> | ||
<artifactId>smallrye-config</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.assertj</groupId> | ||
<artifactId>assertj-core</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.awaitility</groupId> | ||
<artifactId>awaitility</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mockito</groupId> | ||
<artifactId>mockito-core</artifactId> | ||
<scope>test</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>io.smallrye.stork</groupId> | ||
<artifactId>stork-core</artifactId> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.smallrye.stork</groupId> | ||
<artifactId>stork-configuration-generator</artifactId> | ||
<scope>provided</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
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,56 @@ | ||
[ | ||
{ | ||
"extension": "revapi.java", | ||
"id": "java", | ||
"configuration": { | ||
"missing-classes": { | ||
"behavior": "report", | ||
"ignoreMissingAnnotations": false | ||
} | ||
} | ||
}, | ||
{ | ||
"extension": "revapi.filter", | ||
"configuration": { | ||
"elements": { | ||
"include": [ | ||
{ | ||
"matcher": "java-package", | ||
"match": "/io\\.smallrye\\.stork\\.loadbalancer\\.sticky(\\..*)?/" | ||
} | ||
] | ||
} | ||
} | ||
}, | ||
{ | ||
"extension": "revapi.differences", | ||
"id": "breaking-changes", | ||
"configuration": { | ||
"criticality": "highlight", | ||
"minSeverity": "POTENTIALLY_BREAKING", | ||
"minCriticality": "documented", | ||
"differences": [ | ||
|
||
] | ||
} | ||
}, | ||
{ | ||
"extension": "revapi.reporter.json", | ||
"configuration": { | ||
"minSeverity": "POTENTIALLY_BREAKING", | ||
"minCriticality": "documented", | ||
"output": "target/compatibility.json", | ||
"indent": true, | ||
"append": false, | ||
"keepEmptyFile": true | ||
} | ||
}, | ||
{ | ||
"extension": "revapi.reporter.text", | ||
"configuration": { | ||
"minSeverity": "POTENTIALLY_BREAKING", | ||
"minCriticality": "documented", | ||
"output": "out" | ||
} | ||
} | ||
] |
110 changes: 110 additions & 0 deletions
110
...lancer/sticky/src/main/java/io/smallrye/stork/loadbalancer/random/StickyLoadBalancer.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,110 @@ | ||
package io.smallrye.stork.loadbalancer.random; | ||
|
||
import java.time.Duration; | ||
import java.util.Collection; | ||
import java.util.Iterator; | ||
import java.util.LinkedHashMap; | ||
import java.util.Map; | ||
import java.util.stream.Collectors; | ||
|
||
import io.smallrye.stork.api.LoadBalancer; | ||
import io.smallrye.stork.api.NoAcceptableServiceInstanceFoundException; | ||
import io.smallrye.stork.api.NoServiceInstanceFoundException; | ||
import io.smallrye.stork.api.ServiceInstance; | ||
import io.smallrye.stork.impl.ServiceInstanceWithStatGathering; | ||
import io.smallrye.stork.spi.CallStatisticsCollector; | ||
|
||
/** | ||
* Select a single instance and use it until it fails. | ||
* On failure, store the time of failure in `failedInstances` map. | ||
* <p/> | ||
* When new instance selection is needed: | ||
* <ul> | ||
* <li>if there's an instance for which we don't have a failure recorded, use it</li> | ||
* <li>otherwise, pick the instance whose failure was the longest time away and: | ||
* <ul> | ||
* <li>return it if {@code failureBackOff} has passed since its last failure</li> | ||
* <li>throw NoAcceptableServiceInstanceFoundException if it has not</li> | ||
* </ul> | ||
* </li> | ||
* </ul> | ||
*/ | ||
public class StickyLoadBalancer implements LoadBalancer, CallStatisticsCollector { | ||
|
||
private final long failureBackOffNs; | ||
|
||
private final LinkedHashMap<Long, Long> failedInstances = new LinkedHashMap<>(); | ||
private volatile ServiceInstanceWithStatGathering lastSelected; | ||
|
||
public StickyLoadBalancer(Duration failureBackOff) { | ||
this.failureBackOffNs = failureBackOff.toNanos(); | ||
} | ||
|
||
// TODO start randomly, maybe sort the collection by service instance ids | ||
// TODO: flow chart for how it works alongside the text | ||
@Override | ||
public ServiceInstance selectServiceInstance(Collection<ServiceInstance> serviceInstances) { | ||
if (serviceInstances.isEmpty()) { | ||
throw new NoServiceInstanceFoundException("No service instance found"); | ||
} | ||
|
||
if (lastSelected != null) { | ||
for (var instance : serviceInstances) { | ||
if (instance.getId() == lastSelected.getId()) { | ||
return lastSelected; | ||
} | ||
} | ||
} | ||
Map<Long, ServiceInstance> instanceMap = serviceInstances.stream() | ||
.collect(Collectors.toMap(ServiceInstance::getId, i -> i)); | ||
lastSelected = selectNextInstance(instanceMap); | ||
return lastSelected; | ||
} | ||
|
||
private ServiceInstanceWithStatGathering selectNextInstance(Map<Long, ServiceInstance> serviceInstances) { | ||
for (ServiceInstance serviceInstance : serviceInstances.values()) { | ||
if (!failedInstances.containsKey(serviceInstance.getId())) { | ||
return new ServiceInstanceWithStatGathering(serviceInstance, this); | ||
} | ||
} | ||
|
||
Iterator<Map.Entry<Long, Long>> failedInstanceIterator = failedInstances.entrySet().iterator(); | ||
|
||
long now = System.nanoTime(); | ||
while (failedInstanceIterator.hasNext()) { | ||
Map.Entry<Long, Long> oldestFailedInstance = failedInstanceIterator.next(); | ||
|
||
Long instanceId = oldestFailedInstance.getKey(); | ||
Long lastFailureTime = oldestFailedInstance.getValue(); | ||
if (now - lastFailureTime < failureBackOffNs) { | ||
break; | ||
} | ||
failedInstanceIterator.remove(); | ||
ServiceInstance serviceInstance = serviceInstances.get(instanceId); | ||
if (serviceInstance != null) { | ||
lastSelected = new ServiceInstanceWithStatGathering(serviceInstance, this); | ||
return lastSelected; | ||
} | ||
} | ||
throw new NoAcceptableServiceInstanceFoundException("Each of the available service instances failed " + | ||
"within the configured failure-backoff-time"); | ||
} | ||
|
||
@Override | ||
public void recordEnd(long serviceInstanceId, Throwable error) { | ||
recordEndAtTime(serviceInstanceId, error, System.nanoTime()); | ||
} | ||
|
||
// exposed for tests only | ||
void recordEndAtTime(long serviceInstanceId, Throwable error, long currentTime) { | ||
if (error != null) { | ||
failedInstances.put(serviceInstanceId, currentTime); | ||
lastSelected = null; | ||
} | ||
} | ||
|
||
@Deprecated // for tests only | ||
LinkedHashMap<Long, Long> getFailedInstances() { | ||
return new LinkedHashMap<>(failedInstances); | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
...ticky/src/main/java/io/smallrye/stork/loadbalancer/random/StickyLoadBalancerProvider.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,25 @@ | ||
package io.smallrye.stork.loadbalancer.random; | ||
|
||
import io.smallrye.stork.api.LoadBalancer; | ||
import io.smallrye.stork.api.ServiceDiscovery; | ||
import io.smallrye.stork.api.config.LoadBalancerAttribute; | ||
import io.smallrye.stork.api.config.LoadBalancerType; | ||
import io.smallrye.stork.spi.LoadBalancerProvider; | ||
import io.smallrye.stork.utils.DurationUtils; | ||
|
||
@LoadBalancerType(StickyLoadBalancerProvider.TYPE) | ||
@LoadBalancerAttribute(name = StickyLoadBalancerProvider.FAILURE_BACKOFF_TIME, defaultValue = "0", description = "After how much time, " | ||
+ | ||
"a service instance that has failed can be reused") | ||
public class StickyLoadBalancerProvider | ||
implements LoadBalancerProvider<StickyConfiguration> { | ||
|
||
static final String TYPE = "sticky"; | ||
static final String FAILURE_BACKOFF_TIME = "failure-backoff-time"; | ||
|
||
@Override | ||
public LoadBalancer createLoadBalancer(StickyConfiguration config, | ||
ServiceDiscovery serviceDiscovery) { | ||
return new StickyLoadBalancer(DurationUtils.parseDuration(config.getFailureBackoffTime())); | ||
} | ||
} |
Oops, something went wrong.