From 75643d1a56582131113ed63fe23a60c4a3bdca24 Mon Sep 17 00:00:00 2001 From: bstorozhuk Date: Wed, 23 Nov 2016 22:07:38 +0200 Subject: [PATCH 1/7] Issue #12 Initial RateLimiter implementation and JavaDocs --- build.gradle | 5 +- .../javaslang/ratelimiter/RateLimiter.java | 184 +++++++++++++++ .../ratelimiter/RateLimiterConfig.java | 116 +++++++++ .../ratelimiter/RateLimiterRegistry.java | 41 ++++ .../ratelimiter/RequestNotPermitted.java | 17 ++ .../internal/InMemoryRateLimiterRegistry.java | 71 ++++++ .../SemaphoreBasedRateLimiterImpl.java | 157 +++++++++++++ .../ratelimiter/RateLimiterConfigTest.java | 91 ++++++++ .../ratelimiter/RateLimiterTest.java | 200 ++++++++++++++++ .../InMemoryRateLimiterRegistryTest.java | 118 ++++++++++ .../SemaphoreBasedRateLimiterImplTest.java | 221 ++++++++++++++++++ 11 files changed, 1220 insertions(+), 1 deletion(-) create mode 100644 src/main/java/javaslang/ratelimiter/RateLimiter.java create mode 100644 src/main/java/javaslang/ratelimiter/RateLimiterConfig.java create mode 100644 src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java create mode 100644 src/main/java/javaslang/ratelimiter/RequestNotPermitted.java create mode 100644 src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java create mode 100644 src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java create mode 100644 src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java create mode 100644 src/test/java/javaslang/ratelimiter/RateLimiterTest.java create mode 100644 src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java create mode 100644 src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java diff --git a/build.gradle b/build.gradle index cf834540fe..8220612338 100644 --- a/build.gradle +++ b/build.gradle @@ -62,13 +62,16 @@ jmh { dependencies { compile "io.javaslang:javaslang:2.0.4" compile "org.slf4j:slf4j-api:1.7.13" + testCompile "io.dropwizard.metrics:metrics-core:3.1.2" testCompile "junit:junit:4.11" testCompile "org.assertj:assertj-core:3.0.0" testCompile "ch.qos.logback:logback-classic:0.9.26" testCompile "io.dropwizard.metrics:metrics-healthchecks:3.1.2" - testCompile "org.mockito:mockito-all:1.10.19" + testCompile "org.mockito:mockito-core:1.10.19" testCompile "io.projectreactor:reactor-core:2.5.0.M2" + testCompile "com.jayway.awaitility:awaitility:1.7.0" + jmh "ch.qos.logback:logback-classic:0.9.26" } diff --git a/src/main/java/javaslang/ratelimiter/RateLimiter.java b/src/main/java/javaslang/ratelimiter/RateLimiter.java new file mode 100644 index 0000000000..1a5ff9c2b2 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RateLimiter.java @@ -0,0 +1,184 @@ +package javaslang.ratelimiter; + +import javaslang.control.Try; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A RateLimiter distributes permits at a configurable rate. {@link #getPermission} blocks if necessary + * until a permit is available, and then takes it. Once acquired, permits need not be released. + */ +public interface RateLimiter { + + /** + * Acquires a permission from this rate limiter, blocking until one is + * available. + *

+ *

If the current thread is {@linkplain Thread#interrupt interrupted} + * while waiting for a permit then it won't throw {@linkplain InterruptedException}, + * but its interrupt status will be set. + * + * @return {@code true} if a permit was acquired and {@code false} + * if waiting time elapsed before a permit was acquired + */ + boolean getPermission(Duration timeoutDuration); + + /** + * Get the name of this RateLimiter + * + * @return the name of this RateLimiter + */ + String getName(); + + /** + * Get the RateLimiterConfig of this RateLimiter. + * + * @return the RateLimiterConfig of this RateLimiter + */ + RateLimiterConfig getRateLimiterConfig(); + + /** + * Get the Metrics of this RateLimiter. + * + * @return the Metrics of this RateLimiter + */ + Metrics getMetrics(); + + interface Metrics { + /** + * Returns an estimate of the number of threads waiting for permission + * in this JVM process. + * + * @return estimate of the number of threads waiting for permission. + */ + int getNumberOfWaitingThreads(); + } + + /** + * Creates a supplier which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param supplier the original supplier + * @return a supplier which is restricted by a RateLimiter. + */ + static Try.CheckedSupplier decorateCheckedSupplier(RateLimiter rateLimiter, Try.CheckedSupplier supplier) { + Try.CheckedSupplier decoratedSupplier = () -> { + waitForPermission(rateLimiter); + T result = supplier.get(); + return result; + }; + return decoratedSupplier; + } + + /** + * Creates a runnable which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param runnable the original runnable + * @return a runnable which is restricted by a RateLimiter. + */ + static Try.CheckedRunnable decorateCheckedRunnable(RateLimiter rateLimiter, Try.CheckedRunnable runnable) { + + Try.CheckedRunnable decoratedRunnable = () -> { + waitForPermission(rateLimiter); + runnable.run(); + }; + return decoratedRunnable; + } + + /** + * Creates a function which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param function the original function + * @return a function which is restricted by a RateLimiter. + */ + static Try.CheckedFunction decorateCheckedFunction(RateLimiter rateLimiter, Try.CheckedFunction function) { + Try.CheckedFunction decoratedFunction = (T t) -> { + waitForPermission(rateLimiter); + R result = function.apply(t); + return result; + }; + return decoratedFunction; + } + + /** + * Creates a supplier which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param supplier the original supplier + * @return a supplier which is restricted by a RateLimiter. + */ + static Supplier decorateSupplier(RateLimiter rateLimiter, Supplier supplier) { + Supplier decoratedSupplier = () -> { + waitForPermission(rateLimiter); + T result = supplier.get(); + return result; + }; + return decoratedSupplier; + } + + /** + * Creates a consumer which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param consumer the original consumer + * @return a consumer which is restricted by a RateLimiter. + */ + static Consumer decorateConsumer(RateLimiter rateLimiter, Consumer consumer) { + Consumer decoratedConsumer = (T t) -> { + waitForPermission(rateLimiter); + consumer.accept(t); + }; + return decoratedConsumer; + } + + /** + * Creates a runnable which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param runnable the original runnable + * @return a runnable which is restricted by a RateLimiter. + */ + static Runnable decorateRunnable(RateLimiter rateLimiter, Runnable runnable) { + Runnable decoratedRunnable = () -> { + waitForPermission(rateLimiter); + runnable.run(); + }; + return decoratedRunnable; + } + + /** + * Creates a function which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param function the original function + * @return a function which is restricted by a RateLimiter. + */ + static Function decorateFunction(RateLimiter rateLimiter, Function function) { + Function decoratedFunction = (T t) -> { + waitForPermission(rateLimiter); + R result = function.apply(t); + return result; + }; + return decoratedFunction; + } + + /** + * Will wait for permission within default timeout duration. + * Throws {@link RequestNotPermitted} if waiting time elapsed before a permit was acquired. + * + * @param rateLimiter the RateLimiter to get permission from + */ + static void waitForPermission(final RateLimiter rateLimiter) { + RateLimiterConfig rateLimiterConfig = rateLimiter.getRateLimiterConfig(); + Duration timeoutDuration = rateLimiterConfig.getTimeoutDuration(); + boolean permission = rateLimiter.getPermission(timeoutDuration); + if (!permission) { + throw new RequestNotPermitted("Request not permitted for limiter: " + rateLimiter.getName()); + } + } +} diff --git a/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java new file mode 100644 index 0000000000..17e2ccd3f3 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java @@ -0,0 +1,116 @@ +package javaslang.ratelimiter; + +import static java.util.Objects.requireNonNull; + +import java.time.Duration; + +public class RateLimiterConfig { + private static final String TIMEOUT_DURATION_MUST_NOT_BE_NULL = "TimeoutDuration must not be null"; + private static final String LIMIT_REFRESH_PERIOD_MUST_NOT_BE_NULL = "LimitRefreshPeriod must not be null"; + + private static final Duration ACCEPTABLE_REFRESH_PERIOD = Duration.ofNanos(500L); // TODO: use jmh to find real one + + private final Duration timeoutDuration; + private final Duration limitRefreshPeriod; + private final int limitForPeriod; + + private RateLimiterConfig(final Duration timeoutDuration, final Duration limitRefreshPeriod, final int limitForPeriod) { + this.timeoutDuration = checkTimeoutDuration(timeoutDuration); + this.limitRefreshPeriod = checkLimitRefreshPeriod(limitRefreshPeriod); + this.limitForPeriod = checkLimitForPeriod(limitForPeriod); + } + + public static Builder builder() { + return new Builder(); + } + + public Duration getTimeoutDuration() { + return timeoutDuration; + } + + public Duration getLimitRefreshPeriod() { + return limitRefreshPeriod; + } + + public int getLimitForPeriod() { + return limitForPeriod; + } + + public static class Builder { + + private Duration timeoutDuration; + private Duration limitRefreshPeriod; + private int limitForPeriod; + + /** + * Builds a RateLimiterConfig + * + * @return the RateLimiterConfig + */ + public RateLimiterConfig build() { + return new RateLimiterConfig( + timeoutDuration, + limitRefreshPeriod, + limitForPeriod + ); + } + + /** + * Configures the default wait for permission duration. + * + * @param timeoutDuration the default wait for permission duration + * @return the RateLimiterConfig.Builder + */ + public Builder timeoutDuration(final Duration timeoutDuration) { + this.timeoutDuration = checkTimeoutDuration(timeoutDuration); + return this; + } + + /** + * Configures the period of limit refresh. + * After each period rate limiter sets its permissions + * count to {@link RateLimiterConfig#limitForPeriod} value. + * + * @param limitRefreshPeriod the period of limit refresh + * @return the RateLimiterConfig.Builder + */ + public Builder limitRefreshPeriod(final Duration limitRefreshPeriod) { + this.limitRefreshPeriod = checkLimitRefreshPeriod(limitRefreshPeriod); + return this; + } + + /** + * Configures the permissions limit for refresh period. + * Count of permissions available during one rate limiter period + * specified by {@link RateLimiterConfig#limitRefreshPeriod} value. + * + * @param limitForPeriod the permissions limit for refresh period + * @return the RateLimiterConfig.Builder + */ + public Builder limitForPeriod(final int limitForPeriod) { + this.limitForPeriod = checkLimitForPeriod(limitForPeriod); + return this; + } + + } + + private static Duration checkTimeoutDuration(final Duration timeoutDuration) { + return requireNonNull(timeoutDuration, TIMEOUT_DURATION_MUST_NOT_BE_NULL); + } + + private static Duration checkLimitRefreshPeriod(Duration limitRefreshPeriod) { + requireNonNull(limitRefreshPeriod, LIMIT_REFRESH_PERIOD_MUST_NOT_BE_NULL); + boolean refreshPeriodIsTooShort = limitRefreshPeriod.compareTo(ACCEPTABLE_REFRESH_PERIOD) < 0; + if (refreshPeriodIsTooShort) { + throw new IllegalArgumentException("LimitRefreshPeriod is too short"); + } + return limitRefreshPeriod; + } + + private static int checkLimitForPeriod(final int limitForPeriod) { + if (limitForPeriod < 1) { + throw new IllegalArgumentException("LimitForPeriod should be greater than 0"); + } + return limitForPeriod; + } +} diff --git a/src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java b/src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java new file mode 100644 index 0000000000..111db8374e --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java @@ -0,0 +1,41 @@ +package javaslang.ratelimiter; + +import javaslang.ratelimiter.internal.InMemoryRateLimiterRegistry; + +import java.util.function.Supplier; + +/** + * Manages all RateLimiter instances. + */ +public interface RateLimiterRegistry { + + /** + * Returns a managed {@link RateLimiter} or creates a new one with the default RateLimiter configuration. + * + * @param name the name of the RateLimiter + * @return The {@link RateLimiter} + */ + RateLimiter rateLimiter(String name); + + /** + * Returns a managed {@link RateLimiter} or creates a new one with a custom RateLimiter configuration. + * + * @param name the name of the RateLimiter + * @param rateLimiterConfig a custom RateLimiter configuration + * @return The {@link RateLimiter} + */ + RateLimiter rateLimiter(String name, RateLimiterConfig rateLimiterConfig); + + /** + * Returns a managed {@link RateLimiterConfig} or creates a new one with a custom RateLimiterConfig configuration. + * + * @param name the name of the RateLimiterConfig + * @param rateLimiterConfigSupplier a supplier of a custom RateLimiterConfig configuration + * @return The {@link RateLimiterConfig} + */ + RateLimiter rateLimiter(String name, Supplier rateLimiterConfigSupplier); + + static RateLimiterRegistry of(RateLimiterConfig defaultRateLimiterConfig) { + return new InMemoryRateLimiterRegistry(defaultRateLimiterConfig); + } +} diff --git a/src/main/java/javaslang/ratelimiter/RequestNotPermitted.java b/src/main/java/javaslang/ratelimiter/RequestNotPermitted.java new file mode 100644 index 0000000000..3c862d80c2 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RequestNotPermitted.java @@ -0,0 +1,17 @@ +package javaslang.ratelimiter; + +/** + * Exception that indicates that current thread was not able to acquire permission + * from {@link RateLimiter}. + */ +public class RequestNotPermitted extends RuntimeException { + + /** + * The constructor with a message. + * + * @param message The message. + */ + public RequestNotPermitted(final String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java b/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java new file mode 100644 index 0000000000..2a74e591af --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java @@ -0,0 +1,71 @@ +package javaslang.ratelimiter.internal; + +import static java.util.Objects.requireNonNull; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import javaslang.ratelimiter.RateLimiterRegistry; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * Backend RateLimiter manager. + * Constructs backend RateLimiters according to configuration values. + */ +public class InMemoryRateLimiterRegistry implements RateLimiterRegistry { + + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + private static final String CONFIG_MUST_NOT_BE_NULL = "Config must not be null"; + private static final String SUPPLIER_MUST_NOT_BE_NULL = "Supplier must not be null"; + + private final RateLimiterConfig defaultRateLimiterConfig; + /** + * The RateLimiters, indexed by name of the backend. + */ + private final Map rateLimiters; + + public InMemoryRateLimiterRegistry(final RateLimiterConfig defaultRateLimiterConfig) { + this.defaultRateLimiterConfig = requireNonNull(defaultRateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + rateLimiters = new ConcurrentHashMap<>(); + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiter rateLimiter(final String name) { + return rateLimiter(name, defaultRateLimiterConfig); + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiter rateLimiter(final String name, final RateLimiterConfig rateLimiterConfig) { + requireNonNull(name, NAME_MUST_NOT_BE_NULL); + requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + return rateLimiters.computeIfAbsent( + name, + limitName -> new SemaphoreBasedRateLimiterImpl(name, rateLimiterConfig) + ); + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiter rateLimiter(final String name, final Supplier rateLimiterConfigSupplier) { + requireNonNull(name, NAME_MUST_NOT_BE_NULL); + requireNonNull(rateLimiterConfigSupplier, SUPPLIER_MUST_NOT_BE_NULL); + return rateLimiters.computeIfAbsent( + name, + limitName -> { + RateLimiterConfig rateLimiterConfig = rateLimiterConfigSupplier.get(); + requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + return new SemaphoreBasedRateLimiterImpl(limitName, rateLimiterConfig); + } + ); + } +} diff --git a/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java b/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java new file mode 100644 index 0000000000..832c9af529 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java @@ -0,0 +1,157 @@ +package javaslang.ratelimiter.internal; + +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; + +import javaslang.control.Option; +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; + +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * A RateLimiter implementation that consists of {@link Semaphore} + * and scheduler that will refresh permissions + * after each {@link RateLimiterConfig#limitRefreshPeriod}. + */ +public class SemaphoreBasedRateLimiterImpl implements RateLimiter { + + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + private static final String CONFIG_MUST_NOT_BE_NULL = "RateLimiterConfig must not be null"; + + private final String name; + private final RateLimiterConfig rateLimiterConfig; + private final ScheduledExecutorService scheduler; + private final Semaphore semaphore; + private final SemaphoreBasedRateLimiterMetrics metrics; + + /** + * Creates a RateLimiter. + * + * @param name the name of the RateLimiter + * @param rateLimiterConfig The RateLimiter configuration. + */ + public SemaphoreBasedRateLimiterImpl(final String name, final RateLimiterConfig rateLimiterConfig) { + this(name, rateLimiterConfig, null); + } + + /** + * Creates a RateLimiter. + * + * @param name the name of the RateLimiter + * @param rateLimiterConfig The RateLimiter configuration. + * @param scheduler executor that will refresh permissions + */ + public SemaphoreBasedRateLimiterImpl(String name, RateLimiterConfig rateLimiterConfig, + ScheduledExecutorService scheduler) { + this.name = requireNonNull(name, NAME_MUST_NOT_BE_NULL); + this.rateLimiterConfig = requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + + this.scheduler = Option.of(scheduler).getOrElse(this::configureScheduler); + this.semaphore = new Semaphore(this.rateLimiterConfig.getLimitForPeriod(), true); + this.metrics = this.new SemaphoreBasedRateLimiterMetrics(); + + scheduleLimitRefresh(); + } + + private ScheduledExecutorService configureScheduler() { + ThreadFactory threadFactory = target -> { + Thread thread = new Thread(target, "SchedulerForSemaphoreBasedRateLimiterImpl-" + name); + thread.setDaemon(true); + return thread; + }; + return newSingleThreadScheduledExecutor(threadFactory); + } + + private void scheduleLimitRefresh() { + scheduler.scheduleAtFixedRate( + this::refreshLimit, + this.rateLimiterConfig.getLimitRefreshPeriod().toNanos(), + this.rateLimiterConfig.getLimitRefreshPeriod().toNanos(), + TimeUnit.NANOSECONDS + ); + } + + void refreshLimit() { + semaphore.release(this.rateLimiterConfig.getLimitForPeriod()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getPermission(final Duration timeoutDuration) { + try { + boolean success = semaphore.tryAcquire(timeoutDuration.toNanos(), TimeUnit.NANOSECONDS); + return success; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return this.name; + } + + /** + * {@inheritDoc} + */ + @Override + public Metrics getMetrics() { + return this.metrics; + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiterConfig getRateLimiterConfig() { + return this.rateLimiterConfig; + } + + /** + * Get the enhanced Metrics with some implementation specific details. + * + * @return the detailed metrics + */ + public SemaphoreBasedRateLimiterMetrics getDetailedMetrics() { + return this.metrics; + } + + /** + * Enhanced {@link Metrics} with some implementation specific details + */ + public final class SemaphoreBasedRateLimiterMetrics implements Metrics { + private SemaphoreBasedRateLimiterMetrics() { + } + + /** + * Returns the current number of permits available in this request limit + * until the next refresh. + *

+ *

This method is typically used for debugging and testing purposes. + * + * @return the number of permits available in this rate limiter until the next refresh. + */ + public int getAvailablePermits() { + return semaphore.availablePermits(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumberOfWaitingThreads() { + return semaphore.getQueueLength(); + } + } +} diff --git a/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java b/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java new file mode 100644 index 0000000000..d0b0532855 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java @@ -0,0 +1,91 @@ +package javaslang.ratelimiter; + +import static org.assertj.core.api.BDDAssertions.then; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.time.Duration; + + +public class RateLimiterConfigTest { + + private static final int LIMIT = 50; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofNanos(500); + private static final String TIMEOUT_DURATION_MUST_NOT_BE_NULL = "TimeoutDuration must not be null"; + private static final String REFRESH_PERIOD_MUST_NOT_BE_NULL = "RefreshPeriod must not be null"; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + + @Test + public void builderPositive() throws Exception { + RateLimiterConfig config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + + then(config.getLimitForPeriod()).isEqualTo(LIMIT); + then(config.getLimitRefreshPeriod()).isEqualTo(REFRESH_PERIOD); + then(config.getTimeoutDuration()).isEqualTo(TIMEOUT); + } + + @Test + public void builderTimeoutIsNull() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(TIMEOUT_DURATION_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .timeoutDuration(null); + } + + @Test + public void builderTimeoutEmpty() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(TIMEOUT_DURATION_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void builderRefreshPeriodIsNull() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(REFRESH_PERIOD_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .limitRefreshPeriod(null); + } + + @Test + public void builderRefreshPeriodEmpty() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(REFRESH_PERIOD_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void builderRefreshPeriodTooShort() throws Exception { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("RefreshPeriod is too short"); + RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(Duration.ofNanos(499L)) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void builderLimitIsLessThanOne() throws Exception { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("LimitForPeriod should be greater than 0"); + RateLimiterConfig.builder() + .limitForPeriod(0); + } +} diff --git a/src/test/java/javaslang/ratelimiter/RateLimiterTest.java b/src/test/java/javaslang/ratelimiter/RateLimiterTest.java new file mode 100644 index 0000000000..b38f52da36 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/RateLimiterTest.java @@ -0,0 +1,200 @@ +package javaslang.ratelimiter; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javaslang.control.Try; +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + + +@SuppressWarnings("unchecked") +public class RateLimiterTest { + + private static final int LIMIT = 50; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofNanos(500); + + private RateLimiterConfig config; + private RateLimiter limit; + + @Before + public void init() { + config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + limit = mock(RateLimiter.class); + when(limit.getRateLimiterConfig()) + .thenReturn(config); + } + + @Test + public void decorateCheckedSupplier() throws Throwable { + Try.CheckedSupplier supplier = mock(Try.CheckedSupplier.class); + Try.CheckedSupplier decorated = RateLimiter.decorateCheckedSupplier(limit, supplier); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedSupplierResult = Try.of(decorated); + then(decoratedSupplierResult.isFailure()).isTrue(); + then(decoratedSupplierResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(supplier, never()).get(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondSupplierResult = Try.of(decorated); + then(secondSupplierResult.isSuccess()).isTrue(); + verify(supplier, times(1)).get(); + } + + @Test + public void decorateCheckedRunnable() throws Throwable { + Try.CheckedRunnable runnable = mock(Try.CheckedRunnable.class); + Try.CheckedRunnable decorated = RateLimiter.decorateCheckedRunnable(limit, runnable); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedRunnableResult = Try.run(decorated); + then(decoratedRunnableResult.isFailure()).isTrue(); + then(decoratedRunnableResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(runnable, never()).run(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondRunnableResult = Try.run(decorated); + then(secondRunnableResult.isSuccess()).isTrue(); + verify(runnable, times(1)).run(); + } + + @Test + public void decorateCheckedFunction() throws Throwable { + Try.CheckedFunction function = mock(Try.CheckedFunction.class); + Try.CheckedFunction decorated = RateLimiter.decorateCheckedFunction(limit, function); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedFunctionResult = Try.success(1).mapTry(decorated); + then(decoratedFunctionResult.isFailure()).isTrue(); + then(decoratedFunctionResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(function, never()).apply(any()); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondFunctionResult = Try.success(1).mapTry(decorated); + then(secondFunctionResult.isSuccess()).isTrue(); + verify(function, times(1)).apply(1); + } + + @Test + public void decorateSupplier() throws Exception { + Supplier supplier = mock(Supplier.class); + Supplier decorated = RateLimiter.decorateSupplier(limit, supplier); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedSupplierResult = Try.success(decorated).map(Supplier::get); + then(decoratedSupplierResult.isFailure()).isTrue(); + then(decoratedSupplierResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(supplier, never()).get(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondSupplierResult = Try.success(decorated).map(Supplier::get); + then(secondSupplierResult.isSuccess()).isTrue(); + verify(supplier, times(1)).get(); + } + + @Test + public void decorateConsumer() throws Exception { + Consumer consumer = mock(Consumer.class); + Consumer decorated = RateLimiter.decorateConsumer(limit, consumer); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedConsumerResult = Try.success(1).andThen(decorated); + then(decoratedConsumerResult.isFailure()).isTrue(); + then(decoratedConsumerResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(consumer, never()).accept(any()); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondConsumerResult = Try.success(1).andThen(decorated); + then(secondConsumerResult.isSuccess()).isTrue(); + verify(consumer, times(1)).accept(1); + } + + @Test + public void decorateRunnable() throws Exception { + Runnable runnable = mock(Runnable.class); + Runnable decorated = RateLimiter.decorateRunnable(limit, runnable); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedRunnableResult = Try.success(decorated).andThen(Runnable::run); + then(decoratedRunnableResult.isFailure()).isTrue(); + then(decoratedRunnableResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(runnable, never()).run(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondRunnableResult = Try.success(decorated).andThen(Runnable::run); + then(secondRunnableResult.isSuccess()).isTrue(); + verify(runnable, times(1)).run(); + } + + @Test + public void decorateFunction() throws Exception { + Function function = mock(Function.class); + Function decorated = RateLimiter.decorateFunction(limit, function); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedFunctionResult = Try.success(1).map(decorated); + then(decoratedFunctionResult.isFailure()).isTrue(); + then(decoratedFunctionResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(function, never()).apply(any()); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondFunctionResult = Try.success(1).map(decorated); + then(secondFunctionResult.isSuccess()).isTrue(); + verify(function, times(1)).apply(1); + } + + @Test + public void waitForPermissionWithOne() throws Exception { + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + RateLimiter.waitForPermission(limit); + verify(limit, times(1)) + .getPermission(config.getTimeoutDuration()); + } + + @Test(expected = RequestNotPermitted.class) + public void waitForPermissionWithoutOne() throws Exception { + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + RateLimiter.waitForPermission(limit); + verify(limit, times(1)) + .getPermission(config.getTimeoutDuration()); + } +} \ No newline at end of file diff --git a/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java new file mode 100644 index 0000000000..ba8ca819a9 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java @@ -0,0 +1,118 @@ +package javaslang.ratelimiter.internal; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import javaslang.ratelimiter.RateLimiterRegistry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.time.Duration; +import java.util.function.Supplier; + + +public class InMemoryRateLimiterRegistryTest { + + private static final int LIMIT = 50; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofNanos(500); + private static final String CONFIG_MUST_NOT_BE_NULL = "Config must not be null"; + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + @Rule + public ExpectedException exception = ExpectedException.none(); + private RateLimiterConfig config; + + @Before + public void init() { + config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void rateLimiterPositive() throws Exception { + RateLimiterRegistry registry = RateLimiterRegistry.of(config); + RateLimiter firstRateLimiter = registry.rateLimiter("test"); + RateLimiter anotherLimit = registry.rateLimiter("test1"); + RateLimiter sameAsFirst = registry.rateLimiter("test"); + + then(firstRateLimiter).isEqualTo(sameAsFirst); + then(firstRateLimiter).isNotEqualTo(anotherLimit); + } + + @Test + public void rateLimiterPositiveWithSupplier() throws Exception { + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + Supplier rateLimiterConfigSupplier = mock(Supplier.class); + when(rateLimiterConfigSupplier.get()) + .thenReturn(config); + + RateLimiter firstRateLimiter = registry.rateLimiter("test", rateLimiterConfigSupplier); + verify(rateLimiterConfigSupplier, times(1)).get(); + RateLimiter sameAsFirst = registry.rateLimiter("test", rateLimiterConfigSupplier); + verify(rateLimiterConfigSupplier, times(1)).get(); + RateLimiter anotherLimit = registry.rateLimiter("test1", rateLimiterConfigSupplier); + verify(rateLimiterConfigSupplier, times(2)).get(); + + then(firstRateLimiter).isEqualTo(sameAsFirst); + then(firstRateLimiter).isNotEqualTo(anotherLimit); + } + + @Test + public void rateLimiterConfigIsNull() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + new InMemoryRateLimiterRegistry(null); + } + + @Test + public void rateLimiterNewWithNullName() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + registry.rateLimiter(null); + } + + @Test + public void rateLimiterNewWithNullNonDefaultConfig() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + RateLimiterConfig rateLimiterConfig = null; + registry.rateLimiter("name", rateLimiterConfig); + } + + @Test + public void rateLimiterNewWithNullNameAndNonDefaultConfig() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + registry.rateLimiter(null, config); + } + + @Test + public void rateLimiterNewWithNullNameAndConfigSupplier() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + registry.rateLimiter(null, () -> config); + } + + @Test + public void rateLimiterNewWithNullConfigSupplier() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage("Supplier must not be null"); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + Supplier rateLimiterConfigSupplier = null; + registry.rateLimiter("name", rateLimiterConfigSupplier); + } +} \ No newline at end of file diff --git a/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java b/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java new file mode 100644 index 0000000000..24da434548 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java @@ -0,0 +1,221 @@ +package javaslang.ratelimiter.internal; + +import static com.jayway.awaitility.Awaitility.await; +import static com.jayway.awaitility.Duration.FIVE_HUNDRED_MILLISECONDS; +import static java.lang.Thread.State.RUNNABLE; +import static java.lang.Thread.State.TERMINATED; +import static java.lang.Thread.State.TIMED_WAITING; +import static java.time.Duration.ZERO; +import static javaslang.control.Try.run; +import static org.assertj.core.api.BDDAssertions.then; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.jayway.awaitility.core.ConditionFactory; +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; + +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + + +public class SemaphoreBasedRateLimiterImplTest { + + private static final int LIMIT = 2; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofMillis(100); + private static final String CONFIG_MUST_NOT_BE_NULL = "RateLimiterConfig must not be null"; + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + private static final Object O = new Object(); + @Rule + public ExpectedException exception = ExpectedException.none(); + private RateLimiterConfig config; + + private static ConditionFactory awaitImpatiently() { + return await() + .pollDelay(1, TimeUnit.MICROSECONDS) + .pollInterval(2, TimeUnit.MILLISECONDS); + } + + @Before + public void init() { + config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void rateLimiterCreationWithProvidedScheduler() throws Exception { + ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); + RateLimiterConfig configSpy = spy(config); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + + ArgumentCaptor refreshLimitRunnableCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(scheduledExecutorService) + .scheduleAtFixedRate( + refreshLimitRunnableCaptor.capture(), + eq(config.getLimitRefreshPeriod().toNanos()), + eq(config.getLimitRefreshPeriod().toNanos()), + eq(TimeUnit.NANOSECONDS) + ); + + Runnable refreshLimitRunnable = refreshLimitRunnableCaptor.getValue(); + + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isFalse(); + + Thread.sleep(REFRESH_PERIOD.toMillis() * 2); + verify(configSpy, times(1)).getLimitForPeriod(); + + refreshLimitRunnable.run(); + + verify(configSpy, times(2)).getLimitForPeriod(); + + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isFalse(); + } + + @Test + public void rateLimiterCreationWithDefaultScheduler() throws Exception { + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config); + awaitImpatiently().atMost(FIVE_HUNDRED_MILLISECONDS) + .until(() -> limit.getPermission(ZERO), equalTo(false)); + awaitImpatiently().atMost(110, TimeUnit.MILLISECONDS) + .until(() -> limit.getPermission(ZERO), equalTo(true)); + } + + @Test + public void getPermissionAndMetrics() throws Exception { + ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); + RateLimiterConfig configSpy = spy(config); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + SemaphoreBasedRateLimiterImpl.SemaphoreBasedRateLimiterMetrics detailedMetrics = limit.getDetailedMetrics(); + + SynchronousQueue synchronousQueue = new SynchronousQueue(); + Thread thread = new Thread(() -> { + run(() -> { + for (int i = 0; i < LIMIT; i++) { + System.out.println("SLAVE -> WAITING FOR MASTER"); + synchronousQueue.put(O); + System.out.println("SLAVE -> HAVE COMMAND FROM MASTER"); + limit.getPermission(TIMEOUT); + System.out.println("SLAVE -> ACQUIRED PERMISSION"); + } + System.out.println("SLAVE -> LAST PERMISSION ACQUIRE"); + limit.getPermission(TIMEOUT); + System.out.println("SLAVE -> I'M DONE"); + }); + }); + thread.setDaemon(true); + thread.start(); + + for (int i = 0; i < LIMIT; i++) { + System.out.println("MASTER -> TAKE PERMISSION"); + synchronousQueue.take(); + } + + System.out.println("MASTER -> CHECK IF SLAVE IS WAITING FOR PERMISSION"); + awaitImpatiently() + .atMost(100, TimeUnit.MILLISECONDS).until(detailedMetrics::getAvailablePermits, equalTo(0)); + System.out.println("MASTER -> SLAVE CONSUMED ALL PERMISSIONS"); + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TIMED_WAITING)); + then(detailedMetrics.getAvailablePermits()).isEqualTo(0); + System.out.println("MASTER -> SLAVE WAS WAITING"); + + limit.refreshLimit(); + awaitImpatiently() + .atMost(100, TimeUnit.MILLISECONDS).until(detailedMetrics::getAvailablePermits, equalTo(1)); + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TERMINATED)); + then(detailedMetrics.getAvailablePermits()).isEqualTo(1); + } + + @Test + public void getPermissionInterruption() throws Exception { + ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); + RateLimiterConfig configSpy = spy(config); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + limit.getPermission(ZERO); + limit.getPermission(ZERO); + + Thread thread = new Thread(() -> { + limit.getPermission(TIMEOUT); + while (true) { + Function.identity().apply(1); + } + }); + thread.setDaemon(true); + thread.start(); + + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TIMED_WAITING)); + + thread.interrupt(); + + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(RUNNABLE)); + then(thread.isInterrupted()).isTrue(); + } + + @Test + public void getName() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + then(limit.getName()).isEqualTo("test"); + } + + @Test + public void getMetrics() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + RateLimiter.Metrics metrics = limit.getMetrics(); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + } + + @Test + public void getRateLimiterConfig() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + then(limit.getRateLimiterConfig()).isEqualTo(config); + } + + @Test + public void getDetailedMetrics() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + SemaphoreBasedRateLimiterImpl.SemaphoreBasedRateLimiterMetrics metrics = limit.getDetailedMetrics(); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + then(metrics.getAvailablePermits()).isEqualTo(2); + } + + @Test + public void constructionWithNullName() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + new SemaphoreBasedRateLimiterImpl(null, config, null); + } + + @Test + public void constructionWithNullConfig() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + new SemaphoreBasedRateLimiterImpl("test", null, null); + } +} \ No newline at end of file From c260629050afbad50eae55e5e79f99d480db6e1e Mon Sep 17 00:00:00 2001 From: bstorozhuk Date: Wed, 23 Nov 2016 22:07:38 +0200 Subject: [PATCH 2/7] Issue #12 Initial RateLimiter implementation and JavaDocs --- build.gradle | 5 +- .../javaslang/ratelimiter/RateLimiter.java | 184 +++++++++++++++ .../ratelimiter/RateLimiterConfig.java | 116 +++++++++ .../ratelimiter/RateLimiterRegistry.java | 41 ++++ .../ratelimiter/RequestNotPermitted.java | 17 ++ .../internal/InMemoryRateLimiterRegistry.java | 71 ++++++ .../SemaphoreBasedRateLimiterImpl.java | 157 +++++++++++++ .../ratelimiter/RateLimiterConfigTest.java | 91 ++++++++ .../ratelimiter/RateLimiterTest.java | 200 ++++++++++++++++ .../InMemoryRateLimiterRegistryTest.java | 118 ++++++++++ .../SemaphoreBasedRateLimiterImplTest.java | 221 ++++++++++++++++++ 11 files changed, 1220 insertions(+), 1 deletion(-) create mode 100644 src/main/java/javaslang/ratelimiter/RateLimiter.java create mode 100644 src/main/java/javaslang/ratelimiter/RateLimiterConfig.java create mode 100644 src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java create mode 100644 src/main/java/javaslang/ratelimiter/RequestNotPermitted.java create mode 100644 src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java create mode 100644 src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java create mode 100644 src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java create mode 100644 src/test/java/javaslang/ratelimiter/RateLimiterTest.java create mode 100644 src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java create mode 100644 src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java diff --git a/build.gradle b/build.gradle index cf834540fe..8220612338 100644 --- a/build.gradle +++ b/build.gradle @@ -62,13 +62,16 @@ jmh { dependencies { compile "io.javaslang:javaslang:2.0.4" compile "org.slf4j:slf4j-api:1.7.13" + testCompile "io.dropwizard.metrics:metrics-core:3.1.2" testCompile "junit:junit:4.11" testCompile "org.assertj:assertj-core:3.0.0" testCompile "ch.qos.logback:logback-classic:0.9.26" testCompile "io.dropwizard.metrics:metrics-healthchecks:3.1.2" - testCompile "org.mockito:mockito-all:1.10.19" + testCompile "org.mockito:mockito-core:1.10.19" testCompile "io.projectreactor:reactor-core:2.5.0.M2" + testCompile "com.jayway.awaitility:awaitility:1.7.0" + jmh "ch.qos.logback:logback-classic:0.9.26" } diff --git a/src/main/java/javaslang/ratelimiter/RateLimiter.java b/src/main/java/javaslang/ratelimiter/RateLimiter.java new file mode 100644 index 0000000000..1a5ff9c2b2 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RateLimiter.java @@ -0,0 +1,184 @@ +package javaslang.ratelimiter; + +import javaslang.control.Try; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * A RateLimiter distributes permits at a configurable rate. {@link #getPermission} blocks if necessary + * until a permit is available, and then takes it. Once acquired, permits need not be released. + */ +public interface RateLimiter { + + /** + * Acquires a permission from this rate limiter, blocking until one is + * available. + *

