Added new metric 'roompicker_locked_rooms'

This commit is contained in:
Andrey Terentev 2024-03-15 20:38:58 +07:00 committed by Andrey Terentev
parent cf76f2f8ce
commit d185072ee0
8 changed files with 125 additions and 38 deletions

View File

@ -9,12 +9,17 @@ import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.dragonestia.picker.event.UpdateRoomLockStateEvent;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -28,64 +33,94 @@ public class UserMetricsAspect {
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
private final AtomicInteger totalUsers = new AtomicInteger(0); private final AtomicInteger totalUsers = new AtomicInteger(0);
private final Map<String, Gauge> nodeGauges = new ConcurrentHashMap<>(); private final Map<String, NodeData> data = new ConcurrentHashMap<>();
private final Map<String, Integer> nodeUsers = new ConcurrentHashMap<>();
private final Map<String, Counter> pickPerMinute = new ConcurrentHashMap<>();
@PostConstruct @PostConstruct
void init() { void init() {
meterRegistry.gauge("roompicker_total_users", totalUsers); meterRegistry.gauge("roompicker_total_users", totalUsers);
} }
@After("execution(* ru.dragonestia.picker.repository.UserRepository.linkWithRoom(..))") @After(value = "execution(* ru.dragonestia.picker.repository.UserRepository.linkWithRoom(ru.dragonestia.picker.model.Room, ..)) && args(room, ..)", argNames = "room")
void onLinkUsers() { void onLinkUsers(Room room) {
countAllUsers(); countAllUsers(room);
} }
@After("execution(void ru.dragonestia.picker.repository.UserRepository.unlinkWithRoom(..))") @After(value = "execution(void ru.dragonestia.picker.repository.UserRepository.unlinkWithRoom(ru.dragonestia.picker.model.Room, ..)) && args(room, ..)", argNames = "room")
void onUnlinkUsers() { void onUnlinkUsers(Room room) {
countAllUsers(); countAllUsers(room);
} }
@After("execution(void ru.dragonestia.picker.repository.UserRepository.onRemoveRoom(..))") @AfterReturning(value = "execution(void ru.dragonestia.picker.repository.RoomRepository.create(ru.dragonestia.picker.model.Room)) && args(room)", argNames = "room")
void onRemoveRoom() { void onCreateRoom(Room room) {
countAllUsers(); checkRoom(room);
} }
private void countAllUsers() { @After(value = "execution(void ru.dragonestia.picker.repository.UserRepository.onRemoveRoom(ru.dragonestia.picker.model.Room)) && args(room, ..)", argNames = "room")
void onRemoveRoom(Room room) {
countAllUsers(room);
}
private void countAllUsers(Room room) {
totalUsers.set(userRepository.countAllUsers()); totalUsers.set(userRepository.countAllUsers());
checkRoom(room);
}
private void checkRoom(Room room) {
var set = data.get(room.getNodeIdentifier()).locked();
if (room.isLocked()) {
set.add(room);
return;
}
if (!room.hasUnlimitedSlots() && userRepository.usersOf(room).size() >= room.getMaxSlots()) {
set.add(room);
return;
}
set.remove(room);
} }
@After(value = "execution(void ru.dragonestia.picker.repository.NodeRepository.create(ru.dragonestia.picker.model.Node)) && args(node)", argNames = "node") @After(value = "execution(void ru.dragonestia.picker.repository.NodeRepository.create(ru.dragonestia.picker.model.Node)) && args(node)", argNames = "node")
void onCreateNode(Node node) { void onCreateNode(Node node) {
var nodeId = node.getIdentifier(); var nodeId = node.getIdentifier();
var gauge = Gauge.builder("roompicker_node_users_total", () -> nodeUsers.getOrDefault(nodeId, 0)) var gauge = Gauge.builder("roompicker_node_users_total", () -> data.get(nodeId).users())
.tag("nodeId", nodeId) .tag("nodeId", nodeId)
.register(meterRegistry); .register(meterRegistry);
nodeGauges.put(nodeId, gauge);
var counter = Counter.builder("roompicker_picks") var counter = Counter.builder("roompicker_picks")
.tag("nodeId", nodeId) .tag("nodeId", nodeId)
.baseUnit("1s") .baseUnit("1s")
.register(meterRegistry); .register(meterRegistry);
pickPerMinute.put(nodeId, counter);
var lockedGauge = Gauge.builder("roompicker_locked_rooms", () -> data.get(nodeId).locked().size())
.tag("nodeId", nodeId)
.register(meterRegistry);
data.put(nodeId, new NodeData(gauge, new AtomicInteger(0), counter, new HashSet<>(), lockedGauge));
} }
@After(value = "execution(* ru.dragonestia.picker.repository.NodeRepository.delete(ru.dragonestia.picker.model.Node)) && args(node)", argNames = "node") @After(value = "execution(* ru.dragonestia.picker.repository.NodeRepository.delete(ru.dragonestia.picker.model.Node)) && args(node)", argNames = "node")
void onDeleteNode(Node node) { void onDeleteNode(Node node) {
meterRegistry.remove(nodeGauges.remove(node.getIdentifier())); var data = this.data.remove(node.getIdentifier());
meterRegistry.remove(pickPerMinute.remove(node.getIdentifier()));
nodeUsers.remove(node.getIdentifier()); meterRegistry.remove(data.usersGauge());
meterRegistry.remove(data.picksPerMinute());
meterRegistry.remove(data.lockedGauge());
} }
@AfterReturning(value = "execution(* ru.dragonestia.picker.repository.RoomRepository.pickFree(ru.dragonestia.picker.model.Node, *)) && args(node, ..)", argNames = "node") @AfterReturning(value = "execution(* ru.dragonestia.picker.repository.RoomRepository.pickFree(ru.dragonestia.picker.model.Node, *)) && args(node, ..)", argNames = "node")
void onPickRoom(Node node) { void onPickRoom(Node node) {
pickPerMinute.get(node.getIdentifier()).increment(); data.get(node.getIdentifier()).picksPerMinute().increment();
}
@EventListener
void onRoomChangeLockState(UpdateRoomLockStateEvent event) {
checkRoom(event.room());
} }
@Scheduled(fixedDelay = 3_000) @Scheduled(fixedDelay = 3_000)
void updateUserMetrics() { void updateUserMetrics() {
nodeUsers.putAll(userRepository.countUsersForNodes()); userRepository.countUsersForNodes().forEach((nodeId, users) -> data.get(nodeId).users().set(users));
} }
private record NodeData(Gauge usersGauge, AtomicInteger users, Counter picksPerMinute, Set<Room> locked, Gauge lockedGauge) {}
} }

