Fully implemented LeastPickedPicker

This commit is contained in:
Andrey Terentev 2024-01-12 13:56:06 +07:00
parent 0bbc831513
commit c22a8f82f0
13 changed files with 500 additions and 17 deletions

View File

@ -0,0 +1,64 @@
package ru.dragonestia.picker.config;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.model.type.SlotLimit;
import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.UserRepository;
import java.util.List;
@Profile("test_pickers")
@Configuration
@RequiredArgsConstructor
public class TestPickersConfig {
private final NodeRepository nodeRepository;
private final RoomRepository roomRepository;
private final UserRepository userRepository;
@Bean
void createSequentialFillingNode() {
var node = new Node("seq", PickingMode.SEQUENTIAL_FILLING);
nodeRepository.create(node);
fillNode(node);
}
@Bean
void createRoundRobinNode() {
var node = new Node("round", PickingMode.ROUND_ROBIN);
nodeRepository.create(node);
fillNode(node);
}
@Bean
void createLeastPickerNode() {
var node = new Node("least", PickingMode.LEAST_PICKED);
nodeRepository.create(node);
fillNode(node);
}
private void fillNode(Node node) {
for (int i = 0, n = 5; i < n; i++) {
for (int j = 0; j < 3; j++) {
var room = Room.create("room-" + i + "-" + j, node, SlotLimit.of(n), "");
roomRepository.create(room);
for (int k = n - i - 1; k >= 0; k--) {
var user = new User("user-" + k);
userRepository.linkWithRoom(room, List.of(user), false);
}
}
}
}
}

View File