+ *

If the current thread is {@linkplain Thread#interrupt interrupted} + * while waiting for a permit then it won't throw {@linkplain InterruptedException}, + * but its interrupt status will be set. + * + * @return {@code true} if a permit was acquired and {@code false} + * if waiting time elapsed before a permit was acquired + */ + boolean getPermission(Duration timeoutDuration); + + /** + * Get the name of this RateLimiter + * + * @return the name of this RateLimiter + */ + String getName(); + + /** + * Get the RateLimiterConfig of this RateLimiter. + * + * @return the RateLimiterConfig of this RateLimiter + */ + RateLimiterConfig getRateLimiterConfig(); + + /** + * Get the Metrics of this RateLimiter. + * + * @return the Metrics of this RateLimiter + */ + Metrics getMetrics(); + + interface Metrics { + /** + * Returns an estimate of the number of threads waiting for permission + * in this JVM process. + * + * @return estimate of the number of threads waiting for permission. + */ + int getNumberOfWaitingThreads(); + } + + /** + * Creates a supplier which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param supplier the original supplier + * @return a supplier which is restricted by a RateLimiter. + */ + static Try.CheckedSupplier decorateCheckedSupplier(RateLimiter rateLimiter, Try.CheckedSupplier supplier) { + Try.CheckedSupplier decoratedSupplier = () -> { + waitForPermission(rateLimiter); + T result = supplier.get(); + return result; + }; + return decoratedSupplier; + } + + /** + * Creates a runnable which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param runnable the original runnable + * @return a runnable which is restricted by a RateLimiter. + */ + static Try.CheckedRunnable decorateCheckedRunnable(RateLimiter rateLimiter, Try.CheckedRunnable runnable) { + + Try.CheckedRunnable decoratedRunnable = () -> { + waitForPermission(rateLimiter); + runnable.run(); + }; + return decoratedRunnable; + } + + /** + * Creates a function which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param function the original function + * @return a function which is restricted by a RateLimiter. + */ + static Try.CheckedFunction decorateCheckedFunction(RateLimiter rateLimiter, Try.CheckedFunction function) { + Try.CheckedFunction decoratedFunction = (T t) -> { + waitForPermission(rateLimiter); + R result = function.apply(t); + return result; + }; + return decoratedFunction; + } + + /** + * Creates a supplier which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param supplier the original supplier + * @return a supplier which is restricted by a RateLimiter. + */ + static Supplier decorateSupplier(RateLimiter rateLimiter, Supplier supplier) { + Supplier decoratedSupplier = () -> { + waitForPermission(rateLimiter); + T result = supplier.get(); + return result; + }; + return decoratedSupplier; + } + + /** + * Creates a consumer which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param consumer the original consumer + * @return a consumer which is restricted by a RateLimiter. + */ + static Consumer decorateConsumer(RateLimiter rateLimiter, Consumer consumer) { + Consumer decoratedConsumer = (T t) -> { + waitForPermission(rateLimiter); + consumer.accept(t); + }; + return decoratedConsumer; + } + + /** + * Creates a runnable which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param runnable the original runnable + * @return a runnable which is restricted by a RateLimiter. + */ + static Runnable decorateRunnable(RateLimiter rateLimiter, Runnable runnable) { + Runnable decoratedRunnable = () -> { + waitForPermission(rateLimiter); + runnable.run(); + }; + return decoratedRunnable; + } + + /** + * Creates a function which is restricted by a RateLimiter. + * + * @param rateLimiter the RateLimiter + * @param function the original function + * @return a function which is restricted by a RateLimiter. + */ + static Function decorateFunction(RateLimiter rateLimiter, Function function) { + Function decoratedFunction = (T t) -> { + waitForPermission(rateLimiter); + R result = function.apply(t); + return result; + }; + return decoratedFunction; + } + + /** + * Will wait for permission within default timeout duration. + * Throws {@link RequestNotPermitted} if waiting time elapsed before a permit was acquired. + * + * @param rateLimiter the RateLimiter to get permission from + */ + static void waitForPermission(final RateLimiter rateLimiter) { + RateLimiterConfig rateLimiterConfig = rateLimiter.getRateLimiterConfig(); + Duration timeoutDuration = rateLimiterConfig.getTimeoutDuration(); + boolean permission = rateLimiter.getPermission(timeoutDuration); + if (!permission) { + throw new RequestNotPermitted("Request not permitted for limiter: " + rateLimiter.getName()); + } + } +} diff --git a/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java new file mode 100644 index 0000000000..17e2ccd3f3 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java @@ -0,0 +1,116 @@ +package javaslang.ratelimiter; + +import static java.util.Objects.requireNonNull; + +import java.time.Duration; + +public class RateLimiterConfig { + private static final String TIMEOUT_DURATION_MUST_NOT_BE_NULL = "TimeoutDuration must not be null"; + private static final String LIMIT_REFRESH_PERIOD_MUST_NOT_BE_NULL = "LimitRefreshPeriod must not be null"; + + private static final Duration ACCEPTABLE_REFRESH_PERIOD = Duration.ofNanos(500L); // TODO: use jmh to find real one + + private final Duration timeoutDuration; + private final Duration limitRefreshPeriod; + private final int limitForPeriod; + + private RateLimiterConfig(final Duration timeoutDuration, final Duration limitRefreshPeriod, final int limitForPeriod) { + this.timeoutDuration = checkTimeoutDuration(timeoutDuration); + this.limitRefreshPeriod = checkLimitRefreshPeriod(limitRefreshPeriod); + this.limitForPeriod = checkLimitForPeriod(limitForPeriod); + } + + public static Builder builder() { + return new Builder(); + } + + public Duration getTimeoutDuration() { + return timeoutDuration; + } + + public Duration getLimitRefreshPeriod() { + return limitRefreshPeriod; + } + + public int getLimitForPeriod() { + return limitForPeriod; + } + + public static class Builder { + + private Duration timeoutDuration; + private Duration limitRefreshPeriod; + private int limitForPeriod; + + /** + * Builds a RateLimiterConfig + * + * @return the RateLimiterConfig + */ + public RateLimiterConfig build() { + return new RateLimiterConfig( + timeoutDuration, + limitRefreshPeriod, + limitForPeriod + ); + } + + /** + * Configures the default wait for permission duration. + * + * @param timeoutDuration the default wait for permission duration + * @return the RateLimiterConfig.Builder + */ + public Builder timeoutDuration(final Duration timeoutDuration) { + this.timeoutDuration = checkTimeoutDuration(timeoutDuration); + return this; + } + + /** + * Configures the period of limit refresh. + * After each period rate limiter sets its permissions + * count to {@link RateLimiterConfig#limitForPeriod} value. + * + * @param limitRefreshPeriod the period of limit refresh + * @return the RateLimiterConfig.Builder + */ + public Builder limitRefreshPeriod(final Duration limitRefreshPeriod) { + this.limitRefreshPeriod = checkLimitRefreshPeriod(limitRefreshPeriod); + return this; + } + + /** + * Configures the permissions limit for refresh period. + * Count of permissions available during one rate limiter period + * specified by {@link RateLimiterConfig#limitRefreshPeriod} value. + * + * @param limitForPeriod the permissions limit for refresh period + * @return the RateLimiterConfig.Builder + */ + public Builder limitForPeriod(final int limitForPeriod) { + this.limitForPeriod = checkLimitForPeriod(limitForPeriod); + return this; + } + + } + + private static Duration checkTimeoutDuration(final Duration timeoutDuration) { + return requireNonNull(timeoutDuration, TIMEOUT_DURATION_MUST_NOT_BE_NULL); + } + + private static Duration checkLimitRefreshPeriod(Duration limitRefreshPeriod) { + requireNonNull(limitRefreshPeriod, LIMIT_REFRESH_PERIOD_MUST_NOT_BE_NULL); + boolean refreshPeriodIsTooShort = limitRefreshPeriod.compareTo(ACCEPTABLE_REFRESH_PERIOD) < 0; + if (refreshPeriodIsTooShort) { + throw new IllegalArgumentException("LimitRefreshPeriod is too short"); + } + return limitRefreshPeriod; + } + + private static int checkLimitForPeriod(final int limitForPeriod) { + if (limitForPeriod < 1) { + throw new IllegalArgumentException("LimitForPeriod should be greater than 0"); + } + return limitForPeriod; + } +} diff --git a/src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java b/src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java new file mode 100644 index 0000000000..111db8374e --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RateLimiterRegistry.java @@ -0,0 +1,41 @@ +package javaslang.ratelimiter; + +import javaslang.ratelimiter.internal.InMemoryRateLimiterRegistry; + +import java.util.function.Supplier; + +/** + * Manages all RateLimiter instances. + */ +public interface RateLimiterRegistry { + + /** + * Returns a managed {@link RateLimiter} or creates a new one with the default RateLimiter configuration. + * + * @param name the name of the RateLimiter + * @return The {@link RateLimiter} + */ + RateLimiter rateLimiter(String name); + + /** + * Returns a managed {@link RateLimiter} or creates a new one with a custom RateLimiter configuration. + * + * @param name the name of the RateLimiter + * @param rateLimiterConfig a custom RateLimiter configuration + * @return The {@link RateLimiter} + */ + RateLimiter rateLimiter(String name, RateLimiterConfig rateLimiterConfig); + + /** + * Returns a managed {@link RateLimiterConfig} or creates a new one with a custom RateLimiterConfig configuration. + * + * @param name the name of the RateLimiterConfig + * @param rateLimiterConfigSupplier a supplier of a custom RateLimiterConfig configuration + * @return The {@link RateLimiterConfig} + */ + RateLimiter rateLimiter(String name, Supplier rateLimiterConfigSupplier); + + static RateLimiterRegistry of(RateLimiterConfig defaultRateLimiterConfig) { + return new InMemoryRateLimiterRegistry(defaultRateLimiterConfig); + } +} diff --git a/src/main/java/javaslang/ratelimiter/RequestNotPermitted.java b/src/main/java/javaslang/ratelimiter/RequestNotPermitted.java new file mode 100644 index 0000000000..3c862d80c2 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/RequestNotPermitted.java @@ -0,0 +1,17 @@ +package javaslang.ratelimiter; + +/** + * Exception that indicates that current thread was not able to acquire permission + * from {@link RateLimiter}. + */ +public class RequestNotPermitted extends RuntimeException { + + /** + * The constructor with a message. + * + * @param message The message. + */ + public RequestNotPermitted(final String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java b/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java new file mode 100644 index 0000000000..2a74e591af --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java @@ -0,0 +1,71 @@ +package javaslang.ratelimiter.internal; + +import static java.util.Objects.requireNonNull; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import javaslang.ratelimiter.RateLimiterRegistry; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +/** + * Backend RateLimiter manager. + * Constructs backend RateLimiters according to configuration values. + */ +public class InMemoryRateLimiterRegistry implements RateLimiterRegistry { + + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + private static final String CONFIG_MUST_NOT_BE_NULL = "Config must not be null"; + private static final String SUPPLIER_MUST_NOT_BE_NULL = "Supplier must not be null"; + + private final RateLimiterConfig defaultRateLimiterConfig; + /** + * The RateLimiters, indexed by name of the backend. + */ + private final Map rateLimiters; + + public InMemoryRateLimiterRegistry(final RateLimiterConfig defaultRateLimiterConfig) { + this.defaultRateLimiterConfig = requireNonNull(defaultRateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + rateLimiters = new ConcurrentHashMap<>(); + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiter rateLimiter(final String name) { + return rateLimiter(name, defaultRateLimiterConfig); + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiter rateLimiter(final String name, final RateLimiterConfig rateLimiterConfig) { + requireNonNull(name, NAME_MUST_NOT_BE_NULL); + requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + return rateLimiters.computeIfAbsent( + name, + limitName -> new SemaphoreBasedRateLimiterImpl(name, rateLimiterConfig) + ); + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiter rateLimiter(final String name, final Supplier rateLimiterConfigSupplier) { + requireNonNull(name, NAME_MUST_NOT_BE_NULL); + requireNonNull(rateLimiterConfigSupplier, SUPPLIER_MUST_NOT_BE_NULL); + return rateLimiters.computeIfAbsent( + name, + limitName -> { + RateLimiterConfig rateLimiterConfig = rateLimiterConfigSupplier.get(); + requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + return new SemaphoreBasedRateLimiterImpl(limitName, rateLimiterConfig); + } + ); + } +} diff --git a/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java b/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java new file mode 100644 index 0000000000..832c9af529 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java @@ -0,0 +1,157 @@ +package javaslang.ratelimiter.internal; + +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor; + +import javaslang.control.Option; +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; + +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.Semaphore; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * A RateLimiter implementation that consists of {@link Semaphore} + * and scheduler that will refresh permissions + * after each {@link RateLimiterConfig#limitRefreshPeriod}. + */ +public class SemaphoreBasedRateLimiterImpl implements RateLimiter { + + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + private static final String CONFIG_MUST_NOT_BE_NULL = "RateLimiterConfig must not be null"; + + private final String name; + private final RateLimiterConfig rateLimiterConfig; + private final ScheduledExecutorService scheduler; + private final Semaphore semaphore; + private final SemaphoreBasedRateLimiterMetrics metrics; + + /** + * Creates a RateLimiter. + * + * @param name the name of the RateLimiter + * @param rateLimiterConfig The RateLimiter configuration. + */ + public SemaphoreBasedRateLimiterImpl(final String name, final RateLimiterConfig rateLimiterConfig) { + this(name, rateLimiterConfig, null); + } + + /** + * Creates a RateLimiter. + * + * @param name the name of the RateLimiter + * @param rateLimiterConfig The RateLimiter configuration. + * @param scheduler executor that will refresh permissions + */ + public SemaphoreBasedRateLimiterImpl(String name, RateLimiterConfig rateLimiterConfig, + ScheduledExecutorService scheduler) { + this.name = requireNonNull(name, NAME_MUST_NOT_BE_NULL); + this.rateLimiterConfig = requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); + + this.scheduler = Option.of(scheduler).getOrElse(this::configureScheduler); + this.semaphore = new Semaphore(this.rateLimiterConfig.getLimitForPeriod(), true); + this.metrics = this.new SemaphoreBasedRateLimiterMetrics(); + + scheduleLimitRefresh(); + } + + private ScheduledExecutorService configureScheduler() { + ThreadFactory threadFactory = target -> { + Thread thread = new Thread(target, "SchedulerForSemaphoreBasedRateLimiterImpl-" + name); + thread.setDaemon(true); + return thread; + }; + return newSingleThreadScheduledExecutor(threadFactory); + } + + private void scheduleLimitRefresh() { + scheduler.scheduleAtFixedRate( + this::refreshLimit, + this.rateLimiterConfig.getLimitRefreshPeriod().toNanos(), + this.rateLimiterConfig.getLimitRefreshPeriod().toNanos(), + TimeUnit.NANOSECONDS + ); + } + + void refreshLimit() { + semaphore.release(this.rateLimiterConfig.getLimitForPeriod()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getPermission(final Duration timeoutDuration) { + try { + boolean success = semaphore.tryAcquire(timeoutDuration.toNanos(), TimeUnit.NANOSECONDS); + return success; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return this.name; + } + + /** + * {@inheritDoc} + */ + @Override + public Metrics getMetrics() { + return this.metrics; + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiterConfig getRateLimiterConfig() { + return this.rateLimiterConfig; + } + + /** + * Get the enhanced Metrics with some implementation specific details. + * + * @return the detailed metrics + */ + public SemaphoreBasedRateLimiterMetrics getDetailedMetrics() { + return this.metrics; + } + + /** + * Enhanced {@link Metrics} with some implementation specific details + */ + public final class SemaphoreBasedRateLimiterMetrics implements Metrics { + private SemaphoreBasedRateLimiterMetrics() { + } + + /** + * Returns the current number of permits available in this request limit + * until the next refresh. + *

+ *

This method is typically used for debugging and testing purposes. + * + * @return the number of permits available in this rate limiter until the next refresh. + */ + public int getAvailablePermits() { + return semaphore.availablePermits(); + } + + /** + * {@inheritDoc} + */ + @Override + public int getNumberOfWaitingThreads() { + return semaphore.getQueueLength(); + } + } +} diff --git a/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java b/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java new file mode 100644 index 0000000000..d0b0532855 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java @@ -0,0 +1,91 @@ +package javaslang.ratelimiter; + +import static org.assertj.core.api.BDDAssertions.then; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.time.Duration; + + +public class RateLimiterConfigTest { + + private static final int LIMIT = 50; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofNanos(500); + private static final String TIMEOUT_DURATION_MUST_NOT_BE_NULL = "TimeoutDuration must not be null"; + private static final String REFRESH_PERIOD_MUST_NOT_BE_NULL = "RefreshPeriod must not be null"; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + + @Test + public void builderPositive() throws Exception { + RateLimiterConfig config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + + then(config.getLimitForPeriod()).isEqualTo(LIMIT); + then(config.getLimitRefreshPeriod()).isEqualTo(REFRESH_PERIOD); + then(config.getTimeoutDuration()).isEqualTo(TIMEOUT); + } + + @Test + public void builderTimeoutIsNull() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(TIMEOUT_DURATION_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .timeoutDuration(null); + } + + @Test + public void builderTimeoutEmpty() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(TIMEOUT_DURATION_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void builderRefreshPeriodIsNull() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(REFRESH_PERIOD_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .limitRefreshPeriod(null); + } + + @Test + public void builderRefreshPeriodEmpty() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(REFRESH_PERIOD_MUST_NOT_BE_NULL); + RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void builderRefreshPeriodTooShort() throws Exception { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("RefreshPeriod is too short"); + RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(Duration.ofNanos(499L)) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void builderLimitIsLessThanOne() throws Exception { + exception.expect(IllegalArgumentException.class); + exception.expectMessage("LimitForPeriod should be greater than 0"); + RateLimiterConfig.builder() + .limitForPeriod(0); + } +} diff --git a/src/test/java/javaslang/ratelimiter/RateLimiterTest.java b/src/test/java/javaslang/ratelimiter/RateLimiterTest.java new file mode 100644 index 0000000000..b38f52da36 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/RateLimiterTest.java @@ -0,0 +1,200 @@ +package javaslang.ratelimiter; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javaslang.control.Try; +import org.junit.Before; +import org.junit.Test; + +import java.time.Duration; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + + +@SuppressWarnings("unchecked") +public class RateLimiterTest { + + private static final int LIMIT = 50; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofNanos(500); + + private RateLimiterConfig config; + private RateLimiter limit; + + @Before + public void init() { + config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + limit = mock(RateLimiter.class); + when(limit.getRateLimiterConfig()) + .thenReturn(config); + } + + @Test + public void decorateCheckedSupplier() throws Throwable { + Try.CheckedSupplier supplier = mock(Try.CheckedSupplier.class); + Try.CheckedSupplier decorated = RateLimiter.decorateCheckedSupplier(limit, supplier); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedSupplierResult = Try.of(decorated); + then(decoratedSupplierResult.isFailure()).isTrue(); + then(decoratedSupplierResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(supplier, never()).get(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondSupplierResult = Try.of(decorated); + then(secondSupplierResult.isSuccess()).isTrue(); + verify(supplier, times(1)).get(); + } + + @Test + public void decorateCheckedRunnable() throws Throwable { + Try.CheckedRunnable runnable = mock(Try.CheckedRunnable.class); + Try.CheckedRunnable decorated = RateLimiter.decorateCheckedRunnable(limit, runnable); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedRunnableResult = Try.run(decorated); + then(decoratedRunnableResult.isFailure()).isTrue(); + then(decoratedRunnableResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(runnable, never()).run(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondRunnableResult = Try.run(decorated); + then(secondRunnableResult.isSuccess()).isTrue(); + verify(runnable, times(1)).run(); + } + + @Test + public void decorateCheckedFunction() throws Throwable { + Try.CheckedFunction function = mock(Try.CheckedFunction.class); + Try.CheckedFunction decorated = RateLimiter.decorateCheckedFunction(limit, function); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedFunctionResult = Try.success(1).mapTry(decorated); + then(decoratedFunctionResult.isFailure()).isTrue(); + then(decoratedFunctionResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(function, never()).apply(any()); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondFunctionResult = Try.success(1).mapTry(decorated); + then(secondFunctionResult.isSuccess()).isTrue(); + verify(function, times(1)).apply(1); + } + + @Test + public void decorateSupplier() throws Exception { + Supplier supplier = mock(Supplier.class); + Supplier decorated = RateLimiter.decorateSupplier(limit, supplier); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedSupplierResult = Try.success(decorated).map(Supplier::get); + then(decoratedSupplierResult.isFailure()).isTrue(); + then(decoratedSupplierResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(supplier, never()).get(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondSupplierResult = Try.success(decorated).map(Supplier::get); + then(secondSupplierResult.isSuccess()).isTrue(); + verify(supplier, times(1)).get(); + } + + @Test + public void decorateConsumer() throws Exception { + Consumer consumer = mock(Consumer.class); + Consumer decorated = RateLimiter.decorateConsumer(limit, consumer); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedConsumerResult = Try.success(1).andThen(decorated); + then(decoratedConsumerResult.isFailure()).isTrue(); + then(decoratedConsumerResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(consumer, never()).accept(any()); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondConsumerResult = Try.success(1).andThen(decorated); + then(secondConsumerResult.isSuccess()).isTrue(); + verify(consumer, times(1)).accept(1); + } + + @Test + public void decorateRunnable() throws Exception { + Runnable runnable = mock(Runnable.class); + Runnable decorated = RateLimiter.decorateRunnable(limit, runnable); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedRunnableResult = Try.success(decorated).andThen(Runnable::run); + then(decoratedRunnableResult.isFailure()).isTrue(); + then(decoratedRunnableResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(runnable, never()).run(); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondRunnableResult = Try.success(decorated).andThen(Runnable::run); + then(secondRunnableResult.isSuccess()).isTrue(); + verify(runnable, times(1)).run(); + } + + @Test + public void decorateFunction() throws Exception { + Function function = mock(Function.class); + Function decorated = RateLimiter.decorateFunction(limit, function); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + + Try decoratedFunctionResult = Try.success(1).map(decorated); + then(decoratedFunctionResult.isFailure()).isTrue(); + then(decoratedFunctionResult.getCause()).isInstanceOf(RequestNotPermitted.class); + verify(function, never()).apply(any()); + + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + Try secondFunctionResult = Try.success(1).map(decorated); + then(secondFunctionResult.isSuccess()).isTrue(); + verify(function, times(1)).apply(1); + } + + @Test + public void waitForPermissionWithOne() throws Exception { + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(true); + RateLimiter.waitForPermission(limit); + verify(limit, times(1)) + .getPermission(config.getTimeoutDuration()); + } + + @Test(expected = RequestNotPermitted.class) + public void waitForPermissionWithoutOne() throws Exception { + when(limit.getPermission(config.getTimeoutDuration())) + .thenReturn(false); + RateLimiter.waitForPermission(limit); + verify(limit, times(1)) + .getPermission(config.getTimeoutDuration()); + } +} \ No newline at end of file diff --git a/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java new file mode 100644 index 0000000000..ba8ca819a9 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java @@ -0,0 +1,118 @@ +package javaslang.ratelimiter.internal; + +import static org.assertj.core.api.BDDAssertions.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import javaslang.ratelimiter.RateLimiterRegistry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.time.Duration; +import java.util.function.Supplier; + + +public class InMemoryRateLimiterRegistryTest { + + private static final int LIMIT = 50; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofNanos(500); + private static final String CONFIG_MUST_NOT_BE_NULL = "Config must not be null"; + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + @Rule + public ExpectedException exception = ExpectedException.none(); + private RateLimiterConfig config; + + @Before + public void init() { + config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void rateLimiterPositive() throws Exception { + RateLimiterRegistry registry = RateLimiterRegistry.of(config); + RateLimiter firstRateLimiter = registry.rateLimiter("test"); + RateLimiter anotherLimit = registry.rateLimiter("test1"); + RateLimiter sameAsFirst = registry.rateLimiter("test"); + + then(firstRateLimiter).isEqualTo(sameAsFirst); + then(firstRateLimiter).isNotEqualTo(anotherLimit); + } + + @Test + public void rateLimiterPositiveWithSupplier() throws Exception { + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + Supplier rateLimiterConfigSupplier = mock(Supplier.class); + when(rateLimiterConfigSupplier.get()) + .thenReturn(config); + + RateLimiter firstRateLimiter = registry.rateLimiter("test", rateLimiterConfigSupplier); + verify(rateLimiterConfigSupplier, times(1)).get(); + RateLimiter sameAsFirst = registry.rateLimiter("test", rateLimiterConfigSupplier); + verify(rateLimiterConfigSupplier, times(1)).get(); + RateLimiter anotherLimit = registry.rateLimiter("test1", rateLimiterConfigSupplier); + verify(rateLimiterConfigSupplier, times(2)).get(); + + then(firstRateLimiter).isEqualTo(sameAsFirst); + then(firstRateLimiter).isNotEqualTo(anotherLimit); + } + + @Test + public void rateLimiterConfigIsNull() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + new InMemoryRateLimiterRegistry(null); + } + + @Test + public void rateLimiterNewWithNullName() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + registry.rateLimiter(null); + } + + @Test + public void rateLimiterNewWithNullNonDefaultConfig() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + RateLimiterConfig rateLimiterConfig = null; + registry.rateLimiter("name", rateLimiterConfig); + } + + @Test + public void rateLimiterNewWithNullNameAndNonDefaultConfig() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + registry.rateLimiter(null, config); + } + + @Test + public void rateLimiterNewWithNullNameAndConfigSupplier() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + registry.rateLimiter(null, () -> config); + } + + @Test + public void rateLimiterNewWithNullConfigSupplier() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage("Supplier must not be null"); + RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); + Supplier rateLimiterConfigSupplier = null; + registry.rateLimiter("name", rateLimiterConfigSupplier); + } +} \ No newline at end of file diff --git a/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java b/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java new file mode 100644 index 0000000000..24da434548 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java @@ -0,0 +1,221 @@ +package javaslang.ratelimiter.internal; + +import static com.jayway.awaitility.Awaitility.await; +import static com.jayway.awaitility.Duration.FIVE_HUNDRED_MILLISECONDS; +import static java.lang.Thread.State.RUNNABLE; +import static java.lang.Thread.State.TERMINATED; +import static java.lang.Thread.State.TIMED_WAITING; +import static java.time.Duration.ZERO; +import static javaslang.control.Try.run; +import static org.assertj.core.api.BDDAssertions.then; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.jayway.awaitility.core.ConditionFactory; +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.ArgumentCaptor; + +import java.time.Duration; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + + +public class SemaphoreBasedRateLimiterImplTest { + + private static final int LIMIT = 2; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration REFRESH_PERIOD = Duration.ofMillis(100); + private static final String CONFIG_MUST_NOT_BE_NULL = "RateLimiterConfig must not be null"; + private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; + private static final Object O = new Object(); + @Rule + public ExpectedException exception = ExpectedException.none(); + private RateLimiterConfig config; + + private static ConditionFactory awaitImpatiently() { + return await() + .pollDelay(1, TimeUnit.MICROSECONDS) + .pollInterval(2, TimeUnit.MILLISECONDS); + } + + @Before + public void init() { + config = RateLimiterConfig.builder() + .timeoutDuration(TIMEOUT) + .limitRefreshPeriod(REFRESH_PERIOD) + .limitForPeriod(LIMIT) + .build(); + } + + @Test + public void rateLimiterCreationWithProvidedScheduler() throws Exception { + ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); + RateLimiterConfig configSpy = spy(config); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + + ArgumentCaptor refreshLimitRunnableCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(scheduledExecutorService) + .scheduleAtFixedRate( + refreshLimitRunnableCaptor.capture(), + eq(config.getLimitRefreshPeriod().toNanos()), + eq(config.getLimitRefreshPeriod().toNanos()), + eq(TimeUnit.NANOSECONDS) + ); + + Runnable refreshLimitRunnable = refreshLimitRunnableCaptor.getValue(); + + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isFalse(); + + Thread.sleep(REFRESH_PERIOD.toMillis() * 2); + verify(configSpy, times(1)).getLimitForPeriod(); + + refreshLimitRunnable.run(); + + verify(configSpy, times(2)).getLimitForPeriod(); + + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isTrue(); + then(limit.getPermission(ZERO)).isFalse(); + } + + @Test + public void rateLimiterCreationWithDefaultScheduler() throws Exception { + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config); + awaitImpatiently().atMost(FIVE_HUNDRED_MILLISECONDS) + .until(() -> limit.getPermission(ZERO), equalTo(false)); + awaitImpatiently().atMost(110, TimeUnit.MILLISECONDS) + .until(() -> limit.getPermission(ZERO), equalTo(true)); + } + + @Test + public void getPermissionAndMetrics() throws Exception { + ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); + RateLimiterConfig configSpy = spy(config); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + SemaphoreBasedRateLimiterImpl.SemaphoreBasedRateLimiterMetrics detailedMetrics = limit.getDetailedMetrics(); + + SynchronousQueue synchronousQueue = new SynchronousQueue(); + Thread thread = new Thread(() -> { + run(() -> { + for (int i = 0; i < LIMIT; i++) { + System.out.println("SLAVE -> WAITING FOR MASTER"); + synchronousQueue.put(O); + System.out.println("SLAVE -> HAVE COMMAND FROM MASTER"); + limit.getPermission(TIMEOUT); + System.out.println("SLAVE -> ACQUIRED PERMISSION"); + } + System.out.println("SLAVE -> LAST PERMISSION ACQUIRE"); + limit.getPermission(TIMEOUT); + System.out.println("SLAVE -> I'M DONE"); + }); + }); + thread.setDaemon(true); + thread.start(); + + for (int i = 0; i < LIMIT; i++) { + System.out.println("MASTER -> TAKE PERMISSION"); + synchronousQueue.take(); + } + + System.out.println("MASTER -> CHECK IF SLAVE IS WAITING FOR PERMISSION"); + awaitImpatiently() + .atMost(100, TimeUnit.MILLISECONDS).until(detailedMetrics::getAvailablePermits, equalTo(0)); + System.out.println("MASTER -> SLAVE CONSUMED ALL PERMISSIONS"); + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TIMED_WAITING)); + then(detailedMetrics.getAvailablePermits()).isEqualTo(0); + System.out.println("MASTER -> SLAVE WAS WAITING"); + + limit.refreshLimit(); + awaitImpatiently() + .atMost(100, TimeUnit.MILLISECONDS).until(detailedMetrics::getAvailablePermits, equalTo(1)); + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TERMINATED)); + then(detailedMetrics.getAvailablePermits()).isEqualTo(1); + } + + @Test + public void getPermissionInterruption() throws Exception { + ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); + RateLimiterConfig configSpy = spy(config); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + limit.getPermission(ZERO); + limit.getPermission(ZERO); + + Thread thread = new Thread(() -> { + limit.getPermission(TIMEOUT); + while (true) { + Function.identity().apply(1); + } + }); + thread.setDaemon(true); + thread.start(); + + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(TIMED_WAITING)); + + thread.interrupt(); + + awaitImpatiently() + .atMost(2, TimeUnit.SECONDS).until(thread::getState, equalTo(RUNNABLE)); + then(thread.isInterrupted()).isTrue(); + } + + @Test + public void getName() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + then(limit.getName()).isEqualTo("test"); + } + + @Test + public void getMetrics() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + RateLimiter.Metrics metrics = limit.getMetrics(); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + } + + @Test + public void getRateLimiterConfig() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + then(limit.getRateLimiterConfig()).isEqualTo(config); + } + + @Test + public void getDetailedMetrics() throws Exception { + ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); + SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + SemaphoreBasedRateLimiterImpl.SemaphoreBasedRateLimiterMetrics metrics = limit.getDetailedMetrics(); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + then(metrics.getAvailablePermits()).isEqualTo(2); + } + + @Test + public void constructionWithNullName() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(NAME_MUST_NOT_BE_NULL); + new SemaphoreBasedRateLimiterImpl(null, config, null); + } + + @Test + public void constructionWithNullConfig() throws Exception { + exception.expect(NullPointerException.class); + exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); + new SemaphoreBasedRateLimiterImpl("test", null, null); + } +} \ No newline at end of file From 15bcef0f9accdf8be1fab0d0127631f30dbc5012 Mon Sep 17 00:00:00 2001 From: bstorozhuk Date: Wed, 30 Nov 2016 20:55:11 +0200 Subject: [PATCH 3/7] Issue #12 TimeBasedRateLimiter and AtomicRateLimiter implementations + benchmarks --- build.gradle | 12 +- gradlew | 10 +- ...016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt | 41 ++++ .../CircuitBreakerBenchmark.java | 80 +++--- .../circuitbreaker/RateLimiterBenchmark.java | 89 +++++++ .../circuitbreaker/RingBitSetBenachmark.java | 53 ++-- .../javaslang/ratelimiter/RateLimiter.java | 97 ++++---- .../ratelimiter/RateLimiterConfig.java | 2 +- .../internal/AtomicRateLimiter.java | 232 ++++++++++++++++++ .../internal/InMemoryRateLimiterRegistry.java | 4 +- ...pl.java => SemaphoreBasedRateLimiter.java} | 8 +- .../internal/TimeBasedRateLimiter.java | 178 ++++++++++++++ .../InMemoryRateLimiterRegistryTest.java | 1 + .../SemaphoreBasedRateLimiterImplTest.java | 26 +- .../internal/TimeBasedRateLimiterTest.java | 53 ++++ 15 files changed, 734 insertions(+), 152 deletions(-) create mode 100644 src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt create mode 100644 src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java create mode 100644 src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java rename src/main/java/javaslang/ratelimiter/internal/{SemaphoreBasedRateLimiterImpl.java => SemaphoreBasedRateLimiter.java} (93%) create mode 100644 src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java create mode 100644 src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java diff --git a/build.gradle b/build.gradle index 8220612338..d022610ffa 100644 --- a/build.gradle +++ b/build.gradle @@ -2,11 +2,14 @@ buildscript { repositories { jcenter() mavenCentral() + maven { + url "https://plugins.gradle.org/m2/" + } } dependencies { classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.0.1' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' - classpath 'me.champeau.gradle:jmh-gradle-plugin:0.2.0' + classpath 'me.champeau.gradle:jmh-gradle-plugin:0.3.1' classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.0' classpath "org.asciidoctor:asciidoctor-gradle-plugin:1.5.3" classpath "org.ajoberstar:gradle-git:1.3.2" @@ -50,12 +53,7 @@ repositories { } jmh { - benchmarkMode = 'all' - jmhVersion = '1.11.2' - fork = 1 - threads = 10 - iterations = 2 - warmupIterations = 2 + jmhVersion = '1.17' include='' } diff --git a/gradlew b/gradlew index 91a7e269e1..9d82f78915 100755 --- a/gradlew +++ b/gradlew @@ -42,11 +42,6 @@ case "`uname`" in ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" @@ -61,9 +56,9 @@ while [ -h "$PRG" ] ; do fi done SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- +cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" -cd "$SAVED" >&- +cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -114,6 +109,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` diff --git a/src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt b/src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt new file mode 100644 index 0000000000..c9c091354e --- /dev/null +++ b/src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt @@ -0,0 +1,41 @@ +Benchmark Mode Cnt Score Error Units + +RateLimiterBenchmark.atomicPermission thrpt 10 7.274 ± 0.132 ops/us +RateLimiterBenchmark.semaphoreBasedPermission thrpt 10 17.335 ± 3.441 ops/us +RateLimiterBenchmark.timeBasedPermission thrpt 10 3.522 ± 0.495 ops/us + +RateLimiterBenchmark.atomicPermission avgt 10 0.294 ± 0.038 us/op +RateLimiterBenchmark.semaphoreBasedPermission avgt 10 0.120 ± 0.018 us/op +RateLimiterBenchmark.timeBasedPermission avgt 10 0.562 ± 0.045 us/op + +RateLimiterBenchmark.atomicPermission sample 535765 1.480 ± 0.036 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p0.00 sample 0.040 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p0.50 sample 0.383 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p0.90 sample 4.288 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p0.95 sample 7.368 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p0.99 sample 14.080 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p0.999 sample 18.048 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p0.9999 sample 58.449 us/op +RateLimiterBenchmark.atomicPermission:atomicPermission·p1.00 sample 1654.784 us/op +RateLimiterBenchmark.semaphoreBasedPermission sample 635614 0.166 ± 0.010 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.00 sample 0.001 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.50 sample 0.135 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.90 sample 0.219 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.95 sample 0.236 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.99 sample 0.333 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.999 sample 2.468 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.9999 sample 15.519 us/op +RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p1.00 sample 1372.160 us/op +RateLimiterBenchmark.timeBasedPermission sample 553560 0.800 ± 0.053 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.00 sample 0.054 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.50 sample 0.550 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.90 sample 0.749 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.95 sample 0.826 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.99 sample 8.256 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.999 sample 33.920 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.9999 sample 160.221 us/op +RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p1.00 sample 5742.592 us/op + +RateLimiterBenchmark.atomicPermission ss 10 17.140 ± 5.640 us/op +RateLimiterBenchmark.semaphoreBasedPermission ss 10 9.724 ± 4.602 us/op +RateLimiterBenchmark.timeBasedPermission ss 10 26.875 ± 10.869 us/op diff --git a/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java b/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java index 124033f1e7..2618cb1ebf 100644 --- a/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java @@ -18,46 +18,40 @@ */ package javaslang.circuitbreaker; -import org.openjdk.jmh.annotations.*; - -import java.time.Duration; -import java.util.concurrent.TimeUnit; -import java.util.function.Supplier; - -@State(Scope.Benchmark) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@BenchmarkMode(Mode.Throughput) -public class CircuitBreakerBenchmark { - - private CircuitBreaker circuitBreaker; - private Supplier supplier; - private static final int ITERATION_COUNT = 10; - private static final int WARMUP_COUNT = 10; - private static final int THREAD_COUNT = 10; - - @Setup - public void setUp() { - CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(CircuitBreakerConfig.custom() - .failureRateThreshold(1) - .waitDurationInOpenState(Duration.ofSeconds(1)) - .build()); - circuitBreaker = circuitBreakerRegistry.circuitBreaker("testCircuitBreaker"); - - supplier = CircuitBreaker.decorateSupplier(() -> { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - return "Hello Benchmark"; - }, circuitBreaker); - } - - @Benchmark - @Threads(value = THREAD_COUNT) - @Warmup(iterations = WARMUP_COUNT) - @Measurement(iterations = ITERATION_COUNT) - public String invokeSupplier(){ - return supplier.get(); - } -} +//@State(Scope.Benchmark) +//@OutputTimeUnit(TimeUnit.MILLISECONDS) +//@BenchmarkMode(Mode.Throughput) +//public class CircuitBreakerBenchmark { +// +// private CircuitBreaker circuitBreaker; +// private Supplier supplier; +// private static final int ITERATION_COUNT = 10; +// private static final int WARMUP_COUNT = 10; +// private static final int THREAD_COUNT = 10; +// +// @Setup +// public void setUp() { +// CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(CircuitBreakerConfig.custom() +// .failureRateThreshold(1) +// .waitDurationInOpenState(Duration.ofSeconds(1)) +// .build()); +// circuitBreaker = circuitBreakerRegistry.circuitBreaker("testCircuitBreaker"); +// +// supplier = CircuitBreaker.decorateSupplier(() -> { +// try { +// Thread.sleep(100); +// } catch (InterruptedException e) { +// e.printStackTrace(); +// } +// return "Hello Benchmark"; +// }, circuitBreaker); +// } +// +// @Benchmark +// @Threads(value = THREAD_COUNT) +// @Warmup(iterations = WARMUP_COUNT) +// @Measurement(iterations = ITERATION_COUNT) +// public String invokeSupplier(){ +// return supplier.get(); +// } +//} diff --git a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java new file mode 100644 index 0000000000..80cdcc75a8 --- /dev/null +++ b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java @@ -0,0 +1,89 @@ +package javaslang.circuitbreaker; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import javaslang.ratelimiter.internal.AtomicRateLimiter; +import javaslang.ratelimiter.internal.SemaphoreBasedRateLimiter; +import javaslang.ratelimiter.internal.TimeBasedRateLimiter; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.All) +public class RateLimiterBenchmark { + + public static final int FORK_COUNT = 2; + private static final int WARMUP_COUNT = 10; + private static final int ITERATION_COUNT = 5; + private static final int THREAD_COUNT = 2; + + private RateLimiter semaphoreBasedRateLimiter; + private RateLimiter timeBasedRateLimiter; + private RateLimiter atomicRateLimiter; + + private Supplier semaphoreGuardedSupplier; + private Supplier timeGuardedSupplier; + private Supplier atomicGuardedSupplier; + + @Setup + public void setUp() { + RateLimiterConfig rateLimiterConfig = RateLimiterConfig.builder() + .limitForPeriod(Integer.MAX_VALUE) + .limitRefreshPeriod(Duration.ofNanos(10)) + .timeoutDuration(Duration.ofSeconds(5)) + .build(); + semaphoreBasedRateLimiter = new SemaphoreBasedRateLimiter("semaphoreBased", rateLimiterConfig); + timeBasedRateLimiter = new TimeBasedRateLimiter("timeBased", rateLimiterConfig); + atomicRateLimiter = new AtomicRateLimiter("atomicBased", rateLimiterConfig); + + Supplier stringSupplier = () -> { + Blackhole.consumeCPU(1); + return "Hello Benchmark"; + }; + semaphoreGuardedSupplier = RateLimiter.decorateSupplier(semaphoreBasedRateLimiter, stringSupplier); + timeGuardedSupplier = RateLimiter.decorateSupplier(timeBasedRateLimiter, stringSupplier); + atomicGuardedSupplier = RateLimiter.decorateSupplier(atomicRateLimiter, stringSupplier); + } + + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Fork(value = FORK_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public String semaphoreBasedPermission() { + return semaphoreGuardedSupplier.get(); + } + + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Fork(value = FORK_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public String timeBasedPermission() { + return timeGuardedSupplier.get(); + } + + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Fork(value = FORK_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public String atomicPermission() { + return atomicGuardedSupplier.get(); + } +} \ No newline at end of file diff --git a/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java b/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java index fbf23dc66f..9512465d2b 100644 --- a/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java @@ -18,32 +18,27 @@ */ package javaslang.circuitbreaker; -import javaslang.circuitbreaker.internal.RingBitSet; -import org.openjdk.jmh.annotations.*; - -import java.util.concurrent.TimeUnit; - -@State(Scope.Benchmark) -@OutputTimeUnit(TimeUnit.MILLISECONDS) -@BenchmarkMode(Mode.Throughput) -public class RingBitSetBenachmark { - - private RingBitSet ringBitSet; - private static final int ITERATION_COUNT = 10; - private static final int WARMUP_COUNT = 10; - private static final int THREAD_COUNT = 10; - - @Setup - public void setUp() { - ringBitSet = new RingBitSet(1000); - } - - @Benchmark - @Threads(value = THREAD_COUNT) - @Warmup(iterations = WARMUP_COUNT) - @Measurement(iterations = ITERATION_COUNT) - public void setBits(){ - ringBitSet.setNextBit(true); - ringBitSet.setNextBit(false); - } -} +//@State(Scope.Benchmark) +//@OutputTimeUnit(TimeUnit.MILLISECONDS) +//@BenchmarkMode(Mode.Throughput) +//public class RingBitSetBenachmark { +// +// private RingBitSet ringBitSet; +// private static final int ITERATION_COUNT = 10; +// private static final int WARMUP_COUNT = 10; +// private static final int THREAD_COUNT = 10; +// +// @Setup +// public void setUp() { +// ringBitSet = new RingBitSet(1000); +// } +// +// @Benchmark +// @Threads(value = THREAD_COUNT) +// @Warmup(iterations = WARMUP_COUNT) +// @Measurement(iterations = ITERATION_COUNT) +// public void setBits(){ +// ringBitSet.setNextBit(true); +// ringBitSet.setNextBit(false); +// } +//} diff --git a/src/main/java/javaslang/ratelimiter/RateLimiter.java b/src/main/java/javaslang/ratelimiter/RateLimiter.java index 1a5ff9c2b2..a70eaaaea1 100644 --- a/src/main/java/javaslang/ratelimiter/RateLimiter.java +++ b/src/main/java/javaslang/ratelimiter/RateLimiter.java @@ -13,50 +13,6 @@ */ public interface RateLimiter { - /** - * Acquires a permission from this rate limiter, blocking until one is - * available. - *

