From 632a1c2fd727dc08aaa6e4bd72a9ec8a838c80b5 Mon Sep 17 00:00:00 2001 From: kmelnikovmh Date: Sat, 23 May 2026 17:05:29 +0300 Subject: [PATCH] limiter: initial v1.0 --- .../lectures/lesson7/limiter/RateLimiter.java | 44 ++++++++++----- .../lesson7/limiter/RateLimiterTest.java | 56 +++++++++++++++++++ 2 files changed, 85 insertions(+), 15 deletions(-) create mode 100644 src/test/java/hse/java/lectures/lesson7/limiter/RateLimiterTest.java diff --git a/src/main/java/hse/java/lectures/lesson7/limiter/RateLimiter.java b/src/main/java/hse/java/lectures/lesson7/limiter/RateLimiter.java index 08df1b13..99903974 100644 --- a/src/main/java/hse/java/lectures/lesson7/limiter/RateLimiter.java +++ b/src/main/java/hse/java/lectures/lesson7/limiter/RateLimiter.java @@ -1,26 +1,40 @@ package hse.java.lectures.lesson7.limiter; import java.time.temporal.ChronoUnit; +import java.util.LinkedList; -/** - * Скользящий рейтлимитер: не больше заданного числа успешных {@link #check()} за последнюю секунду или минуту. - */ public class RateLimiter { - /** - * @param unit длина окна — только {@link ChronoUnit#SECONDS} или {@link ChronoUnit#MINUTES} - * (скользящее окно 1 секунда или 1 минута) - * @param maxRequests максимум успешных {@link #check()} за окно (должно быть > 0) - */ + private long windowNanos; + private int maxRequests; + private LinkedList stamps = new LinkedList<>(); + private int currentSize = 0; + public RateLimiter(ChronoUnit unit, int maxRequests) { - throw new UnsupportedOperationException("Not implemented"); + if (unit != ChronoUnit.SECONDS && unit != ChronoUnit.MINUTES) { + throw new IllegalArgumentException("bad unit"); + } + if (1 > maxRequests) { + throw new IllegalArgumentException("bad max"); + } + this.windowNanos = unit.getDuration().toNanos(); + this.maxRequests = maxRequests; } - /** - * Регистрирует попытку и возвращает, разрешена ли она в пределах лимита. - */ - public boolean check() { - throw new UnsupportedOperationException("Not implemented"); + public synchronized boolean check() { + long now = System.nanoTime(); + long border = now - windowNanos; + int removed = 0; + while (!stamps.isEmpty() && stamps.getFirst() <= border) { + stamps.removeFirst(); + ++removed; + } + currentSize = currentSize - removed; + if (currentSize < maxRequests) { + stamps.addLast(now); + ++currentSize; + return true; + } + return false; } - } diff --git a/src/test/java/hse/java/lectures/lesson7/limiter/RateLimiterTest.java b/src/test/java/hse/java/lectures/lesson7/limiter/RateLimiterTest.java new file mode 100644 index 00000000..4e0c1a6b --- /dev/null +++ b/src/test/java/hse/java/lectures/lesson7/limiter/RateLimiterTest.java @@ -0,0 +1,56 @@ +package hse.java.lectures.lesson7.limiter; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.time.temporal.ChronoUnit; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("limiter") +class RateLimiterTest { + + @Test + void allowsThenRejects() { + RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, 3); + for (int i = 0; i < 3; ++i) { + assertTrue(limiter.check()); + } + assertFalse(limiter.check()); + } + + @Test + void rejectedDontTakeSlots() throws InterruptedException { + RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, 1); + assertTrue(limiter.check()); + for (int i = 0; i < 50; ++i) { + assertFalse(limiter.check()); + } + Thread.sleep(1100); + assertTrue(limiter.check()); + } + + @Test + void threadSafe() throws Exception { + int limit = 5; + int threads = 20; + RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, limit); + ExecutorService pool = Executors.newFixedThreadPool(threads); + CountDownLatch latch = new CountDownLatch(1); + AtomicInteger ok = new AtomicInteger(0); + for (int i = 0; i < threads; ++i) { + pool.submit(() -> { + try { latch.await(); } catch (InterruptedException e) { return; } + if (limiter.check()) { ok.incrementAndGet(); } + }); + } + latch.countDown(); + pool.shutdown(); + while (!pool.isTerminated()) { Thread.sleep(50); } + assertEquals(limit, ok.get()); + } +}