@ -5,6 +5,7 @@ import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.model.Node; import ru.dragonestia.picker.model.Node;
import ru.dragonestia.picker.repository.RoomRepository; import ru.dragonestia.picker.repository.RoomRepository;
import ru.dragonestia.picker.repository.NodeRepository; import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -17,6 +18,7 @@ public class NodeRepositoryImpl implements NodeRepository {
private final RoomRepository roomRepository; private final RoomRepository roomRepository;
private final PickerRepository pickerRepository; private final PickerRepository pickerRepository;
private final NodeId2PickerModeCache nodeId2PickerModeCache;
private final Map<String, Node> nodeMap = new ConcurrentHashMap<>(); private final Map<String, Node> nodeMap = new ConcurrentHashMap<>();
@Override @Override
@ -27,7 +29,8 @@ public class NodeRepositoryImpl implements NodeRepository {
} }
nodeMap.put(node.id(), node); nodeMap.put(node.id(), node);
pickerRepository.create(node.id(), node.mode()); var picker = pickerRepository.create(node.id(), node.mode());
nodeId2PickerModeCache.put(node.id(), picker);
} }
roomRepository.onCreateNode(node); roomRepository.onCreateNode(node);
@ -38,6 +41,7 @@ public class NodeRepositoryImpl implements NodeRepository {
synchronized (nodeMap) { synchronized (nodeMap) {
nodeMap.remove(node.id()); nodeMap.remove(node.id());
pickerRepository.remove(node.id()); pickerRepository.remove(node.id());
nodeId2PickerModeCache.remove(node.id());
} }
roomRepository.onRemoveNode(node); roomRepository.onRemoveNode(node);

View File

@ -6,10 +6,7 @@ import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode; import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker; import ru.dragonestia.picker.repository.impl.picker.*;
import ru.dragonestia.picker.repository.impl.picker.Picker;
import ru.dragonestia.picker.repository.impl.picker.RoundRobinPicker;
import ru.dragonestia.picker.repository.impl.picker.SequentialFillingPicker;
import java.security.InvalidParameterException; import java.security.InvalidParameterException;
import java.util.Collection; import java.util.Collection;
@ -21,17 +18,19 @@ import java.util.concurrent.ConcurrentHashMap;
public class PickerRepository { public class PickerRepository {
private final UserRepository userRepository; private final UserRepository userRepository;
private final Map<String, Picker<Room, User>> pickers = new ConcurrentHashMap<>(); private final Map<String, RoomPicker> pickers = new ConcurrentHashMap<>();
public void create(String nodeId, PickingMode mode) { public RoomPicker create(String nodeId, PickingMode mode) {
pickers.put(nodeId, of(mode)); var picker = of(mode);
pickers.put(nodeId, picker);
return picker;
} }
public void remove(String nodeId) { public void remove(String nodeId) {
pickers.remove(nodeId); pickers.remove(nodeId);
} }
public Picker<Room, User> find(String nodeId) { public RoomPicker find(String nodeId) {
return pickers.get(nodeId); return pickers.get(nodeId);
} }
@ -39,7 +38,7 @@ public class PickerRepository {
return pickers.get(nodeId).pick(users); return pickers.get(nodeId).pick(users);
} }
private Picker<Room, User> of(PickingMode mode) { private RoomPicker of(PickingMode mode) {
switch (mode) { switch (mode) {
case SEQUENTIAL_FILLING -> { case SEQUENTIAL_FILLING -> {
return new SequentialFillingPicker(userRepository); return new SequentialFillingPicker(userRepository);

View File

@ -1,17 +1,24 @@
package ru.dragonestia.picker.repository.impl; package ru.dragonestia.picker.repository.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.NodeRepository;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.cache.NodeId2PickerModeCache;
import ru.dragonestia.picker.repository.impl.picker.LeastPickedPicker;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@Repository @Repository
@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepository { public class UserRepositoryImpl implements UserRepository {
private final NodeId2PickerModeCache nodeId2PickerModeCache;
private final Map<User, Set<Room>> usersMap = new ConcurrentHashMap<>(); private final Map<User, Set<Room>> usersMap = new ConcurrentHashMap<>();
private final Map<NodeRoomPath, Set<User>> roomUsers = new ConcurrentHashMap<>(); private final Map<NodeRoomPath, Set<User>> roomUsers = new ConcurrentHashMap<>();
@ -44,6 +51,11 @@ public class UserRepositoryImpl implements UserRepository {
usersSet.addAll(users); usersSet.addAll(users);
roomUsers.put(path, usersSet); roomUsers.put(path, usersSet);
var picker = nodeId2PickerModeCache.get(room.getNodeId());
if (picker instanceof LeastPickedPicker leastPickedPicker) {
leastPickedPicker.updateUsersAmount(room, roomUsers.get(path).size());
}
} }
return result; return result;
@ -72,6 +84,11 @@ public class UserRepositoryImpl implements UserRepository {
} else { } else {
roomUsers.put(path, set); roomUsers.put(path, set);
} }
var picker = nodeId2PickerModeCache.get(room.getNodeId());
if (picker instanceof LeastPickedPicker leastPickedPicker) {
leastPickedPicker.updateUsersAmount(room, roomUsers.get(path).size());
}
} }
return counter.get(); return counter.get();
} }

View File

@ -0,0 +1,25 @@
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,179 @@
package ru.dragonestia.picker.repository.impl.collection;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
public class DynamicSortedMap<ITEM extends DynamicSortedMap.Item> {
private final TreeMap<Integer, LinkedList<Node<ITEM>>> tree = new TreeMap<>();
private final HashMap<String, Node<ITEM>> indexes = new HashMap<>();
public void put(ITEM item) {
var node = new Node<>(item, room -> {
// TODO: update map state
});
indexes.put(node.object.getId(), node);
var list = tree.getOrDefault(node.cachedScore, new LinkedList<>());
if (list.isEmpty()) tree.put(node.cachedScore, list);
list.add(node);
}
public void removeById(String id) {
if (!indexes.containsKey(id)) return;
remove(indexes.get(id).object);
}
public void remove(ITEM item) {
if (!indexes.containsKey(item.getId())) return;
var node = indexes.get(item.getId());
node.removed = true;
indexes.remove(item.getId());
var list = tree.get(node.cachedScore);
list.remove(node);
if (list.isEmpty()) tree.remove(node.cachedScore);
}
public int size() {
return indexes.size();
}
public ITEM getMinimum() {
return getMinimum(0);
}
public ITEM getMinimum(int needAppendScore) {
if (size() == 0) {
throw new RuntimeException("Map is empty");
}
ITEM result = null;
rootLoop:
for (var index: tree.navigableKeySet()) {
var list = tree.get(index);
for (var node: list) {
if (!node.object.canBeUsed(needAppendScore)) continue;
result = node.object;
break rootLoop;
}
}
if (result == null) {
throw new RuntimeException("Cant get available item");
}
return result;
}
public ITEM getMaximum() {
if (size() == 0) {
throw new RuntimeException("Map is empty");
}
ITEM result = null;
rootLoop:
for (var index: tree.descendingKeySet()) {
var list = tree.get(index);
for (var node: list) {
if (!node.object.canBeUsed(0)) continue;
result = node.object;
break rootLoop;
}
}
if (result == null) {
throw new RuntimeException("Cant get available item");
}
return result;
}
public Set<Node<ITEM>> getItems() {
var set = new LinkedHashSet<Node<ITEM>>();
for (var index: tree.navigableKeySet()) {
set.addAll(tree.get(index));
}
return set;
}
public Node<ITEM> getNode(String id) {
return indexes.get(id);
}
public void updateItem(String id, Function<Integer, Integer> setter) {
var node = indexes.get(id);
var prevScore = node.cachedScore;
node.object.updateScore(setter.apply(node.cachedScore));
var newScore = node.cachedScore;
var prevList = tree.get(prevScore);
prevList.remove(node);
if (prevList.isEmpty()) {
tree.remove(prevScore);
}
var newList = tree.getOrDefault(newScore, new LinkedList<>());
if (newList.isEmpty()) tree.put(newScore, newList);
newList.add(node);
}
public static class Node<ITEM extends Item> {
private final ITEM object;
private int cachedScore;
private boolean removed = false;
private Node(ITEM object, Consumer<ITEM> onUpdate) {
this.object = object;
cachedScore = object.getScore();
object.setOnUpdateScore(newScore -> {
cachedScore = newScore;
onUpdate.accept(object);
});
}
public int getScore() {
return cachedScore;
}
@Override
public String toString() {
return "{Node id=%s, score=%s, removed=%s }".formatted(
object.getId(),
getScore(),
removed
);
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj instanceof DynamicSortedMap.Node<?> node) {
return object.getId().equals(node.object.getId());
}
return false;
}
}
public interface Item {
String getId();
int getScore();
void updateScore(int value);
void setOnUpdateScore(Consumer<Integer> setter);
default boolean canBeUsed(int units) {
return true;
}
}
}

View File

@ -2,13 +2,16 @@ package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap;
import java.util.Collection; import java.util.Collection;
public class LeastPickedPicker implements Picker<Room, User> { public class LeastPickedPicker implements RoomPicker {
private final UserRepository userRepository; private final UserRepository userRepository;
private final DynamicSortedMap<RoomWrapper> map = new DynamicSortedMap<>();
public LeastPickedPicker(UserRepository userRepository) { public LeastPickedPicker(UserRepository userRepository) {
this.userRepository = userRepository; this.userRepository = userRepository;
@ -16,16 +19,43 @@ public class LeastPickedPicker implements Picker<Room, User> {
@Override @Override
public void add(Room room) { public void add(Room room) {
throw new UnsupportedOperationException("Not implemented"); synchronized (map) {
map.put(new RoomWrapper(room, () -> userRepository.usersOf(room).size()));
}
} }
@Override @Override
public void remove(Room room) { public void remove(Room room) {
throw new UnsupportedOperationException("Not implemented"); synchronized (map) {
map.removeById(room.getId());
}
} }
@Override @Override
public Room pick(Collection<User> users) { public Room pick(Collection<User> users) {
throw new UnsupportedOperationException("Not implemented"); RoomWrapper wrapper;
synchronized (map) {
try {
wrapper = map.getMinimum();
if (wrapper.isFull()) throw new RuntimeException();
} catch (RuntimeException ex) {
throw new RuntimeException("There are no rooms available");
}
}
return wrapper.getItem();
}
public void updateUsersAmount(Room room, int users) {
synchronized (map) {
map.updateItem(room.getId(), prevValue -> users);
}
}
@Override
public PickingMode getPickingMode() {
return PickingMode.LEAST_PICKED;
} }
} }

View File

@ -0,0 +1,10 @@
package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
public interface RoomPicker extends Picker<Room, User> {
PickingMode getPickingMode();
}

View File

@ -2,13 +2,16 @@ package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList; import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList;
import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap;
import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
public class RoomWrapper implements ItemWrapper<Room>, QueuedLinkedList.Item { public class RoomWrapper implements ItemWrapper<Room>, QueuedLinkedList.Item, DynamicSortedMap.Item {
private final Room room; private final Room room;
private final Supplier<Integer> userCountSupplier; private final Supplier<Integer> userCountSupplier;
private Consumer<Integer> setter;
public RoomWrapper(Room room, Supplier<Integer> userCountSupplier) { public RoomWrapper(Room room, Supplier<Integer> userCountSupplier) {
this.room = room; this.room = room;
@ -39,4 +42,26 @@ public class RoomWrapper implements ItemWrapper<Room>, QueuedLinkedList.Item {
public boolean canAddUnits(int amount) { public boolean canAddUnits(int amount) {
return ItemWrapper.super.canAddUnits(amount) && !room.isLocked(); return ItemWrapper.super.canAddUnits(amount) && !room.isLocked();
} }
@Override
public int getScore() {
return countUnits();
}
@Override
public void updateScore(int value) {
if (setter == null) return;
setter.accept(value);
}
@Override
public void setOnUpdateScore(Consumer<Integer> setter) {
this.setter = setter;
}
@Override
public boolean canBeUsed(int units) {
return canAddUnits(units);
}
} }

View File

@ -2,12 +2,13 @@ package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList; import ru.dragonestia.picker.repository.impl.collection.QueuedLinkedList;
import java.util.Collection; import java.util.Collection;
public class RoundRobinPicker implements Picker<Room, User> { public class RoundRobinPicker implements RoomPicker {
private final UserRepository userRepository; private final UserRepository userRepository;
private final QueuedLinkedList<RoomWrapper> list = new QueuedLinkedList<>(); private final QueuedLinkedList<RoomWrapper> list = new QueuedLinkedList<>();
@ -45,4 +46,9 @@ public class RoundRobinPicker implements Picker<Room, User> {
return wrapper.getItem(); return wrapper.getItem();
} }
@Override
public PickingMode getPickingMode() {
return PickingMode.ROUND_ROBIN;
}
} }

View File

@ -2,13 +2,14 @@ package ru.dragonestia.picker.repository.impl.picker;
import ru.dragonestia.picker.model.Room; import ru.dragonestia.picker.model.Room;
import ru.dragonestia.picker.model.User; import ru.dragonestia.picker.model.User;
import ru.dragonestia.picker.model.type.PickingMode;
import ru.dragonestia.picker.repository.UserRepository; import ru.dragonestia.picker.repository.UserRepository;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
public class SequentialFillingPicker implements Picker<Room, User> { public class SequentialFillingPicker implements RoomPicker {
private final UserRepository userRepository; private final UserRepository userRepository;
private final Map<String, RoomWrapper> wrappers = new LinkedHashMap<>(); private final Map<String, RoomWrapper> wrappers = new LinkedHashMap<>();
@ -45,4 +46,9 @@ public class SequentialFillingPicker implements Picker<Room, User> {
throw new RuntimeException("There are no rooms available"); throw new RuntimeException("There are no rooms available");
} }
@Override
public PickingMode getPickingMode() {
return PickingMode.SEQUENTIAL_FILLING;
}
} }

View File

@ -0,0 +1,103 @@
package ru.dragonestia.picker.collection;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import ru.dragonestia.picker.repository.impl.collection.DynamicSortedMap;
import java.util.function.Consumer;
public class DynamicSortedMapTests {
@Test
void testMap() {
var map = new DynamicSortedMap<Item>();
for (int i = 0; i < 10; i++) {
int value = i;
if ((i & 1) == 1) value = 10 - value;
var item = new Item(Integer.toString(i), value);
map.put(item);
}
printMap(map);
Assertions.assertEquals(new Item("0", 0), map.getMinimum());
Assertions.assertEquals(new Item("1", 9), map.getMaximum());
var minId = map.getMinimum().id;
var maxId = map.getMaximum().id;
setFor(map, minId, 100);
setFor(map, maxId, -100);
printMap(map);
Assertions.assertEquals(new Item("1", -100), map.getMinimum());
Assertions.assertEquals(new Item("0", 100), map.getMaximum());
}
private void setFor(DynamicSortedMap<Item> map, String id, int score) {
var before = map.getNode(id).toString();
map.updateItem(id, (current) -> score);
var after = map.getNode(id).toString();
System.out.printf("Updated '%s' from %s to %s\n", id, before, after);
}
private void printMap(DynamicSortedMap<Item> map) {
var list = map.getItems().stream().toList();
var sb = new StringBuilder("Map(" + map.size() + "): ");
for (int i = 0, n = map.size(); i < n; i++) {
sb.append(list.get(i));
if (i + 1 == n) sb.append(", ");
}
System.out.println(sb);
}
public static class Item implements DynamicSortedMap.Item {
private final String id;
private Consumer<Integer> setter = val -> {};
private int score;
public Item(String id, int score) {
this.id = id;
this.score = score;
}
@Override
public String getId() {
return id;
}
@Override
public int getScore() {
return score;
}
@Override
public void updateScore(int value) {
setter.accept(value);
score = value;
}
@Override
public void setOnUpdateScore(Consumer<Integer> setter) {
this.setter = setter;
}
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj instanceof Item other) {
return id.equals(other.id) && score == other.score;
}
return false;
}
@Override
public String toString() {
return "{Item id='%s' score=%s}".formatted(id, score);
}
}
}

View File

@ -80,5 +80,20 @@ public class QueuedLinkedListTests {
public String getId() { public String getId() {
return id; return id;
} }
@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj instanceof Item other) {
return id.equals(other.id);
}
return false;
}
@Override
public String toString() {
return "{Item id='%s' }".formatted(id);
}
} }
} }