Skip to content

Commit

Permalink
Add registeredNameOrIp cardinality limiter (#1043)
Browse files Browse the repository at this point in the history
We have tag values which may be IP addresses, which are very
high cardinality and low value, or registered names (Eureka VIPs
or DNS hostnames), which are lower cardinality and high value.
This limiter allows us to apply different cardinality policies to each
case.
  • Loading branch information
fedorka authored Mar 17, 2023
1 parent a604c08 commit 47d6bf7
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.netflix.spectator.api.Clock;
import com.netflix.spectator.api.Utils;
import com.netflix.spectator.impl.PatternMatcher;

import java.io.Serializable;
import java.util.Comparator;
Expand All @@ -28,6 +29,7 @@
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
Expand Down Expand Up @@ -155,6 +157,25 @@ public static Function<String, String> rollup(int n) {
return new RollupLimiter(n);
}

/**
* Restrict the cardinality independently for values which appear to be IP literals,
* which are likely high-cardinality and low value, and Registered names (eg: Eureka
* VIPs or DNS names), which are low-to-mid cardinality and high value. Values should
* be RFC3986 3.2.2 hosts, behavior with non-host strings is not guaranteed.
*
* @param registeredNameLimiter
* The limiter applied to values which appear to be registered names.
* @param ipLimiter
* The limiter applied to values which appear to be IP literals.
*
* @return
* The result according to the the matched limiter.
*/
public static Function<String, String> registeredNameOrIp(Function<String, String> registeredNameLimiter,
Function<String, String> ipLimiter) {
return new RegisteredNameOrIpLimiter(registeredNameLimiter, ipLimiter);
}

private static class FirstLimiter implements Function<String, String>, Serializable {

private static final long serialVersionUID = 1L;
Expand Down Expand Up @@ -357,4 +378,36 @@ private static class RollupLimiter implements Function<String, String>, Serializ
return "RollupLimiter(" + state + ")";
}
}

private static class RegisteredNameOrIpLimiter implements Function<String, String>, Serializable {

private static final long serialVersionUID = 1L;

//From RFC 3986 3.2.2, we can quickly identify IP literals (other than IPv4) from the first and last characters.
private static final Predicate<String> IS_IP_LITERAL = s -> s.startsWith("[") && s.endsWith("]");

//Approximating the logic from RFC 3986 3.2.2 without strictly enforcing the octect range
private static final Predicate<String> IS_IPV4_ADDRESS = PatternMatcher.compile(
"^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$")::matches;
private final Function<String, String> registeredNameLimiter;

private final Function<String, String> ipLimiter;

RegisteredNameOrIpLimiter(Function<String, String> registeredNameLimiter, Function<String, String> ipLimiter) {
this.registeredNameLimiter = registeredNameLimiter;
this.ipLimiter = ipLimiter;
}
@Override public String apply(String input) {
if (IS_IP_LITERAL.test(input) || IS_IPV4_ADDRESS.test(input)) {
return ipLimiter.apply(input);
} else {
return registeredNameLimiter.apply(input);
}
}

@Override public String toString() {
return "RegisteredNameOrIpLimiter(registeredNameLimiter=" + registeredNameLimiter.toString()
+ ", ipLimiter=" + ipLimiter.toString() + ")";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,50 @@ public void rollup2() {
Assertions.assertEquals(CardinalityLimiters.AUTO_ROLLUP, f.apply("a"));
}

@Test
public void registeredNameOrIp() {
Function<String, String> registeredNamelimiter = CardinalityLimiters.first(2);
Function<String, String> ipNamelimiter = CardinalityLimiters.first(2);
Function<String, String> limiter = CardinalityLimiters.registeredNameOrIp(registeredNamelimiter, ipNamelimiter);

//Allow two IPs
Assertions.assertEquals("127.0.0.1", limiter.apply("127.0.0.1"));
Assertions.assertEquals("[::1]", limiter.apply("[::1]"));

//Further IPs are limited
Assertions.assertEquals(CardinalityLimiters.OTHERS, limiter.apply("127.0.0.2"));
Assertions.assertEquals(CardinalityLimiters.OTHERS, limiter.apply("[::2]"));
Assertions.assertEquals(CardinalityLimiters.OTHERS, limiter.apply("[v1.::1]"));
Assertions.assertEquals(CardinalityLimiters.OTHERS, limiter.apply("[::1%0]"));

//Allow two registry names
Assertions.assertEquals("example.com", limiter.apply("example.com"));
Assertions.assertEquals("spectator", limiter.apply("spectator"));

//Further registry names are rolled up
Assertions.assertEquals(CardinalityLimiters.OTHERS, limiter.apply("subdomain.example.com"));

//Original IP are still allowed
Assertions.assertEquals("127.0.0.1", limiter.apply("127.0.0.1"));
Assertions.assertEquals("[::1]", limiter.apply("[::1]"));
}

@Test
public void registeredNameOrIpAnchorsIps() {
Function<String, String> registeredNamelimiter = CardinalityLimiters.first(4);
Function<String, String> ipNamelimiter = CardinalityLimiters.first(0);
Function<String, String> limiter = CardinalityLimiters.registeredNameOrIp(Function.identity(), s -> CardinalityLimiters.OTHERS);

//Confirm test setup that IPs limited
Assertions.assertEquals(CardinalityLimiters.OTHERS, limiter.apply("127.0.0.1"));

//IP-like registered names are allowed
Assertions.assertEquals("127.0.0.1.example.com", limiter.apply("127.0.0.1.example.com"));
Assertions.assertEquals("vip-127.0.0.1", limiter.apply("vip-127.0.0.1"));
Assertions.assertEquals("[::1]-vip", limiter.apply("[::1]-vip"));
Assertions.assertEquals("vip-[::1]", limiter.apply("vip-[::1]"));
}

@SuppressWarnings("unchecked")
static void checkSerde(Function<String, String> limiter) {
try {
Expand Down Expand Up @@ -360,4 +404,17 @@ public void rollupSerializability() {
limiter.apply("c");
checkSerde(limiter);
}

@Test
public void registeredNameOrIpSerializability() {
Function<String, String> registeredNamelimiter = CardinalityLimiters.first(5);
Function<String, String> ipNamelimiter = CardinalityLimiters.first(2);
Function<String, String> limiter = CardinalityLimiters.registeredNameOrIp(registeredNamelimiter, ipNamelimiter);

limiter.apply("127.0.0.1");
limiter.apply("[::1]");
limiter.apply("example.com");

checkSerde(limiter);
}
}

0 comments on commit 47d6bf7

Please sign in to comment.