diff --git a/pom.xml b/pom.xml
index bb6d9b8a..f30e7d03 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,6 +63,11 @@
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
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..1362015f 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,51 @@
package hse.java.lectures.lesson7.limiter;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayDeque;
+import java.util.Deque;
/**
* Скользящий рейтлимитер: не больше заданного числа успешных {@link #check()} за последнюю секунду или минуту.
*/
public class RateLimiter {
+ private final long windowNanos;
+ private final int maxRequests;
+ private final Deque successTimestamps = new ArrayDeque<>();
+
/**
* @param unit длина окна — только {@link ChronoUnit#SECONDS} или {@link ChronoUnit#MINUTES}
* (скользящее окно 1 секунда или 1 минута)
* @param maxRequests максимум успешных {@link #check()} за окно (должно быть > 0)
*/
public RateLimiter(ChronoUnit unit, int maxRequests) {
- throw new UnsupportedOperationException("Not implemented");
+ if (unit != ChronoUnit.SECONDS && unit != ChronoUnit.MINUTES) {
+ throw new IllegalArgumentException("Only SECONDS or MINUTES are supported");
+ }
+ if (maxRequests <= 0) {
+ throw new IllegalArgumentException("maxRequests must be > 0");
+ }
+ 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 windowStart = now - windowNanos;
+
+ while (!successTimestamps.isEmpty() && successTimestamps.peekFirst() <= windowStart) {
+ successTimestamps.pollFirst();
+ }
+
+ if (successTimestamps.size() < maxRequests) {
+ successTimestamps.addLast(now);
+ 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..18e2c791
--- /dev/null
+++ b/src/test/java/hse/java/lectures/lesson7/limiter/RateLimiterTest.java
@@ -0,0 +1,91 @@
+package hse.java.lectures.lesson7.limiter;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.time.temporal.ChronoUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@Tag("limiter")
+class RateLimiterTest {
+
+ @Test
+ void illegalUnit_throwsException() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new RateLimiter(ChronoUnit.HOURS, 5));
+ }
+
+ @Test
+ void zeroMaxRequests_throwsException() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new RateLimiter(ChronoUnit.SECONDS, 0));
+ }
+
+ @Test
+ void negativeMaxRequests_throwsException() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new RateLimiter(ChronoUnit.SECONDS, -1));
+ }
+
+ @Test
+ void withinLimit_allCallsSucceed() {
+ RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, 3);
+
+ assertTrue(limiter.check());
+ assertTrue(limiter.check());
+ assertTrue(limiter.check());
+ }
+
+ @Test
+ void overLimit_extraCallFails() {
+ RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, 3);
+
+ limiter.check();
+ limiter.check();
+ limiter.check();
+
+ assertFalse(limiter.check());
+ }
+
+ @Test
+ void failedCall_doesNotOccupySlot() {
+ RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, 2);
+
+ limiter.check();
+ limiter.check();
+ limiter.check();
+
+ assertFalse(limiter.check());
+ }
+
+ @Test
+ void slidingWindow_oldCallsExpire() throws InterruptedException {
+ RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, 2);
+
+ assertTrue(limiter.check());
+ assertTrue(limiter.check());
+ assertFalse(limiter.check());
+
+ Thread.sleep(1100);
+
+ assertTrue(limiter.check());
+ assertTrue(limiter.check());
+ assertFalse(limiter.check());
+ }
+
+ @Test
+ void slidingWindow_partialExpiry() throws InterruptedException {
+ RateLimiter limiter = new RateLimiter(ChronoUnit.SECONDS, 2);
+
+ assertTrue(limiter.check());
+ Thread.sleep(600);
+ assertTrue(limiter.check());
+ assertFalse(limiter.check());
+
+ Thread.sleep(500);
+
+ assertTrue(limiter.check());
+ assertFalse(limiter.check());
+ }
+}