Implemented getting bucket users

This commit is contained in:
Andrey Terentev 2023-12-04 20:45:29 +07:00
parent 070068aecc
commit b8dc6a24df
20 changed files with 384 additions and 69 deletions

View File

@ -0,0 +1,44 @@
package ru.dragonestia.loadbalancer.web.component;
import com.vaadin.flow.component.grid.ColumnTextAlign;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import ru.dragonestia.loadbalancer.web.model.Bucket;
import ru.dragonestia.loadbalancer.web.model.User;
import java.util.ArrayList;
import java.util.List;
public class UserList extends VerticalLayout {
private final Bucket bucket;
private final Grid<User> usersGrid;
private final Span totalUsers = new Span();
private final Span occupancy = new Span();
private List<User> cachedUsers = new ArrayList<>();
public UserList(Bucket bucket, List<User> users) {
this.bucket = bucket;
add(usersGrid = createUsersGrid());
update(users);
}
private Grid<User> createUsersGrid() {
var grid = new Grid<User>();
grid.addColumn(User::identifier).setHeader("User Identifier").setFooter(totalUsers);
grid.addColumn(user -> 0).setTextAlign(ColumnTextAlign.CENTER).setHeader("Linked with buckets") // TODO
.setFooter(occupancy);
grid.addComponentColumn(user -> new Span("buttons")).setHeader("Manage"); // TODO
return grid;
}
public void update(List<User> users) {
cachedUsers = users;
usersGrid.setItems(users);
totalUsers.setText("Total users: " + users.size());
occupancy.setText("Occupancy: %s".formatted(bucket.getUsingPercentage(users.size())));
}
}

View File

@ -63,4 +63,10 @@ public class Bucket {
public URI createApiURI() {
return URI.create("/nodes/" + nodeIdentifier + "/buckets/" + identifier);
}
public String getUsingPercentage(int used) {
if (getSlots().isUnlimited()) return "0%";
double percent = used / (double) getSlots().slots() * 100;
return ((int) percent) + "%";
}
}

View File

