Refactored repositories, deleted shit code

This commit is contained in:
Andrey Terentev 2024-03-16 15:05:27 +07:00 committed by Andrey Terentev
parent 087a67f021
commit 8b655bfb4b
30 changed files with 452 additions and 521 deletions

View File

@ -115,7 +115,7 @@ public class UserMetricsAspect {
meterRegistry.remove(data.roomsGauge());
}
@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.pick(ru.dragonestia.picker.model.Node, *)) && args(node, ..)", argNames = "node")
void onPickRoom(Node node) {
data.get(node.getIdentifier()).picksPerMinute().increment();
}

View File

@ -8,17 +8,21 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.model.node.PickingMethod;
import ru.dragonestia.picker.api.model.user.UserDefinition;
import ru.dragonestia.picker.api.repository.response.NodeDetailsResponse;
import ru.dragonestia.picker.api.repository.response.NodeListResponse;
import ru.dragonestia.picker.api.repository.response.PickedRoomResponse;
import ru.dragonestia.picker.api.repository.type.NodeIdentifier;
import ru.dragonestia.picker.api.repository.type.UserIdentifier;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.util.DetailsParser;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.Arrays;
import java.util.stream.Collectors;
@Tag(name = "Nodes", description = "Node management")
@RestController
@ -82,7 +86,9 @@ public class NodeController {
namingValidator.validateNodeId(nodeId);
var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId));
var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
var users = Arrays.stream(userIds.split(","))
.map(userId -> new User(UserIdentifier.of(userId)))
.collect(Collectors.toSet());
var response = roomService.pickAvailable(node, users);
return ResponseEntity.ok(response);

View File

@ -10,8 +10,10 @@ import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.api.repository.response.LinkUsersWithRoomResponse;
import ru.dragonestia.picker.api.repository.response.RoomUserListResponse;
import ru.dragonestia.picker.api.repository.type.UserIdentifier;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.service.RoomService;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.service.UserService;
@ -19,6 +21,7 @@ import ru.dragonestia.picker.util.DetailsParser;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.Arrays;
import java.util.stream.Collectors;
@Tag(name = "Users", description = "User management")
@RequiredArgsConstructor
@ -54,7 +57,9 @@ public class UserRoomController {
@Parameter(description = "Ignore slot limitation") @RequestParam(name = "force") boolean force
) {
var room = getNodeAndRoom(nodeId, roomId).room();
var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
var users = Arrays.stream(userIds.split(","))
.map(userId -> new User(UserIdentifier.of(userId)))
.collect(Collectors.toSet());
userService.linkUsersWithRoom(room, users, force);
var usedSlots = userService.getRoomUsers(room).size();
return ResponseEntity.ok(new LinkUsersWithRoomResponse(usedSlots, room.getMaxSlots()));
@ -69,7 +74,9 @@ public class UserRoomController {
) {
var room = getNodeAndRoom(nodeId, roomId).room();
var users = namingValidator.validateUserIds(Arrays.stream(userIds.split(",")).toList());
var users = Arrays.stream(userIds.split(","))
.map(userId -> new User(UserIdentifier.of(userId)))
.collect(Collectors.toSet());
userService.unlinkUsersFromRoom(room, users);
return ResponseEntity.ok().build();
}

View File

@ -58,4 +58,9 @@ public class Node implements INode {
}
return false;
}
@Override
public String toString() {
return "{Node id='%s'}".formatted(identifier);
}
}

View File

@ -2,7 +2,6 @@ package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import java.util.List;
import java.util.Optional;
@ -11,9 +10,9 @@ public interface NodeRepository {
void create(Node node) throws NodeAlreadyExistException;
List<Room> delete(Node node);
void delete(Node node);
Optional<Node> find(String nodeId);
Optional<Node> findById(String nodeId);
List<Node> all();
}

View File

