Skip to content

Commit

Permalink
Merge pull request #274 from michalszynkiewicz/sticky-load-balancer
Browse files Browse the repository at this point in the history
`sticky` LoadBalancer implementation
  • Loading branch information
cescoffier authored Mar 29, 2022
2 parents 8b33dc3 + 8578e3b commit 068e958
Show file tree
Hide file tree
Showing 8 changed files with 543 additions and 0 deletions.
30 changes: 30 additions & 0 deletions docs/load-balancer/sticky.md
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"
77 changes: 77 additions & 0 deletions load-balancer/sticky/pom.xml
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>
56 changes: 56 additions & 0 deletions load-balancer/sticky/revapi.json
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"
}
}
]
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);
}
}
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()));
}
}
Loading

0 comments on commit 068e958

Please sign in to comment.