Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DNS service discovery #333

Merged
merged 1 commit into from
Jul 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@
<artifactId>stork-service-discovery-consul</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-dns</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-eureka</artifactId>
Expand Down
10 changes: 9 additions & 1 deletion core/revapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@
"criticality" : "highlight",
"minSeverity" : "POTENTIALLY_BREAKING",
"minCriticality" : "documented",
"differences" : [ ]
"differences" : [
{
"ignore": true,
"code": "java.method.numberOfParametersChanged",
"old": "method java.time.Duration io.smallrye.stork.utils.DurationUtils::parseDuration(java.lang.String)",
"new": "method java.time.Duration io.smallrye.stork.utils.DurationUtils::parseDuration(java.lang.String, java.lang.String)",
"justification": "Parameter name added to the `parseDuration` method. The old version assumed the parsing is done for refresh-period which is not always true"
}
]
}
}, {
"extension" : "revapi.reporter.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
public abstract class CachingServiceDiscovery implements ServiceDiscovery {

private static final Logger log = LoggerFactory.getLogger(CachingServiceDiscovery.class);
public static final String REFRESH_PERIOD = "refresh-period";

public final Duration refreshPeriod;

public static final String DEFAULT_REFRESH_INTERVAL = "5M";

private volatile List<ServiceInstance> lastResults;
Expand All @@ -33,9 +35,9 @@ public abstract class CachingServiceDiscovery implements ServiceDiscovery {
public CachingServiceDiscovery(String refreshPeriod) {
try {
// TODO: document it
this.refreshPeriod = DurationUtils.parseDuration(refreshPeriod);
this.refreshPeriod = DurationUtils.parseDuration(refreshPeriod, REFRESH_PERIOD);
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("refresh-period for service discovery should be a number, got: " +
throw new IllegalArgumentException(REFRESH_PERIOD + " for service discovery should be a number, got: " +
refreshPeriod,
e);
}
Expand Down
5 changes: 3 additions & 2 deletions core/src/main/java/io/smallrye/stork/utils/DurationUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ private DurationUtils() {
* Otherwise, tries to convert the value assuming that it is in the accepted ISO-8601 duration format.
*
* @param duration duration as String
* @param parameter the parameter for which we parse the value to duration
* @return {@link Duration}
*/
public static Duration parseDuration(String duration) {
public static Duration parseDuration(String duration, String parameter) {
if (duration.startsWith("-")) {
throw new IllegalArgumentException("Negative refresh-period specified for service discovery: " + duration);
throw new IllegalArgumentException("Negative " + parameter + " specified for service discovery: " + duration);
}
if (DIGITS.asPredicate().test(duration)) {
return Duration.ofSeconds(Long.parseLong(duration));
Expand Down
32 changes: 16 additions & 16 deletions core/src/main/java/io/smallrye/stork/utils/StorkAddressUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@
public final class StorkAddressUtils {

/**
* Creates a new {@link HostAndPort} instance.
* Creates a new {@link HostAndPort} instance from an address.
*
* @param serviceAddress the address
* @param defaultPort the default port
* @param serviceName the service name
* @param address the address, either {@code host:port} or just {@code host}
* @param defaultPort the default port, used when the address doesn't provide the port
* @param configPlace the location of the address in the configuration, for logging purposes
* @return the new HostAndPort
*/
public static HostAndPort parseToHostAndPort(String serviceAddress,
public static HostAndPort parseToHostAndPort(String address,
int defaultPort,
String serviceName) {
if (serviceAddress == null || serviceAddress.isBlank()) {
throw new IllegalArgumentException("Blank or null address: '" + serviceAddress + "'");
String configPlace) {
if (address == null || address.isBlank()) {
throw new IllegalArgumentException("Blank or null address: '" + address + "'");
}
if (serviceAddress.charAt(0) == '[') {
return parseIpV6AddressWithSquareBracket(serviceAddress, defaultPort, serviceName);
} else if (countColons(serviceAddress) > 1) {
return new HostAndPort(serviceAddress, defaultPort);
if (address.charAt(0) == '[') {
return parseIpV6AddressWithSquareBracket(address, defaultPort, configPlace);
} else if (countColons(address) > 1) {
return new HostAndPort(address, defaultPort);
} else {
return parseNonIpv6Adress(serviceAddress, defaultPort, serviceName);
return parseNonIpv6Adress(address, defaultPort, configPlace);
}
}

Expand Down Expand Up @@ -67,14 +67,14 @@ private static HostAndPort parseIpV6AddressWithSquareBracket(String serviceAddre
if (!done) {
throw new IllegalArgumentException(
format("IPv6 Address with a square bracket '[' does not have a matching closing square bracket ']' " +
"in address '%s' for service: '%s'", serviceAddress, serviceName));
"in address '%s' for: '%s'", serviceAddress, serviceName));
}

if (++i == serviceAddress.length()) {
return new HostAndPort(host.toString(), defaultPort);
} else if (serviceAddress.charAt(i) != ':') {
throw new IllegalArgumentException(
format("Unexpected character '%c' at character %d in address '%s' for service: '%s'",
format("Unexpected character '%c' at character %d in address '%s' for: '%s'",
serviceAddress.charAt(i), i, serviceAddress, serviceName));
} else {
int port = 0;
Expand All @@ -86,7 +86,7 @@ private static HostAndPort parseIpV6AddressWithSquareBracket(String serviceAddre
} else {
throw new IllegalArgumentException(
format("Unexpected character '%c' while parsing port number in " +
"address '%s' for service '%s', at character %d, expected a digit", c, serviceName,
"address '%s' for '%s', at character %d, expected a digit", c, serviceName,
serviceAddress, i));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,22 @@ public class DurationUtilsTest {
@Test
public void testOnlyNumberValueProvided() {
Duration expectedDuration = Duration.ofSeconds(3);
Duration actualDuration = DurationUtils.parseDuration("3");
Duration actualDuration = DurationUtils.parseDuration("3", "refresh-period");
assertEquals(expectedDuration, actualDuration);
}

@Test
public void testNumberWithUnitValueProvided() {
Duration expectedDuration = Duration.ofMinutes(3);
Duration actualDuration = DurationUtils.parseDuration("3M");
Duration actualDuration = DurationUtils.parseDuration("3M", "refresh-period");
assertEquals(expectedDuration, actualDuration);
}

@Test
public void testValueStartingWithNumberAndInCorrectFormatProvided() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> {
DurationUtils.parseDuration("-5");
DurationUtils.parseDuration("-5", "refresh-period");
}).withMessage("Negative refresh-period specified for service discovery: -5");

}
Expand Down
5 changes: 5 additions & 0 deletions coverage/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
<artifactId>stork-service-discovery-consul</artifactId>
</dependency>

<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-dns</artifactId>
</dependency>

<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-kubernetes</artifactId>
Expand Down
31 changes: 31 additions & 0 deletions docs/diagrams/srv_sequence.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
@startuml
skinparam participant {
BackgroundColor AliceBlue
ArrowColor DarkGrey
BorderColor DarkGrey
}

skinparam roundcorner 20

skinparam sequence {
ArrowColor DarkGrey
ActorBorderColor DarkGrey
LifeLineBorderColor DarkGrey
LifeLineBackgroundColor #A9DCDF

}
skinparam sequenceMessageAlign center


participant Application
participant Stork
participant "DNS Server"

Application -> Stork : get service instances
Stork -> "DNS Server" : get SRV records for hostname
"DNS Server" -> Stork : list of SRV records
Stork -> "DNS Server" : get A/AAAA records for each SRV record target
"DNS Server" -> Stork : list of A/AAAA
Stork -> Application : list of ServiceInstances

@enduml
56 changes: 56 additions & 0 deletions docs/service-discovery/dns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# DNS Service Discovery

DNS is a name resolution protocol used to determine IP addresses for hostnames.
That makes it a natural fit for service discovery.
Consul and AWS Cloud Map provide DNS resolutions for service discovery.

This page explains how Stork can use DNS to handle the service discovery.

## DNS records

DNS supports a [variety of record types](https://en.wikipedia.org/wiki/List_of_DNS_record_types). Stork can resolve hostnames to addresses based on [_SRV_](https://en.wikipedia.org/wiki/SRV_record), A and AAAA records.
All these types of records may return multiple addresses for a single hostname.

While _A_ and _AAAA_ records are quite similar, they just carry an IP (_v4_ for _A_ and _v6_ for _AAAA_), the _SRV_ records are different.
They contain a _weight_, a _target_ and a _port_ for a service instance.
The _target_ returned in an _SRV_ record needs to be resolved further by an _A_ or an _AAAA_ record.

In short, it works as follows:
![SRV resolution](../target/srv_sequence.png)

## Dependency

To use the DNS service discovery, you need to add the Stork DNS Service Discovery provider dependency to your project:

```xml
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-dns</artifactId>
<version>{{version.current}}</version>
</dependency>
```

## Configuration

Next, set the service discovery `type` to `dns`.
Additionally, you would usually specify the DNS server, or servers, to use for the discovery.
All in all, your configuration could look as follows:

=== "stork standalone"
```properties
stork.my-service.service-discovery.type=dns
# optional dns servers:
stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2
michalszynkiewicz marked this conversation as resolved.
Show resolved Hide resolved
```

=== "stork in quarkus"
```properties
quarkus.stork.my-service.service-discovery.type=dns

# optional dns servers:
quarkus.stork.my-service.service-discovery.dns-servers=my-dns-server:8221,my-dns-server2
```

cescoffier marked this conversation as resolved.
Show resolved Hide resolved
All the available parameters are as follows:

--8<-- "service-discovery/dns/target/classes/META-INF/stork-docs/dns-sd-attributes.txt"
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.smallrye.stork.loadbalancer.leastresponsetime;

import static io.smallrye.stork.impl.CachingServiceDiscovery.REFRESH_PERIOD;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
Expand Down Expand Up @@ -35,7 +37,7 @@ public class LeastResponseTimeLoadBalancer implements LoadBalancer {
* @param config the configuration, must not be {@code null}
*/
public LeastResponseTimeLoadBalancer(LeastResponseTimeConfiguration config) {
long errorPenalty = DurationUtils.parseDuration(config.getErrorPenalty()).toNanos();
long errorPenalty = DurationUtils.parseDuration(config.getErrorPenalty(), REFRESH_PERIOD).toNanos();
double decliningFactor = Double.parseDouble(config.getDecliningFactor());
powersOfDecliningFactor = new FastPower(decliningFactor);
callStatistics = new CallStatistics(errorPenalty, powersOfDecliningFactor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public class StickyLoadBalancerProvider
@Override
public LoadBalancer createLoadBalancer(StickyConfiguration config,
ServiceDiscovery serviceDiscovery) {
return new StickyLoadBalancer(DurationUtils.parseDuration(config.getFailureBackoffTime()));
return new StickyLoadBalancer(DurationUtils.parseDuration(config.getFailureBackoffTime(), FAILURE_BACKOFF_TIME));
}
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ nav:
- Using Stork with Quarkus: './quarkus.md'
- Service Discovery:
- Consul: 'service-discovery/consul.md'
- DNS: 'service-discovery/dns.md'
- Kubernetes: 'service-discovery/kubernetes.md'
- Eureka: 'service-discovery/eureka.md'
- Composite: 'service-discovery/composite.md'
Expand Down
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@

<vertx.version>4.2.2</vertx.version>
<version.vertx-mutiny-bindings>2.22.0</version.vertx-mutiny-bindings>
<!-- override the testcontainers version with the newest one -->
<version.testcontainers>1.17.3</version.testcontainers>

<kubernetes-client.version>5.12.2</kubernetes-client.version>
<version.slf4j>1.7.36</version.slf4j>
Expand Down Expand Up @@ -311,6 +313,7 @@
<module>microprofile</module>
<module>service-discovery/static-list</module>
<module>service-discovery/consul</module>
<module>service-discovery/dns</module>
<module>service-discovery/kubernetes</module>
<module>service-discovery/eureka</module>
<module>service-discovery/composite</module>
Expand Down
2 changes: 1 addition & 1 deletion service-discovery/consul/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

<artifactId>stork-service-discovery-consul</artifactId>

<name>SmallRye Stork Service Discovery : Consul List</name>
<name>SmallRye Stork Service Discovery : Consul</name>

<properties>
<revapi.skip>false</revapi.skip>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.smallrye.stork.api.config.ServiceConfig;
import io.smallrye.stork.api.config.ServiceDiscoveryAttribute;
import io.smallrye.stork.api.config.ServiceDiscoveryType;
import io.smallrye.stork.impl.CachingServiceDiscovery;
import io.smallrye.stork.spi.ServiceDiscoveryProvider;
import io.smallrye.stork.spi.StorkInfrastructure;
import io.vertx.core.Vertx;
Expand All @@ -15,7 +16,7 @@
@ServiceDiscoveryAttribute(name = "consul-port", description = "The Consul port.", defaultValue = "8500")
@ServiceDiscoveryAttribute(name = "use-health-checks", description = "Whether to use health check.", defaultValue = "true")
@ServiceDiscoveryAttribute(name = "application", description = "The application name; if not defined Stork service name will be used.")
@ServiceDiscoveryAttribute(name = "refresh-period", description = "Service discovery cache refresh period.", defaultValue = "5M")
@ServiceDiscoveryAttribute(name = "refresh-period", description = "Service discovery cache refresh period.", defaultValue = CachingServiceDiscovery.DEFAULT_REFRESH_INTERVAL)
@ServiceDiscoveryAttribute(name = "secure", description = "whether the connection with the service should be encrypted with TLS.")
@ServiceDiscoveryType("consul")
public class ConsulServiceDiscoveryProvider implements ServiceDiscoveryProvider<ConsulConfiguration> {
Expand Down
Loading