- *

If the current thread is {@linkplain Thread#interrupt interrupted} - * while waiting for a permit then it won't throw {@linkplain InterruptedException}, - * but its interrupt status will be set. - * - * @return {@code true} if a permit was acquired and {@code false} - * if waiting time elapsed before a permit was acquired - */ - boolean getPermission(Duration timeoutDuration); - - /** - * Get the name of this RateLimiter - * - * @return the name of this RateLimiter - */ - String getName(); - - /** - * Get the RateLimiterConfig of this RateLimiter. - * - * @return the RateLimiterConfig of this RateLimiter - */ - RateLimiterConfig getRateLimiterConfig(); - - /** - * Get the Metrics of this RateLimiter. - * - * @return the Metrics of this RateLimiter - */ - Metrics getMetrics(); - - interface Metrics { - /** - * Returns an estimate of the number of threads waiting for permission - * in this JVM process. - * - * @return estimate of the number of threads waiting for permission. - */ - int getNumberOfWaitingThreads(); - } - /** * Creates a supplier which is restricted by a RateLimiter. * @@ -167,18 +123,67 @@ static Function decorateFunction(RateLimiter rateLimiter, Function< return decoratedFunction; } + /** * Will wait for permission within default timeout duration. - * Throws {@link RequestNotPermitted} if waiting time elapsed before a permit was acquired. * * @param rateLimiter the RateLimiter to get permission from + * @throws RequestNotPermitted if waiting time elapsed before a permit was acquired. + * @throws IllegalStateException if thread was interrupted during permission wait */ - static void waitForPermission(final RateLimiter rateLimiter) { + static void waitForPermission(final RateLimiter rateLimiter) throws IllegalStateException, RequestNotPermitted { RateLimiterConfig rateLimiterConfig = rateLimiter.getRateLimiterConfig(); Duration timeoutDuration = rateLimiterConfig.getTimeoutDuration(); boolean permission = rateLimiter.getPermission(timeoutDuration); + if (Thread.interrupted()) { + throw new IllegalStateException("Thread was interrupted during permission wait"); + } if (!permission) { throw new RequestNotPermitted("Request not permitted for limiter: " + rateLimiter.getName()); } } + + /** + * Acquires a permission from this rate limiter, blocking until one is + * available. + *

+ *

If the current thread is {@linkplain Thread#interrupt interrupted} + * while waiting for a permit then it won't throw {@linkplain InterruptedException}, + * but its interrupt status will be set. + * + * @return {@code true} if a permit was acquired and {@code false} + * if waiting time elapsed before a permit was acquired + */ + boolean getPermission(Duration timeoutDuration); + + /** + * Get the name of this RateLimiter + * + * @return the name of this RateLimiter + */ + String getName(); + + /** + * Get the RateLimiterConfig of this RateLimiter. + * + * @return the RateLimiterConfig of this RateLimiter + */ + RateLimiterConfig getRateLimiterConfig(); + + /** + * Get the Metrics of this RateLimiter. + * + * @return the Metrics of this RateLimiter + */ + Metrics getMetrics(); + + interface Metrics { + /** + * Returns an estimate of the number of threads waiting for permission + * in this JVM process. + * + * @return estimate of the number of threads waiting for permission. + */ + int getNumberOfWaitingThreads(); + } } diff --git a/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java index 17e2ccd3f3..21fa3d93b8 100644 --- a/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java +++ b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java @@ -8,7 +8,7 @@ public class RateLimiterConfig { private static final String TIMEOUT_DURATION_MUST_NOT_BE_NULL = "TimeoutDuration must not be null"; private static final String LIMIT_REFRESH_PERIOD_MUST_NOT_BE_NULL = "LimitRefreshPeriod must not be null"; - private static final Duration ACCEPTABLE_REFRESH_PERIOD = Duration.ofNanos(500L); // TODO: use jmh to find real one + private static final Duration ACCEPTABLE_REFRESH_PERIOD = Duration.ofNanos(1L); // TODO: use jmh to find real one private final Duration timeoutDuration; private final Duration limitRefreshPeriod; diff --git a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java new file mode 100644 index 0000000000..d0cb61ec56 --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java @@ -0,0 +1,232 @@ +package javaslang.ratelimiter.internal; + +import static java.lang.Long.min; +import static java.lang.System.nanoTime; +import static java.lang.Thread.currentThread; +import static java.util.concurrent.locks.LockSupport.parkNanos; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +/** + * {@link AtomicRateLimiter} splits all nanoseconds from the start of epoch into cycles. + *

Each cycle has duration of {@link RateLimiterConfig#limitRefreshPeriod} in nanoseconds. + *

+ *

By contract on start of each cycle {@link AtomicRateLimiter} should + * set {@link State#activePermissions} to {@link RateLimiterConfig#limitForPeriod}. + * For the {@link AtomicRateLimiter} callers it is really looks so, but under the hood there is + * some optimisations that will skip this refresh if {@link AtomicRateLimiter} is not used actively. + *

+ *

All {@link AtomicRateLimiter} updates are atomic and state is encapsulated in {@link AtomicReference} to + * {@link AtomicRateLimiter.State} + */ +public class AtomicRateLimiter implements RateLimiter { + + private final String name; + private final RateLimiterConfig rateLimiterConfig; + private final long cyclePeriodInNanos; + private final int permissionsPerCycle; + private final AtomicInteger waitingThreads; + private final AtomicReference state; + + + public AtomicRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { + this.name = name; + this.rateLimiterConfig = rateLimiterConfig; + + cyclePeriodInNanos = rateLimiterConfig.getLimitRefreshPeriod().toNanos(); + permissionsPerCycle = rateLimiterConfig.getLimitForPeriod(); + + waitingThreads = new AtomicInteger(0); + long activeCycle = nanoTime() / cyclePeriodInNanos; + int activePermissions = permissionsPerCycle; + state = new AtomicReference<>(new State(activeCycle, activePermissions, 0)); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getPermission(final Duration timeoutDuration) { + long timeoutInNanos = timeoutDuration.toNanos(); + State modifiedState = state.updateAndGet( + activeState -> calculateNextState(timeoutInNanos, activeState) + ); + return waitForPermissionIfNecessary(timeoutInNanos, modifiedState.nanosToWait); + } + + /** + * A side-effect-free function that can calculate next {@link State} from current. + * It determines time duration that you should wait for permission and reserves it for you, + * if you'll be able to wait long enough. + * + * @param timeoutInNanos max time that caller can wait for permission in nanoseconds + * @param activeState current state of {@link AtomicRateLimiter} + * @return next {@link State} + */ + private State calculateNextState(final long timeoutInNanos, final State activeState) { + long currentNanos = nanoTime(); + long currentCycle = currentNanos / cyclePeriodInNanos; + + long nextCycle = activeState.activeCycle; + int nextPermissions = activeState.activePermissions; + if (nextCycle != currentCycle) { + long elapsedCycles = currentCycle - nextCycle; + long accumulatedPermissions = elapsedCycles * permissionsPerCycle; + nextCycle = currentCycle; + nextPermissions = (int) min(nextPermissions + accumulatedPermissions, permissionsPerCycle); + } + long nextNanosToWait = nanosToWaitForPermission(nextPermissions, currentNanos, currentCycle); + State nextState = reservePermissions(timeoutInNanos, nextCycle, nextPermissions, nextNanosToWait); + return nextState; + } + + /** + * Calculates time to wait for next permission as + * [time to the next cycle] + [duration of full cycles until reserved permissions expire] + * + * @param availablePermissions currently available permissions, can be negative if some permissions have been reserved + * @param currentNanos current time in nanoseconds + * @param currentCycle current {@link AtomicRateLimiter} cycle + * @return nanoseconds to wait for the next permission + */ + private long nanosToWaitForPermission(final int availablePermissions, final long currentNanos, final long currentCycle) { + if (availablePermissions > 0) { + return 0L; + } else { + long nextCycleTimeInNanos = (currentCycle + 1) * cyclePeriodInNanos; + long nanosToNextCycle = nextCycleTimeInNanos - currentNanos; + int fullCyclesToWait = (-availablePermissions) / permissionsPerCycle; + return (fullCyclesToWait * cyclePeriodInNanos) + nanosToNextCycle; + } + } + + /** + * Determines whether caller can acquire permission before timeout or not and then creates corresponding {@link State}. + * Reserves permissions only if caller can successfully wait for permission. + * + * @param timeoutInNanos max time that caller can wait for permission in nanoseconds + * @param cycle cycle for new {@link State} + * @param permissions permissions for new {@link State} + * @param nanosToWait nanoseconds to wait for the next permission + * @return new {@link State} with possibly reserved permissions and time to wait + */ + private State reservePermissions(final long timeoutInNanos, final long cycle, final int permissions, final long nanosToWait) { + boolean canAcquireInTime = timeoutInNanos >= nanosToWait; + int permissionsWithReservation = permissions; + if (canAcquireInTime) { + permissionsWithReservation--; + } + return new State(cycle, permissionsWithReservation, nanosToWait); + } + + /** + * If nanosToWait is bigger than 0 it tries to park {@link Thread} for nanosToWait but not longer then timeoutInNanos. + * + * @param timeoutInNanos max time that caller can wait + * @param nanosToWait nanoseconds caller need to wait + * @return true if caller was able to wait for nanosToWait without {@link Thread#interrupt} and not exceed timeout + */ + private boolean waitForPermissionIfNecessary(final long timeoutInNanos, final long nanosToWait) { + boolean canAcquireImmediately = nanosToWait <= 0; + boolean canAcquireInTime = timeoutInNanos >= nanosToWait; + + if (canAcquireImmediately) { + return true; + } + if (canAcquireInTime) { + return waitForPermission(nanosToWait); + } + waitForPermission(timeoutInNanos); + return false; + } + + /** + * Parks {@link Thread} for nanosToWait. + * + * @param nanosToWait nanoseconds caller need to wait + * @return true if caller was not {@link Thread#interrupted} while waiting + */ + private boolean waitForPermission(final long nanosToWait) { + waitingThreads.incrementAndGet(); + long deadline = nanoTime() + nanosToWait; + while (nanoTime() < deadline || currentThread().isInterrupted()) { + long sleepBlockDuration = deadline - nanoTime(); + parkNanos(sleepBlockDuration); + } + waitingThreads.decrementAndGet(); + return !currentThread().isInterrupted(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return name; + } + + /** + * {@inheritDoc} + */ + @Override + public RateLimiterConfig getRateLimiterConfig() { + return rateLimiterConfig; + } + + /** + * {@inheritDoc} + */ + @Override + public Metrics getMetrics() { + return new AtomicRateLimiterMetrics(); + } + + /** + *

{@link AtomicRateLimiter.State} represents immutable state of {@link AtomicRateLimiter} where: + *

+ */ + private static class State { + private final long activeCycle; + private final int activePermissions; + private final long nanosToWait; + + public State(final long activeCycle, final int activePermissions, final long nanosToWait) { + this.activeCycle = activeCycle; + this.activePermissions = activePermissions; + this.nanosToWait = nanosToWait; + } + } + + /** + * Enhanced {@link Metrics} with some implementation specific details + */ + public final class AtomicRateLimiterMetrics implements Metrics { + private AtomicRateLimiterMetrics() { + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public int getNumberOfWaitingThreads() { + return waitingThreads.get(); + } + } +} diff --git a/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java b/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java index 2a74e591af..1dd10a2760 100644 --- a/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java +++ b/src/main/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistry.java @@ -48,7 +48,7 @@ public RateLimiter rateLimiter(final String name, final RateLimiterConfig rateLi requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); return rateLimiters.computeIfAbsent( name, - limitName -> new SemaphoreBasedRateLimiterImpl(name, rateLimiterConfig) + limitName -> new SemaphoreBasedRateLimiter(name, rateLimiterConfig) ); } @@ -64,7 +64,7 @@ public RateLimiter rateLimiter(final String name, final Supplier { RateLimiterConfig rateLimiterConfig = rateLimiterConfigSupplier.get(); requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); - return new SemaphoreBasedRateLimiterImpl(limitName, rateLimiterConfig); + return new SemaphoreBasedRateLimiter(limitName, rateLimiterConfig); } ); } diff --git a/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java b/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiter.java similarity index 93% rename from src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java rename to src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiter.java index 832c9af529..63f5628979 100644 --- a/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImpl.java +++ b/src/main/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiter.java @@ -18,7 +18,7 @@ * and scheduler that will refresh permissions * after each {@link RateLimiterConfig#limitRefreshPeriod}. */ -public class SemaphoreBasedRateLimiterImpl implements RateLimiter { +public class SemaphoreBasedRateLimiter implements RateLimiter { private static final String NAME_MUST_NOT_BE_NULL = "Name must not be null"; private static final String CONFIG_MUST_NOT_BE_NULL = "RateLimiterConfig must not be null"; @@ -35,7 +35,7 @@ public class SemaphoreBasedRateLimiterImpl implements RateLimiter { * @param name the name of the RateLimiter * @param rateLimiterConfig The RateLimiter configuration. */ - public SemaphoreBasedRateLimiterImpl(final String name, final RateLimiterConfig rateLimiterConfig) { + public SemaphoreBasedRateLimiter(final String name, final RateLimiterConfig rateLimiterConfig) { this(name, rateLimiterConfig, null); } @@ -46,8 +46,8 @@ public SemaphoreBasedRateLimiterImpl(final String name, final RateLimiterConfig * @param rateLimiterConfig The RateLimiter configuration. * @param scheduler executor that will refresh permissions */ - public SemaphoreBasedRateLimiterImpl(String name, RateLimiterConfig rateLimiterConfig, - ScheduledExecutorService scheduler) { + public SemaphoreBasedRateLimiter(String name, RateLimiterConfig rateLimiterConfig, + ScheduledExecutorService scheduler) { this.name = requireNonNull(name, NAME_MUST_NOT_BE_NULL); this.rateLimiterConfig = requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL); diff --git a/src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java b/src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java new file mode 100644 index 0000000000..cca265114f --- /dev/null +++ b/src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java @@ -0,0 +1,178 @@ +package javaslang.ratelimiter.internal; + +import static java.lang.System.nanoTime; +import static java.lang.Thread.currentThread; +import static java.util.concurrent.locks.LockSupport.parkNanos; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +/** + * @author bstorozhuk + */ +public class TimeBasedRateLimiter implements RateLimiter { + + private final String name; + private final RateLimiterConfig rateLimiterConfig; + private final long cyclePeriodInNanos; + private final int permissionsPerCycle; + private final ReentrantLock lock; + private final AtomicInteger waitingThreads; + private long activeCycle; + private volatile int activePermissions; + + public TimeBasedRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { + this.name = name; + this.rateLimiterConfig = rateLimiterConfig; + + cyclePeriodInNanos = rateLimiterConfig.getLimitRefreshPeriod().toNanos(); + permissionsPerCycle = rateLimiterConfig.getLimitForPeriod(); + + activeCycle = nanoTime() / cyclePeriodInNanos; + waitingThreads = new AtomicInteger(0); + lock = new ReentrantLock(false); + + activePermissions = permissionsPerCycle; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean getPermission(final Duration timeoutDuration) { + Supplier permissionSupplier = () -> { + long currentNanos = nanoTime(); + long currentCycle = currentNanos / cyclePeriodInNanos; +// System.out.println(MessageFormat.format( +// "Thread {0}: START activeCycle={1}; permissions={2}; currentNanos={3}; currentCycle={4};", +// currentThread().getId(), activeCycle, activePermissions, currentNanos, currentCycle) +// ); + if (activeCycle != currentCycle) { + refreshLimiterState(currentCycle); + } + return acquirePermission(currentNanos, timeoutDuration); + }; + return executeConcurrently(permissionSupplier); + } + + private void refreshLimiterState(final long currentCycle) { + assert lock.isHeldByCurrentThread(); + activeCycle = currentCycle; + activePermissions = Integer.min(activePermissions + permissionsPerCycle, permissionsPerCycle); +// System.out.println(MessageFormat.format( +// "Thread {0}: AFTER REFRESH activeCycle={1}; permissions={2}; currentCycle={3};", +// currentThread().getId(), activeCycle, activePermissions, currentCycle) +// ); + } + + private boolean acquirePermission(final long currentNanos, final Duration timeoutDuration) { + assert lock.isHeldByCurrentThread(); + long currentCycle = currentNanos / cyclePeriodInNanos; + long timeoutInNanos = timeoutDuration.toNanos(); + long nanosToWait = nanosToWaitForPermission(currentNanos, currentCycle); + if (timeoutInNanos < nanosToWait) { + waitForPermission(timeoutInNanos); + return false; + } + activePermissions--; + if (nanosToWait <= 0) { +// System.out.println(MessageFormat.format( +// "Thread {0}: ACQUIRE IMMEDIATELY activeCycle={1}; permissions={2}; currentCycle={3}; nanosToWait={4};", +// currentThread().getId(), activeCycle, activePermissions, currentCycle, nanosToWait) +// ); + return true; + } + lock.unlock(); + return waitForPermission(nanosToWait); + } + + private long nanosToWaitForPermission(final long currentNanos, final long currentCycle) { + if (activePermissions > 0) { + return 0L; + } + long nextCycleTimeInNanos = (currentCycle + 1) * cyclePeriodInNanos; + long nanosToNextCycle = nextCycleTimeInNanos - currentNanos; + int fullCyclesToWait = (-activePermissions) / permissionsPerCycle; + return (fullCyclesToWait * cyclePeriodInNanos) + nanosToNextCycle; + } + + private boolean waitForPermission(final long nanosToWait) { +// System.out.println(MessageFormat.format( +// "Thread {0}: WAIT activeCycle={1}; permissions={2}; nanosToWait={3};", +// currentThread().getId(), activeCycle, activePermissions, nanosToWait) +// ); + waitingThreads.incrementAndGet(); + long deadline = nanoTime() + nanosToWait; + while (nanoTime() < deadline || currentThread().isInterrupted()) { + long sleepBlockDuration = deadline - nanoTime(); + parkNanos(sleepBlockDuration); + } + waitingThreads.decrementAndGet(); + return !currentThread().isInterrupted(); + } + + private T executeConcurrently(final Supplier permissionSupplier) { + lock.lock(); + try { + return permissionSupplier.get(); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + @Override + public String getName() { + return name; + } + + @Override + public RateLimiterConfig getRateLimiterConfig() { + return rateLimiterConfig; + } + + @Override + public Metrics getMetrics() { + return null; + } + + /** + * Enhanced {@link Metrics} with some implementation specific details + */ + public final class TimeBasedRateLimiterMetrics implements Metrics { + private TimeBasedRateLimiterMetrics() { + } + + /** + * {@inheritDoc} + * + * @return + */ + @Override + public int getNumberOfWaitingThreads() { + return waitingThreads.get(); + } + + /** + * Returns the estimated time in nanos to wait for permission. + *

+ *

This method is typically used for debugging and testing purposes. + * + * @return the estimated time in nanos to wait for permission. + */ + public long nanosToWait() { + long currentNanos = nanoTime(); + long currentCycle = currentNanos / cyclePeriodInNanos; + if (currentCycle == activeCycle) { + return 0; + } + return nanosToWaitForPermission(currentNanos, currentCycle); + } + } +} diff --git a/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java index ba8ca819a9..ce0aa4621f 100644 --- a/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java +++ b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java @@ -18,6 +18,7 @@ import java.util.function.Supplier; +@SuppressWarnings("unchecked") public class InMemoryRateLimiterRegistryTest { private static final int LIMIT = 50; diff --git a/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java b/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java index 24da434548..485290dabb 100644 --- a/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java +++ b/src/test/java/javaslang/ratelimiter/internal/SemaphoreBasedRateLimiterImplTest.java @@ -62,7 +62,7 @@ public void init() { public void rateLimiterCreationWithProvidedScheduler() throws Exception { ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); RateLimiterConfig configSpy = spy(config); - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", configSpy, scheduledExecutorService); ArgumentCaptor refreshLimitRunnableCaptor = ArgumentCaptor.forClass(Runnable.class); verify(scheduledExecutorService) @@ -93,7 +93,7 @@ public void rateLimiterCreationWithProvidedScheduler() throws Exception { @Test public void rateLimiterCreationWithDefaultScheduler() throws Exception { - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config); awaitImpatiently().atMost(FIVE_HUNDRED_MILLISECONDS) .until(() -> limit.getPermission(ZERO), equalTo(false)); awaitImpatiently().atMost(110, TimeUnit.MILLISECONDS) @@ -104,10 +104,10 @@ public void rateLimiterCreationWithDefaultScheduler() throws Exception { public void getPermissionAndMetrics() throws Exception { ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); RateLimiterConfig configSpy = spy(config); - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); - SemaphoreBasedRateLimiterImpl.SemaphoreBasedRateLimiterMetrics detailedMetrics = limit.getDetailedMetrics(); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", configSpy, scheduledExecutorService); + SemaphoreBasedRateLimiter.SemaphoreBasedRateLimiterMetrics detailedMetrics = limit.getDetailedMetrics(); - SynchronousQueue synchronousQueue = new SynchronousQueue(); + SynchronousQueue synchronousQueue = new SynchronousQueue<>(); Thread thread = new Thread(() -> { run(() -> { for (int i = 0; i < LIMIT; i++) { @@ -151,7 +151,7 @@ public void getPermissionAndMetrics() throws Exception { public void getPermissionInterruption() throws Exception { ScheduledExecutorService scheduledExecutorService = mock(ScheduledExecutorService.class); RateLimiterConfig configSpy = spy(config); - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", configSpy, scheduledExecutorService); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", configSpy, scheduledExecutorService); limit.getPermission(ZERO); limit.getPermission(ZERO); @@ -177,14 +177,14 @@ public void getPermissionInterruption() throws Exception { @Test public void getName() throws Exception { ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler); then(limit.getName()).isEqualTo("test"); } @Test public void getMetrics() throws Exception { ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler); RateLimiter.Metrics metrics = limit.getMetrics(); then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); } @@ -192,15 +192,15 @@ public void getMetrics() throws Exception { @Test public void getRateLimiterConfig() throws Exception { ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler); then(limit.getRateLimiterConfig()).isEqualTo(config); } @Test public void getDetailedMetrics() throws Exception { ScheduledExecutorService scheduler = mock(ScheduledExecutorService.class); - SemaphoreBasedRateLimiterImpl limit = new SemaphoreBasedRateLimiterImpl("test", config, scheduler); - SemaphoreBasedRateLimiterImpl.SemaphoreBasedRateLimiterMetrics metrics = limit.getDetailedMetrics(); + SemaphoreBasedRateLimiter limit = new SemaphoreBasedRateLimiter("test", config, scheduler); + SemaphoreBasedRateLimiter.SemaphoreBasedRateLimiterMetrics metrics = limit.getDetailedMetrics(); then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); then(metrics.getAvailablePermits()).isEqualTo(2); } @@ -209,13 +209,13 @@ public void getDetailedMetrics() throws Exception { public void constructionWithNullName() throws Exception { exception.expect(NullPointerException.class); exception.expectMessage(NAME_MUST_NOT_BE_NULL); - new SemaphoreBasedRateLimiterImpl(null, config, null); + new SemaphoreBasedRateLimiter(null, config, null); } @Test public void constructionWithNullConfig() throws Exception { exception.expect(NullPointerException.class); exception.expectMessage(CONFIG_MUST_NOT_BE_NULL); - new SemaphoreBasedRateLimiterImpl("test", null, null); + new SemaphoreBasedRateLimiter("test", null, null); } } \ No newline at end of file diff --git a/src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java b/src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java new file mode 100644 index 0000000000..f45a2b5dbb --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java @@ -0,0 +1,53 @@ +package javaslang.ratelimiter.internal; + +import javaslang.ratelimiter.RateLimiter; +import javaslang.ratelimiter.RateLimiterConfig; +import org.junit.Test; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author bstorozhuk + */ +public class TimeBasedRateLimiterTest { + + public static final int N_THREADS = 4; + public static final AtomicLong counter = new AtomicLong(0); + public static final AtomicBoolean required = new AtomicBoolean(false); + + @Test + public void test() throws Exception { + RateLimiterConfig config = RateLimiterConfig.builder() + .limitForPeriod(10) + .limitRefreshPeriod(Duration.ofMillis(500)) + .timeoutDuration(Duration.ZERO) + .build(); + RateLimiter limiter = new AtomicRateLimiter("test", config); + + Runnable guarded = () -> { + if (limiter.getPermission(Duration.ofSeconds(10))) { + counter.incrementAndGet(); + } + }; + + ExecutorService pool = Executors.newFixedThreadPool(N_THREADS); + for (int i = 0; i < N_THREADS; i++) { + pool.execute(() -> { + while (true) { + if (required.get()) { + guarded.run(); + } + } + }); + } + required.set(true); + Thread.sleep(2200); + required.set(false); + System.out.println("COUNTER: " + counter); + pool.shutdownNow(); + } +} \ No newline at end of file From e9c67b6cb17a62827c5ae8917f2b9fe21c939cd6 Mon Sep 17 00:00:00 2001 From: bstorozhuk Date: Fri, 2 Dec 2016 15:38:17 +0200 Subject: [PATCH 4/7] Issue #12 AtomicRateLimiter tests and additional documentation --- build.gradle | 3 + ...016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt | 41 --- .../circuitbreaker/RateLimiterBenchmark.java | 14 -- .../ratelimiter/RateLimiterConfig.java | 3 +- .../internal/AtomicRateLimiter.java | 56 ++++- .../internal/TimeBasedRateLimiter.java | 178 ------------- .../ratelimiter/RateLimiterConfigTest.java | 2 +- .../internal/AtomicRateLimiterTest.java | 233 ++++++++++++++++++ .../InMemoryRateLimiterRegistryTest.java | 3 +- .../internal/TimeBasedRateLimiterTest.java | 53 ---- 10 files changed, 286 insertions(+), 300 deletions(-) delete mode 100644 src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt delete mode 100644 src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java create mode 100644 src/test/java/javaslang/ratelimiter/internal/AtomicRateLimiterTest.java delete mode 100644 src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java diff --git a/build.gradle b/build.gradle index d022610ffa..ca4369842c 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,9 @@ dependencies { testCompile "ch.qos.logback:logback-classic:0.9.26" testCompile "io.dropwizard.metrics:metrics-healthchecks:3.1.2" testCompile "org.mockito:mockito-core:1.10.19" + testCompile "org.powermock:powermock:1.6.6" + testCompile "org.powermock:powermock-api-mockito:1.6.6" + testCompile "org.powermock:powermock-module-junit4:1.6.6" testCompile "io.projectreactor:reactor-core:2.5.0.M2" testCompile "com.jayway.awaitility:awaitility:1.7.0" diff --git a/src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt b/src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt deleted file mode 100644 index c9c091354e..0000000000 --- a/src/jmh/java/javaslang/circuitbreaker/30-Nov-2016-i7-I7-5557U-OSX-Java-1.8.0_112-x64.txt +++ /dev/null @@ -1,41 +0,0 @@ -Benchmark Mode Cnt Score Error Units - -RateLimiterBenchmark.atomicPermission thrpt 10 7.274 ± 0.132 ops/us -RateLimiterBenchmark.semaphoreBasedPermission thrpt 10 17.335 ± 3.441 ops/us -RateLimiterBenchmark.timeBasedPermission thrpt 10 3.522 ± 0.495 ops/us - -RateLimiterBenchmark.atomicPermission avgt 10 0.294 ± 0.038 us/op -RateLimiterBenchmark.semaphoreBasedPermission avgt 10 0.120 ± 0.018 us/op -RateLimiterBenchmark.timeBasedPermission avgt 10 0.562 ± 0.045 us/op - -RateLimiterBenchmark.atomicPermission sample 535765 1.480 ± 0.036 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p0.00 sample 0.040 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p0.50 sample 0.383 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p0.90 sample 4.288 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p0.95 sample 7.368 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p0.99 sample 14.080 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p0.999 sample 18.048 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p0.9999 sample 58.449 us/op -RateLimiterBenchmark.atomicPermission:atomicPermission·p1.00 sample 1654.784 us/op -RateLimiterBenchmark.semaphoreBasedPermission sample 635614 0.166 ± 0.010 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.00 sample 0.001 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.50 sample 0.135 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.90 sample 0.219 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.95 sample 0.236 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.99 sample 0.333 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.999 sample 2.468 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p0.9999 sample 15.519 us/op -RateLimiterBenchmark.semaphoreBasedPermission:semaphoreBasedPermission·p1.00 sample 1372.160 us/op -RateLimiterBenchmark.timeBasedPermission sample 553560 0.800 ± 0.053 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.00 sample 0.054 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.50 sample 0.550 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.90 sample 0.749 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.95 sample 0.826 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.99 sample 8.256 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.999 sample 33.920 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p0.9999 sample 160.221 us/op -RateLimiterBenchmark.timeBasedPermission:timeBasedPermission·p1.00 sample 5742.592 us/op - -RateLimiterBenchmark.atomicPermission ss 10 17.140 ± 5.640 us/op -RateLimiterBenchmark.semaphoreBasedPermission ss 10 9.724 ± 4.602 us/op -RateLimiterBenchmark.timeBasedPermission ss 10 26.875 ± 10.869 us/op diff --git a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java index 80cdcc75a8..fbdc3c13b3 100644 --- a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java @@ -4,7 +4,6 @@ import javaslang.ratelimiter.RateLimiterConfig; import javaslang.ratelimiter.internal.AtomicRateLimiter; import javaslang.ratelimiter.internal.SemaphoreBasedRateLimiter; -import javaslang.ratelimiter.internal.TimeBasedRateLimiter; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -33,11 +32,9 @@ public class RateLimiterBenchmark { private static final int THREAD_COUNT = 2; private RateLimiter semaphoreBasedRateLimiter; - private RateLimiter timeBasedRateLimiter; private RateLimiter atomicRateLimiter; private Supplier semaphoreGuardedSupplier; - private Supplier timeGuardedSupplier; private Supplier atomicGuardedSupplier; @Setup @@ -48,7 +45,6 @@ public void setUp() { .timeoutDuration(Duration.ofSeconds(5)) .build(); semaphoreBasedRateLimiter = new SemaphoreBasedRateLimiter("semaphoreBased", rateLimiterConfig); - timeBasedRateLimiter = new TimeBasedRateLimiter("timeBased", rateLimiterConfig); atomicRateLimiter = new AtomicRateLimiter("atomicBased", rateLimiterConfig); Supplier stringSupplier = () -> { @@ -56,7 +52,6 @@ public void setUp() { return "Hello Benchmark"; }; semaphoreGuardedSupplier = RateLimiter.decorateSupplier(semaphoreBasedRateLimiter, stringSupplier); - timeGuardedSupplier = RateLimiter.decorateSupplier(timeBasedRateLimiter, stringSupplier); atomicGuardedSupplier = RateLimiter.decorateSupplier(atomicRateLimiter, stringSupplier); } @@ -69,15 +64,6 @@ public String semaphoreBasedPermission() { return semaphoreGuardedSupplier.get(); } - @Benchmark - @Threads(value = THREAD_COUNT) - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - public String timeBasedPermission() { - return timeGuardedSupplier.get(); - } - @Benchmark @Threads(value = THREAD_COUNT) @Warmup(iterations = WARMUP_COUNT) diff --git a/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java index 21fa3d93b8..f725e97703 100644 --- a/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java +++ b/src/main/java/javaslang/ratelimiter/RateLimiterConfig.java @@ -7,8 +7,7 @@ public class RateLimiterConfig { private static final String TIMEOUT_DURATION_MUST_NOT_BE_NULL = "TimeoutDuration must not be null"; private static final String LIMIT_REFRESH_PERIOD_MUST_NOT_BE_NULL = "LimitRefreshPeriod must not be null"; - - private static final Duration ACCEPTABLE_REFRESH_PERIOD = Duration.ofNanos(1L); // TODO: use jmh to find real one + private static final Duration ACCEPTABLE_REFRESH_PERIOD = Duration.ofNanos(1L); private final Duration timeoutDuration; private final Duration limitRefreshPeriod; diff --git a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java index d0cb61ec56..430d7ba841 100644 --- a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java +++ b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java @@ -42,9 +42,7 @@ public AtomicRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { permissionsPerCycle = rateLimiterConfig.getLimitForPeriod(); waitingThreads = new AtomicInteger(0); - long activeCycle = nanoTime() / cyclePeriodInNanos; - int activePermissions = permissionsPerCycle; - state = new AtomicReference<>(new State(activeCycle, activePermissions, 0)); + state = new AtomicReference<>(new State(0, 0, 0)); } /** @@ -69,7 +67,7 @@ public boolean getPermission(final Duration timeoutDuration) { * @return next {@link State} */ private State calculateNextState(final long timeoutInNanos, final State activeState) { - long currentNanos = nanoTime(); + long currentNanos = currentNanoTime(); long currentCycle = currentNanos / cyclePeriodInNanos; long nextCycle = activeState.activeCycle; @@ -85,6 +83,7 @@ private State calculateNextState(final long timeoutInNanos, final State activeSt return nextState; } + /** * Calculates time to wait for next permission as * [time to the next cycle] + [duration of full cycles until reserved permissions expire] @@ -147,19 +146,27 @@ private boolean waitForPermissionIfNecessary(final long timeoutInNanos, final lo /** * Parks {@link Thread} for nanosToWait. + *

If the current thread is {@linkplain Thread#interrupted} + * while waiting for a permit then it won't throw {@linkplain InterruptedException}, + * but its interrupt status will be set. * * @param nanosToWait nanoseconds caller need to wait * @return true if caller was not {@link Thread#interrupted} while waiting */ private boolean waitForPermission(final long nanosToWait) { waitingThreads.incrementAndGet(); - long deadline = nanoTime() + nanosToWait; - while (nanoTime() < deadline || currentThread().isInterrupted()) { - long sleepBlockDuration = deadline - nanoTime(); + long deadline = currentNanoTime() + nanosToWait; + boolean wasInterrupted = false; + while (currentNanoTime() < deadline && !wasInterrupted) { + long sleepBlockDuration = deadline - currentNanoTime(); parkNanos(sleepBlockDuration); + wasInterrupted = Thread.interrupted(); } waitingThreads.decrementAndGet(); - return !currentThread().isInterrupted(); + if (wasInterrupted) { + currentThread().interrupt(); + } + return !wasInterrupted; } /** @@ -182,7 +189,7 @@ public RateLimiterConfig getRateLimiterConfig() { * {@inheritDoc} */ @Override - public Metrics getMetrics() { + public AtomicRateLimiterMetrics getMetrics() { return new AtomicRateLimiterMetrics(); } @@ -201,6 +208,7 @@ public Metrics getMetrics() { * */ private static class State { + private final long activeCycle; private final int activePermissions; private final long nanosToWait; @@ -210,12 +218,14 @@ public State(final long activeCycle, final int activePermissions, final long nan this.activePermissions = activePermissions; this.nanosToWait = nanosToWait; } + } /** * Enhanced {@link Metrics} with some implementation specific details */ public final class AtomicRateLimiterMetrics implements Metrics { + private AtomicRateLimiterMetrics() { } @@ -228,5 +238,33 @@ private AtomicRateLimiterMetrics() { public int getNumberOfWaitingThreads() { return waitingThreads.get(); } + + /** + * @return estimated time duration in nanos to wait for the next permission + */ + public long getNanosToWait() { + State currentState = state.get(); + State estimatedState = calculateNextState(-1, currentState); + return estimatedState.nanosToWait; + } + + /** + * Estimates count of permissions available permissions. + * Can be negative if some permissions where reserved. + * + * @return estimated count of permissions + */ + public long getAvailablePermissions() { + State currentState = state.get(); + State estimatedState = calculateNextState(-1, currentState); + return estimatedState.activePermissions; + } + } + + /** + * Created only for test purposes. Simply calls {@link System#nanoTime()} + */ + private long currentNanoTime() { + return nanoTime(); } } diff --git a/src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java b/src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java deleted file mode 100644 index cca265114f..0000000000 --- a/src/main/java/javaslang/ratelimiter/internal/TimeBasedRateLimiter.java +++ /dev/null @@ -1,178 +0,0 @@ -package javaslang.ratelimiter.internal; - -import static java.lang.System.nanoTime; -import static java.lang.Thread.currentThread; -import static java.util.concurrent.locks.LockSupport.parkNanos; - -import javaslang.ratelimiter.RateLimiter; -import javaslang.ratelimiter.RateLimiterConfig; - -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Supplier; - -/** - * @author bstorozhuk - */ -public class TimeBasedRateLimiter implements RateLimiter { - - private final String name; - private final RateLimiterConfig rateLimiterConfig; - private final long cyclePeriodInNanos; - private final int permissionsPerCycle; - private final ReentrantLock lock; - private final AtomicInteger waitingThreads; - private long activeCycle; - private volatile int activePermissions; - - public TimeBasedRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { - this.name = name; - this.rateLimiterConfig = rateLimiterConfig; - - cyclePeriodInNanos = rateLimiterConfig.getLimitRefreshPeriod().toNanos(); - permissionsPerCycle = rateLimiterConfig.getLimitForPeriod(); - - activeCycle = nanoTime() / cyclePeriodInNanos; - waitingThreads = new AtomicInteger(0); - lock = new ReentrantLock(false); - - activePermissions = permissionsPerCycle; - } - - /** - * {@inheritDoc} - */ - @Override - public boolean getPermission(final Duration timeoutDuration) { - Supplier permissionSupplier = () -> { - long currentNanos = nanoTime(); - long currentCycle = currentNanos / cyclePeriodInNanos; -// System.out.println(MessageFormat.format( -// "Thread {0}: START activeCycle={1}; permissions={2}; currentNanos={3}; currentCycle={4};", -// currentThread().getId(), activeCycle, activePermissions, currentNanos, currentCycle) -// ); - if (activeCycle != currentCycle) { - refreshLimiterState(currentCycle); - } - return acquirePermission(currentNanos, timeoutDuration); - }; - return executeConcurrently(permissionSupplier); - } - - private void refreshLimiterState(final long currentCycle) { - assert lock.isHeldByCurrentThread(); - activeCycle = currentCycle; - activePermissions = Integer.min(activePermissions + permissionsPerCycle, permissionsPerCycle); -// System.out.println(MessageFormat.format( -// "Thread {0}: AFTER REFRESH activeCycle={1}; permissions={2}; currentCycle={3};", -// currentThread().getId(), activeCycle, activePermissions, currentCycle) -// ); - } - - private boolean acquirePermission(final long currentNanos, final Duration timeoutDuration) { - assert lock.isHeldByCurrentThread(); - long currentCycle = currentNanos / cyclePeriodInNanos; - long timeoutInNanos = timeoutDuration.toNanos(); - long nanosToWait = nanosToWaitForPermission(currentNanos, currentCycle); - if (timeoutInNanos < nanosToWait) { - waitForPermission(timeoutInNanos); - return false; - } - activePermissions--; - if (nanosToWait <= 0) { -// System.out.println(MessageFormat.format( -// "Thread {0}: ACQUIRE IMMEDIATELY activeCycle={1}; permissions={2}; currentCycle={3}; nanosToWait={4};", -// currentThread().getId(), activeCycle, activePermissions, currentCycle, nanosToWait) -// ); - return true; - } - lock.unlock(); - return waitForPermission(nanosToWait); - } - - private long nanosToWaitForPermission(final long currentNanos, final long currentCycle) { - if (activePermissions > 0) { - return 0L; - } - long nextCycleTimeInNanos = (currentCycle + 1) * cyclePeriodInNanos; - long nanosToNextCycle = nextCycleTimeInNanos - currentNanos; - int fullCyclesToWait = (-activePermissions) / permissionsPerCycle; - return (fullCyclesToWait * cyclePeriodInNanos) + nanosToNextCycle; - } - - private boolean waitForPermission(final long nanosToWait) { -// System.out.println(MessageFormat.format( -// "Thread {0}: WAIT activeCycle={1}; permissions={2}; nanosToWait={3};", -// currentThread().getId(), activeCycle, activePermissions, nanosToWait) -// ); - waitingThreads.incrementAndGet(); - long deadline = nanoTime() + nanosToWait; - while (nanoTime() < deadline || currentThread().isInterrupted()) { - long sleepBlockDuration = deadline - nanoTime(); - parkNanos(sleepBlockDuration); - } - waitingThreads.decrementAndGet(); - return !currentThread().isInterrupted(); - } - - private T executeConcurrently(final Supplier permissionSupplier) { - lock.lock(); - try { - return permissionSupplier.get(); - } finally { - if (lock.isHeldByCurrentThread()) { - lock.unlock(); - } - } - } - - @Override - public String getName() { - return name; - } - - @Override - public RateLimiterConfig getRateLimiterConfig() { - return rateLimiterConfig; - } - - @Override - public Metrics getMetrics() { - return null; - } - - /** - * Enhanced {@link Metrics} with some implementation specific details - */ - public final class TimeBasedRateLimiterMetrics implements Metrics { - private TimeBasedRateLimiterMetrics() { - } - - /** - * {@inheritDoc} - * - * @return - */ - @Override - public int getNumberOfWaitingThreads() { - return waitingThreads.get(); - } - - /** - * Returns the estimated time in nanos to wait for permission. - *

- *

This method is typically used for debugging and testing purposes. - * - * @return the estimated time in nanos to wait for permission. - */ - public long nanosToWait() { - long currentNanos = nanoTime(); - long currentCycle = currentNanos / cyclePeriodInNanos; - if (currentCycle == activeCycle) { - return 0; - } - return nanosToWaitForPermission(currentNanos, currentCycle); - } - } -} diff --git a/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java b/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java index d0b0532855..b2e93a3d39 100644 --- a/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java +++ b/src/test/java/javaslang/ratelimiter/RateLimiterConfigTest.java @@ -76,7 +76,7 @@ public void builderRefreshPeriodTooShort() throws Exception { exception.expectMessage("RefreshPeriod is too short"); RateLimiterConfig.builder() .timeoutDuration(TIMEOUT) - .limitRefreshPeriod(Duration.ofNanos(499L)) + .limitRefreshPeriod(Duration.ZERO) .limitForPeriod(LIMIT) .build(); } diff --git a/src/test/java/javaslang/ratelimiter/internal/AtomicRateLimiterTest.java b/src/test/java/javaslang/ratelimiter/internal/AtomicRateLimiterTest.java new file mode 100644 index 0000000000..55e9dc2779 --- /dev/null +++ b/src/test/java/javaslang/ratelimiter/internal/AtomicRateLimiterTest.java @@ -0,0 +1,233 @@ +package javaslang.ratelimiter.internal; + +import static com.jayway.awaitility.Awaitility.await; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.assertj.core.api.BDDAssertions.then; +import static org.hamcrest.CoreMatchers.equalTo; + +import com.jayway.awaitility.core.ConditionFactory; +import javaslang.ratelimiter.RateLimiterConfig; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.api.mockito.PowerMockito; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(AtomicRateLimiter.class) +public class AtomicRateLimiterTest { + + public static final String LIMITER_NAME = "test"; + public static final long CYCLE_IN_NANOS = 500_000_000L; + public static final long POLL_INTERVAL_IN_NANOS = 2_000_000L; + private RateLimiterConfig rateLimiterConfig; + private AtomicRateLimiter rateLimiter; + private AtomicRateLimiter.AtomicRateLimiterMetrics metrics; + + private static ConditionFactory awaitImpatiently() { + return await() + .pollDelay(1, TimeUnit.MICROSECONDS) + .pollInterval(POLL_INTERVAL_IN_NANOS, TimeUnit.NANOSECONDS); + } + + private void setTimeOnNanos(long nanoTime) throws Exception { + PowerMockito.doReturn(nanoTime) + .when(rateLimiter, "currentNanoTime"); + } + + @Before + public void setup() { + rateLimiterConfig = RateLimiterConfig.builder() + .limitForPeriod(1) + .limitRefreshPeriod(Duration.ofNanos(CYCLE_IN_NANOS)) + .timeoutDuration(Duration.ZERO) + .build(); + AtomicRateLimiter testLimiter = new AtomicRateLimiter(LIMITER_NAME, rateLimiterConfig); + rateLimiter = PowerMockito.spy(testLimiter); + metrics = rateLimiter.getMetrics(); + } + + @Test + public void acquireAndRefresh() throws Exception { + setTimeOnNanos(CYCLE_IN_NANOS); + boolean permission = rateLimiter.getPermission(Duration.ZERO); + then(permission).isTrue(); + then(metrics.getAvailablePermissions()).isEqualTo(0); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + boolean secondPermission = rateLimiter.getPermission(Duration.ZERO); + then(secondPermission).isFalse(); + then(metrics.getAvailablePermissions()).isEqualTo(0); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + + setTimeOnNanos(CYCLE_IN_NANOS * 2); + boolean thirdPermission = rateLimiter.getPermission(Duration.ZERO); + then(thirdPermission).isTrue(); + then(metrics.getAvailablePermissions()).isEqualTo(0); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + boolean fourthPermission = rateLimiter.getPermission(Duration.ZERO); + then(fourthPermission).isFalse(); + then(metrics.getAvailablePermissions()).isEqualTo(0); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + } + + @Test + public void reserveAndRefresh() throws Exception { + setTimeOnNanos(CYCLE_IN_NANOS); + boolean permission = rateLimiter.getPermission(Duration.ZERO); + then(permission).isTrue(); + then(metrics.getAvailablePermissions()).isEqualTo(0); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + + AtomicReference reservedPermission = new AtomicReference<>(null); + Thread caller = new Thread( + () -> reservedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS)))); + caller.setDaemon(true); + caller.start(); + awaitImpatiently() + .atMost(10, MILLISECONDS) + .until(caller::getState, equalTo(Thread.State.TIMED_WAITING)); + then(metrics.getAvailablePermissions()).isEqualTo(-1); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS + CYCLE_IN_NANOS); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(1); + + setTimeOnNanos(CYCLE_IN_NANOS * 2 + 10); + awaitImpatiently() + .atMost(CYCLE_IN_NANOS, NANOSECONDS) + .until(reservedPermission::get, equalTo(true)); + + then(metrics.getAvailablePermissions()).isEqualTo(0); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS - 10); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + } + + @Test + public void reserveFewThenSkipCyclesBeforeRefresh() throws Exception { + setTimeOnNanos(CYCLE_IN_NANOS); + boolean permission = rateLimiter.getPermission(Duration.ZERO); + then(permission).isTrue(); + then(metrics.getAvailablePermissions()).isEqualTo(0); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + + AtomicReference firstReservedPermission = new AtomicReference<>(null); + Thread firstCaller = new Thread( + () -> firstReservedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS)))); + firstCaller.setDaemon(true); + firstCaller.start(); + awaitImpatiently() + .atMost(50, MILLISECONDS) + .until(firstCaller::getState, equalTo(Thread.State.TIMED_WAITING)); + then(metrics.getAvailablePermissions()).isEqualTo(-1); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS * 2); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(1); + + + AtomicReference secondReservedPermission = new AtomicReference<>(null); + Thread secondCaller = new Thread( + () -> secondReservedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS * 2)))); + secondCaller.setDaemon(true); + secondCaller.start(); + awaitImpatiently() + .atMost(50, MILLISECONDS) + .until(secondCaller::getState, equalTo(Thread.State.TIMED_WAITING)); + then(metrics.getAvailablePermissions()).isEqualTo(-2); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS * 3); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(2); + + setTimeOnNanos(CYCLE_IN_NANOS * 6 + 10); + awaitImpatiently() + .atMost(CYCLE_IN_NANOS + POLL_INTERVAL_IN_NANOS, NANOSECONDS) + .until(firstReservedPermission::get, equalTo(true)); + awaitImpatiently() + .atMost(CYCLE_IN_NANOS * 2 + POLL_INTERVAL_IN_NANOS, NANOSECONDS) + .until(secondReservedPermission::get, equalTo(true)); + then(metrics.getAvailablePermissions()).isEqualTo(1L); + then(metrics.getNanosToWait()).isEqualTo(0L); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + } + + @Test + public void rejectedByTimeout() throws Exception { + setTimeOnNanos(CYCLE_IN_NANOS); + boolean permission = rateLimiter.getPermission(Duration.ZERO); + then(permission).isTrue(); + then(metrics.getAvailablePermissions()).isEqualTo(0L); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + + AtomicReference declinedPermission = new AtomicReference<>(null); + Thread caller = new Thread( + () -> declinedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS - 1)))); + caller.setDaemon(true); + caller.start(); + + awaitImpatiently() + .atMost(100, MILLISECONDS) + .until(caller::getState, equalTo(Thread.State.TIMED_WAITING)); + then(metrics.getAvailablePermissions()).isEqualTo(0L); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(1); + + setTimeOnNanos(CYCLE_IN_NANOS * 2 - 1); + awaitImpatiently() + .atMost(CYCLE_IN_NANOS + POLL_INTERVAL_IN_NANOS, NANOSECONDS) + .until(declinedPermission::get, equalTo(false)); + then(metrics.getAvailablePermissions()).isEqualTo(0L); + then(metrics.getNanosToWait()).isEqualTo(1L); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + } + + @Test + public void waitingThreadIsInterrupted() throws Exception { + setTimeOnNanos(CYCLE_IN_NANOS); + boolean permission = rateLimiter.getPermission(Duration.ZERO); + then(permission).isTrue(); + then(metrics.getAvailablePermissions()).isEqualTo(0L); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + + AtomicReference declinedPermission = new AtomicReference<>(null); + AtomicBoolean wasInterrupted = new AtomicBoolean(false); + Thread caller = new Thread( + () -> { + declinedPermission.set(rateLimiter.getPermission(Duration.ofNanos(CYCLE_IN_NANOS - 1))); + wasInterrupted.set(Thread.currentThread().isInterrupted()); + } + ); + caller.isDaemon(); + caller.start(); + + awaitImpatiently() + .atMost(100, MILLISECONDS) + .until(caller::getState, equalTo(Thread.State.TIMED_WAITING)); + then(metrics.getAvailablePermissions()).isEqualTo(0L); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(1); + + caller.interrupt(); + awaitImpatiently() + .atMost(CYCLE_IN_NANOS + POLL_INTERVAL_IN_NANOS, NANOSECONDS) + .until(declinedPermission::get, equalTo(false)); + then(wasInterrupted.get()).isTrue(); + then(metrics.getAvailablePermissions()).isEqualTo(0L); + then(metrics.getNanosToWait()).isEqualTo(CYCLE_IN_NANOS); + then(metrics.getNumberOfWaitingThreads()).isEqualTo(0); + } + + @Test + public void namePropagation() { + then(rateLimiter.getName()).isEqualTo(LIMITER_NAME); + } + + @Test + public void configPropagation() { + then(rateLimiter.getRateLimiterConfig()).isEqualTo(rateLimiterConfig); + } +} \ No newline at end of file diff --git a/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java index ce0aa4621f..3df57b5799 100644 --- a/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java +++ b/src/test/java/javaslang/ratelimiter/internal/InMemoryRateLimiterRegistryTest.java @@ -17,8 +17,6 @@ import java.time.Duration; import java.util.function.Supplier; - -@SuppressWarnings("unchecked") public class InMemoryRateLimiterRegistryTest { private static final int LIMIT = 50; @@ -51,6 +49,7 @@ public void rateLimiterPositive() throws Exception { } @Test + @SuppressWarnings("unchecked") public void rateLimiterPositiveWithSupplier() throws Exception { RateLimiterRegistry registry = new InMemoryRateLimiterRegistry(config); Supplier rateLimiterConfigSupplier = mock(Supplier.class); diff --git a/src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java b/src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java deleted file mode 100644 index f45a2b5dbb..0000000000 --- a/src/test/java/javaslang/ratelimiter/internal/TimeBasedRateLimiterTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package javaslang.ratelimiter.internal; - -import javaslang.ratelimiter.RateLimiter; -import javaslang.ratelimiter.RateLimiterConfig; -import org.junit.Test; - -import java.time.Duration; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - -/** - * @author bstorozhuk - */ -public class TimeBasedRateLimiterTest { - - public static final int N_THREADS = 4; - public static final AtomicLong counter = new AtomicLong(0); - public static final AtomicBoolean required = new AtomicBoolean(false); - - @Test - public void test() throws Exception { - RateLimiterConfig config = RateLimiterConfig.builder() - .limitForPeriod(10) - .limitRefreshPeriod(Duration.ofMillis(500)) - .timeoutDuration(Duration.ZERO) - .build(); - RateLimiter limiter = new AtomicRateLimiter("test", config); - - Runnable guarded = () -> { - if (limiter.getPermission(Duration.ofSeconds(10))) { - counter.incrementAndGet(); - } - }; - - ExecutorService pool = Executors.newFixedThreadPool(N_THREADS); - for (int i = 0; i < N_THREADS; i++) { - pool.execute(() -> { - while (true) { - if (required.get()) { - guarded.run(); - } - } - }); - } - required.set(true); - Thread.sleep(2200); - required.set(false); - System.out.println("COUNTER: " + counter); - pool.shutdownNow(); - } -} \ No newline at end of file From 576aad14908192194969dcf80050a31523874675 Mon Sep 17 00:00:00 2001 From: bstorozhuk Date: Fri, 2 Dec 2016 17:02:47 +0200 Subject: [PATCH 5/7] Issue #12 CAS with back-off benchmark --- .../circuitbreaker/RateLimiterBenchmark.java | 56 ++++++++++++++++++- .../circuitbreaker/casWithBackOff.txt | 6 ++ .../internal/AtomicRateLimiter.java | 14 ++--- 3 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt diff --git a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java index fbdc3c13b3..265ff125c3 100644 --- a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java @@ -19,11 +19,12 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; import java.util.function.Supplier; @State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.MICROSECONDS) -@BenchmarkMode(Mode.All) +@BenchmarkMode(Mode.Throughput) public class RateLimiterBenchmark { public static final int FORK_COUNT = 2; @@ -32,7 +33,9 @@ public class RateLimiterBenchmark { private static final int THREAD_COUNT = 2; private RateLimiter semaphoreBasedRateLimiter; - private RateLimiter atomicRateLimiter; + private AtomicRateLimiter atomicRateLimiter; + private AtomicRateLimiter.State state; + private static final Object mutex = new Object(); private Supplier semaphoreGuardedSupplier; private Supplier atomicGuardedSupplier; @@ -46,6 +49,7 @@ public void setUp() { .build(); semaphoreBasedRateLimiter = new SemaphoreBasedRateLimiter("semaphoreBased", rateLimiterConfig); atomicRateLimiter = new AtomicRateLimiter("atomicBased", rateLimiterConfig); + state = atomicRateLimiter.state.get(); Supplier stringSupplier = () -> { Blackhole.consumeCPU(1); @@ -55,6 +59,54 @@ public void setUp() { atomicGuardedSupplier = RateLimiter.decorateSupplier(atomicRateLimiter, stringSupplier); } + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Fork(value = FORK_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public void mutex(Blackhole bh) { + synchronized (mutex) { + state = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); + } + } + + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Fork(value = FORK_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public void atomic(Blackhole bh) { + atomicRateLimiter.state.updateAndGet(state -> { + return atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); + }); + } + + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Fork(value = FORK_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public void atomicBackOf(Blackhole bh) { + AtomicRateLimiter.State prev; + AtomicRateLimiter.State next; + do { + prev = atomicRateLimiter.state.get(); + next = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), prev); + } while (!compareAndSet(prev, next)); + } + + /* + https://arxiv.org/abs/1305.5800 https://dzone.com/articles/wanna-get-faster-wait-bit + */ + public boolean compareAndSet(final AtomicRateLimiter.State current, final AtomicRateLimiter.State next) { + if (atomicRateLimiter.state.compareAndSet(current, next)) { + return true; + } else { + LockSupport.parkNanos(1); + return false; + } + } + @Benchmark @Threads(value = THREAD_COUNT) @Warmup(iterations = WARMUP_COUNT) diff --git a/src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt b/src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt new file mode 100644 index 0000000000..8423672aea --- /dev/null +++ b/src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt @@ -0,0 +1,6 @@ +Benchmark Mode Cnt Score Error Units +RateLimiterBenchmark.atomic thrpt 10 8.016 ± 0.327 ops/us +RateLimiterBenchmark.atomicBackOf thrpt 10 14.104 ± 0.097 ops/us +RateLimiterBenchmark.atomicPermission thrpt 10 7.429 ± 0.299 ops/us +RateLimiterBenchmark.mutex thrpt 10 7.520 ± 0.304 ops/us +RateLimiterBenchmark.semaphoreBasedPermission thrpt 10 17.923 ± 6.043 ops/us diff --git a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java index 430d7ba841..2053824a0c 100644 --- a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java +++ b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java @@ -31,7 +31,7 @@ public class AtomicRateLimiter implements RateLimiter { private final long cyclePeriodInNanos; private final int permissionsPerCycle; private final AtomicInteger waitingThreads; - private final AtomicReference state; + public final AtomicReference state; public AtomicRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { @@ -66,7 +66,7 @@ public boolean getPermission(final Duration timeoutDuration) { * @param activeState current state of {@link AtomicRateLimiter} * @return next {@link State} */ - private State calculateNextState(final long timeoutInNanos, final State activeState) { + public State calculateNextState(final long timeoutInNanos, final State activeState) { long currentNanos = currentNanoTime(); long currentCycle = currentNanos / cyclePeriodInNanos; @@ -93,7 +93,7 @@ private State calculateNextState(final long timeoutInNanos, final State activeSt * @param currentCycle current {@link AtomicRateLimiter} cycle * @return nanoseconds to wait for the next permission */ - private long nanosToWaitForPermission(final int availablePermissions, final long currentNanos, final long currentCycle) { + public long nanosToWaitForPermission(final int availablePermissions, final long currentNanos, final long currentCycle) { if (availablePermissions > 0) { return 0L; } else { @@ -114,7 +114,7 @@ private long nanosToWaitForPermission(final int availablePermissions, final long * @param nanosToWait nanoseconds to wait for the next permission * @return new {@link State} with possibly reserved permissions and time to wait */ - private State reservePermissions(final long timeoutInNanos, final long cycle, final int permissions, final long nanosToWait) { + public State reservePermissions(final long timeoutInNanos, final long cycle, final int permissions, final long nanosToWait) { boolean canAcquireInTime = timeoutInNanos >= nanosToWait; int permissionsWithReservation = permissions; if (canAcquireInTime) { @@ -130,7 +130,7 @@ private State reservePermissions(final long timeoutInNanos, final long cycle, fi * @param nanosToWait nanoseconds caller need to wait * @return true if caller was able to wait for nanosToWait without {@link Thread#interrupt} and not exceed timeout */ - private boolean waitForPermissionIfNecessary(final long timeoutInNanos, final long nanosToWait) { + public boolean waitForPermissionIfNecessary(final long timeoutInNanos, final long nanosToWait) { boolean canAcquireImmediately = nanosToWait <= 0; boolean canAcquireInTime = timeoutInNanos >= nanosToWait; @@ -153,7 +153,7 @@ private boolean waitForPermissionIfNecessary(final long timeoutInNanos, final lo * @param nanosToWait nanoseconds caller need to wait * @return true if caller was not {@link Thread#interrupted} while waiting */ - private boolean waitForPermission(final long nanosToWait) { + public boolean waitForPermission(final long nanosToWait) { waitingThreads.incrementAndGet(); long deadline = currentNanoTime() + nanosToWait; boolean wasInterrupted = false; @@ -207,7 +207,7 @@ public AtomicRateLimiterMetrics getMetrics() { * the last {@link AtomicRateLimiter#getPermission(Duration)} call. * */ - private static class State { + public static class State { private final long activeCycle; private final int activePermissions; From 08312eae24bb7e4b472a2c2d3dcd1932676b0f7c Mon Sep 17 00:00:00 2001 From: bstorozhuk Date: Fri, 2 Dec 2016 17:46:49 +0200 Subject: [PATCH 6/7] Issue #12 state calculation benchmark shows that most time is spending in System.nanoTime --- .../circuitbreaker/RateLimiterBenchmark.java | 117 ++++++++++++------ .../circuitbreaker/casWithBackOff.txt | 6 - .../stateCalculationBenchmark.txt | 5 + .../internal/AtomicRateLimiter.java | 2 +- 4 files changed, 84 insertions(+), 46 deletions(-) delete mode 100644 src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt create mode 100644 src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt diff --git a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java index 265ff125c3..3029140d48 100644 --- a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java @@ -19,12 +19,11 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; import java.util.function.Supplier; @State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.MICROSECONDS) -@BenchmarkMode(Mode.Throughput) +@BenchmarkMode(Mode.AverageTime) public class RateLimiterBenchmark { public static final int FORK_COUNT = 2; @@ -64,10 +63,9 @@ public void setUp() { @Warmup(iterations = WARMUP_COUNT) @Fork(value = FORK_COUNT) @Measurement(iterations = ITERATION_COUNT) - public void mutex(Blackhole bh) { - synchronized (mutex) { - state = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); - } + public void calculateNextState(Blackhole bh) { + AtomicRateLimiter.State next = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), this.state); + bh.consume(next); } @Benchmark @@ -75,10 +73,9 @@ public void mutex(Blackhole bh) { @Warmup(iterations = WARMUP_COUNT) @Fork(value = FORK_COUNT) @Measurement(iterations = ITERATION_COUNT) - public void atomic(Blackhole bh) { - atomicRateLimiter.state.updateAndGet(state -> { - return atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); - }); + public void nanosToWaitForPermission(Blackhole bh) { + long next = atomicRateLimiter.nanosToWaitForPermission(1, 315L, 31L); + bh.consume(next); } @Benchmark @@ -86,25 +83,9 @@ public void atomic(Blackhole bh) { @Warmup(iterations = WARMUP_COUNT) @Fork(value = FORK_COUNT) @Measurement(iterations = ITERATION_COUNT) - public void atomicBackOf(Blackhole bh) { - AtomicRateLimiter.State prev; - AtomicRateLimiter.State next; - do { - prev = atomicRateLimiter.state.get(); - next = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), prev); - } while (!compareAndSet(prev, next)); - } - - /* - https://arxiv.org/abs/1305.5800 https://dzone.com/articles/wanna-get-faster-wait-bit - */ - public boolean compareAndSet(final AtomicRateLimiter.State current, final AtomicRateLimiter.State next) { - if (atomicRateLimiter.state.compareAndSet(current, next)) { - return true; - } else { - LockSupport.parkNanos(1); - return false; - } + public void reservePermissions(Blackhole bh) { + AtomicRateLimiter.State next = atomicRateLimiter.reservePermissions(0L, 31L, 1, 0L); + bh.consume(next); } @Benchmark @@ -112,16 +93,74 @@ public boolean compareAndSet(final AtomicRateLimiter.State current, final Atomic @Warmup(iterations = WARMUP_COUNT) @Fork(value = FORK_COUNT) @Measurement(iterations = ITERATION_COUNT) - public String semaphoreBasedPermission() { - return semaphoreGuardedSupplier.get(); + public void currentNanoTime(Blackhole bh) { + long next = atomicRateLimiter.currentNanoTime(); + bh.consume(next); } - @Benchmark - @Threads(value = THREAD_COUNT) - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - public String atomicPermission() { - return atomicGuardedSupplier.get(); - } +// @Benchmark +// @Threads(value = THREAD_COUNT) +// @Warmup(iterations = WARMUP_COUNT) +// @Fork(value = FORK_COUNT) +// @Measurement(iterations = ITERATION_COUNT) +// public void mutex(Blackhole bh) { +// synchronized (mutex) { +// state = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); +// } +// } +// +// @Benchmark +// @Threads(value = THREAD_COUNT) +// @Warmup(iterations = WARMUP_COUNT) +// @Fork(value = FORK_COUNT) +// @Measurement(iterations = ITERATION_COUNT) +// public void atomic(Blackhole bh) { +// atomicRateLimiter.state.updateAndGet(state -> { +// return atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); +// }); +// } +// +// @Benchmark +// @Threads(value = THREAD_COUNT) +// @Warmup(iterations = WARMUP_COUNT) +// @Fork(value = FORK_COUNT) +// @Measurement(iterations = ITERATION_COUNT) +// public void atomicBackOf(Blackhole bh) { +// AtomicRateLimiter.State prev; +// AtomicRateLimiter.State next; +// do { +// prev = atomicRateLimiter.state.get(); +// next = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), prev); +// } while (!compareAndSet(prev, next)); +// } +// +// /* +// https://arxiv.org/abs/1305.5800 https://dzone.com/articles/wanna-get-faster-wait-bit +// */ +// public boolean compareAndSet(final AtomicRateLimiter.State current, final AtomicRateLimiter.State next) { +// if (atomicRateLimiter.state.compareAndSet(current, next)) { +// return true; +// } else { +// LockSupport.parkNanos(1); +// return false; +// } +// } +// +// @Benchmark +// @Threads(value = THREAD_COUNT) +// @Warmup(iterations = WARMUP_COUNT) +// @Fork(value = FORK_COUNT) +// @Measurement(iterations = ITERATION_COUNT) +// public String semaphoreBasedPermission() { +// return semaphoreGuardedSupplier.get(); +// } +// +// @Benchmark +// @Threads(value = THREAD_COUNT) +// @Warmup(iterations = WARMUP_COUNT) +// @Fork(value = FORK_COUNT) +// @Measurement(iterations = ITERATION_COUNT) +// public String atomicPermission() { +// return atomicGuardedSupplier.get(); +// } } \ No newline at end of file diff --git a/src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt b/src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt deleted file mode 100644 index 8423672aea..0000000000 --- a/src/jmh/java/javaslang/circuitbreaker/casWithBackOff.txt +++ /dev/null @@ -1,6 +0,0 @@ -Benchmark Mode Cnt Score Error Units -RateLimiterBenchmark.atomic thrpt 10 8.016 ± 0.327 ops/us -RateLimiterBenchmark.atomicBackOf thrpt 10 14.104 ± 0.097 ops/us -RateLimiterBenchmark.atomicPermission thrpt 10 7.429 ± 0.299 ops/us -RateLimiterBenchmark.mutex thrpt 10 7.520 ± 0.304 ops/us -RateLimiterBenchmark.semaphoreBasedPermission thrpt 10 17.923 ± 6.043 ops/us diff --git a/src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt b/src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt new file mode 100644 index 0000000000..698c6561f5 --- /dev/null +++ b/src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt @@ -0,0 +1,5 @@ +Benchmark Mode Cnt Score Error Units +RateLimiterBenchmark.calculateNextState avgt 10 0.101 ± 0.008 us/op +RateLimiterBenchmark.currentNanoTime avgt 10 0.077 ± 0.001 us/op +RateLimiterBenchmark.nanosToWaitForPermission avgt 10 0.003 ± 0.001 us/op +RateLimiterBenchmark.reservePermissions avgt 10 0.007 ± 0.001 us/op diff --git a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java index 2053824a0c..5e25faeb78 100644 --- a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java +++ b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java @@ -264,7 +264,7 @@ public long getAvailablePermissions() { /** * Created only for test purposes. Simply calls {@link System#nanoTime()} */ - private long currentNanoTime() { + public long currentNanoTime() { return nanoTime(); } } From 49fbe859e072f2417ab2fe208587263a271499d4 Mon Sep 17 00:00:00 2001 From: bstorozhuk Date: Fri, 2 Dec 2016 19:09:05 +0200 Subject: [PATCH 7/7] Issue #12 speed optimisations and cleanup --- .../CircuitBreakerBenchmark.java | 89 ++++++++------- .../circuitbreaker/RateLimiterBenchmark.java | 103 +----------------- .../circuitbreaker/RingBitSetBenachmark.java | 62 +++++++---- .../stateCalculationBenchmark.txt | 5 - .../internal/AtomicRateLimiter.java | 72 ++++++++++-- 5 files changed, 156 insertions(+), 175 deletions(-) delete mode 100644 src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt diff --git a/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java b/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java index 2618cb1ebf..48298a7b0b 100644 --- a/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/CircuitBreakerBenchmark.java @@ -18,40 +18,55 @@ */ package javaslang.circuitbreaker; -//@State(Scope.Benchmark) -//@OutputTimeUnit(TimeUnit.MILLISECONDS) -//@BenchmarkMode(Mode.Throughput) -//public class CircuitBreakerBenchmark { -// -// private CircuitBreaker circuitBreaker; -// private Supplier supplier; -// private static final int ITERATION_COUNT = 10; -// private static final int WARMUP_COUNT = 10; -// private static final int THREAD_COUNT = 10; -// -// @Setup -// public void setUp() { -// CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(CircuitBreakerConfig.custom() -// .failureRateThreshold(1) -// .waitDurationInOpenState(Duration.ofSeconds(1)) -// .build()); -// circuitBreaker = circuitBreakerRegistry.circuitBreaker("testCircuitBreaker"); -// -// supplier = CircuitBreaker.decorateSupplier(() -> { -// try { -// Thread.sleep(100); -// } catch (InterruptedException e) { -// e.printStackTrace(); -// } -// return "Hello Benchmark"; -// }, circuitBreaker); -// } -// -// @Benchmark -// @Threads(value = THREAD_COUNT) -// @Warmup(iterations = WARMUP_COUNT) -// @Measurement(iterations = ITERATION_COUNT) -// public String invokeSupplier(){ -// return supplier.get(); -// } -//} +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@BenchmarkMode(Mode.Throughput) +public class CircuitBreakerBenchmark { + + private static final int ITERATION_COUNT = 10; + private static final int WARMUP_COUNT = 10; + private static final int THREAD_COUNT = 10; + private CircuitBreaker circuitBreaker; + private Supplier supplier; + + @Setup + public void setUp() { + CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(CircuitBreakerConfig.custom() + .failureRateThreshold(1) + .waitDurationInOpenState(Duration.ofSeconds(1)) + .build()); + circuitBreaker = circuitBreakerRegistry.circuitBreaker("testCircuitBreaker"); + + supplier = CircuitBreaker.decorateSupplier(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return "Hello Benchmark"; + }, circuitBreaker); + } + + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public String invokeSupplier() { + return supplier.get(); + } +} diff --git a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java index 3029140d48..b3aa79d145 100644 --- a/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/RateLimiterBenchmark.java @@ -23,18 +23,16 @@ @State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.MICROSECONDS) -@BenchmarkMode(Mode.AverageTime) +@BenchmarkMode(Mode.All) public class RateLimiterBenchmark { public static final int FORK_COUNT = 2; private static final int WARMUP_COUNT = 10; - private static final int ITERATION_COUNT = 5; + private static final int ITERATION_COUNT = 10; private static final int THREAD_COUNT = 2; private RateLimiter semaphoreBasedRateLimiter; private AtomicRateLimiter atomicRateLimiter; - private AtomicRateLimiter.State state; - private static final Object mutex = new Object(); private Supplier semaphoreGuardedSupplier; private Supplier atomicGuardedSupplier; @@ -48,7 +46,6 @@ public void setUp() { .build(); semaphoreBasedRateLimiter = new SemaphoreBasedRateLimiter("semaphoreBased", rateLimiterConfig); atomicRateLimiter = new AtomicRateLimiter("atomicBased", rateLimiterConfig); - state = atomicRateLimiter.state.get(); Supplier stringSupplier = () -> { Blackhole.consumeCPU(1); @@ -63,9 +60,8 @@ public void setUp() { @Warmup(iterations = WARMUP_COUNT) @Fork(value = FORK_COUNT) @Measurement(iterations = ITERATION_COUNT) - public void calculateNextState(Blackhole bh) { - AtomicRateLimiter.State next = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), this.state); - bh.consume(next); + public String semaphoreBasedPermission() { + return semaphoreGuardedSupplier.get(); } @Benchmark @@ -73,94 +69,7 @@ public void calculateNextState(Blackhole bh) { @Warmup(iterations = WARMUP_COUNT) @Fork(value = FORK_COUNT) @Measurement(iterations = ITERATION_COUNT) - public void nanosToWaitForPermission(Blackhole bh) { - long next = atomicRateLimiter.nanosToWaitForPermission(1, 315L, 31L); - bh.consume(next); + public String atomicPermission() { + return atomicGuardedSupplier.get(); } - - @Benchmark - @Threads(value = THREAD_COUNT) - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - public void reservePermissions(Blackhole bh) { - AtomicRateLimiter.State next = atomicRateLimiter.reservePermissions(0L, 31L, 1, 0L); - bh.consume(next); - } - - @Benchmark - @Threads(value = THREAD_COUNT) - @Warmup(iterations = WARMUP_COUNT) - @Fork(value = FORK_COUNT) - @Measurement(iterations = ITERATION_COUNT) - public void currentNanoTime(Blackhole bh) { - long next = atomicRateLimiter.currentNanoTime(); - bh.consume(next); - } - -// @Benchmark -// @Threads(value = THREAD_COUNT) -// @Warmup(iterations = WARMUP_COUNT) -// @Fork(value = FORK_COUNT) -// @Measurement(iterations = ITERATION_COUNT) -// public void mutex(Blackhole bh) { -// synchronized (mutex) { -// state = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); -// } -// } -// -// @Benchmark -// @Threads(value = THREAD_COUNT) -// @Warmup(iterations = WARMUP_COUNT) -// @Fork(value = FORK_COUNT) -// @Measurement(iterations = ITERATION_COUNT) -// public void atomic(Blackhole bh) { -// atomicRateLimiter.state.updateAndGet(state -> { -// return atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), state); -// }); -// } -// -// @Benchmark -// @Threads(value = THREAD_COUNT) -// @Warmup(iterations = WARMUP_COUNT) -// @Fork(value = FORK_COUNT) -// @Measurement(iterations = ITERATION_COUNT) -// public void atomicBackOf(Blackhole bh) { -// AtomicRateLimiter.State prev; -// AtomicRateLimiter.State next; -// do { -// prev = atomicRateLimiter.state.get(); -// next = atomicRateLimiter.calculateNextState(Duration.ZERO.toNanos(), prev); -// } while (!compareAndSet(prev, next)); -// } -// -// /* -// https://arxiv.org/abs/1305.5800 https://dzone.com/articles/wanna-get-faster-wait-bit -// */ -// public boolean compareAndSet(final AtomicRateLimiter.State current, final AtomicRateLimiter.State next) { -// if (atomicRateLimiter.state.compareAndSet(current, next)) { -// return true; -// } else { -// LockSupport.parkNanos(1); -// return false; -// } -// } -// -// @Benchmark -// @Threads(value = THREAD_COUNT) -// @Warmup(iterations = WARMUP_COUNT) -// @Fork(value = FORK_COUNT) -// @Measurement(iterations = ITERATION_COUNT) -// public String semaphoreBasedPermission() { -// return semaphoreGuardedSupplier.get(); -// } -// -// @Benchmark -// @Threads(value = THREAD_COUNT) -// @Warmup(iterations = WARMUP_COUNT) -// @Fork(value = FORK_COUNT) -// @Measurement(iterations = ITERATION_COUNT) -// public String atomicPermission() { -// return atomicGuardedSupplier.get(); -// } } \ No newline at end of file diff --git a/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java b/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java index 9512465d2b..02369eb3f6 100644 --- a/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java +++ b/src/jmh/java/javaslang/circuitbreaker/RingBitSetBenachmark.java @@ -18,27 +18,41 @@ */ package javaslang.circuitbreaker; -//@State(Scope.Benchmark) -//@OutputTimeUnit(TimeUnit.MILLISECONDS) -//@BenchmarkMode(Mode.Throughput) -//public class RingBitSetBenachmark { -// -// private RingBitSet ringBitSet; -// private static final int ITERATION_COUNT = 10; -// private static final int WARMUP_COUNT = 10; -// private static final int THREAD_COUNT = 10; -// -// @Setup -// public void setUp() { -// ringBitSet = new RingBitSet(1000); -// } -// -// @Benchmark -// @Threads(value = THREAD_COUNT) -// @Warmup(iterations = WARMUP_COUNT) -// @Measurement(iterations = ITERATION_COUNT) -// public void setBits(){ -// ringBitSet.setNextBit(true); -// ringBitSet.setNextBit(false); -// } -//} +import javaslang.circuitbreaker.internal.RingBitSet; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@BenchmarkMode(Mode.Throughput) +public class RingBitSetBenachmark { + + private static final int ITERATION_COUNT = 10; + private static final int WARMUP_COUNT = 10; + private static final int THREAD_COUNT = 10; + private RingBitSet ringBitSet; + + @Setup + public void setUp() { + ringBitSet = new RingBitSet(1000); + } + + @Benchmark + @Threads(value = THREAD_COUNT) + @Warmup(iterations = WARMUP_COUNT) + @Measurement(iterations = ITERATION_COUNT) + public void setBits() { + ringBitSet.setNextBit(true); + ringBitSet.setNextBit(false); + } +} diff --git a/src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt b/src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt deleted file mode 100644 index 698c6561f5..0000000000 --- a/src/jmh/java/javaslang/circuitbreaker/stateCalculationBenchmark.txt +++ /dev/null @@ -1,5 +0,0 @@ -Benchmark Mode Cnt Score Error Units -RateLimiterBenchmark.calculateNextState avgt 10 0.101 ± 0.008 us/op -RateLimiterBenchmark.currentNanoTime avgt 10 0.077 ± 0.001 us/op -RateLimiterBenchmark.nanosToWaitForPermission avgt 10 0.003 ± 0.001 us/op -RateLimiterBenchmark.reservePermissions avgt 10 0.007 ± 0.001 us/op diff --git a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java index 5e25faeb78..c22afe4999 100644 --- a/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java +++ b/src/main/java/javaslang/ratelimiter/internal/AtomicRateLimiter.java @@ -11,6 +11,7 @@ import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.UnaryOperator; /** * {@link AtomicRateLimiter} splits all nanoseconds from the start of epoch into cycles. @@ -31,7 +32,7 @@ public class AtomicRateLimiter implements RateLimiter { private final long cyclePeriodInNanos; private final int permissionsPerCycle; private final AtomicInteger waitingThreads; - public final AtomicReference state; + private final AtomicReference state; public AtomicRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { @@ -51,12 +52,57 @@ public AtomicRateLimiter(String name, RateLimiterConfig rateLimiterConfig) { @Override public boolean getPermission(final Duration timeoutDuration) { long timeoutInNanos = timeoutDuration.toNanos(); - State modifiedState = state.updateAndGet( - activeState -> calculateNextState(timeoutInNanos, activeState) - ); + State modifiedState = updateStateWithBackOff(timeoutInNanos); return waitForPermissionIfNecessary(timeoutInNanos, modifiedState.nanosToWait); } + /** + * Atomically updates the current {@link State} with the results of + * applying the {@link AtomicRateLimiter#calculateNextState}, returning the updated {@link State}. + * It differs from {@link AtomicReference#updateAndGet(UnaryOperator)} by constant back off. + * It means that after one try to {@link AtomicReference#compareAndSet(Object, Object)} + * this method will wait for a while before try one more time. + * This technique was originally described in this + * paper + * and showed great results with {@link AtomicRateLimiter} in benchmark tests. + * + * @param timeoutInNanos a side-effect-free function + * @return the updated value + */ + private State updateStateWithBackOff(final long timeoutInNanos) { + AtomicRateLimiter.State prev; + AtomicRateLimiter.State next; + do { + prev = state.get(); + next = calculateNextState(timeoutInNanos, prev); + } while (!compareAndSet(prev, next)); + return next; + } + + /** + * Atomically sets the value to the given updated value + * if the current value {@code ==} the expected value. + * It differs from {@link AtomicReference#updateAndGet(UnaryOperator)} by constant back off. + * It means that after one try to {@link AtomicReference#compareAndSet(Object, Object)} + * this method will wait for a while before try one more time. + * This technique was originally described in this + * paper + * and showed great results with {@link AtomicRateLimiter} in benchmark tests. + * + * @param current the expected value + * @param next the new value + * @return {@code true} if successful. False return indicates that + * the actual value was not equal to the expected value. + */ + private boolean compareAndSet(final State current, final State next) { + if (state.compareAndSet(current, next)) { + return true; + } else { + parkNanos(1); // back-off + return false; + } + } + /** * A side-effect-free function that can calculate next {@link State} from current. * It determines time duration that you should wait for permission and reserves it for you, @@ -66,7 +112,7 @@ public boolean getPermission(final Duration timeoutDuration) { * @param activeState current state of {@link AtomicRateLimiter} * @return next {@link State} */ - public State calculateNextState(final long timeoutInNanos, final State activeState) { + private State calculateNextState(final long timeoutInNanos, final State activeState) { long currentNanos = currentNanoTime(); long currentCycle = currentNanos / cyclePeriodInNanos; @@ -93,7 +139,7 @@ public State calculateNextState(final long timeoutInNanos, final State activeSta * @param currentCycle current {@link AtomicRateLimiter} cycle * @return nanoseconds to wait for the next permission */ - public long nanosToWaitForPermission(final int availablePermissions, final long currentNanos, final long currentCycle) { + private long nanosToWaitForPermission(final int availablePermissions, final long currentNanos, final long currentCycle) { if (availablePermissions > 0) { return 0L; } else { @@ -114,7 +160,7 @@ public long nanosToWaitForPermission(final int availablePermissions, final long * @param nanosToWait nanoseconds to wait for the next permission * @return new {@link State} with possibly reserved permissions and time to wait */ - public State reservePermissions(final long timeoutInNanos, final long cycle, final int permissions, final long nanosToWait) { + private State reservePermissions(final long timeoutInNanos, final long cycle, final int permissions, final long nanosToWait) { boolean canAcquireInTime = timeoutInNanos >= nanosToWait; int permissionsWithReservation = permissions; if (canAcquireInTime) { @@ -130,7 +176,7 @@ public State reservePermissions(final long timeoutInNanos, final long cycle, fin * @param nanosToWait nanoseconds caller need to wait * @return true if caller was able to wait for nanosToWait without {@link Thread#interrupt} and not exceed timeout */ - public boolean waitForPermissionIfNecessary(final long timeoutInNanos, final long nanosToWait) { + private boolean waitForPermissionIfNecessary(final long timeoutInNanos, final long nanosToWait) { boolean canAcquireImmediately = nanosToWait <= 0; boolean canAcquireInTime = timeoutInNanos >= nanosToWait; @@ -153,7 +199,7 @@ public boolean waitForPermissionIfNecessary(final long timeoutInNanos, final lon * @param nanosToWait nanoseconds caller need to wait * @return true if caller was not {@link Thread#interrupted} while waiting */ - public boolean waitForPermission(final long nanosToWait) { + private boolean waitForPermission(final long nanosToWait) { waitingThreads.incrementAndGet(); long deadline = currentNanoTime() + nanosToWait; boolean wasInterrupted = false; @@ -207,13 +253,14 @@ public AtomicRateLimiterMetrics getMetrics() { * the last {@link AtomicRateLimiter#getPermission(Duration)} call. * */ - public static class State { + private static class State { private final long activeCycle; + private final int activePermissions; private final long nanosToWait; - public State(final long activeCycle, final int activePermissions, final long nanosToWait) { + private State(final long activeCycle, final int activePermissions, final long nanosToWait) { this.activeCycle = activeCycle; this.activePermissions = activePermissions; this.nanosToWait = nanosToWait; @@ -259,12 +306,13 @@ public long getAvailablePermissions() { State estimatedState = calculateNextState(-1, currentState); return estimatedState.activePermissions; } + } /** * Created only for test purposes. Simply calls {@link System#nanoTime()} */ - public long currentNanoTime() { + private long currentNanoTime() { return nanoTime(); } }