diff --git a/pom.xml b/pom.xml index 8e8ccebb..6e6c0515 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ UTF-8 - 31.0.1-jre + 33.2.0-jre 5.5.0 1.7.32 @@ -92,7 +92,7 @@ 1.18.22 3.0.1u2 - 5.8.2 + 5.8.2 4.1.1 2.0.0 @@ -108,7 +108,7 @@ org.junit junit-bom - ${junit.jupiter.version} + ${junit.version} pom import diff --git a/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerate.json b/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerate.json index f19b937b..6022cc23 100644 --- a/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerate.json +++ b/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerate.json @@ -4,5 +4,5 @@ "iterations" : 4, "threads" : 1, "forks" : 3, - "mean_ops" : 644166.1778513143 + "mean_ops" : 738950.7655028169 } \ No newline at end of file diff --git a/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerateBase36.json b/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerateBase36.json index 272db000..c9c3280f 100644 --- a/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerateBase36.json +++ b/ranger-discovery-bundle/perf/results/io.appform.ranger.discovery.bundle.id.IdGeneratorPerfTest.testGenerateBase36.json @@ -4,5 +4,5 @@ "iterations" : 4, "threads" : 1, "forks" : 3, - "mean_ops" : 502644.4941310657 + "mean_ops" : 573183.379799968 } \ No newline at end of file diff --git a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/CollisionChecker.java b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/CollisionChecker.java index b3e947a7..3e080069 100644 --- a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/CollisionChecker.java +++ b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/CollisionChecker.java @@ -17,9 +17,11 @@ package io.appform.ranger.discovery.bundle.id; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import java.util.BitSet; +import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -33,15 +35,22 @@ public class CollisionChecker { private final Lock dataLock = new ReentrantLock(); + private final TimeUnit resolution; + public CollisionChecker() { - //Nothing to do here + this(TimeUnit.MILLISECONDS); + } + + public CollisionChecker(@NonNull TimeUnit resolution) { + this.resolution = resolution; } - public boolean check(long time, int location) { + public boolean check(long timeInMillis, int location) { dataLock.lock(); try { - if (currentInstant != time) { - currentInstant = time; + long resolvedTime = resolution.convert(timeInMillis, TimeUnit.MILLISECONDS);; + if (currentInstant != resolvedTime) { + currentInstant = resolvedTime; bitSet.clear(); } diff --git a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/Domain.java b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/Domain.java new file mode 100644 index 00000000..b3a980d0 --- /dev/null +++ b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/Domain.java @@ -0,0 +1,41 @@ +package io.appform.ranger.discovery.bundle.id; + + +import io.appform.ranger.discovery.bundle.id.constraints.IdValidationConstraint; +import io.appform.ranger.discovery.bundle.id.formatter.DefaultIdFormatter; +import io.appform.ranger.discovery.bundle.id.formatter.IdFormatter; +import io.appform.ranger.discovery.bundle.id.formatter.IdFormatters; +import lombok.Builder; +import lombok.Getter; +import lombok.NonNull; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Getter +public class Domain { + public static final String DEFAULT_DOMAIN_NAME = "__DEFAULT_DOMAIN__"; + public static final Domain DEFAULT = new Domain(DEFAULT_DOMAIN_NAME, + List.of(), + new DefaultIdFormatter(), + TimeUnit.MILLISECONDS); + + private final String domain; + private final List constraints; + private final IdFormatter idFormatter; + private final CollisionChecker collisionChecker; + + + @Builder + public Domain(@NonNull String domain, + @NonNull List constraints, + IdFormatter idFormatter, + TimeUnit resolution) { + this.domain = domain; + this.constraints = constraints; + this.idFormatter = Objects.requireNonNullElse(idFormatter, IdFormatters.original()); + this.collisionChecker = new CollisionChecker(Objects.requireNonNullElse(resolution, TimeUnit.MILLISECONDS)); + } + +} diff --git a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/IdGenerator.java b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/IdGenerator.java index 3a0f9dbd..7e99e0f2 100644 --- a/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/IdGenerator.java +++ b/ranger-discovery-bundle/src/main/java/io/appform/ranger/discovery/bundle/id/IdGenerator.java @@ -18,6 +18,7 @@ package io.appform.ranger.discovery.bundle.id; import com.google.common.base.Preconditions; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import dev.failsafe.Failsafe; import dev.failsafe.FailsafeExecutor; @@ -26,6 +27,7 @@ import io.appform.ranger.discovery.bundle.id.formatter.IdFormatter; import io.appform.ranger.discovery.bundle.id.formatter.IdFormatters; import io.appform.ranger.discovery.bundle.id.request.IdGenerationRequest; +import lombok.NonNull; import lombok.Value; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -35,6 +37,8 @@ import java.security.SecureRandom; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** @@ -45,9 +49,13 @@ public class IdGenerator { private static final int MINIMUM_ID_LENGTH = 22; - private static final SecureRandom SECURE_RANDOM = new SecureRandom(Long.toBinaryString(System.currentTimeMillis()).getBytes()); + private static final SecureRandom SECURE_RANDOM = new SecureRandom(Long.toBinaryString(System.currentTimeMillis()) + .getBytes()); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormat.forPattern("yyMMddHHmmssSSS"); - private static final CollisionChecker COLLISION_CHECKER = new CollisionChecker(); + + private static final Map REGISTERED_DOMAINS = + new ConcurrentHashMap<>(Map.of(Domain.DEFAULT_DOMAIN_NAME, + Domain.DEFAULT)); private static final RetryPolicy RETRY_POLICY = RetryPolicy.builder() .withMaxAttempts(readRetryCount()) .handleIf(throwable -> true) @@ -55,9 +63,12 @@ public class IdGenerator { .handleResultIf(generationResult -> generationResult.getState() == IdValidationState.INVALID_RETRYABLE) .onRetry(event -> { val res = event.getLastResult(); - if(null != res && !res.getState().equals(IdValidationState.VALID)) { + if (null != res && !res.getState().equals(IdValidationState.VALID)) { val id = res.getId(); - COLLISION_CHECKER.free(id.getGeneratedDate().getTime(), id.getExponent()); + val collisionChecker = Strings.isNullOrEmpty(res.getDomain()) + ? Domain.DEFAULT.getCollisionChecker() + : REGISTERED_DOMAINS.get(res.getDomain()).getCollisionChecker(); + collisionChecker.free(id.getGeneratedDate().getTime(), id.getExponent()); } }) .build(); @@ -66,7 +77,6 @@ public class IdGenerator { private static final Pattern PATTERN = Pattern.compile("(.*)([0-9]{15})([0-9]{4})([0-9]{3})"); private static final List GLOBAL_CONSTRAINTS = new ArrayList<>(); - private static final Map> DOMAIN_SPECIFIC_CONSTRAINTS = new HashMap<>(); private static int nodeId; public static void initialize(int node) { @@ -75,21 +85,33 @@ public static void initialize(int node) { public static synchronized void cleanUp() { GLOBAL_CONSTRAINTS.clear(); - DOMAIN_SPECIFIC_CONSTRAINTS.clear(); + REGISTERED_DOMAINS.clear(); } - public static synchronized void initialize( - int node, List globalConstraints, Map> domainSpecificConstraints) { + int node, List globalConstraints, + Map> domainSpecificConstraints) { nodeId = node; - if(null != globalConstraints) { + if (null != globalConstraints) { IdGenerator.GLOBAL_CONSTRAINTS.addAll(globalConstraints); } - if(null != domainSpecificConstraints) { - IdGenerator.DOMAIN_SPECIFIC_CONSTRAINTS.putAll(domainSpecificConstraints); + + if (null != domainSpecificConstraints) { + domainSpecificConstraints + .forEach((domain, constraints) -> REGISTERED_DOMAINS.put(domain, Domain.builder() + .domain(domain) + .constraints(Objects.requireNonNullElse(constraints, List.of())) + .idFormatter(IdFormatters.original()) + .resolution(TimeUnit.MILLISECONDS) + .build())); } } + public static void registerDomain(Domain domain) { + REGISTERED_DOMAINS.put(domain.getDomain(), domain); + } + + public static synchronized void registerGlobalConstraints(IdValidationConstraint... constraints) { registerGlobalConstraints(ImmutableList.copyOf(constraints)); } @@ -109,10 +131,15 @@ public static synchronized void registerDomainSpecificConstraints( String domain, List validationConstraints) { Preconditions.checkArgument(null != validationConstraints && !validationConstraints.isEmpty()); - DOMAIN_SPECIFIC_CONSTRAINTS.computeIfAbsent(domain, key -> new ArrayList<>()) - .addAll(validationConstraints); + REGISTERED_DOMAINS.computeIfAbsent(domain, key -> Domain.builder() + .domain(domain) + .constraints(validationConstraints) + .idFormatter(IdFormatters.original()) + .resolution(TimeUnit.MILLISECONDS) + .build()); } + /** * Generate id with given prefix * @@ -120,12 +147,20 @@ public static synchronized void registerDomainSpecificConstraints( * @return Generated Id */ public static Id generate(String prefix) { - return generate(prefix, IdFormatters.original()); + return generate(prefix, IdFormatters.original(), Domain.DEFAULT.getCollisionChecker()); + } + + public static Id generate( + final String prefix, + final IdFormatter idFormatter) { + return generate(prefix, idFormatter, Domain.DEFAULT.getCollisionChecker()); } - public static Id generate(final String prefix, - final IdFormatter idFormatter) { - val idInfo = random(); + private static Id generate( + final String prefix, + final IdFormatter idFormatter, + final CollisionChecker collisionChecker) { + val idInfo = random(collisionChecker); val dateTime = new DateTime(idInfo.time); val id = String.format("%s%s", prefix, idFormatter.format(dateTime, nodeId, idInfo.exponent)); return Id.builder() @@ -145,8 +180,8 @@ public static Id generate(final String prefix, * @param domain Domain for constraint selection * @return Return generated id or empty if it was impossible to satisfy constraints and generate */ - public static Optional generateWithConstraints(String prefix, String domain) { - return generateWithConstraints(prefix, DOMAIN_SPECIFIC_CONSTRAINTS.getOrDefault(domain, Collections.emptyList()), true); + public static Optional generateWithConstraints(String prefix, @NonNull String domain) { + return generateWithConstraints(prefix, domain, true); } /** @@ -159,8 +194,8 @@ public static Optional generateWithConstraints(String prefix, String domain) * @param skipGlobal Skip global constrains and use only passed ones * @return Id if it could be generated */ - public static Optional generateWithConstraints(String prefix, String domain, boolean skipGlobal) { - return generateWithConstraints(prefix, DOMAIN_SPECIFIC_CONSTRAINTS.getOrDefault(domain, Collections.emptyList()), skipGlobal); + public static Optional generateWithConstraints(String prefix, @NonNull String domain, boolean skipGlobal) { + return generateWithConstraints(prefix, REGISTERED_DOMAINS.getOrDefault(domain, Domain.DEFAULT), skipGlobal); } /** @@ -172,7 +207,9 @@ public static Optional generateWithConstraints(String prefix, String domain, * @param inConstraints Constraints that need to be validated. * @return Id if it could be generated */ - public static Optional generateWithConstraints(String prefix, final List inConstraints) { + public static Optional generateWithConstraints( + String prefix, + final List inConstraints) { return generateWithConstraints(prefix, inConstraints, false); } @@ -191,11 +228,11 @@ public static Optional parse(final String idString) { val matcher = PATTERN.matcher(idString); if (matcher.find()) { return Optional.of(Id.builder() - .id(idString) - .node(Integer.parseInt(matcher.group(3))) - .exponent(Integer.parseInt(matcher.group(4))) - .generatedDate(DATE_TIME_FORMATTER.parseDateTime(matcher.group(2)).toDate()) - .build()); + .id(idString) + .node(Integer.parseInt(matcher.group(3))) + .exponent(Integer.parseInt(matcher.group(4))) + .generatedDate(DATE_TIME_FORMATTER.parseDateTime(matcher.group(2)).toDate()) + .build()); } return Optional.empty(); } @@ -215,32 +252,66 @@ public static Optional parse(final String idString) { * @param skipGlobal Skip global constrains and use only passed ones * @return Id if it could be generated */ - public static Optional generateWithConstraints(String prefix, final List inConstraints, boolean skipGlobal) { + public static Optional generateWithConstraints( + String prefix, + final List inConstraints, + boolean skipGlobal) { return generate(IdGenerationRequest.builder() - .prefix(prefix) - .constraints(inConstraints) - .skipGlobal(skipGlobal) - .idFormatter(IdFormatters.original()) - .build()); + .prefix(prefix) + .constraints(inConstraints) + .skipGlobal(skipGlobal) + .idFormatter(IdFormatters.original()) + .build()); + } + + /** + * Generate id that mathces all passed constraints. + * NOTE: There are performance implications for this. + * The evaluation of constraints will take it's toll on id generation rates. Tun rests to check speed. + * + * @param prefix String prefix + * @param skipGlobal Skip global constrains and use only passed ones + * @param domain Domain + * @return Id if it could be generated + */ + private static Optional generateWithConstraints( + String prefix, + final Domain domain, + boolean skipGlobal) { + return generate(IdGenerationRequest.builder() + .prefix(prefix) + .constraints(domain.getConstraints()) + .skipGlobal(skipGlobal) + .domain(domain.getDomain()) + .idFormatter(domain.getIdFormatter()) + .build()); } public static Optional generate(final IdGenerationRequest request) { return Optional.ofNullable(RETRIER.get( () -> { - Id id = generate(request.getPrefix(), request.getIdFormatter()); - return new GenerationResult(id, validateId(request.getConstraints(), id, request.isSkipGlobal())); + Id id = generate(request.getPrefix(), request.getIdFormatter(), + !Strings.isNullOrEmpty(request.getDomain()) + ? REGISTERED_DOMAINS.getOrDefault(request.getDomain(), Domain.DEFAULT) + .getCollisionChecker() + : Domain.DEFAULT.getCollisionChecker()); + return new GenerationResult(id, + validateId(request.getConstraints(), + id, + request.isSkipGlobal()), + request.getDomain()); })) .filter(generationResult -> generationResult.getState() == IdValidationState.VALID) .map(GenerationResult::getId); } - private static IdInfo random() { + private static IdInfo random(CollisionChecker collisionChecker) { int randomGen; long time; do { time = System.currentTimeMillis(); randomGen = SECURE_RANDOM.nextInt(Constants.MAX_ID_PER_MS); - } while (!COLLISION_CHECKER.check(time, randomGen)); + } while (!collisionChecker.check(time, randomGen)); return new IdInfo(randomGen, time); } @@ -310,5 +381,6 @@ public IdInfo(int exponent, long time) { private static class GenerationResult { Id id; IdValidationState state; + String domain; } } diff --git a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/id/IdGeneratorTest.java b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/id/IdGeneratorTest.java index 0089eaac..a1ef34eb 100644 --- a/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/id/IdGeneratorTest.java +++ b/ranger-discovery-bundle/src/test/java/io/appform/ranger/discovery/bundle/id/IdGeneratorTest.java @@ -26,12 +26,14 @@ import lombok.extern.slf4j.Slf4j; import lombok.val; import org.awaitility.Awaitility; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.time.*; import java.util.Collections; import java.util.Date; +import java.util.Map; import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.Executors; @@ -81,6 +83,11 @@ public Long call() { } } + @AfterEach + void cleanup() { + IdGenerator.cleanUp(); + } + @Test void testGenerate() { IdGenerator.initialize(23); @@ -113,6 +120,34 @@ void testGenerateBase36() { Assertions.assertEquals(18, id.length()); } + @Test + void testGenerateWithConstraints() { + IdGenerator.initialize(23, Collections.emptyList(), Map.of("TEST", Collections.emptyList())); + Optional id = IdGenerator.generateWithConstraints("TEST", "TEST"); + + Assertions.assertTrue(id.isPresent()); + Assertions.assertEquals(26, id.get().getId().length()); + + // Unregistered Domain + id = IdGenerator.generateWithConstraints("TEST", "TEST1"); + Assertions.assertTrue(id.isPresent()); + Assertions.assertEquals(26, id.get().getId().length()); + } + + @Test + void testGenerateWithConstraintsFailedWithLocalConstraint() { + IdGenerator.initialize(23, Collections.emptyList(), Map.of("TEST", Collections.singletonList(id -> false))); + Optional id = IdGenerator.generateWithConstraints("TEST", "TEST"); + Assertions.assertFalse(id.isPresent()); + } + + @Test + void testGenerateWithConstraintsFailedWithGlobalConstraint() { + IdGenerator.initialize(23, Collections.singletonList(id -> false), Map.of("TEST", Collections.singletonList(id -> false))); + Optional id = IdGenerator.generateWithConstraints("TEST", "TEST", false); + Assertions.assertFalse(id.isPresent()); + } + @Test void testGenerateWithConstraintsNoConstraint() { @@ -177,7 +212,7 @@ void testParseSuccess() { Assertions.assertEquals(247, id.getExponent()); Assertions.assertEquals(3972, id.getNode()); Assertions.assertEquals(generateDate(2020, 11, 25, 9, 59, 3, 64, ZoneId.systemDefault()), - id.getGeneratedDate()); + id.getGeneratedDate()); } @Test @@ -199,11 +234,11 @@ private Date generateDate(int year, int month, int day, int hour, int min, int s ZonedDateTime.of( LocalDateTime.of( year, month, day, hour, min, sec, Math.multiplyExact(ms, 1000000) - ), + ), zoneId - ) - ) - ); + ) + ) + ); }