From f12780efb0efa80c656cfede32614bd6c75960f9 Mon Sep 17 00:00:00 2001 From: kmelnikovmh Date: Sat, 21 Mar 2026 14:27:27 +0300 Subject: [PATCH] dauservice: mvp v1.0 --- .../java/lectures/lesson7/dau/DauService.java | 54 +++++- .../lectures/lesson7/dau/DauServiceTest.java | 155 ++++++++++++++++++ 2 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src/test/java/hse/java/lectures/lesson7/dau/DauServiceTest.java diff --git a/src/main/java/hse/java/lectures/lesson7/dau/DauService.java b/src/main/java/hse/java/lectures/lesson7/dau/DauService.java index a02a0384..8b3b76ba 100644 --- a/src/main/java/hse/java/lectures/lesson7/dau/DauService.java +++ b/src/main/java/hse/java/lectures/lesson7/dau/DauService.java @@ -1,7 +1,8 @@ package hse.java.lectures.lesson7.dau; -import java.util.List; -import java.util.Map; +import java.time.*; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; public interface DauService { @@ -11,4 +12,53 @@ public interface DauService { Long getAuthorDauStatistics(int authorId); + class BaseImpl implements DauService { + + private final Clock clock; + private LocalDate currDay; + private final ConcurrentHashMap> currData = new ConcurrentHashMap<>(); + private final ConcurrentHashMap> prevData = new ConcurrentHashMap<>(); + + public BaseImpl(Clock clock) { + this.clock = clock; + this.currDay = LocalDate.now(clock); + } + + private void rotateIfNeeded() { + LocalDate now = LocalDate.now(clock); + synchronized (this) { + if (!now.equals(currDay)) { + prevData.clear(); + prevData.putAll(currData); + currData.clear(); + currDay = now; + } + } + } + + @Override + public void postEvent(Event event) { + rotateIfNeeded(); + currData.computeIfAbsent(event.authorId(), + k -> ConcurrentHashMap.newKeySet()) + .add(event.userId()); + } + + @Override + public Map getDauStatistics(List authorIds) { + rotateIfNeeded(); + Map result = new HashMap<>(); + for (Integer authorId : authorIds) { + result.put(authorId, getAuthorDauStatistics(authorId)); + } + return result; + } + + @Override + public Long getAuthorDauStatistics(int authorId) { + rotateIfNeeded(); + Set users = prevData.get(authorId); + return (users == null) ? 0L : users.size(); + } + } } diff --git a/src/test/java/hse/java/lectures/lesson7/dau/DauServiceTest.java b/src/test/java/hse/java/lectures/lesson7/dau/DauServiceTest.java new file mode 100644 index 00000000..aee1f0ee --- /dev/null +++ b/src/test/java/hse/java/lectures/lesson7/dau/DauServiceTest.java @@ -0,0 +1,155 @@ +package hse.java.lectures.lesson7.dau; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.time.*; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("dauservice") +class BaseImplDauServiceTest { + + static class MutableClock extends Clock { + private Instant instant; + + MutableClock() { + this.instant = LocalDateTime.of(2026, 1, 1, 10, 0) + .toInstant(ZoneOffset.UTC); + } + void nextDay() { + instant = instant.plusSeconds(24 * 60 * 60); + } + @Override public Instant instant() { return instant; } + @Override public ZoneId getZone() { return ZoneOffset.UTC; } + @Override public Clock withZone(ZoneId zone) { return this; } + } + + @Test + void TestNoEventsForAuthor() { + MutableClock clock = new MutableClock(); + + DauService service = new DauService.BaseImpl(clock); + + assertEquals(0, service.getAuthorDauStatistics(9)); + } + + @Test + void TestSingleAuthorMultipleUsersSingleDay() { + MutableClock clock = new MutableClock(); + DauService service = new DauService.BaseImpl(clock); + + service.postEvent(new Event(1, 100)); + service.postEvent(new Event(1, 100)); + service.postEvent(new Event(2, 100)); + + clock.nextDay(); + + assertEquals(2, service.getAuthorDauStatistics(100)); + } + + @Test + void TestMultipleAuthorsShareUsers() { + MutableClock clock = new MutableClock(); + DauService service = new DauService.BaseImpl(clock); + + service.postEvent(new Event(1, 100)); + service.postEvent(new Event(1, 200)); + service.postEvent(new Event(2, 200)); + + clock.nextDay(); + + Map DataDay = service.getDauStatistics(List.of(100, 200)); + assertEquals(1, DataDay.get(100)); + assertEquals(2, DataDay.get(200)); + } + + @Test + void TestNextDayClearsOldData() { + MutableClock clock = new MutableClock(); + DauService service = new DauService.BaseImpl(clock); + + service.postEvent(new Event(1, 100)); + service.postEvent(new Event(2, 100)); + + clock.nextDay(); + assertEquals(2, service.getAuthorDauStatistics(100)); + + clock.nextDay(); + assertEquals(0, service.getAuthorDauStatistics(100)); + } + + @Test + void TestMultipleAuthorsMultipleUsersMultipleDays() { + MutableClock clock = new MutableClock(); + DauService service = new DauService.BaseImpl(clock); + + service.postEvent(new Event(1, 100)); + service.postEvent(new Event(2, 200)); + service.postEvent(new Event(3, 200)); + + clock.nextDay(); + + service.postEvent(new Event(4, 100)); + service.postEvent(new Event(5, 200)); + + Map DataDayOne = service.getDauStatistics(List.of(100, 200)); + assertEquals(1, DataDayOne.get(100)); + assertEquals(2, DataDayOne.get(200)); + + clock.nextDay(); + + Map DataDayTwo = service.getDauStatistics(List.of(100, 200)); + assertEquals(1, DataDayTwo.get(100)); + assertEquals(1, DataDayTwo.get(200)); + } + + @Test + void TestManyAuthorsManyUsers() { + MutableClock clock = new MutableClock(); + DauService service = new DauService.BaseImpl(clock); + + for (int user = 1; user <= 5; ++user) { + for (int author = 100; author <= 102; ++author) { + service.postEvent(new Event(user, author)); + } + } + + clock.nextDay(); + + Map stats = service.getDauStatistics(List.of(100, 101, 102, 103)); + assertEquals(5, stats.get(100)); + assertEquals(5, stats.get(101)); + assertEquals(5, stats.get(102)); + assertEquals(0, stats.get(103)); + } + + @Test + void TestConcurrentPosts() throws InterruptedException { + MutableClock clock = new MutableClock(); + DauService service = new DauService.BaseImpl(clock); + + int threads = 10; + int usersPerThread = 100; + int authorId = 500; + Thread[] ts = IntStream.range(0, threads) + .mapToObj(threadId -> new Thread(() -> { + for (int user = 1; user <= usersPerThread; ++user) { + int userId = threadId * usersPerThread + user; + service.postEvent(new Event(userId, authorId)); + } + })) + .toArray(Thread[]::new); + + + for (Thread t : ts) t.start(); + for (Thread t : ts) t.join(); + + clock.nextDay(); + + assertEquals((threads * usersPerThread), service.getAuthorDauStatistics(authorId)); + } +}