@ -1,13 +1,14 @@
package ru.dragonestia.picker.repository;
import ru.dragonestia.picker.api.exception.NoRoomsAvailableException;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public interface RoomRepository {
@ -17,11 +18,7 @@ public interface RoomRepository {
Optional<Room> find(Node node, String identifier);
List<Room> all(Node node);
Collection<Room> all(Node node);
Optional<Room> pickFree(Node node, Collection<User> users);
void onCreateNode(Node node);
List<Room> onRemoveNode(Node node);
Room pick(Node node, Set<User> users) throws NoRoomsAvailableException;
}

View File

@ -10,17 +10,15 @@ import java.util.Map;
public interface UserRepository {
int linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException;
void linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException;
void unlinkWithRoom(Room room, Collection<User> users);
List<Room> findAllLinkedUserRooms(User user);
Collection<Room> findAllLinkedUserRooms(User user);
void onRemoveRoom(Room room);
Collection<User> usersOf(Room room);
List<User> usersOf(Room room);
List<User> search(String input);
Collection<User> search(String input);
int countAllUsers();

View File

@ -0,0 +1,47 @@
package ru.dragonestia.picker.repository.impl;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.repository.impl.container.NodeContainer;
import ru.dragonestia.picker.repository.impl.type.UserTransaction;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class ContainerRepository {
private final Map<String, NodeContainer> containers = new ConcurrentHashMap<>();
private UserTransaction.Listener transactionListener = transaction -> {};
public void create(Node node) throws NodeAlreadyExistException {
if (containers.containsKey(node.getIdentifier())) {
throw new NodeAlreadyExistException(node.getIdentifier());
}
var container = new NodeContainer(node, transactionListener);
containers.put(node.getIdentifier(), container);
}
public void remove(@NotNull String nodeId) {
containers.remove(nodeId);
}
public @NotNull Optional<NodeContainer> findById(@NotNull String nodeId) {
return Optional.ofNullable(containers.get(nodeId));
}
public @NotNull Collection<NodeContainer> all() {
return containers.values();
}
public void setTransactionListener(@NotNull UserTransaction.Listener transactionListener) {
this.transactionListener = transactionListener;
}
}

View File

@ -1,80 +1,39 @@
package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.exception.NodeAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache;
import ru.dragonestia.picker.repository.impl.container.NodeContainer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Repository
@Component
@RequiredArgsConstructor
public class NodeRepositoryImpl implements NodeRepository {
private final RoomRepository roomRepository;
private final PickerRepository pickerRepository;
private final NodeId2PickerModeCache nodeId2PickerModeCache;
private final Map<String, Node> nodeMap = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final ContainerRepository containerRepository;
@Override
public void create(Node node) throws NodeAlreadyExistException {
lock.writeLock().lock();
try {
if (nodeMap.containsKey(node.getIdentifier())) {
throw new NodeAlreadyExistException(node.getIdentifier());
}
nodeMap.put(node.getIdentifier(), node);
var picker = pickerRepository.create(node.getIdentifier(), node.getPickingMethod());
nodeId2PickerModeCache.put(node.getIdentifier(), picker);
roomRepository.onCreateNode(node);
} finally {
lock.writeLock().unlock();
}
containerRepository.create(node);
}
@Override
public List<Room> delete(Node node) {
lock.writeLock().lock();
try {
nodeMap.remove(node.getIdentifier());
pickerRepository.remove(node.getIdentifier());
nodeId2PickerModeCache.remove(node.getIdentifier());
return roomRepository.onRemoveNode(node);
} finally {
lock.writeLock().unlock();
}
public void delete(Node node) {
containerRepository.remove(node.getIdentifier());
}
@Override
public Optional<Node> find(String nodeId) {
lock.readLock().lock();
try {
return nodeMap.containsKey(nodeId)? Optional.of(nodeMap.get(nodeId)) : Optional.empty();
} finally {
lock.readLock().unlock();
}
public Optional<Node> findById(String nodeId) {
return containerRepository.findById(nodeId).map(NodeContainer::getNode);
}
@Override
public List<Node> all() {
lock.readLock().lock();
try {
return nodeMap.values().stream().toList();
} finally {
lock.readLock().unlock();
}
return containerRepository.all().stream().map(NodeContainer::getNode).toList();
}
}

View File

@ -1,58 +0,0 @@
package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.model.node.PickingMethod;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.picker.*;
import java.security.InvalidParameterException;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@RequiredArgsConstructor
public class PickerRepository {
private final UserRepository userRepository;
private final Map<String, RoomPicker> pickers = new ConcurrentHashMap<>();
public RoomPicker create(String nodeId, PickingMethod mode) {
var picker = of(mode);
pickers.put(nodeId, picker);
return picker;
}
public void remove(String nodeId) {
pickers.remove(nodeId);
}
public RoomPicker find(String nodeId) {
return pickers.get(nodeId);
}
public Room pick(String nodeId, Collection<User> users) {
return pickers.get(nodeId).pick(users);
}
private RoomPicker of(PickingMethod mode) {
switch (mode) {
case SEQUENTIAL_FILLING -> {
return new SequentialFillingPicker(userRepository);
}
case ROUND_ROBIN -> {
return new RoundRobinPicker(userRepository);
}
case LEAST_PICKED -> {
return new LeastPickedPicker(userRepository);
}
default -> throw new InvalidParameterException("Taken: " + mode);
}
}
}

View File

@ -1,163 +1,61 @@
package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.exception.NoRoomsAvailableException;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.container.NodeContainer;
import ru.dragonestia.picker.repository.impl.container.RoomContainer;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
@Repository
@Component
@RequiredArgsConstructor
public class RoomRepositoryImpl implements RoomRepository {
private final UserRepository userRepository;
private final PickerRepository pickerRepository;
private final Map<Node, Rooms> node2roomsMap = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock(true);
private final ContainerRepository containerRepository;
@Override
public void create(Room room) throws RoomAlreadyExistException {
var nodeId = room.getNodeIdentifier();
lock.writeLock().lock();
try {
var node = node2roomsMap.keySet().stream()
.filter(n -> room.getNodeIdentifier().equals(n.getIdentifier()))
.findFirst();
if (node.isEmpty()) {
throw new IllegalArgumentException("Node '" + nodeId + "' does not exist");
}
var rooms = node2roomsMap.get(node.get());
if (rooms.containsKey(room.getIdentifier())) {
throw new RoomAlreadyExistException(room.getNodeIdentifier(), room.getIdentifier());
}
rooms.put(room.getIdentifier(), new RoomContainer(room, new AtomicInteger(0)));
pickerRepository.find(room.getNodeIdentifier()).add(room);
} finally {
lock.writeLock().unlock();
}
containerRepository.findById(room.getNodeIdentifier())
.orElseThrow(() -> new NodeNotFoundException(room.getNodeIdentifier()))
.addRoom(room);
}
@Override
public void remove(Room room) {
var nodeId = room.getNodeIdentifier();
var node = node2roomsMap.keySet().stream()
.filter(n -> room.getNodeIdentifier().equals(n.getIdentifier()))
.findFirst();
lock.writeLock().lock();
try {
if (node.isEmpty()) {
throw new NodeNotFoundException("Node '" + nodeId + "' does not exist");
}
node2roomsMap.get(node.get()).remove(room.getIdentifier());
pickerRepository.find(room.getNodeIdentifier()).remove(room);
userRepository.onRemoveRoom(room);
} finally {
lock.writeLock().unlock();
}
containerRepository.findById(room.getNodeIdentifier())
.orElseThrow(() -> new NodeNotFoundException(room.getNodeIdentifier()))
.removeRoom(room);
}
@Override
public Optional<Room> find(Node node, String identifier) {
lock.readLock().lock();
try {
if (!node2roomsMap.containsKey(node)) {
throw new NodeNotFoundException("Node '" + node.getIdentifier() + "' does not exist");
}
var result = node2roomsMap.get(node).getOrDefault(identifier, null);
return result == null? Optional.empty() : Optional.of(result.room());
} finally {
lock.readLock().unlock();
}
return containerRepository.findById(node.getIdentifier())
.orElseThrow(() -> new NodeNotFoundException(node.getIdentifier()))
.findRoomById(identifier)
.map(RoomContainer::getRoom);
}
@Override
public List<Room> all(Node node) {
lock.readLock().lock();
try {
if (!node2roomsMap.containsKey(node)) {
throw new NodeNotFoundException("Node '%s' does not exists".formatted(node.getIdentifier()));
}
return node2roomsMap.get(node).values().stream().map(RoomContainer::room).toList();
} finally {
lock.readLock().unlock();
}
public Collection<Room> all(Node node) {
return containerRepository.findById(node.getIdentifier())
.orElseThrow(() -> new NodeNotFoundException(node.getIdentifier()))
.allRooms()
.stream().map(RoomContainer::getRoom).toList();
}
@Override
public Optional<Room> pickFree(Node node, Collection<User> users) {
lock.writeLock().lock();
try {
if (!node2roomsMap.containsKey(node)) {
throw new NodeNotFoundException("Node '" + node.getIdentifier() + "' does not exist");
public Room pick(Node node, Set<User> users) throws NoRoomsAvailableException {
return containerRepository.findById(node.getIdentifier())
.orElseThrow(() -> new NodeNotFoundException(node.getIdentifier()))
.pick(users);
}
Room room = null;
try {
room = pickerRepository.pick(node.getIdentifier(), users);
} catch (RuntimeException ignore) {} // TODO: may be problem. Check it later
Optional<RoomContainer> container = room == null?
Optional.empty() :
Optional.of(node2roomsMap.get(node).get(room.getIdentifier()));
if (container.isPresent()) {
var cont = container.get();
var addedUsers = userRepository.linkWithRoom(cont.room(), users, false);
cont.used().getAndAdd(addedUsers);
}
return container.map(RoomContainer::room);
} finally {
lock.writeLock().unlock();
}
}
@Override
public void onCreateNode(Node node) {
lock.writeLock().lock();
try {
node2roomsMap.put(node, new Rooms());
} finally {
lock.writeLock().unlock();
}
}
@Override
public List<Room> onRemoveNode(Node node) {
lock.writeLock().lock();
try {
var deleted = node2roomsMap.get(node).values().stream().map(container -> container.room).toList();
node2roomsMap.remove(node);
return deleted;
} finally {
lock.writeLock().unlock();
}
}
private record RoomContainer(Room room, AtomicInteger used) {
public boolean isAvailable(int requiredSlots) {
return room.isAvailable(used.get(), requiredSlots);
}
}
private static class Rooms extends LinkedHashMap<String, RoomContainer> {}
}

View File

@ -1,207 +1,104 @@
package ru.dragonestia.picker.repository.impl;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.api.exception.NodeNotFoundException;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.api.exception.RoomNotFoundException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache;
import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker;
import ru.dragonestia.picker.repository.impl.container.RoomContainer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Repository
@Component
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository {
private final NodeId2PickerModeCache nodeId2PickerModeCache;
private final Map<User, Set<Room>> usersMap = new ConcurrentHashMap<>();
private final Map<NodeRoomPath, Set<User>> roomUsers = new ConcurrentHashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock(true);
private final ContainerRepository containerRepository;
private final Map<User, Set<Room>> userRooms = new ConcurrentHashMap<>();
@PostConstruct
void init() {
containerRepository.setTransactionListener(transaction -> {
synchronized (userRooms) {
for (var user: transaction.target()) {
var set = userRooms.computeIfAbsent(user, k -> new HashSet<>());
set.add(transaction.room());
}
}
});
}
@Override
public int linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException {
var toAdd = new HashSet<User>();
public void linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException {
synchronized (userRooms) {
getRoomContainer(room).addUsers(users, force);
lock.writeLock().lock();
try {
var path = new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier());
var usersSet = roomUsers.getOrDefault(path, new HashSet<>());
if (!force && !room.hasUnlimitedSlots()) {
if (room.getMaxSlots() < usersSet.size() + users.size()) {
throw new RoomAreFullException(room.getNodeIdentifier(), room.getIdentifier());
}
}
users.forEach(user -> {
var set = usersMap.getOrDefault(user, new HashSet<>());
if (!set.contains(room)) {
toAdd.add(user);
for (var user: users) {
var set = userRooms.computeIfAbsent(user, k -> new HashSet<>());
set.add(room);
}
usersMap.put(user, set);
});
usersSet.addAll(toAdd);
roomUsers.put(path, usersSet);
var picker = nodeId2PickerModeCache.get(room.getNodeIdentifier());
if (picker instanceof LeastPickedPicker leastPickedPicker) {
leastPickedPicker.updateUsersAmount(room, roomUsers.get(path).size());
}
} finally {
lock.writeLock().unlock();
}
return toAdd.size();
}
@Override
public void unlinkWithRoom(Room room, Collection<User> users) {
var counter = new AtomicInteger();
lock.writeLock().lock();
try {
usersMap.forEach((user, set) -> {
if (!set.contains(room)) return;
synchronized (userRooms) {
getRoomContainer(room).removeUsers(users);
for (var user: users) {
var set = userRooms.get(user);
if (set == null) continue;
set.remove(room);
counter.incrementAndGet();
if (set.isEmpty()) {
usersMap.remove(user);
if (set.isEmpty()) userRooms.remove(user);
}
});
var path = new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier());
var set = roomUsers.getOrDefault(path, new HashSet<>());
set.removeAll(users);
if (set.isEmpty()) {
roomUsers.remove(path);
} else {
roomUsers.put(path, set);
}
var picker = nodeId2PickerModeCache.get(room.getNodeIdentifier());
if (picker instanceof LeastPickedPicker leastPickedPicker) {
leastPickedPicker.updateUsersAmount(room, set.size());
}
} finally {
lock.writeLock().unlock();
}
counter.get();
}
@Override
public List<Room> findAllLinkedUserRooms(User user) {
lock.writeLock().lock();
try {
return usersMap.getOrDefault(user, new HashSet<>()).stream().toList();
} finally {
lock.writeLock().unlock();
}
}
@Override
public void onRemoveRoom(Room room) {
lock.writeLock().lock();
try {
var users = roomUsers.remove(new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier()));
if (users == null) return;
users.forEach(user -> {
var set = usersMap.getOrDefault(user, new HashSet<>());
set.remove(room);
if (set.isEmpty()) {
usersMap.remove(user);
}
});
} finally {
lock.writeLock().unlock();
}
public Collection<Room> findAllLinkedUserRooms(User user) {
return Collections.unmodifiableSet(userRooms.get(user));
}
@Override
public List<User> usersOf(Room room) {
lock.readLock().lock();
try {
return roomUsers.getOrDefault(new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier()), new HashSet<>())
.stream()
.toList();
} finally {
lock.readLock().unlock();
}
public Collection<User> usersOf(Room room) {
return getRoomContainer(room).allUsers();
}
@Override
public List<User> search(String input) {
lock.readLock().lock();
try {
return usersMap.keySet().stream()
.filter(user -> user.getIdentifier().startsWith(input))
.sorted(Comparator.comparing(User::getIdentifier))
.toList();
} finally {
lock.readLock().unlock();
}
public Collection<User> search(String input) {
return userRooms.keySet().stream().filter(user -> user.getIdentifier().startsWith(input)).toList();
}
@Override
public int countAllUsers() {
lock.readLock().lock();
try {
return usersMap.size();
} finally {
lock.readLock().unlock();
}
return userRooms.size();
}
@Override
public Map<String, Integer> countUsersForNodes() {
var map = new HashMap<String, Set<User>>();
lock.readLock().lock();
try {
roomUsers.forEach((path, users) -> {
if (map.containsKey(path.node)) {
map.get(path.node).addAll(users);
return;
}
map.put(path.node, new HashSet<>(users));
});
} finally {
lock.readLock().unlock();
}
var result = new HashMap<String, Integer>();
map.forEach((node, users) -> result.put(node, users.size()));
containerRepository.all().forEach(nodeContainer -> {
var nodeId = nodeContainer.getNode().getIdentifier();
nodeContainer.allRooms().forEach(roomContainer -> {
result.put(nodeId, result.getOrDefault(nodeId, 0) + roomContainer.countUsers());
});
});
return result;
}
private record NodeRoomPath(String node, String room) {
@Override
public int hashCode() {
return Objects.hash(node, room);
}
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (o == this) return true;
if (o instanceof NodeRoomPath other) {
return other.node().equals(node()) && other.room().equals(room());
}
return false;
}
private RoomContainer getRoomContainer(Room room) {
return containerRepository.findById(room.getNodeIdentifier())
.orElseThrow(() -> new NodeNotFoundException(room.getNodeIdentifier()))
.findRoomById(room.getIdentifier())
.orElseThrow(() -> new RoomNotFoundException(room.getNodeIdentifier(), room.getIdentifier()));
}
}

View File

@ -1,25 +0,0 @@
package ru.dragonestia.picker.repository.impl.cache;
import org.springframework.stereotype.Component;
import ru.dragonestia.picker.repository.impl.picker.RoomPicker;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class NodeId2PickerModeCache {
private final Map<String, RoomPicker> cache = new ConcurrentHashMap<>();
public void put(String nodeId, RoomPicker picker) {
cache.put(nodeId, picker);
}
public void remove(String nodeId) {
cache.remove(nodeId);
}
public RoomPicker get(String nodeId) {
return cache.get(nodeId);
}
}

View File

@ -0,0 +1,104 @@
package ru.dragonestia.picker.repository.impl.container;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.picker.api.exception.RoomAlreadyExistException;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker;
import ru.dragonestia.picker.repository.impl.picker.RoomPicker;
import ru.dragonestia.picker.repository.impl.picker.RoundRobinPicker;
import ru.dragonestia.picker.repository.impl.picker.SequentialFillingPicker;
import ru.dragonestia.picker.repository.impl.type.UserTransaction;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class NodeContainer {
@Getter
private final Node node;
private final UserTransaction.Listener transactionListener;
private final RoomPicker picker;
private final ReadWriteLock roomLock = new ReentrantReadWriteLock();
private final Map<String, RoomContainer> rooms = new ConcurrentHashMap<>();
public NodeContainer(@NotNull Node node, @NotNull UserTransaction.Listener transactionListener) {
this.node = node;
this.transactionListener = transactionListener;
this.picker = initPicker();
}
private @NotNull RoomPicker initPicker() {
return switch (node.getPickingMethod()) {
case SEQUENTIAL_FILLING -> new SequentialFillingPicker();
case ROUND_ROBIN -> new RoundRobinPicker();
case LEAST_PICKED -> new LeastPickedPicker();
};
}
public void addRoom(Room room) throws RoomAlreadyExistException {
roomLock.writeLock().lock();
try {
if (rooms.containsKey(room.getIdentifier())) {
throw new RoomAlreadyExistException(node.getIdentifier(), room.getIdentifier());
}
var container = new RoomContainer(room, this);
rooms.put(room.getIdentifier(), container);
picker.add(container);
} finally {
roomLock.writeLock().unlock();
}
}
public void removeRoom(@NotNull Room room) {
roomLock.writeLock().lock();
try {
picker.remove(rooms.remove(room.getIdentifier()));
} finally {
roomLock.writeLock().unlock();
}
}
public void removeRoomsByIds(@NotNull Collection<String> roomIds) {
roomLock.writeLock().lock();
try {
roomIds.forEach(roomId -> picker.remove(rooms.remove(roomId)));
} finally {
roomLock.writeLock().unlock();
}
}
public @NotNull Optional<RoomContainer> findRoomById(@NotNull String roomId) {
roomLock.readLock().lock();
try {
return Optional.ofNullable(rooms.get(roomId));
} finally {
roomLock.readLock().unlock();
}
}
public @NotNull Collection<RoomContainer> allRooms() {
roomLock.readLock().lock();
try {
return rooms.values();
} finally {
roomLock.readLock().unlock();
}
}
public @NotNull Room pick(@NotNull Set<User> users) {
var room = picker.pick(users);
transactionListener.accept(new UserTransaction(room.getRoom(), users));
return room.getRoom();
}
public @NotNull RoomPicker getPicker() {
return picker;
}
}

View File

@ -0,0 +1,105 @@
package ru.dragonestia.picker.repository.impl.container;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.picker.api.exception.RoomAreFullException;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class RoomContainer {
@Getter
private final Room room;
private final NodeContainer container;
private final ReadWriteLock usersLock = new ReentrantReadWriteLock();
private final Set<User> users = new HashSet<>();
public RoomContainer(@NotNull Room room, @NotNull NodeContainer container) {
this.room = room;
this.container = container;
}
public void addUsers(@NotNull Collection<User> toAdd, boolean force) {
usersLock.writeLock().lock();
try {
if (force || canAdd0(toAdd.size())) {
users.addAll(toAdd);
noticePickersAboutUserNumberUpdate();
} else {
throw new RoomAreFullException(room.getNodeIdentifier(), room.getIdentifier());
}
} finally {
usersLock.writeLock().unlock();
}
}
public void removeUsers(@NotNull Collection<User> toRemove) {
usersLock.writeLock().lock();
try {
users.removeAll(toRemove);
noticePickersAboutUserNumberUpdate();
} finally {
usersLock.writeLock().unlock();
}
}
public @NotNull Collection<User> removeAllUsers() {
usersLock.writeLock().lock();
try {
var set = new HashSet<>(users);
users.clear();
noticePickersAboutUserNumberUpdate();
return set;
} finally {
usersLock.writeLock().unlock();
}
}
public @NotNull Collection<User> allUsers() {
usersLock.readLock().lock();
try {
return Collections.unmodifiableSet(users);
} finally {
usersLock.readLock().unlock();
}
}
public int countUsers() {
return users.size();
}
private boolean canAdd0(int users) {
return room.hasUnlimitedSlots() || users + countUsers() <= room.getMaxSlots();
}
public boolean canAdd(int users) {
try {
return canAdd0(users);
} finally {
usersLock.readLock().unlock();
}
}
public boolean canBePicked(int users) {
try {
return !room.isLocked() && canAdd0(users);
} finally {
usersLock.readLock().unlock();
}
}
private void noticePickersAboutUserNumberUpdate() {
if (container.getPicker() instanceof LeastPickedPicker picker) {
picker.updateUsersAmount(room, countUsers());
}
}
}

View File

@ -5,34 +5,30 @@ import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap;
import ru.dragonestia.picker.repository.impl.container.RoomContainer;
import java.util.Collection;
public class LeastPickedPicker implements RoomPicker {
private final UserRepository userRepository;
private final DynamicSortedMap<RoomWrapper> map = new DynamicSortedMap<>();
public LeastPickedPicker(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void add(Room room) {
public void add(RoomContainer container) {
synchronized (map) {
map.put(new RoomWrapper(room, () -> userRepository.usersOf(room).size()));
map.put(new RoomWrapper(container));
}
}
@Override
public void remove(Room room) {
public void remove(RoomContainer container) {
synchronized (map) {
map.removeById(room.getIdentifier());
map.removeById(container.getRoom().getIdentifier());
}
}
@Override
public Room pick(Collection<User> users) {
public RoomContainer pick(Collection<User> users) {
RoomWrapper wrapper;
synchronized (map) {

View File

@ -1,10 +1,10 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.api.model.node.PickingMethod;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.impl.container.RoomContainer;
public interface RoomPicker extends Picker<Room, User> {
public interface RoomPicker extends Picker<RoomContainer, User> {
PickingMethod getPickingMode();
}

View File

@ -1,46 +1,43 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList;
import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap;
import ru.dragonestia.picker.repository.impl.container.RoomContainer;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class RoomWrapper implements ItemWrapper<Room>, QueuedLinkedList.Item, DynamicSortedMap.Item {
public class RoomWrapper implements ItemWrapper<RoomContainer>, QueuedLinkedList.Item, DynamicSortedMap.Item {
private final Room room;
private final Supplier<Integer> userCountSupplier;
private final RoomContainer container;
private Consumer<Integer> setter;
public RoomWrapper(Room room, Supplier<Integer> userCountSupplier) {
this.room = room;
this.userCountSupplier = userCountSupplier;
public RoomWrapper(RoomContainer container) {
this.container = container;
}
@Override
public String getId() {
return room.getIdentifier();
return container.getRoom().getIdentifier();
}
@Override
public int countUnits() {
return userCountSupplier.get();
return container.countUsers();
}
@Override
public int maxUnits() {
return room.getMaxSlots();
return container.getRoom().getMaxSlots();
}
@Override
public Room getItem() {
return room;
public RoomContainer getItem() {
return container;
}
@Override
public boolean canAddUnits(int amount) {
return ItemWrapper.super.canAddUnits(amount) && !room.isLocked();
return container.canBePicked(amount);
}
@Override

View File

@ -1,40 +1,34 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.api.model.node.PickingMethod;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList;
import ru.dragonestia.picker.repository.impl.container.RoomContainer;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
public class RoundRobinPicker implements RoomPicker {
private final UserRepository userRepository;
private final AtomicInteger addition = new AtomicInteger(0);
private final QueuedLinkedList<RoomWrapper> list = new QueuedLinkedList<>(wrapper -> wrapper.canAddUnits(addition.get()));
public RoundRobinPicker(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void add(Room room) {
public void add(RoomContainer container) {
synchronized (list) {
list.add(new RoomWrapper(room, () -> userRepository.usersOf(room).size()));
list.add(new RoomWrapper(container));
}
}
@Override
public void remove(Room room) {
public void remove(RoomContainer container) {
synchronized (list) {
list.removeById(room.getIdentifier());
list.removeById(container.getRoom().getIdentifier());
}
}
@Override
public Room pick(Collection<User> users) {
public RoomContainer pick(Collection<User> users) {
int amount = users.size();
RoomWrapper wrapper;

View File

@ -1,9 +1,8 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.api.model.node.PickingMethod;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.container.RoomContainer;
import java.util.Collection;
import java.util.LinkedHashMap;
@ -11,29 +10,24 @@ import java.util.Map;
public class SequentialFillingPicker implements RoomPicker {
private final UserRepository userRepository;
private final Map<String, RoomWrapper> wrappers = new LinkedHashMap<>();
public SequentialFillingPicker(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void add(Room room) {
public void add(RoomContainer container) {
synchronized (wrappers) {
wrappers.put(room.getIdentifier(), new RoomWrapper(room, () -> userRepository.usersOf(room).size()));
wrappers.put(container.getRoom().getIdentifier(), new RoomWrapper(container));
}
}
@Override
public void remove(Room room) {
public void remove(RoomContainer container) {
synchronized (wrappers) {
wrappers.remove(room.getIdentifier());
wrappers.remove(container.getRoom().getIdentifier());
}
}
@Override
public Room pick(Collection<User> users) {
public RoomContainer pick(Collection<User> users) {
int amount = users.size();
synchronized (wrappers) {

View File

@ -0,0 +1,13 @@
package ru.dragonestia.picker.repository.impl.type;
import org.jetbrains.annotations.NotNull;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import java.util.Collection;
import java.util.function.Consumer;
public record UserTransaction(@NotNull Room room, Collection<User> target) {
public interface Listener extends Consumer<UserTransaction> {}
}

View File

@ -9,6 +9,7 @@ import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.User;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@ -21,11 +22,11 @@ public interface RoomService {
Optional<Room> find(Node node, String roomId);
List<Room> all(Node node);
Collection<Room> all(Node node);
List<ShortResponseRoom> getAllRoomsWithDetailsResponse(Node node, Set<RoomDetails> details);
PickedRoomResponse pickAvailable(Node node, List<User> users);
PickedRoomResponse pickAvailable(Node node, Set<User> users);
void updateState(Room room);
}

View File

@ -14,7 +14,7 @@ import java.util.Set;
public interface UserService {
List<Room> getUserRooms(User user);
Collection<Room> getUserRooms(User user);
List<ShortResponseRoom> getUserRoomsWithDetails(User user, Set<RoomDetails> details);
@ -22,7 +22,7 @@ public interface UserService {
void unlinkUsersFromRoom(Room room, Collection<User> users);
List<User> getRoomUsers(Room room);
Collection<User> getRoomUsers(Room room);
List<ResponseUser> getRoomUsersWithDetailsResponse(Room room, Set<UserDetails> details);

View File

@ -8,6 +8,7 @@ import ru.dragonestia.picker.api.model.node.NodeDetails;
import ru.dragonestia.picker.api.model.node.ResponseNode;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.service.NodeService;
import ru.dragonestia.picker.storage.NodeAndRoomStorage;
import ru.dragonestia.picker.util.DetailsExtractor;
@ -23,22 +24,24 @@ import java.util.Set;
public class NodeServiceImpl implements NodeService {
private final NodeRepository nodeRepository;
private final RoomRepository roomRepository;
private final DetailsExtractor detailsExtractor;
private final NamingValidator namingValidator;
private final NodeAndRoomStorage storage;
@Override
public void create(Node node) throws InvalidNodeIdentifierException, NodeAlreadyExistException {
namingValidator.validateNodeId(node.getIdentifier());
nodeRepository.create(node);
storage.saveNode(node);
}
@Override
public void remove(Node node) {
for (var room: nodeRepository.delete(node)) {
for (var room: roomRepository.all(node)) {
storage.removeRoom(room);
}
nodeRepository.delete(node);
storage.removeNode(node);
}
@ -58,6 +61,6 @@ public class NodeServiceImpl implements NodeService {
@Override
public Optional<Node> find(String nodeId) {
return nodeRepository.find(nodeId);
return nodeRepository.findById(nodeId);
}
}

View File

@ -21,10 +21,7 @@ import ru.dragonestia.picker.storage.NodeAndRoomStorage;
import ru.dragonestia.picker.util.DetailsExtractor;
import ru.dragonestia.picker.util.NamingValidator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
@Log4j2
@ -43,7 +40,7 @@ public class RoomServiceImpl implements RoomService {
public void create(Room room) throws InvalidRoomIdentifierException, RoomAlreadyExistException, NotPersistedNodeException {
namingValidator.validateRoomId(room.getNodeIdentifier(), room.getIdentifier());
var node = nodeRepository.find(room.getNodeIdentifier()).orElseThrow(() -> new NodeNotFoundException(room.getNodeIdentifier()));
var node = nodeRepository.findById(room.getNodeIdentifier()).orElseThrow(() -> new NodeNotFoundException(room.getNodeIdentifier()));
if (!node.isPersist() && room.isPersist()) {
throw new NotPersistedNodeException(node.getIdentifier(), room.getIdentifier());
}
@ -64,7 +61,7 @@ public class RoomServiceImpl implements RoomService {
}
@Override
public List<Room> all(Node node) {
public Collection<Room> all(Node node) {
return roomRepository.all(node);
}
@ -78,9 +75,8 @@ public class RoomServiceImpl implements RoomService {
}
@Override
public PickedRoomResponse pickAvailable(Node node, List<User> users) {
var room = roomRepository.pickFree(node, users)
.orElseThrow(() -> new RuntimeException("There are no rooms available. Given users count: " + users.size()));
public PickedRoomResponse pickAvailable(Node node, Set<User> users) {
var room = roomRepository.pick(node, users);
var roomUsers = userRepository.usersOf(room);
return new PickedRoomResponse(

View File

@ -23,7 +23,7 @@ public class UserServiceImpl implements UserService {
private final DetailsExtractor detailsExtractor;
@Override
public List<Room> getUserRooms(User user) {
public Collection<Room> getUserRooms(User user) {
return userRepository.findAllLinkedUserRooms(user);
}
@ -47,7 +47,7 @@ public class UserServiceImpl implements UserService {
}
@Override
public List<User> getRoomUsers(Room room) {
public Collection<User> getRoomUsers(Room room) {
return userRepository.usersOf(room);
}

View File

@ -8,6 +8,7 @@ import ru.dragonestia.picker.api.repository.type.UserIdentifier;
import ru.dragonestia.picker.api.util.IdentifierValidator;
import ru.dragonestia.picker.model.User;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@ -30,7 +31,7 @@ public class NamingValidator {
return IdentifierValidator.forUser(input);
}
public List<User> validateUserIds(List<String> input) throws InvalidUsernamesException {
public void validateUserIds(Collection<String> input) throws InvalidUsernamesException {
var users = new LinkedList<User>();
var invalid = new LinkedList<String>();
@ -44,9 +45,7 @@ public class NamingValidator {
}
if (!invalid.isEmpty()) {
throw new InvalidUsernamesException(input, invalid);
throw new InvalidUsernamesException(input.stream().toList(), invalid);
}
return users;
}
}

View File

@ -40,7 +40,7 @@ public class LeastPickedTests {
@ParameterizedTest
@ArgumentsSource(PickingArgumentProvider.class)
void testPicking(String expectedRoomId, int usersAmount) {
var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(usersAmount));
var roomOpt = roomRepository.pick(node, userFiller.createRandomUsers(usersAmount));
Assertions.assertTrue(roomOpt.isPresent());
var room = roomOpt.get();
@ -73,7 +73,7 @@ public class LeastPickedTests {
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
@Test
void testNoOneRoomExpected() { // Take 9 users. expected none result
var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(9));
var roomOpt = roomRepository.pick(node, userFiller.createRandomUsers(9));
Assertions.assertTrue(roomOpt.isEmpty());
}
}

View File

@ -8,7 +8,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import ru.dragonestia.picker.api.model.node.INode;
import ru.dragonestia.picker.config.FillingNodesConfig;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.repository.RoomRepository;
@ -39,7 +38,7 @@ public class RoundRobinTests {
@ParameterizedTest
@ArgumentsSource(PickingArgumentProvider.class)
void testPicking(String expectedRoomId, int usersAmount) {
var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(usersAmount));
var roomOpt = roomRepository.pick(node, userFiller.createRandomUsers(usersAmount));
Assertions.assertTrue(roomOpt.isPresent());
var room = roomOpt.get();
@ -69,7 +68,7 @@ public class RoundRobinTests {
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
@Test
void testNoOneRoomExpected() { // Take 9 users. expected none result
var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(9));
var roomOpt = roomRepository.pick(node, userFiller.createRandomUsers(9));
Assertions.assertTrue(roomOpt.isEmpty());
}
}

View File

@ -40,7 +40,7 @@ public class SequentialFillingTests {
@ParameterizedTest
@ArgumentsSource(PickingArgumentProvider.class)
void testPicking(String expectedRoomId, int usersAmount) {
var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(usersAmount));
var roomOpt = roomRepository.pick(node, userFiller.createRandomUsers(usersAmount));
Assertions.assertTrue(roomOpt.isPresent());
var room = roomOpt.get();
@ -70,7 +70,7 @@ public class SequentialFillingTests {
@Timeout(value = 1, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
@Test
void testNoOneRoomExpected() { // Take 9 users. expected none result
var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(9));
var roomOpt = roomRepository.pick(node, userFiller.createRandomUsers(9));
Assertions.assertTrue(roomOpt.isEmpty());
}
}