diff --git a/.github/img/grafana.png b/.github/img/grafana.png new file mode 100644 index 0000000..c8b4f8c Binary files /dev/null and b/.github/img/grafana.png differ diff --git a/README.md b/README.md index 0f9e075..ddfbb91 100644 --- a/README.md +++ b/README.md @@ -59,4 +59,13 @@ class Example { } } } -``` \ No newline at end of file +``` + + +Stress testing +---------- + +If you want to stress test the server, you can run the script from the `noiser` module. +Before starting stress testing, you'd better run Prometheus and Grafana from `docker-compose.yml`. +In Grafana, you can see this view: + \ No newline at end of file diff --git a/RoomPicker-Grafana-Dashboard.json b/RoomPicker-Grafana-Dashboard.json index 8225074..e2abb7c 100644 --- a/RoomPicker-Grafana-Dashboard.json +++ b/RoomPicker-Grafana-Dashboard.json @@ -453,7 +453,7 @@ "axisPlacement": "left", "barAlignment": 0, "drawStyle": "bars", - "fillOpacity": 30, + "fillOpacity": 26, "gradientMode": "none", "hideFrom": { "legend": false, @@ -462,7 +462,7 @@ }, "insertNulls": false, "lineInterpolation": "stepAfter", - "lineWidth": 2, + "lineWidth": 3, "pointSize": 3, "scaleDistribution": { "type": "linear" @@ -471,7 +471,7 @@ "spanNulls": false, "stacking": { "group": "A", - "mode": "normal" + "mode": "none" }, "thresholdsStyle": { "mode": "off" @@ -725,13 +725,13 @@ "list": [] }, "time": { - "from": "now-6h", + "from": "now-15m", "to": "now" }, "timepicker": {}, "timezone": "", "title": "RoomPicker", "uid": "e3009deb-a342-4a32-9d13-3b44ed3461bc", - "version": 24, + "version": 26, "weekStart": "" } \ No newline at end of file diff --git a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/NodeRepositoryImpl.java b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/NodeRepositoryImpl.java index 08034a3..2c195b3 100644 --- a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/NodeRepositoryImpl.java +++ b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/repository/NodeRepositoryImpl.java @@ -76,6 +76,10 @@ public void saveNode(@NotNull NodeDefinition definition) { @Override public @NotNull PickedRoomResponse pickRoom(@NotNull NodeIdentifier identifier, @NotNull Set<UserIdentifier> users) { - throw new UnsupportedOperationException("Not implemented"); + return rest.queryPostWithBody( + "/nodes/" + identifier.getValue() + "/pick", + PickedRoomResponse.class, + params -> {}, String.join(",", users.stream().map(user -> user.getValue()).toList()) + ); } } diff --git a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java index 4d1dbbe..9e440be 100644 --- a/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java +++ b/client-impl/src/main/java/ru/dragonestia/picker/api/impl/util/RestTemplate.java @@ -3,9 +3,7 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import okhttp3.FormBody; -import okhttp3.OkHttpClient; -import okhttp3.Response; +import okhttp3.*; import org.jetbrains.annotations.ApiStatus.Internal; import ru.dragonestia.picker.api.exception.ExceptionFactory; import ru.dragonestia.picker.api.impl.RoomPickerClient; @@ -78,6 +76,22 @@ public <T> T query(String uri, HttpMethod method, Class<T> clazz, ParamsConsumer } } + public <T> T queryPostWithBody(String uri, Class<T> clazz, ParamsConsumer paramsConsumer, String body) { + var request = client.prepareRequestBuilder(uri + queryEncode(paramsConsumer)) + .post(RequestBody.create(body, MediaType.get("text/plain"))) + .build(); + + try (var response = httpClient.newCall(request).execute()) { + checkResponseForErrors(response); + + return json.readValue(new String(Objects.requireNonNull(response.body()).bytes(), StandardCharsets.UTF_8), clazz); + } catch (JsonProcessingException ex) { + throw new RuntimeException("Json processing error", ex); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + private String queryEncode(ParamsConsumer paramsConsumer) { var params = new HashMap<String, String>(); paramsConsumer.accept(params); diff --git a/control-panel/frontend/generated/index.ts b/control-panel/frontend/generated/index.ts new file mode 100644 index 0000000..1b1823b --- /dev/null +++ b/control-panel/frontend/generated/index.ts @@ -0,0 +1,32 @@ +/****************************************************************************** + * This file is auto-generated by Vaadin. + * If you want to customize the entry point, you can copy this file or create + * your own `index.ts` in your frontend directory. + * By default, the `index.ts` file should be in `./frontend/` folder. + * + * NOTE: + * - You need to restart the dev-server after adding the new `index.ts` file. + * After that, all modifications to `index.ts` are recompiled automatically. + * - `index.js` is also supported if you don't want to use TypeScript. + ******************************************************************************/ + +// import Vaadin client-router to handle client-side and server-side navigation +import { Router } from '@vaadin/router'; + +// import Flow module to enable navigation to Vaadin server-side views +import { Flow } from 'Frontend/generated/jar-resources/Flow.js'; + +const { serverSideRoutes } = new Flow({ + imports: () => import('Frontend/generated/flow/generated-flow-imports.js') +}); + +const routes = [ + // for client-side, place routes below (more info https://hilla.dev/docs/lit/guides/routing#initializing-the-router) + + // for server-side, the next magic line sends all unmatched routes: + ...serverSideRoutes // IMPORTANT: this must be the last entry in the array +]; + +// Vaadin router needs an outlet in the index.html page to display views +const router = new Router(document.querySelector('#outlet')); +router.setRoutes(routes); diff --git a/noiser/build.gradle b/noiser/build.gradle new file mode 100644 index 0000000..0efd63f --- /dev/null +++ b/noiser/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +group = 'ru.dragonestia' +version = 'unspecified' + +repositories { + mavenCentral() +} + +dependencies { + implementation project(":client-api") + implementation project(":client-impl") + + testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/noiser/src/main/java/ru/dragonestia/picker/noiser/Main.java b/noiser/src/main/java/ru/dragonestia/picker/noiser/Main.java new file mode 100644 index 0000000..75eba8d --- /dev/null +++ b/noiser/src/main/java/ru/dragonestia/picker/noiser/Main.java @@ -0,0 +1,127 @@ +package ru.dragonestia.picker.noiser; + +import ru.dragonestia.picker.api.impl.RoomPickerClient; +import ru.dragonestia.picker.api.model.node.NodeDefinition; +import ru.dragonestia.picker.api.model.node.PickingMethod; +import ru.dragonestia.picker.api.model.room.RoomDefinition; +import ru.dragonestia.picker.api.repository.query.node.GetAllNodes; +import ru.dragonestia.picker.api.repository.query.user.UnlinkUsersFromRoom; +import ru.dragonestia.picker.api.repository.type.NodeIdentifier; +import ru.dragonestia.picker.api.repository.type.RoomIdentifier; +import ru.dragonestia.picker.api.repository.type.UserIdentifier; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + + +public class Main { + + private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(8); + private final Random random = new Random(); + private final RoomPickerClient client; + private final List<NodeIdentifier> nodes; + + private final Map<NodeIdentifier, AtomicInteger> totalUsers = new ConcurrentHashMap<>(); + private final int expectedUsers = 10000; + + public Main() { + client = new RoomPickerClient("http://localhost:8080", "admin", "qwerty123"); + nodes = initNodes(); + } + + private void removeAll() { + client.getNodeRepository().allNodes(GetAllNodes.JUST) + .forEach(node -> client.getNodeRepository().removeNode(node)); + } + + private List<NodeIdentifier> initNodes() { + removeAll(); + + var list = new ArrayList<NodeIdentifier>(); + + for (int i = 0; i < 5; i++) { + var node = new NodeDefinition(NodeIdentifier.of("test-node-" + i)) + .setPickingMethod(PickingMethod.values()[i % PickingMethod.values().length]); + + client.getNodeRepository().saveNode(node); + + var nodeId = node.getIdentifierObject(); + totalUsers.put(nodeId, new AtomicInteger(0)); + list.add(nodeId); + } + + return list; + } + + private void initRooms() { + final int perNode = expectedUsers / nodes.size(); + final int roomsPerNode = perNode / 10; + + for (var nodeId: nodes) { + for (int i = 0; i < roomsPerNode; i++) { + client.getRoomRepository().saveRoom( + new RoomDefinition(nodeId, RoomIdentifier.of(UUID.randomUUID().toString())).setMaxSlots(10) + ); + } + } + } + + private void pickUsers() { + for (var nodeId: nodes) { + var usersInNode = totalUsers.get(nodeId); + + try { + synchronized (usersInNode) { + var users = new HashSet<UserIdentifier>(); + var maxAdd = Math.min(10, (expectedUsers / nodes.size()) - usersInNode.get()); + + if (maxAdd == 0) return; + var add = maxAdd == 1 ? 1 : (random.nextInt(maxAdd - 1) + 1); + for (int i = 0; i < add; i++) { + users.add(UserIdentifier.of(UUID.randomUUID().toString())); + } + + var request = client.getNodeRepository().pickRoom(nodeId, users); + usersInNode.addAndGet(add); + var roomId = RoomIdentifier.of(request.roomId()); + + System.out.printf("Added %s(total %s) users to %s/%s%n", add, usersInNode.get(), nodeId.getValue(), roomId.getValue()); + + scheduler.schedule(() -> { + try { + client.getUserRepository().unlinkUsersFromRoom(UnlinkUsersFromRoom.builder() + .setNodeId(nodeId) + .setRoomId(roomId) + .setUsers(users) + .build()); + + usersInNode.addAndGet(-add); + System.out.printf("Reduced %s users from %s/%s%n", add, nodeId.getValue(), roomId.getValue()); + } catch (Exception ex) { + System.out.println("Error(" + ex.getClass().getSimpleName() + "): " + ex.getMessage()); + } + }, random.nextInt(10) + 1, TimeUnit.SECONDS); + } + } catch (Exception ex) { + System.out.println("Error(" + ex.getClass().getSimpleName() + "): " + ex.getMessage()); + } + } + } + + public void startNoise() throws InterruptedException { + initRooms(); + + while (true) { + pickUsers(); + Thread.sleep(10); + } + } + + public static void main(String[] args) throws InterruptedException { + new Main().startNoise(); + } +} \ No newline at end of file diff --git a/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java b/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java index a268434..db9fd7c 100644 --- a/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java +++ b/server/src/main/java/ru/dragonestia/picker/aspect/UserMetricsAspect.java @@ -9,18 +9,16 @@ import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; -import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import ru.dragonestia.picker.event.UpdateRoomLockStateEvent; import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.repository.RoomRepository; import ru.dragonestia.picker.repository.UserRepository; +import ru.dragonestia.picker.repository.impl.ContainerRepository; -import java.util.HashSet; import java.util.Map; -import java.util.Set; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -30,6 +28,7 @@ @Log4j2 public class UserMetricsAspect { + private final ContainerRepository containerRepository; private final RoomRepository roomRepository; private final UserRepository userRepository; private final MeterRegistry meterRegistry; @@ -52,33 +51,8 @@ void onUnlinkUsers(Room room) { countAllUsers(room); } - @AfterReturning(value = "execution(void ru.dragonestia.picker.repository.RoomRepository.create(ru.dragonestia.picker.model.Room)) && args(room)", argNames = "room") - void onCreateRoom(Room room) { - checkRoom(room); - } - - @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()); - - 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") @@ -93,7 +67,7 @@ void onCreateNode(Node node) { .baseUnit("1s") .register(meterRegistry); - var lockedGauge = Gauge.builder("roompicker_locked_rooms", () -> data.get(nodeId).locked().size()) + var lockedGauge = Gauge.builder("roompicker_locked_rooms", () -> data.get(nodeId).locked()) .tag("nodeId", nodeId) .register(meterRegistry); @@ -101,7 +75,7 @@ void onCreateNode(Node node) { .tag("nodeId", nodeId) .register(meterRegistry); - data.put(nodeId, new NodeData(gauge, new AtomicInteger(0), counter, new HashSet<>(), lockedGauge, roomsGauge)); + data.put(nodeId, new NodeData(gauge, new AtomicInteger(0), counter, new AtomicInteger(0), lockedGauge, roomsGauge)); } @After(value = "execution(* ru.dragonestia.picker.repository.NodeRepository.delete(ru.dragonestia.picker.model.Node)) && args(node)", argNames = "node") @@ -114,20 +88,28 @@ void onDeleteNode(Node node) { 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(); } - @EventListener - void onRoomChangeLockState(UpdateRoomLockStateEvent event) { - checkRoom(event.room()); - } - @Scheduled(fixedDelay = 3_000) void updateUserMetrics() { - userRepository.countUsersForNodes().forEach((nodeId, users) -> data.get(nodeId).users().set(users)); + userRepository.countUsersForNodes().forEach((nodeId, users) -> { + Optional.ofNullable(data.get(nodeId)).ifPresent(node -> node.users().set(users)); + }); + + containerRepository.all().forEach(nodeContainer -> { + var locked = data.get(nodeContainer.getNode().getIdentifier()).locked(); + locked.set(0); + + nodeContainer.allRooms().forEach(roomContainer -> { + if (roomContainer.canBePicked(1)) return; + + locked.incrementAndGet(); + }); + }); } - private record NodeData(Gauge usersGauge, AtomicInteger users, Counter picksPerMinute, Set<Room> locked, Gauge lockedGauge, Gauge roomsGauge) {} + private record NodeData(Gauge usersGauge, AtomicInteger users, Counter picksPerMinute, AtomicInteger locked, Gauge lockedGauge, Gauge roomsGauge) {} } diff --git a/server/src/main/java/ru/dragonestia/picker/controller/NodeController.java b/server/src/main/java/ru/dragonestia/picker/controller/NodeController.java index c72a527..d07e5d5 100644 --- a/server/src/main/java/ru/dragonestia/picker/controller/NodeController.java +++ b/server/src/main/java/ru/dragonestia/picker/controller/NodeController.java @@ -8,17 +8,21 @@ 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 @@ -77,12 +81,14 @@ ResponseEntity<?> removeNode( @PostMapping("/{nodeId}/pick") ResponseEntity<PickedRoomResponse> pickRoom( @Parameter(description = "Node identifier") @PathVariable("nodeId") String nodeId, - @Parameter(description = "Users to add", example = "user1,user3,user3") @RequestParam(name = "userIds") String userIds + @RequestBody String userIds ) { 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); diff --git a/server/src/main/java/ru/dragonestia/picker/controller/RoomController.java b/server/src/main/java/ru/dragonestia/picker/controller/RoomController.java index 2c1a578..9eda134 100644 --- a/server/src/main/java/ru/dragonestia/picker/controller/RoomController.java +++ b/server/src/main/java/ru/dragonestia/picker/controller/RoomController.java @@ -57,8 +57,8 @@ ResponseEntity<?> register( ) { var node = nodeService.find(nodeId).orElseThrow(() -> new NodeNotFoundException(nodeId)); var room = roomFactory.create(RoomIdentifier.of(roomId), node, slots, payload, persist); - room.setLocked(locked); roomService.create(room); + room.setLocked(locked); return ResponseEntity.ok().build(); } diff --git a/server/src/main/java/ru/dragonestia/picker/controller/UserRoomController.java b/server/src/main/java/ru/dragonestia/picker/controller/UserRoomController.java index 6b5e9b0..180e0fe 100644 --- a/server/src/main/java/ru/dragonestia/picker/controller/UserRoomController.java +++ b/server/src/main/java/ru/dragonestia/picker/controller/UserRoomController.java @@ -10,8 +10,10 @@ 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.NamingValidator; import java.util.Arrays; +import java.util.stream.Collectors; @Tag(name = "Users", description = "User management") @RequiredArgsConstructor @@ -54,7 +57,9 @@ ResponseEntity<LinkUsersWithRoomResponse> linkUserWithRoom( @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 @@ ResponseEntity<?> unlinkUsersForBucket( ) { 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(); } diff --git a/server/src/main/java/ru/dragonestia/picker/model/Node.java b/server/src/main/java/ru/dragonestia/picker/model/Node.java index 36e704c..ab49bdd 100644 --- a/server/src/main/java/ru/dragonestia/picker/model/Node.java +++ b/server/src/main/java/ru/dragonestia/picker/model/Node.java @@ -58,4 +58,9 @@ public boolean equals(Object object) { } return false; } + + @Override + public String toString() { + return "{Node id='%s'}".formatted(identifier); + } } diff --git a/server/src/main/java/ru/dragonestia/picker/model/Room.java b/server/src/main/java/ru/dragonestia/picker/model/Room.java index fb3bfa5..5404fc5 100644 --- a/server/src/main/java/ru/dragonestia/picker/model/Room.java +++ b/server/src/main/java/ru/dragonestia/picker/model/Room.java @@ -19,15 +19,13 @@ public class Room implements IRoom { private final String payload; private final boolean persist; private boolean locked = false; - private final UpdateRoomLockStateEvent.Listener updateLockStateListener; - public Room(@NotNull RoomIdentifier identifier, @NotNull Node node, int slots, @NotNull String payload, boolean persist, @Nullable UpdateRoomLockStateEvent.Listener listener) { + public Room(@NotNull RoomIdentifier identifier, @NotNull Node node, int slots, @NotNull String payload, boolean persist) { this.identifier = identifier.getValue(); this.nodeIdentifier = node.getIdentifier(); this.slots = slots; this.payload = payload; this.persist = persist; - this.updateLockStateListener = listener; } @Override @@ -52,10 +50,6 @@ public boolean isLocked() { public void setLocked(boolean value) { locked = value; - - if (updateLockStateListener != null) { - updateLockStateListener.accept(new UpdateRoomLockStateEvent(this)); - } } @Override diff --git a/server/src/main/java/ru/dragonestia/picker/model/factory/RoomFactory.java b/server/src/main/java/ru/dragonestia/picker/model/factory/RoomFactory.java index 938e6c2..7fee26e 100644 --- a/server/src/main/java/ru/dragonestia/picker/model/factory/RoomFactory.java +++ b/server/src/main/java/ru/dragonestia/picker/model/factory/RoomFactory.java @@ -13,10 +13,8 @@ @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); + return new Room(identifier, node, slots, payload, persist); } } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/NodeRepository.java b/server/src/main/java/ru/dragonestia/picker/repository/NodeRepository.java index 7aa60da..86b4183 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/NodeRepository.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/NodeRepository.java @@ -2,7 +2,6 @@ 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(); } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/RoomRepository.java b/server/src/main/java/ru/dragonestia/picker/repository/RoomRepository.java index 6910fb1..0b17c3a 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/RoomRepository.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/RoomRepository.java @@ -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; } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/UserRepository.java b/server/src/main/java/ru/dragonestia/picker/repository/UserRepository.java index b8ccb9e..88d75d1 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/UserRepository.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/UserRepository.java @@ -10,17 +10,15 @@ 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(); diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/ContainerRepository.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/ContainerRepository.java new file mode 100644 index 0000000..5d9e02c --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/ContainerRepository.java @@ -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; + } +} diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java index defb133..8503c01 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/NodeRepositoryImpl.java @@ -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(); } } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/PickerRepository.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/PickerRepository.java deleted file mode 100644 index 536a0c3..0000000 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/PickerRepository.java +++ /dev/null @@ -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); - } - } -} diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/RoomRepositoryImpl.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/RoomRepositoryImpl.java index 8d76f11..fe570a0 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/RoomRepositoryImpl.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/RoomRepositoryImpl.java @@ -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"); - } - - 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); - } + public Room pick(Node node, Set<User> users) throws NoRoomsAvailableException { + return containerRepository.findById(node.getIdentifier()) + .orElseThrow(() -> new NodeNotFoundException(node.getIdentifier())) + .pick(users); } - - private static class Rooms extends LinkedHashMap<String, RoomContainer> {} } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java index 63ee5ee..6ffdf9b 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/UserRepositoryImpl.java @@ -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; - @Override - public int linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException { - var toAdd = new HashSet<User>(); - - lock.writeLock().lock(); - try { - var path = new NodeRoomPath(room.getNodeIdentifier(), room.getIdentifier()); - var usersSet = roomUsers.getOrDefault(path, new HashSet<>()); + private final Map<User, Set<Room>> userRooms = new ConcurrentHashMap<>(); - if (!force && !room.hasUnlimitedSlots()) { - if (room.getMaxSlots() < usersSet.size() + users.size()) { - throw new RoomAreFullException(room.getNodeIdentifier(), room.getIdentifier()); + @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()); } } + }); + } - users.forEach(user -> { - var set = usersMap.getOrDefault(user, new HashSet<>()); - if (!set.contains(room)) { - toAdd.add(user); - set.add(room); - } - usersMap.put(user, set); - }); - - usersSet.addAll(toAdd); - roomUsers.put(path, usersSet); + @Override + public void linkWithRoom(Room room, Collection<User> users, boolean force) throws RoomAreFullException { + synchronized (userRooms) { + getRoomContainer(room).addUsers(users, force); - var picker = nodeId2PickerModeCache.get(room.getNodeIdentifier()); - if (picker instanceof LeastPickedPicker leastPickedPicker) { - leastPickedPicker.updateUsersAmount(room, roomUsers.get(path).size()); + for (var user: users) { + var set = userRooms.computeIfAbsent(user, k -> new HashSet<>()); + set.add(room); } - } 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); - } - }); - - 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); + if (set.isEmpty()) userRooms.remove(user); } - - 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(); - } + public Collection<Room> findAllLinkedUserRooms(User user) { + return Collections.unmodifiableSet(userRooms.get(user)); } @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(); - } - } - - @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; - } + var result = new HashMap<String, Integer>(); - map.put(path.node, new HashSet<>(users)); - }); - } finally { - lock.readLock().unlock(); - } + containerRepository.all().forEach(nodeContainer -> { + var nodeId = nodeContainer.getNode().getIdentifier(); - var result = new HashMap<String, Integer>(); - map.forEach((node, users) -> result.put(node, users.size())); + 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())); } } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/cache/NodeId2PickerModeCache.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/cache/NodeId2PickerModeCache.java deleted file mode 100644 index 4e1a005..0000000 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/cache/NodeId2PickerModeCache.java +++ /dev/null @@ -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); - } -} diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/container/NodeContainer.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/container/NodeContainer.java new file mode 100644 index 0000000..76716a2 --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/container/NodeContainer.java @@ -0,0 +1,107 @@ +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(this); + case ROUND_ROBIN -> new RoundRobinPicker(this); + case LEAST_PICKED -> new LeastPickedPicker(this); + }; + } + + 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) { + synchronized (picker) { + var room = picker.pick(users); + room.addUsers(users, false); + transactionListener.accept(new UserTransaction(room.getRoom(), users)); + return room.getRoom(); + } + } + + public @NotNull RoomPicker getPicker() { + return picker; + } +} diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/container/RoomContainer.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/container/RoomContainer.java new file mode 100644 index 0000000..6becbf4 --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/container/RoomContainer.java @@ -0,0 +1,106 @@ +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(true); + 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) { + usersLock.readLock().lock(); + try { + return !room.isLocked() && canAdd0(users); + } finally { + usersLock.readLock().unlock(); + } + } + + private void noticePickersAboutUserNumberUpdate() { + if (container.getPicker() instanceof LeastPickedPicker picker) { + picker.updateUsersAmount(room, countUsers()); + } + } +} diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java index 4a81985..aa6863c 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/LeastPickedPicker.java @@ -1,38 +1,39 @@ package ru.dragonestia.picker.repository.impl.picker; +import lombok.RequiredArgsConstructor; +import ru.dragonestia.picker.api.exception.NoRoomsAvailableException; 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.DynamicSortedMap; +import ru.dragonestia.picker.repository.impl.container.NodeContainer; +import ru.dragonestia.picker.repository.impl.container.RoomContainer; import java.util.Collection; +@RequiredArgsConstructor public class LeastPickedPicker implements RoomPicker { - private final UserRepository userRepository; + private final NodeContainer container; 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) { @@ -41,7 +42,7 @@ public Room pick(Collection<User> users) { if (!wrapper.canAddUnits(users.size())) throw new RuntimeException(); } catch (RuntimeException ex) { - throw new RuntimeException("There are no rooms available"); + throw new NoRoomsAvailableException(container.getNode().getIdentifier()); } } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java index b3df329..4e2d2a5 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomPicker.java @@ -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(); } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java index 1d0db9f..ba0dcc2 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoomWrapper.java @@ -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 diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java index 3902619..f423cb0 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/RoundRobinPicker.java @@ -1,40 +1,39 @@ package ru.dragonestia.picker.repository.impl.picker; +import lombok.RequiredArgsConstructor; +import ru.dragonestia.picker.api.exception.NoRoomsAvailableException; 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.NodeContainer; +import ru.dragonestia.picker.repository.impl.container.RoomContainer; import java.util.Collection; import java.util.concurrent.atomic.AtomicInteger; +@RequiredArgsConstructor public class RoundRobinPicker implements RoomPicker { - private final UserRepository userRepository; + private final NodeContainer container; 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; @@ -43,7 +42,7 @@ public Room pick(Collection<User> users) { addition.set(amount); wrapper = list.pick(); } catch (RuntimeException ex) { - throw new RuntimeException("There are no rooms available"); + throw new NoRoomsAvailableException(container.getNode().getIdentifier()); } } diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java index 9cc221c..ca954f5 100644 --- a/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/picker/SequentialFillingPicker.java @@ -1,39 +1,38 @@ package ru.dragonestia.picker.repository.impl.picker; +import lombok.RequiredArgsConstructor; +import ru.dragonestia.picker.api.exception.NoRoomsAvailableException; 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.NodeContainer; +import ru.dragonestia.picker.repository.impl.container.RoomContainer; import java.util.Collection; import java.util.LinkedHashMap; import java.util.Map; +@RequiredArgsConstructor public class SequentialFillingPicker implements RoomPicker { - private final UserRepository userRepository; + private final NodeContainer container; 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) { @@ -44,7 +43,7 @@ public Room pick(Collection<User> users) { } } - throw new RuntimeException("There are no rooms available"); + throw new NoRoomsAvailableException(container.getNode().getIdentifier()); } @Override diff --git a/server/src/main/java/ru/dragonestia/picker/repository/impl/type/UserTransaction.java b/server/src/main/java/ru/dragonestia/picker/repository/impl/type/UserTransaction.java new file mode 100644 index 0000000..1ef9473 --- /dev/null +++ b/server/src/main/java/ru/dragonestia/picker/repository/impl/type/UserTransaction.java @@ -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> {} +} diff --git a/server/src/main/java/ru/dragonestia/picker/service/RoomService.java b/server/src/main/java/ru/dragonestia/picker/service/RoomService.java index 23d39b7..780ee8e 100644 --- a/server/src/main/java/ru/dragonestia/picker/service/RoomService.java +++ b/server/src/main/java/ru/dragonestia/picker/service/RoomService.java @@ -9,6 +9,7 @@ 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); } diff --git a/server/src/main/java/ru/dragonestia/picker/service/UserService.java b/server/src/main/java/ru/dragonestia/picker/service/UserService.java index be7644f..faafa0f 100644 --- a/server/src/main/java/ru/dragonestia/picker/service/UserService.java +++ b/server/src/main/java/ru/dragonestia/picker/service/UserService.java @@ -14,7 +14,7 @@ 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); diff --git a/server/src/main/java/ru/dragonestia/picker/service/impl/NodeServiceImpl.java b/server/src/main/java/ru/dragonestia/picker/service/impl/NodeServiceImpl.java index 3ec93ed..7a4949a 100644 --- a/server/src/main/java/ru/dragonestia/picker/service/impl/NodeServiceImpl.java +++ b/server/src/main/java/ru/dragonestia/picker/service/impl/NodeServiceImpl.java @@ -8,6 +8,7 @@ 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 @@ 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 List<ResponseNode> getAllNodesWithDetailsResponse(Set<NodeDetails> detail @Override public Optional<Node> find(String nodeId) { - return nodeRepository.find(nodeId); + return nodeRepository.findById(nodeId); } } diff --git a/server/src/main/java/ru/dragonestia/picker/service/impl/RoomServiceImpl.java b/server/src/main/java/ru/dragonestia/picker/service/impl/RoomServiceImpl.java index af05a0f..3d4a6b4 100644 --- a/server/src/main/java/ru/dragonestia/picker/service/impl/RoomServiceImpl.java +++ b/server/src/main/java/ru/dragonestia/picker/service/impl/RoomServiceImpl.java @@ -21,10 +21,7 @@ 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 Optional<Room> find(Node node, String roomId) { } @Override - public List<Room> all(Node node) { + public Collection<Room> all(Node node) { return roomRepository.all(node); } @@ -78,9 +75,8 @@ public List<ShortResponseRoom> getAllRoomsWithDetailsResponse(Node node, Set<Roo } @Override - public PickedRoomResponse pickAvailable(Node node, List<User> users) { - var room = roomRepository.pickFree(node, users) - .orElseThrow(() -> new RuntimeException("There are no rooms available")); + public PickedRoomResponse pickAvailable(Node node, Set<User> users) { + var room = roomRepository.pick(node, users); var roomUsers = userRepository.usersOf(room); return new PickedRoomResponse( diff --git a/server/src/main/java/ru/dragonestia/picker/service/impl/UserServiceImpl.java b/server/src/main/java/ru/dragonestia/picker/service/impl/UserServiceImpl.java index 0858493..d2848fa 100644 --- a/server/src/main/java/ru/dragonestia/picker/service/impl/UserServiceImpl.java +++ b/server/src/main/java/ru/dragonestia/picker/service/impl/UserServiceImpl.java @@ -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 void unlinkUsersFromRoom(Room room, Collection<User> users) { } @Override - public List<User> getRoomUsers(Room room) { + public Collection<User> getRoomUsers(Room room) { return userRepository.usersOf(room); } diff --git a/server/src/main/java/ru/dragonestia/picker/util/NamingValidator.java b/server/src/main/java/ru/dragonestia/picker/util/NamingValidator.java index d49c263..52783a2 100644 --- a/server/src/main/java/ru/dragonestia/picker/util/NamingValidator.java +++ b/server/src/main/java/ru/dragonestia/picker/util/NamingValidator.java @@ -8,6 +8,7 @@ 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 boolean validateUserId(String input) { 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 List<User> validateUserIds(List<String> input) throws InvalidUsernamesExc } if (!invalid.isEmpty()) { - throw new InvalidUsernamesException(input, invalid); + throw new InvalidUsernamesException(input.stream().toList(), invalid); } - - return users; } } diff --git a/server/src/test/java/ru/dragonestia/picker/picker/LeastPickedTests.java b/server/src/test/java/ru/dragonestia/picker/picker/LeastPickedTests.java index b8df440..bbdbca6 100644 --- a/server/src/test/java/ru/dragonestia/picker/picker/LeastPickedTests.java +++ b/server/src/test/java/ru/dragonestia/picker/picker/LeastPickedTests.java @@ -10,6 +10,7 @@ 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.exception.NoRoomsAvailableException; import ru.dragonestia.picker.config.FillingNodesConfig; import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.repository.RoomRepository; @@ -40,14 +41,14 @@ public class LeastPickedTests { @ParameterizedTest @ArgumentsSource(PickingArgumentProvider.class) void testPicking(String expectedRoomId, int usersAmount) { - var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(usersAmount)); - Assertions.assertTrue(roomOpt.isPresent()); + var expectedRoomUsers = userRepository.usersOf(roomRepository.find(node, expectedRoomId).orElseThrow()).size(); - var room = roomOpt.get(); + var room = roomRepository.pick(node, userFiller.createRandomUsers(usersAmount)); var slots = room.getMaxSlots(); var users = userRepository.usersOf(room); Assertions.assertTrue(slots == -1 || slots >= users.size()); // check slots limitation + System.out.printf("Room(%s) has %s/%s users. Expected: %s(%s), added: %s%n", room.getIdentifier(), users.size(), slots, expectedRoomId, expectedRoomUsers, usersAmount); Assertions.assertEquals(expectedRoomId, room.getIdentifier()); } @@ -73,7 +74,6 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext extensionCo @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)); - Assertions.assertTrue(roomOpt.isEmpty()); + Assertions.assertThrows(NoRoomsAvailableException.class, () -> roomRepository.pick(node, userFiller.createRandomUsers(9))); } } diff --git a/server/src/test/java/ru/dragonestia/picker/picker/RoundRobinTests.java b/server/src/test/java/ru/dragonestia/picker/picker/RoundRobinTests.java index ec23f94..5ab780d 100644 --- a/server/src/test/java/ru/dragonestia/picker/picker/RoundRobinTests.java +++ b/server/src/test/java/ru/dragonestia/picker/picker/RoundRobinTests.java @@ -8,7 +8,7 @@ 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.api.exception.NoRoomsAvailableException; import ru.dragonestia.picker.config.FillingNodesConfig; import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.repository.RoomRepository; @@ -39,10 +39,7 @@ public class RoundRobinTests { @ParameterizedTest @ArgumentsSource(PickingArgumentProvider.class) void testPicking(String expectedRoomId, int usersAmount) { - var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(usersAmount)); - Assertions.assertTrue(roomOpt.isPresent()); - - var room = roomOpt.get(); + var room = roomRepository.pick(node, userFiller.createRandomUsers(usersAmount)); var slots = room.getMaxSlots(); var users = userRepository.usersOf(room); Assertions.assertTrue(slots == -1 || slots >= users.size()); // check slots limitation @@ -69,7 +66,6 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext extensionCo @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)); - Assertions.assertTrue(roomOpt.isEmpty()); + Assertions.assertThrows(NoRoomsAvailableException.class, () -> roomRepository.pick(node, userFiller.createRandomUsers(9))); } } diff --git a/server/src/test/java/ru/dragonestia/picker/picker/SequentialFillingTests.java b/server/src/test/java/ru/dragonestia/picker/picker/SequentialFillingTests.java index 61fb5c8..4dde327 100644 --- a/server/src/test/java/ru/dragonestia/picker/picker/SequentialFillingTests.java +++ b/server/src/test/java/ru/dragonestia/picker/picker/SequentialFillingTests.java @@ -10,6 +10,7 @@ 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.exception.NoRoomsAvailableException; import ru.dragonestia.picker.config.FillingNodesConfig; import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.repository.RoomRepository; @@ -40,14 +41,14 @@ public class SequentialFillingTests { @ParameterizedTest @ArgumentsSource(PickingArgumentProvider.class) void testPicking(String expectedRoomId, int usersAmount) { - var roomOpt = roomRepository.pickFree(node, userFiller.createRandomUsers(usersAmount)); - Assertions.assertTrue(roomOpt.isPresent()); + var expectedRoomUsers = userRepository.usersOf(roomRepository.find(node, expectedRoomId).orElseThrow()).size(); - var room = roomOpt.get(); + var room = roomRepository.pick(node, userFiller.createRandomUsers(usersAmount)); var slots = room.getMaxSlots(); var users = userRepository.usersOf(room); Assertions.assertTrue(slots == -1 || slots >= users.size()); // check slots limitation + System.out.printf("Room(%s) has %s/%s users. Expected: %s(%s), added: %s%n", room.getIdentifier(), users.size(), slots, expectedRoomId, expectedRoomUsers, usersAmount); Assertions.assertEquals(expectedRoomId, room.getIdentifier()); } @@ -70,7 +71,6 @@ public Stream<? extends Arguments> provideArguments(ExtensionContext extensionCo @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)); - Assertions.assertTrue(roomOpt.isEmpty()); + Assertions.assertThrows(NoRoomsAvailableException.class, () -> roomRepository.pick(node, userFiller.createRandomUsers(9))); } } diff --git a/server/src/test/java/ru/dragonestia/picker/service/RoomServiceTests.java b/server/src/test/java/ru/dragonestia/picker/service/RoomServiceTests.java index 304e87a..0968f54 100644 --- a/server/src/test/java/ru/dragonestia/picker/service/RoomServiceTests.java +++ b/server/src/test/java/ru/dragonestia/picker/service/RoomServiceTests.java @@ -21,6 +21,7 @@ import ru.dragonestia.picker.model.type.SlotLimit; import java.util.List; +import java.util.Set; @SpringBootTest public class RoomServiceTests { @@ -93,7 +94,7 @@ void test_pickRoom() { rooms.forEach(room -> roomService.create(room)); - var users = List.of( + var users = Set.of( new User(UserIdentifier.of("1")), new User(UserIdentifier.of("2")), new User(UserIdentifier.of("3")), @@ -121,6 +122,6 @@ void test_nodeDoesNotExists() { Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.create(room)); Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.remove(room)); Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.find(node, "Bruh")); - Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.pickAvailable(node, List.of(new User(UserIdentifier.of("1"))))); + Assertions.assertThrows(NodeNotFoundException.class, () -> roomService.pickAvailable(node, Set.of(new User(UserIdentifier.of("1"))))); } } diff --git a/server/src/test/java/ru/dragonestia/picker/util/UserFiller.java b/server/src/test/java/ru/dragonestia/picker/util/UserFiller.java index 0c17815..1f6f53f 100644 --- a/server/src/test/java/ru/dragonestia/picker/util/UserFiller.java +++ b/server/src/test/java/ru/dragonestia/picker/util/UserFiller.java @@ -4,18 +4,16 @@ import ru.dragonestia.picker.api.repository.type.UserIdentifier; import ru.dragonestia.picker.model.User; -import java.util.LinkedList; -import java.util.List; -import java.util.UUID; +import java.util.*; @TestComponent public class UserFiller { - public List<User> createRandomUsers(int amount) { - var list = new LinkedList<User>(); + public Set<User> createRandomUsers(int amount) { + var set = new HashSet<User>(); for (int i = 0; i < amount; i++) { - list.add(new User(UserIdentifier.of(UUID.randomUUID().toString()))); + set.add(new User(UserIdentifier.of(UUID.randomUUID().toString()))); } - return list; + return set; } } diff --git a/settings.gradle b/settings.gradle index 0762c33..0c75b03 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,4 +4,5 @@ include 'server' include 'control-panel' include 'client-api' include 'client-impl' +include 'noiser'