@ -18,26 +18,31 @@ import com.vaadin.flow.router.Route;
import org.springframework.beans.factory.annotation.Autowired;
import ru.dragonestia.loadbalancer.web.component.AddUsers;
import ru.dragonestia.loadbalancer.web.component.NavPath;
import ru.dragonestia.loadbalancer.web.component.UserList;
import ru.dragonestia.loadbalancer.web.model.Bucket;
import ru.dragonestia.loadbalancer.web.model.Node;
import ru.dragonestia.loadbalancer.web.repository.BucketRepository;
import ru.dragonestia.loadbalancer.web.repository.NodeRepository;
import ru.dragonestia.loadbalancer.web.repository.UserRepository;
@Route("/nodes/:nodeId/buckets/:bucketId")
public class BucketDetailsPage extends VerticalLayout implements BeforeEnterObserver {
private final NodeRepository nodeRepository;
private final BucketRepository bucketRepository;
private final UserRepository userRepository;
private Node node;
private Bucket bucket;
private AddUsers addUsers;
private UserList userList;
private Button lockBucketButton;
private VerticalLayout bucketInfo;
@Autowired
public BucketDetailsPage(NodeRepository nodeRepository, BucketRepository bucketRepository) {
public BucketDetailsPage(NodeRepository nodeRepository, BucketRepository bucketRepository, UserRepository userRepository) {
this.nodeRepository = nodeRepository;
this.bucketRepository = bucketRepository;
this.userRepository = userRepository;
}
@Override
@ -90,6 +95,7 @@ public class BucketDetailsPage extends VerticalLayout implements BeforeEnterObse
add(addUsers = new AddUsers(bucket));
add(new Hr());
add(new H2("Users"));
add(userList = new UserList(bucket, userRepository.all(bucket)));
}
private void updateBucketInfo() {

View File

@ -0,0 +1,16 @@
package ru.dragonestia.loadbalancer.web.repository;
import ru.dragonestia.loadbalancer.web.model.Bucket;
import ru.dragonestia.loadbalancer.web.model.User;
import java.util.Collection;
import java.util.List;
public interface UserRepository {
void linkWithBucket(Bucket bucket, Collection<User> users);
void unlinkFromBucket(Bucket bucket, Collection<User> users);
List<User> all(Bucket bucket);
}

View File

@ -0,0 +1,46 @@
package ru.dragonestia.loadbalancer.web.repository.impl;
import com.vaadin.flow.spring.annotation.SpringComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.client.HttpClientErrorException;
import ru.dragonestia.loadbalancer.web.model.Bucket;
import ru.dragonestia.loadbalancer.web.model.User;
import ru.dragonestia.loadbalancer.web.repository.UserRepository;
import ru.dragonestia.loadbalancer.web.repository.impl.response.BucketUserListResponse;
import java.net.URI;
import java.util.Collection;
import java.util.List;
@Log4j2
@RequiredArgsConstructor
@SpringComponent
public class UserRepositoryImpl implements UserRepository {
private final RestUtil rest;
@Override
public void linkWithBucket(Bucket bucket, Collection<User> users) {
// TODO
}
@Override
public void unlinkFromBucket(Bucket bucket, Collection<User> users) {
// TODO
}
@Override
public List<User> all(Bucket bucket) {
try {
var response = rest.get(URI.create("/nodes/%s/buckets/%s/users".formatted(bucket.getNodeIdentifier(), bucket.getIdentifier())),
BucketUserListResponse.class,
params -> {});
return response.users();
} catch (Exception ex) {
log.throwing(ex);
throw new Error("Internal error");
}
}
}

View File

@ -0,0 +1,7 @@
package ru.dragonestia.loadbalancer.web.repository.impl.response;
import ru.dragonestia.loadbalancer.web.model.User;
import java.util.List;
public record BucketUserListResponse(int slots, int usedSlots, List<User> users) {}

View File

@ -10,11 +10,16 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import ru.dragonestia.loadbalancer.interceptor.DebugInterceptor;
import ru.dragonestia.loadbalancer.model.Bucket;
import ru.dragonestia.loadbalancer.model.Node;
import ru.dragonestia.loadbalancer.model.User;
import ru.dragonestia.loadbalancer.model.type.LoadBalancingMethod;
import ru.dragonestia.loadbalancer.model.type.SlotLimit;
import ru.dragonestia.loadbalancer.repository.BucketRepository;
import ru.dragonestia.loadbalancer.repository.NodeRepository;
import ru.dragonestia.loadbalancer.repository.UserRepository;
import java.security.SecureRandom;
import java.util.List;
import java.util.Random;
import java.util.UUID;
@Profile("test")
@ -24,6 +29,9 @@ public class TestConfig implements WebMvcConfigurer {
private final NodeRepository nodeRepository;
private final BucketRepository bucketRepository;
private final UserRepository userRepository;
private final Random rand = new Random(0);
@Override
public void addInterceptors(@NonNull InterceptorRegistry registry) {
@ -41,13 +49,26 @@ public class TestConfig implements WebMvcConfigurer {
nodeRepository.createNode(node);
for (int i = 1; i <= 5; i++) {
bucketRepository.createBucket(Bucket.create("test-" + i, node, SlotLimit.of(5 * i), "Some payload"));
var slots = 5 * i;
var bucket = Bucket.create("test-" + i, node, SlotLimit.of(slots), "Some payload");
bucketRepository.createBucket(bucket);
for (int j = 0, n = rand.nextInt(slots + 1); j < n; j++) {
var user = new User("test-user-" + rand.nextInt(20));
userRepository.linkWithBucket(bucket, List.of(user), false);
}
}
for (int i = 0; i < 5; i++) {
var bucket = Bucket.create(UUID.randomUUID().toString(), node, SlotLimit.unlimited(), "Some payload");
var bucket = Bucket.create(randomUUID().toString(), node, SlotLimit.unlimited(), "Some payload");
bucket.setLocked((i & 1) == 0);
bucketRepository.createBucket(bucket);
}
}
private UUID randomUUID() {
byte[] randomBytes = new byte[16];
rand.nextBytes(randomBytes);
return UUID.nameUUIDFromBytes(randomBytes);
}
}

View File

@ -0,0 +1,114 @@
package ru.dragonestia.loadbalancer.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.function.EntityResponse;
import ru.dragonestia.loadbalancer.controller.response.BucketUserListResponse;
import ru.dragonestia.loadbalancer.controller.response.LinkUsersWithBucketResponse;
import ru.dragonestia.loadbalancer.model.Bucket;
import ru.dragonestia.loadbalancer.model.Node;
import ru.dragonestia.loadbalancer.model.User;
import ru.dragonestia.loadbalancer.service.BucketService;
import ru.dragonestia.loadbalancer.service.NodeService;
import ru.dragonestia.loadbalancer.service.UserService;
import ru.dragonestia.loadbalancer.util.NamingValidator;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
@RequiredArgsConstructor
@RestController
@RequestMapping("/nodes/{nodeIdentifier}/buckets/{bucketIdentifier}/users")
public class UserBucketController {
private final NodeService nodeService;
private final BucketService bucketService;
private final UserService userService;
@GetMapping
ResponseEntity<BucketUserListResponse> usersInsideBucket(@PathVariable(name = "nodeIdentifier") String nodeId,
@PathVariable(name = "bucketIdentifier") String bucketId) {
Bucket bucket;
try {
var temp = getNodeAndBucket(nodeId, bucketId);
bucket = temp.bucket();
} catch (Error error) {
return ResponseEntity.notFound().build();
}
var users = userService.getBucketUsers(bucket);
return ResponseEntity.ok(new BucketUserListResponse(bucket.getSlots().getSlots(), users.size(), users));
}
@PostMapping
ResponseEntity<LinkUsersWithBucketResponse> linkUserWithBucket(@PathVariable(name = "nodeIdentifier") String nodeId,
@PathVariable(name = "bucketIdentifier") String bucketId,
@RequestParam(name = "users") String userIds,
@RequestParam(name = "force") boolean force) {
Bucket bucket;
try {
var temp = getNodeAndBucket(nodeId, bucketId);
bucket = temp.bucket();
} catch (Error error) {
return ResponseEntity.status(404).body(new LinkUsersWithBucketResponse(false, error.getMessage()));
}
var list = new LinkedList<User>();
for (var username: userIds.split(",")) {
if (!NamingValidator.validateUserIdentifier(username)) continue;
list.add(new User(username));
}
try {
userService.linkUsersWithBucket(bucket, list, force);
} catch (Error error) {
return ResponseEntity.status(400).body(new LinkUsersWithBucketResponse(false, error.getMessage()));
}
return ResponseEntity.ok(new LinkUsersWithBucketResponse(true, "Success"));
}
@DeleteMapping
ResponseEntity<?> unlinkUsersForBucket(@PathVariable(name = "nodeIdentifier") String nodeId,
@PathVariable(name = "bucketIdentifier") String bucketId,
@RequestParam(name = "users") String userIdentifiers) {
Node node;
Bucket bucket;
try {
var temp = getNodeAndBucket(nodeId, bucketId);
node = temp.node();
bucket = temp.bucket();
} catch (Error error) {
return ResponseEntity.notFound().build();
}
return null;
}
private record NodeAndBucket(Node node, Bucket bucket) {}
private NodeAndBucket getNodeAndBucket(String nodeId, String bucketId) {
if (!NamingValidator.validateNodeIdentifier(nodeId) || !NamingValidator.validateBucketIdentifier(bucketId)) {
throw new Error();
}
var nodeOpt = nodeService.findNode(nodeId);
if (nodeOpt.isEmpty()) {
throw new Error();
}
var bucketOpt = bucketService.findBucket(Objects.requireNonNull(nodeOpt.get()), bucketId);
if (bucketOpt.isEmpty()) {
throw new Error();
}
return new NodeAndBucket(nodeOpt.get(), bucketOpt.get());
}
}

View File

@ -1,29 +0,0 @@
package ru.dragonestia.loadbalancer.controller;
import org.springframework.web.bind.annotation.*;
import ru.dragonestia.loadbalancer.model.Bucket;
import java.util.Collection;
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping
Collection<Bucket> linkedBucketsForUser(@RequestParam(name = "user") String userIdentifier) {
return null;
}
@PostMapping
String linkUserWithBucket(@RequestParam(name = "user") String userIdentifier,
@RequestParam(name = "bucket") String bucketIdentifier,
@RequestParam(name = "force") boolean force) {
return null;
}
@DeleteMapping
String unlinkUsersForBucket(@RequestParam(name = "users") String userIdentifiers,
@RequestParam(name = "bucket") String bucketIdentifier) {
return null;
}
}

View File

@ -0,0 +1,7 @@
package ru.dragonestia.loadbalancer.controller.response;
import ru.dragonestia.loadbalancer.model.User;
import java.util.List;
public record BucketUserListResponse(int slots, int usedSlots, List<User> users) {}

View File

@ -0,0 +1,3 @@
package ru.dragonestia.loadbalancer.controller.response;
public record LinkUsersWithBucketResponse(boolean success, String message) {}

View File

@ -26,8 +26,6 @@ public interface BucketRepository {
Optional<Bucket> pickFreeBucket(Node node, Collection<User> users);
void freeBucket(Bucket bucket, Collection<User> users);
void onCreateNode(Node node);
void onRemoveNode(Node node);

View File

@ -9,11 +9,13 @@ import java.util.Map;
public interface UserRepository {
Map<User, Boolean> linkWithBucket(Bucket bucket, Collection<User> users);
Map<User, Boolean> linkWithBucket(Bucket bucket, Collection<User> users, boolean force);
int unlinkWithBucket(Bucket bucket, Collection<User> users);
List<Bucket> findAllLinkedUserBuckets(User user);
void onRemoveBucket(Bucket bucket);
List<User> usersOf(Bucket bucket);
}

View File

@ -98,7 +98,7 @@ public class BucketRepositoryImpl implements BucketRepository {
if (container.isPresent()) {
var cont = container.get();
var addedUsers = userRepository.linkWithBucket(cont.bucket(), users);
var addedUsers = userRepository.linkWithBucket(cont.bucket(), users, false);
cont.used().getAndAdd((int) addedUsers.values().stream().filter(Boolean.TRUE::equals).count());
}
@ -106,30 +106,6 @@ public class BucketRepositoryImpl implements BucketRepository {
}
}
@Override
public void freeBucket(Bucket bucket, Collection<User> users) {
var nodeId = bucket.getNodeIdentifier();
var node = node2bucketsMap.keySet().stream()
.filter(n -> bucket.getNodeIdentifier().equals(n.identifier()))
.findFirst();
synchronized (node2bucketsMap) {
if (node.isEmpty()) {
throw new IllegalArgumentException("Node '" + nodeId + "' does not exist");
}
var buckets = node2bucketsMap.get(node.get());
if (!buckets.containsKey(bucket.getIdentifier())) {
throw new IllegalArgumentException("Bucket '" + nodeId + "' does not exist");
}
var delta = userRepository.unlinkWithBucket(bucket, users);
if (buckets.get(bucket.getIdentifier()).used().getAndAdd(-delta) < 0) {
throw new RuntimeException("Bucket has less than 0 users");
}
}
}
@Override
public void onCreateNode(Node node) {
synchronized (node2bucketsMap) {

View File

@ -13,17 +13,37 @@ import java.util.concurrent.atomic.AtomicInteger;
public class UserRepositoryImpl implements UserRepository {
private final Map<User, Set<Bucket>> usersMap = new ConcurrentHashMap<>();
private final Map<NodeBucketPath, Set<User>> bucketUsers = new ConcurrentHashMap<>();
@Override
public Map<User, Boolean> linkWithBucket(Bucket bucket, Collection<User> users) {
public Map<User, Boolean> linkWithBucket(Bucket bucket, Collection<User> users, boolean force) {
var result = new HashMap<User, Boolean>();
synchronized (usersMap) {
var path = new NodeBucketPath(bucket.getNodeIdentifier(), bucket.getIdentifier());
var usersSet = bucketUsers.getOrDefault(path, new HashSet<>());
if (force || bucket.getSlots().isUnlimited()) {
users.forEach(user -> result.put(user, true));
} else {
for (var user : users) {
var set = usersMap.getOrDefault(user, new HashSet<>());
result.put(user, set.add(bucket));
result.put(user, !set.contains(bucket));
}
if (bucket.getSlots().getSlots() < usersSet.size() + users.size()) {
throw new Error("Bucket are full");
}
}
for (var user: users) {
var set = usersMap.getOrDefault(user, new HashSet<>());
set.add(bucket);
usersMap.put(user, set);
}
usersSet.addAll(users);
bucketUsers.put(path, usersSet);
}
return result;
@ -43,6 +63,15 @@ public class UserRepositoryImpl implements UserRepository {
usersMap.remove(user);
}
});
var path = new NodeBucketPath(bucket.getNodeIdentifier(), bucket.getIdentifier());
var set = bucketUsers.getOrDefault(path, new HashSet<>());
set.removeAll(users);
if (set.isEmpty()) {
bucketUsers.remove(path);
} else {
bucketUsers.put(path, set);
}
}
return counter.get();
}
@ -65,4 +94,31 @@ public class UserRepositoryImpl implements UserRepository {
});
}
}
@Override
public List<User> usersOf(Bucket bucket) {
synchronized (usersMap) {
return bucketUsers.getOrDefault(new NodeBucketPath(bucket.getNodeIdentifier(), bucket.getIdentifier()), new HashSet<>())
.stream()
.toList();
}
}
private record NodeBucketPath(String node, String bucket) {
@Override
public int hashCode() {
return Objects.hash(node, bucket);
}
@Override
public boolean equals(Object o) {
if (o == null) return false;
if (o == this) return true;
if (o instanceof NodeBucketPath other) {
return other.node().equals(node()) && other.bucket().equals(bucket());
}
return false;
}
}
}

