Skip to content

Commit 0717f64

Browse files
authored
Exposing Canary Rate Threshold to be configurable and usable (mageddo#593)
* fetching files * add missing file * missing utils * [Gradle Release Plugin] - new version commit: '3.30.6-snapshot'. * [Gradle Release Plugin] - new version commit: '3.31.0-snapshot'. * release notes * creating docs * add refs * setup factory
1 parent ff5a0c8 commit 0717f64

File tree

14 files changed

+360
-32
lines changed

14 files changed

+360
-32
lines changed

RELEASE-NOTES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 3.31.0
2+
* Canary Rate Threshold Circuit Breaker. #533
3+
14
## 3.30.5
25
* Bugfix: Treating npe at networks mapping when network hasn't a gateway ip #580.
36

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
title: Remote Solver Circuit Breaker
3+
---
4+
5+
DPS use circuit breaker strategies to choose the most available Remote Server from the configured ones.
6+
7+
## Static Threshold
8+
9+
* Consider all remote servers circuits as **closed** on app start.
10+
* Opens and closes circuits based on fixed number of failures or successes.
11+
12+
13+
#### Configuration Example
14+
15+
```json
16+
{
17+
"version": 2,
18+
"remoteDnsServers": [],
19+
"envs": [],
20+
"activeEnv": "",
21+
"webServerPort": 5380,
22+
"dnsServerPort": 53,
23+
"logLevel": "INFO",
24+
"logFile": "console",
25+
"registerContainerNames": false,
26+
"domain": "docker",
27+
"dpsNetwork": false,
28+
"dpsNetworkAutoConnect": false,
29+
"defaultDns": true,
30+
"hostMachineHostname": "host.docker",
31+
"serverProtocol": "UDP_TCP",
32+
"dockerHost": null,
33+
"resolvConfOverrideNameServers": true,
34+
"noRemoteServers": false,
35+
"noEntriesResponseCode": 3,
36+
"dockerSolverHostMachineFallbackActive": true,
37+
"solverRemote": {
38+
"circuitBreaker": {
39+
"failureThreshold": 3,
40+
"failureThresholdCapacity": 10,
41+
"successThreshold": 5,
42+
"testDelay": "PT20S"
43+
}
44+
}
45+
}
46+
```
47+
48+
* **failureThreshold**: How many attempts before open the circuit?
49+
* **failureThresholdCapacity**: How many attempts store to the stack?
50+
* **successThreshold**: How many attempts before close the circuit?
51+
* **testDelay**: How much time to wait before test the circuit again?, see [Duration docs][1] for format explanation
52+
53+
54+
## Canary Rate Threshold
55+
56+
* Consider all remote servers circuits as **open** on app start
57+
* Opens and closes circuits based on percentage of failure
58+
59+
#### Consider all remote servers circuits as open on app start
60+
61+
Test them on startup and add the healthy ones as HALF_OPEN this will evict to app get resolution fails right on the
62+
start because the first server on the remote servers list is offline.
63+
64+
#### Configuration Example
65+
66+
```json
67+
{
68+
"version": 2,
69+
"remoteDnsServers": [],
70+
"envs": [],
71+
"activeEnv": "",
72+
"webServerPort": 5380,
73+
"dnsServerPort": 53,
74+
"logLevel": "INFO",
75+
"logFile": "console",
76+
"registerContainerNames": false,
77+
"domain": "docker",
78+
"dpsNetwork": false,
79+
"dpsNetworkAutoConnect": false,
80+
"defaultDns": true,
81+
"hostMachineHostname": "host.docker",
82+
"serverProtocol": "UDP_TCP",
83+
"dockerHost": null,
84+
"resolvConfOverrideNameServers": true,
85+
"noRemoteServers": false,
86+
"noEntriesResponseCode": 3,
87+
"dockerSolverHostMachineFallbackActive": true,
88+
"solverRemote": {
89+
"circuitBreaker": {
90+
"strategy": "CANARY_RATE_THRESHOLD",
91+
"failureRateThreshold": 21,
92+
"minimumNumberOfCalls": 50,
93+
"permittedNumberOfCallsInHalfOpenState": 10
94+
}
95+
}
96+
}
97+
```
98+
99+
* **failureRateThreshold**: If the failure rate is equal to or greater than this threshold, the CircuitBreaker will
100+
transition to open. rules: values greater than 0 and not greater than 100.
101+
* **minimumNumberOfCalls**: Configures the minimum number of calls which are required (per sliding window period) before
102+
the CircuitBreaker can calculate the error rate.
103+
* **permittedNumberOfCallsInHalfOpenState**: Configures the number of permitted calls when the CircuitBreaker is half
104+
open.
105+
106+
## Refs
107+
108+
* [A more resilient circuit breaker strategy #533][2]
109+
110+
[1]: https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html#toString--
111+
[2]: https://github.com/mageddo/dns-proxy-server/issues/533

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=3.30.5-snapshot
1+
version=3.31.0-snapshot

src/main/java/com/mageddo/dnsproxyserver/config/dataprovider/mapper/ConfigJsonV2Mapper.java

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
package com.mageddo.dnsproxyserver.config.dataprovider.mapper;
22

3-
import com.mageddo.dnsproxyserver.config.StaticThresholdCircuitBreakerStrategyConfig;
3+
import com.mageddo.dnsproxyserver.config.CanaryRateThresholdCircuitBreakerStrategyConfig;
4+
import com.mageddo.dnsproxyserver.config.CircuitBreakerStrategyConfig;
45
import com.mageddo.dnsproxyserver.config.Config;
56
import com.mageddo.dnsproxyserver.config.SolverRemote;
7+
import com.mageddo.dnsproxyserver.config.StaticThresholdCircuitBreakerStrategyConfig;
68
import com.mageddo.dnsproxyserver.config.dataprovider.vo.ConfigJson;
79
import com.mageddo.dnsproxyserver.config.dataprovider.vo.ConfigJsonV2;
10+
import com.mageddo.dnsproxyserver.config.dataprovider.vo.ConfigJsonV2.CanaryRateThresholdCircuitBreaker;
11+
import com.mageddo.dnsproxyserver.config.dataprovider.vo.ConfigJsonV2.StaticThresholdCircuitBreaker;
812
import com.mageddo.dnsproxyserver.utils.Booleans;
13+
import lombok.extern.slf4j.Slf4j;
914
import org.apache.commons.lang3.ObjectUtils;
1015

1116
import java.nio.file.Path;
1217

18+
@Slf4j
1319
public class ConfigJsonV2Mapper {
1420

1521
public static Config toConfig(ConfigJson json, Path configFileAbsolutePath) {
@@ -64,16 +70,36 @@ static SolverRemote buildCompleteSolverRemote(ConfigJson json, ConfigJsonV2.Circ
6470
return SolverRemote
6571
.builder()
6672
.active(Booleans.reverseWhenNotNull(json.getNoRemoteServers()))
67-
// fixme #533 need to create a dynamic json parser for different strategies,
68-
// then a dynamic mapper to the solver remote
69-
.circuitBreaker(StaticThresholdCircuitBreakerStrategyConfig
70-
.builder()
71-
.failureThreshold(circuitBreaker.getFailureThreshold())
72-
.failureThresholdCapacity(circuitBreaker.getFailureThresholdCapacity())
73-
.successThreshold(circuitBreaker.getSuccessThreshold())
74-
.testDelay(circuitBreaker.getTestDelay())
75-
.build()
76-
)
73+
.circuitBreaker(mapCircuitBreaker(circuitBreaker))
74+
.build();
75+
}
76+
77+
private static CircuitBreakerStrategyConfig mapCircuitBreaker(ConfigJsonV2.CircuitBreaker circuitBreaker) {
78+
log.debug("circuitBreakerConfigStrategy={}", circuitBreaker.strategy());
79+
return switch (circuitBreaker.strategy()){
80+
case STATIC_THRESHOLD -> mapFromStaticCircuitBreaker((StaticThresholdCircuitBreaker) circuitBreaker);
81+
case CANARY_RATE_THRESHOLD -> mapFromCanaryRateThresholdCircuitBreaker((CanaryRateThresholdCircuitBreaker) circuitBreaker);
82+
default -> throw new UnsupportedOperationException("Unrecognized circuit breaker: " + circuitBreaker.strategy());
83+
};
84+
}
85+
86+
private static CircuitBreakerStrategyConfig mapFromCanaryRateThresholdCircuitBreaker(
87+
CanaryRateThresholdCircuitBreaker circuitBreaker
88+
) {
89+
return CanaryRateThresholdCircuitBreakerStrategyConfig.builder()
90+
.failureRateThreshold(circuitBreaker.getFailureRateThreshold())
91+
.minimumNumberOfCalls(circuitBreaker.getMinimumNumberOfCalls())
92+
.permittedNumberOfCallsInHalfOpenState(circuitBreaker.getPermittedNumberOfCallsInHalfOpenState())
93+
.build();
94+
}
95+
96+
private static CircuitBreakerStrategyConfig mapFromStaticCircuitBreaker(StaticThresholdCircuitBreaker circuitBreaker) {
97+
return StaticThresholdCircuitBreakerStrategyConfig
98+
.builder()
99+
.failureThreshold(circuitBreaker.getFailureThreshold())
100+
.failureThresholdCapacity(circuitBreaker.getFailureThresholdCapacity())
101+
.successThreshold(circuitBreaker.getSuccessThreshold())
102+
.testDelay(circuitBreaker.getTestDelay())
77103
.build();
78104
}
79105
}

src/main/java/com/mageddo/dnsproxyserver/config/dataprovider/vo/ConfigJsonV2.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import com.fasterxml.jackson.annotation.JsonIgnore;
44
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import com.fasterxml.jackson.annotation.JsonSubTypes;
6+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
57
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
68
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
79
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer;
810
import com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer;
11+
import com.mageddo.dnsproxyserver.config.CircuitBreakerStrategyConfig;
912
import com.mageddo.dnsproxyserver.config.Config;
1013
import com.mageddo.dnsproxyserver.config.Config.Entry.Type;
1114
import com.mageddo.dnsproxyserver.config.dataprovider.mapper.ConfigJsonV2EnvsMapper;
@@ -167,8 +170,26 @@ public static class SolverRemote {
167170

168171
}
169172

173+
174+
@JsonTypeInfo(
175+
use = JsonTypeInfo.Id.NAME,
176+
include = JsonTypeInfo.As.PROPERTY,
177+
property = "strategy",
178+
defaultImpl = StaticThresholdCircuitBreaker.class
179+
)
180+
@JsonSubTypes({
181+
@JsonSubTypes.Type(value = StaticThresholdCircuitBreaker.class, name = "STATIC_THRESHOLD"),
182+
@JsonSubTypes.Type(value = CanaryRateThresholdCircuitBreaker.class, name = "CANARY_RATE_THRESHOLD")
183+
})
184+
public interface CircuitBreaker {
185+
186+
CircuitBreakerStrategyConfig.Name strategy();
187+
188+
}
189+
190+
170191
@Data
171-
public static class CircuitBreaker {
192+
public static class StaticThresholdCircuitBreaker implements CircuitBreaker {
172193

173194
private Integer failureThreshold;
174195
private Integer failureThresholdCapacity;
@@ -177,5 +198,23 @@ public static class CircuitBreaker {
177198
@JsonSerialize(using = DurationSerializer.class)
178199
@JsonDeserialize(using = DurationDeserializer.class)
179200
private Duration testDelay;
201+
202+
@Override
203+
public CircuitBreakerStrategyConfig.Name strategy() {
204+
return CircuitBreakerStrategyConfig.Name.STATIC_THRESHOLD;
205+
}
206+
}
207+
208+
@Data
209+
public static class CanaryRateThresholdCircuitBreaker implements CircuitBreaker {
210+
211+
private float failureRateThreshold;
212+
private int minimumNumberOfCalls;
213+
private int permittedNumberOfCallsInHalfOpenState;
214+
215+
@Override
216+
public CircuitBreakerStrategyConfig.Name strategy() {
217+
return CircuitBreakerStrategyConfig.Name.CANARY_RATE_THRESHOLD;
218+
}
180219
}
181220
}

src/main/java/com/mageddo/dnsproxyserver/config/validator/CircuitBreakerValidator.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
package com.mageddo.dnsproxyserver.config.validator;
22

3+
import com.mageddo.dnsproxyserver.config.CanaryRateThresholdCircuitBreakerStrategyConfig;
4+
import com.mageddo.dnsproxyserver.config.CircuitBreakerStrategyConfig;
35
import com.mageddo.dnsproxyserver.config.StaticThresholdCircuitBreakerStrategyConfig;
46
import org.apache.commons.lang3.Validate;
57

68
import static com.mageddo.dnsproxyserver.utils.Numbers.positiveOrNull;
79

810
public class CircuitBreakerValidator {
9-
public static void validate(StaticThresholdCircuitBreakerStrategyConfig circuit) {
10-
Validate.notNull(positiveOrNull(circuit.getFailureThreshold()), genMsg("failure theshold must be a positive number"));
11-
Validate.notNull(positiveOrNull(circuit.getSuccessThreshold()), genMsg("success theshold must be positive number"));
12-
Validate.notNull(positiveOrNull(circuit.getFailureThresholdCapacity()), genMsg("success theshold capacity must be positive number"));
13-
Validate.notNull(circuit.getTestDelay(), genMsg("test delay must be not null"));
11+
public static void validate(CircuitBreakerStrategyConfig config) {
12+
13+
switch (config.name()){
14+
case STATIC_THRESHOLD -> validate((StaticThresholdCircuitBreakerStrategyConfig)config);
15+
case CANARY_RATE_THRESHOLD -> validate((CanaryRateThresholdCircuitBreakerStrategyConfig)config);
16+
}
17+
}
18+
19+
static void validate(CanaryRateThresholdCircuitBreakerStrategyConfig config) {
20+
Validate.notNull(positiveOrNull(config.getMinimumNumberOfCalls()), genMsg("failure threshold must be a positive number"));
21+
Validate.notNull(positiveOrNull(config.getPermittedNumberOfCallsInHalfOpenState()), genMsg("success threshold must be positive number"));
22+
Validate.notNull(positiveOrNull(config.getFailureRateThreshold()), genMsg("success thershold capacity must be positive number"));
23+
}
24+
25+
static void validate(StaticThresholdCircuitBreakerStrategyConfig config) {
26+
Validate.notNull(positiveOrNull(config.getFailureThreshold()), genMsg("failure threshold must be a positive number"));
27+
Validate.notNull(positiveOrNull(config.getSuccessThreshold()), genMsg("success threshold must be positive number"));
28+
Validate.notNull(positiveOrNull(config.getFailureThresholdCapacity()), genMsg("success thershold capacity must be positive number"));
29+
Validate.notNull(config.getTestDelay(), genMsg("test delay must be not null"));
1430
}
1531

1632
private static String genMsg(String msg) {

src/main/java/com/mageddo/dnsproxyserver/config/validator/ConfigValidator.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.mageddo.dnsproxyserver.config.validator;
22

33
import com.mageddo.dnsproxyserver.config.Config;
4-
import com.mageddo.dnsproxyserver.config.StaticThresholdCircuitBreakerStrategyConfig;
54
import org.apache.commons.lang3.Validate;
65

76
public class ConfigValidator {
@@ -24,7 +23,6 @@ public static void validate(Config config) {
2423
Validate.notNull(config.getSolverRemote(), "Solver Remote");
2524
Validate.notNull(config.isSolverRemoteActive(), "Solver remote active");
2625

27-
// fixme #533 this could not work every time after new types be created, check it
28-
CircuitBreakerValidator.validate((StaticThresholdCircuitBreakerStrategyConfig) config.getSolverRemoteCircuitBreakerStrategy());
26+
CircuitBreakerValidator.validate(config.getSolverRemoteCircuitBreakerStrategy());
2927
}
3028
}

src/main/java/com/mageddo/dnsproxyserver/solver/remote/application/failsafe/CircuitBreakerFactory.java

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.mageddo.dnsproxyserver.solver.remote.application.failsafe;
22

33
import com.mageddo.commons.lang.tuple.Pair;
4+
import com.mageddo.dnsproxyserver.config.CanaryRateThresholdCircuitBreakerStrategyConfig;
45
import com.mageddo.dnsproxyserver.config.CircuitBreakerStrategyConfig;
56
import com.mageddo.dnsproxyserver.config.StaticThresholdCircuitBreakerStrategyConfig;
67
import com.mageddo.dnsproxyserver.config.application.ConfigService;
@@ -28,11 +29,17 @@
2829
@Slf4j
2930
@Singleton
3031
@RequiredArgsConstructor(onConstructor = @__({@Inject}))
32+
/**
33+
* fixme #533 Move failsafe.CircuitBreakerFactory to another package, this is not specific for failsafe
34+
*/
3135
public class CircuitBreakerFactory {
3236

3337
private final Map<InetSocketAddress, CircuitBreakerDelegate> circuitBreakerMap = new ConcurrentHashMap<>();
3438
private final ConfigService configService;
39+
40+
// fixme #533 Delete CircuitBreakerPingCheckerService from CircuitBreakerFactory and related stuff
3541
private final CircuitBreakerPingCheckerService circuitBreakerCheckerService;
42+
3643
private final FailsafeCircuitBreakerFactory failsafeCircuitBreakerFactory;
3744
private final com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold.CircuitBreakerFactory canaryThresholdFactory;
3845

@@ -41,28 +48,34 @@ public Result check(ResultSupplier sup) {
4148
return circuitBreaker.execute(sup);
4249
}
4350

44-
public CircuitBreakerDelegate findCircuitBreaker(IpAddr serverAddress) {
45-
final var strategy = this.findCircuitBreakerHotLoad(serverAddress);
46-
return this.circuitBreakerMap.computeIfAbsent(
47-
ResolverMapper.toInetSocketAddress(serverAddress),
48-
addr -> strategy
49-
);
51+
public CircuitBreakerDelegate findCircuitBreaker(IpAddr address) {
52+
return this.findCircuitBreaker(ResolverMapper.toInetSocketAddress(address));
5053
}
5154

52-
CircuitBreakerDelegate findCircuitBreakerHotLoad(IpAddr address) {
55+
public CircuitBreakerDelegate findCircuitBreaker(InetSocketAddress address) {
56+
return this.circuitBreakerMap.computeIfAbsent(address, this::findCircuitBreakerHotLoad);
57+
}
58+
59+
CircuitBreakerDelegate findCircuitBreakerHotLoad(InetSocketAddress address) {
5360
final var config = this.findCircuitBreakerConfig();
5461
return switch (config.name()) {
5562
case STATIC_THRESHOLD -> this.buildStaticThresholdFailSafeCircuitBreaker(address, config);
5663
case NON_RESILIENT -> new CircuitBreakerDelegateNonResilient();
64+
case CANARY_RATE_THRESHOLD -> this.buildCanaryRateThreshold(config, address);
5765
default -> throw new UnsupportedOperationException();
5866
};
5967
}
6068

69+
CircuitBreakerDelegate buildCanaryRateThreshold(CircuitBreakerStrategyConfig config, InetSocketAddress address) {
70+
// return this.canaryThresholdFactory.build(config, IpAddrs.from(address));
71+
return this.canaryThresholdFactory.build((CanaryRateThresholdCircuitBreakerStrategyConfig) config);
72+
}
73+
6174
private CircuitBreakerDelegateStaticThresholdFailsafe buildStaticThresholdFailSafeCircuitBreaker(
62-
IpAddr address, CircuitBreakerStrategyConfig config
75+
InetSocketAddress address, CircuitBreakerStrategyConfig config
6376
) {
6477
return new CircuitBreakerDelegateStaticThresholdFailsafe(this.failsafeCircuitBreakerFactory.build(
65-
ResolverMapper.toInetSocketAddress(address),
78+
address,
6679
(StaticThresholdCircuitBreakerStrategyConfig) config
6780
));
6881
}

src/main/java/com/mageddo/dnsproxyserver/utils/Numbers.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ public static Integer positiveOrDefault(Integer v, Integer def) {
1010
return v;
1111
}
1212

13-
public static Integer positiveOrNull(Integer v) {
14-
if (v == null || v <= 0) {
13+
public static <T extends Number> T positiveOrNull(T v) {
14+
if (v == null || v.intValue() < 0) {
1515
return null;
1616
}
1717
return v;

0 commit comments

Comments
 (0)