View File

@ -19,6 +19,7 @@ import ru.dragonestia.picker.interceptor.DebugInterceptor;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.factory.RoomFactory;
import ru.dragonestia.picker.model.type.SlotLimit; import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.repository.RoomRepository; import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository; import ru.dragonestia.picker.repository.NodeRepository;
@ -36,6 +37,7 @@ public class TestConfig implements WebMvcConfigurer {
private final NodeRepository nodeRepository; private final NodeRepository nodeRepository;
private final RoomRepository roomRepository; private final RoomRepository roomRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final RoomFactory roomFactory;
private final Random rand = new Random(0); private final Random rand = new Random(0);
@ -58,7 +60,7 @@ public class TestConfig implements WebMvcConfigurer {
for (int i = 1; i <= 5; i++) { for (int i = 1; i <= 5; i++) {
var slots = 5 * i; var slots = 5 * i;
var room = new Room(RoomIdentifier.of("test-" + i), node, slots, json.writeValueAsString(generatePayload()), false); var room = roomFactory.create(RoomIdentifier.of("test-" + i), node, slots, json.writeValueAsString(generatePayload()), false);
roomRepository.create(room); roomRepository.create(room);
for (int j = 0, n = rand.nextInt(slots + 1); j < n; j++) { for (int j = 0, n = rand.nextInt(slots + 1); j < n; j++) {
@ -68,7 +70,7 @@ public class TestConfig implements WebMvcConfigurer {
} }
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
var room = new Room(RoomIdentifier.of(randomUUID().toString()), node, IRoom.UNLIMITED_SLOTS, json.writeValueAsString(generatePayload()), false); var room = roomFactory.create(RoomIdentifier.of(randomUUID().toString()), node, IRoom.UNLIMITED_SLOTS, json.writeValueAsString(generatePayload()), false);
room.setLocked((i & 1) == 0); room.setLocked((i & 1) == 0);
roomRepository.create(room); roomRepository.create(room);
} }

View File

@ -13,6 +13,7 @@ import ru.dragonestia.picker.api.repository.response.RoomInfoResponse;
import ru.dragonestia.picker.api.repository.response.RoomListResponse; import ru.dragonestia.picker.api.repository.response.RoomListResponse;
import ru.dragonestia.picker.api.repository.type.RoomIdentifier; import ru.dragonestia.picker.api.repository.type.RoomIdentifier;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.factory.RoomFactory;
import ru.dragonestia.picker.service.RoomService; import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.service.NodeService; import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.util.DetailsParser; import ru.dragonestia.picker.util.DetailsParser;
@ -28,6 +29,7 @@ public class RoomController {
private final RoomService roomService; private final RoomService roomService;
private final NamingValidator namingValidator; private final NamingValidator namingValidator;
private final DetailsParser detailsParser; private final DetailsParser detailsParser;
private final RoomFactory roomFactory;
@Operation(summary = "Get all rooms from node") @Operation(summary = "Get all rooms from node")
@GetMapping @GetMapping
@ -54,7 +56,7 @@ public class RoomController {
@Parameter(description = "Save room") @RequestParam(name = "persist", required = false, defaultValue = "false") boolean persist @Parameter(description = "Save room") @RequestParam(name = "persist", required = false, defaultValue = "false") boolean persist
) { ) {
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId)); var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
var room = new Room(RoomIdentifier.of(roomId), node, slots, payload, persist); var room = roomFactory.create(RoomIdentifier.of(roomId), node, slots, payload, persist);
room.setLocked(locked); room.setLocked(locked);
roomService.create(room); roomService.create(room);

View File

@ -0,0 +1,11 @@
package ru.dragonestia.picker.event;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.picker.model.Room;
import java.util.function.Consumer;
public record UpdateRoomLockStateEvent(@NotNull Room room) {
public interface Listener extends Consumer<UpdateRoomLockStateEvent> {}
}

View File

@ -7,6 +7,7 @@ import ru.dragonestia.picker.api.model.room.ResponseRoom;
import ru.dragonestia.picker.api.model.room.RoomDetails; import ru.dragonestia.picker.api.model.room.RoomDetails;
import ru.dragonestia.picker.api.model.room.ShortResponseRoom; import ru.dragonestia.picker.api.model.room.ShortResponseRoom;
import ru.dragonestia.picker.api.repository.type.RoomIdentifier; import ru.dragonestia.picker.api.repository.type.RoomIdentifier;
import ru.dragonestia.picker.event.UpdateRoomLockStateEvent;
import java.util.Objects; import java.util.Objects;
@ -18,13 +19,15 @@ public class Room implements IRoom {
private final String payload; private final String payload;
private final boolean persist; private final boolean persist;
private boolean locked = false; private boolean locked = false;
private final UpdateRoomLockStateEvent.Listener updateLockStateListener;
public Room(@NotNull RoomIdentifier identifier, @NotNull Node node, int slots, @NotNull String payload, boolean persist) { public Room(@NotNull RoomIdentifier identifier, @NotNull Node node, int slots, @NotNull String payload, boolean persist, @Nullable UpdateRoomLockStateEvent.Listener listener) {
this.identifier = identifier.getValue(); this.identifier = identifier.getValue();
this.nodeIdentifier = node.getIdentifier(); this.nodeIdentifier = node.getIdentifier();
this.slots = slots; this.slots = slots;
this.payload = payload; this.payload = payload;
this.persist = persist; this.persist = persist;
this.updateLockStateListener = listener;
} }
@Override @Override
@ -49,6 +52,10 @@ public class Room implements IRoom {
public void setLocked(boolean value) { public void setLocked(boolean value) {
locked = value; locked = value;
if (updateLockStateListener != null) {
updateLockStateListener.accept(new UpdateRoomLockStateEvent(this));
}
} }
@Override @Override

View File

@ -0,0 +1,22 @@
package ru.dragonestia.picker.model.factory;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.repository.type.RoomIdentifier;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
@Component
@RequiredArgsConstructor
public class RoomFactory {
private final ApplicationEventPublisher eventPublisher;
@Contract("_, _, _, _, _ -> new")
public @NotNull Room create(@NotNull RoomIdentifier identifier, @NotNull Node node, int slots, @NotNull String payload, boolean persist) {
return new Room(identifier, node, slots, payload, persist, eventPublisher::publishEvent);
}
}

View File

@ -10,6 +10,7 @@ import ru.dragonestia.picker.api.repository.type.UserIdentifier;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.factory.RoomFactory;
import ru.dragonestia.picker.repository.NodeRepository; import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.RoomRepository; import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
@ -47,6 +48,9 @@ public class FillingNodesConfig {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@Autowired
private RoomFactory roomFactory;
private Node seqNode; private Node seqNode;
private Node roundNode; private Node roundNode;
private Node leastNode; private Node leastNode;
@ -85,7 +89,7 @@ public class FillingNodesConfig {
for (int i = 0, n = 5; i < n; i++) { for (int i = 0, n = 5; i < n; i++) {
for (int j = 0; j < 3; j++) { for (int j = 0; j < 3; j++) {
var roomId = "room-" + i + "-" + j; var roomId = "room-" + i + "-" + j;
var room = new Room(RoomIdentifier.of(roomId), node, n, "", false); var room = roomFactory.create(RoomIdentifier.of(roomId), node, n, "", false);
roomRepository.create(room); roomRepository.create(room);
var users = n - i; var users = n - i;

View File

@ -17,6 +17,7 @@ import ru.dragonestia.picker.api.repository.type.UserIdentifier;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.factory.RoomFactory;
import ru.dragonestia.picker.model.type.SlotLimit; import ru.dragonestia.picker.model.type.SlotLimit;
import java.util.List; import java.util.List;
@ -30,6 +31,9 @@ public class RoomServiceTests {
@Autowired @Autowired
private RoomService roomService; private RoomService roomService;
@Autowired
private RoomFactory roomFactory;
private Node node; private Node node;
@BeforeEach @BeforeEach
@ -43,7 +47,7 @@ public class RoomServiceTests {
@Test @Test
void test_createAndRemove() { void test_createAndRemove() {
var room = new Room(RoomIdentifier.of("test-room"), node, IRoom.UNLIMITED_SLOTS, "", false); var room = roomFactory.create(RoomIdentifier.of("test-room"), node, IRoom.UNLIMITED_SLOTS, "", false);
roomService.create(room); roomService.create(room);
Assertions.assertTrue(roomService.find(node, room.getIdentifier()).isPresent()); Assertions.assertTrue(roomService.find(node, room.getIdentifier()).isPresent());
@ -57,10 +61,10 @@ public class RoomServiceTests {
@Test @Test
void test_allRooms() { void test_allRooms() {
var rooms = List.of( var rooms = List.of(
new Room(RoomIdentifier.of("test-room1"), node, 1, "", false), roomFactory.create(RoomIdentifier.of("test-room1"), node, 1, "", false),
new Room(RoomIdentifier.of("test-room2"), node, 2, "", false), roomFactory.create(RoomIdentifier.of("test-room2"), node, 2, "", false),
new Room(RoomIdentifier.of("test-room3"), node, 3, "", false), roomFactory.create(RoomIdentifier.of("test-room3"), node, 3, "", false),
new Room(RoomIdentifier.of("test-room4"), node, IRoom.UNLIMITED_SLOTS, "", false) roomFactory.create(RoomIdentifier.of("test-room4"), node, IRoom.UNLIMITED_SLOTS, "", false)
); );
rooms.forEach(room -> roomService.create(room)); rooms.forEach(room -> roomService.create(room));
@ -74,17 +78,17 @@ public class RoomServiceTests {
@Test @Test
void test_exceptNotPersistedNode() { void test_exceptNotPersistedNode() {
Assertions.assertThrows(NotPersistedNodeException.class, () -> { Assertions.assertThrows(NotPersistedNodeException.class, () -> {
roomService.create(new Room(RoomIdentifier.of("1"), node, IRoom.UNLIMITED_SLOTS, "", true)); roomService.create(roomFactory.create(RoomIdentifier.of("1"), node, IRoom.UNLIMITED_SLOTS, "", true));
}); });
} }
@Test @Test
void test_pickRoom() { void test_pickRoom() {
var rooms = List.of( var rooms = List.of(
new Room(RoomIdentifier.of("test-room1"), node, 1, "", false), roomFactory.create(RoomIdentifier.of("test-room1"), node, 1, "", false),
new Room(RoomIdentifier.of("test-room2"), node, 2, "", false), roomFactory.create(RoomIdentifier.of("test-room2"), node, 2, "", false),
new Room(RoomIdentifier.of("test-room3"), node, 3, "", false), roomFactory.create(RoomIdentifier.of("test-room3"), node, 3, "", false),
new Room(RoomIdentifier.of("test-room4"), node, IRoom.UNLIMITED_SLOTS, "", false) roomFactory.create(RoomIdentifier.of("test-room4"), node, IRoom.UNLIMITED_SLOTS, "", false)
); );
rooms.forEach(room -> roomService.create(room)); rooms.forEach(room -> roomService.create(room));
@ -112,7 +116,7 @@ public class RoomServiceTests {
@Test @Test
void test_nodeDoesNotExists() { void test_nodeDoesNotExists() {
var node = new Node(NodeIdentifier.of("bruh"), PickingMethod.ROUND_ROBIN, false); var node = new Node(NodeIdentifier.of("bruh"), PickingMethod.ROUND_ROBIN, false);
var room = new Room(RoomIdentifier.of("test"), node, IRoom.UNLIMITED_SLOTS, "", false); var room = roomFactory.create(RoomIdentifier.of("test"), node, IRoom.UNLIMITED_SLOTS, "", false);
Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.create(room)); Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.create(room));
Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.remove(room)); Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.remove(room));