View File

@ -24,6 +24,4 @@ public interface BucketService {
int countAvailableBuckets(Node node, int requiredSlots);
Bucket pickAvailableBucket(Node node, List<User> users);
void freeBucket(Bucket bucket, List<User> users);
}

View File

@ -3,9 +3,16 @@ package ru.dragonestia.loadbalancer.service;
import ru.dragonestia.loadbalancer.model.Bucket;
import ru.dragonestia.loadbalancer.model.User;
import java.util.Collection;
import java.util.List;
public interface UserService {
List<Bucket> getUserBuckets(User user);
void linkUsersWithBucket(Bucket bucket, Collection<User> users, boolean force);
void unlinkUsersFromBucket(Bucket bucket, Collection<User> users);
List<User> getBucketUsers(Bucket bucket);
}

View File

@ -51,9 +51,4 @@ public class BucketServiceImpl implements BucketService {
public Bucket pickAvailableBucket(Node node, List<User> users) {
throw new RuntimeException("Not implemented");
}
@Override
public void freeBucket(Bucket bucket, List<User> users) {
throw new RuntimeException("Not implemented");
}
}

View File

@ -0,0 +1,38 @@
package ru.dragonestia.loadbalancer.service.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.dragonestia.loadbalancer.model.Bucket;
import ru.dragonestia.loadbalancer.model.User;
import ru.dragonestia.loadbalancer.repository.UserRepository;
import ru.dragonestia.loadbalancer.service.UserService;
import java.util.Collection;
import java.util.List;
@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public List<Bucket> getUserBuckets(User user) {
return userRepository.findAllLinkedUserBuckets(user);
}
@Override
public void linkUsersWithBucket(Bucket bucket, Collection<User> users, boolean force) {
userRepository.linkWithBucket(bucket, users, force);
}
@Override
public void unlinkUsersFromBucket(Bucket bucket, Collection<User> users) {
userRepository.unlinkWithBucket(bucket, users);
}
@Override
public List<User> getBucketUsers(Bucket bucket) {
return userRepository.usersOf(bucket);
}
}

View File

@ -12,4 +12,8 @@ public class NamingValidator {
public boolean validateBucketIdentifier(String input) {
return input.matches("^[a-z\\d-]+$");
}
public boolean validateUserIdentifier(String input) {
return input.matches("^[aA-zZ\\d-.\\s:/@%?!~$)(+=_|;*]+$");
}
}