Network upgrade and new reconnection mode (#11527)

Network upgrade and new reconnection mode:
* users can disconnect or close app without game progress loose now;
* disconnect dialog will show active tables stats and additional options;
* all active tables will be restored on reconnect (tables, tourneys, games, drafts, sideboarding, constructing);
* user must use same server and username on next connection;
* there are few minutes for reconnect until server kick off a disconnected player from all player's tables (concede/loose);
* now you can safety reconnect after IP change (after proxy/vpn/wifi/router restart);

Other improvements and fixes:
* gui: main menu - improved switch panel button, added stats about current tables/panels;
* gui: improved data sync and updates (fixes many use cases with empty battlefield, not started games/drafts/tourneys, not updatable drafts, etc);
* gui: improved stability on game updates (fixes some random errors related to wrong threads);
* server: fixed miss messages about player's disconnection problems for other players in the chat;
* refactor: simplified and improved connection and network related code, deleted outdated code, added docs;
* tests: improved load test to support lands only set for more stable performance/network testing (set TEST_AI_RANDOM_DECK_SETS = PELP and run test_TwoAIPlayGame_Multiple);
This commit is contained in:
Oleg Agafonov 2023-12-07 19:56:52 +03:00 committed by GitHub
parent 7f0558ff3c
commit 960e896903
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1274 additions and 802 deletions

View file

@ -10,7 +10,6 @@ import mage.view.ChatMessage.MessageType;
import mage.view.ChatMessage.SoundToPlay;
import org.apache.log4j.Logger;
import java.text.DateFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -19,89 +18,86 @@ import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author BetaSteward_at_googlemail.com
* @author BetaSteward_at_googlemail.com, JayDi85
*/
public class ChatSession {
private static final Logger logger = Logger.getLogger(ChatSession.class);
private static final DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT);
private final ManagerFactory managerFactory;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final ReadWriteLock lock = new ReentrantReadWriteLock(); // TODO: no needs due ConcurrentHashMap usage?
private final ConcurrentMap<UUID, String> clients = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, String> users = new ConcurrentHashMap<>(); // active users
private final Set<UUID> usersHistory = new HashSet<>(); // all users that was here (need for system messages like connection problem)
private final UUID chatId;
private final Date createTime;
private final String info;
public ChatSession(ManagerFactory managerFactory, String info) {
this.managerFactory = managerFactory;
chatId = UUID.randomUUID();
this.chatId = UUID.randomUUID();
this.createTime = new Date();
this.info = info;
}
public void join(UUID userId) {
managerFactory.userManager().getUser(userId).ifPresent(user -> {
if (!clients.containsKey(userId)) {
if (!users.containsKey(userId)) {
String userName = user.getName();
final Lock w = lock.writeLock();
w.lock();
try {
clients.put(userId, userName);
users.put(userId, userName);
usersHistory.add(userId);
} finally {
w.unlock();
}
broadcast(null, userName + " has joined (" + user.getClientVersion() + ')', MessageColor.BLUE, true, null, MessageType.STATUS, null);
logger.trace(userName + " joined chat " + chatId);
broadcast(null, userName + " has joined", MessageColor.BLUE, true, null, MessageType.STATUS, null);
}
});
}
public void kill(UUID userId, DisconnectReason reason) {
public void disconnectUser(UUID userId, DisconnectReason reason) {
// user will reconnect to all chats, so no needs to keep it
// if you kill session then kill all chats too
try {
if (reason == null) {
logger.fatal("User kill without disconnect reason userId: " + userId);
reason = DisconnectReason.Undefined;
String userName = users.getOrDefault(userId, null);
if (userName == null) {
return;
}
if (userId != null && clients.containsKey(userId)) {
String userName = clients.get(userId);
if (reason != DisconnectReason.LostConnection) { // for lost connection the user will be reconnected or session expire so no removeUserFromAllTablesAndChat of chat yet
final Lock w = lock.writeLock();
w.lock();
try {
clients.remove(userId);
} finally {
w.unlock();
}
logger.debug(userName + '(' + reason.toString() + ')' + " removed from chatId " + chatId);
}
String message = reason.getMessage();
if (!message.isEmpty()) {
broadcast(null, userName + message, MessageColor.BLUE, true, null, MessageType.STATUS, null);
}
// remove from chat
final Lock w = lock.writeLock();
w.lock();
try {
users.remove(userId);
} finally {
w.unlock();
}
} catch (Exception ex) {
logger.fatal("exception: " + ex.toString());
logger.debug(userName + " (" + reason + ')' + " removed from chatId " + chatId);
// inform other users about disconnect (lobby, system tab)
if (!reason.messageForUser.isEmpty()) {
broadcast(null, userName + reason.messageForUser, MessageColor.BLUE, true, null, MessageType.STATUS, null);
}
} catch (Exception e) {
logger.fatal("Chat: disconnecting user catch error: " + e, e);
}
}
public boolean broadcastInfoToUser(User toUser, String message) {
if (clients.containsKey(toUser.getId())) {
public void broadcastInfoToUser(User toUser, String message) {
if (users.containsKey(toUser.getId())) {
toUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(null, message, new Date(), null, MessageColor.BLUE, MessageType.USER_INFO, null)));
return true;
}
return false;
}
public boolean broadcastWhisperToUser(User fromUser, User toUser, String message) {
if (clients.containsKey(toUser.getId())) {
if (users.containsKey(toUser.getId())) {
toUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(fromUser.getName(), message, new Date(), null, MessageColor.YELLOW, MessageType.WHISPER_FROM, SoundToPlay.PlayerWhispered)));
if (clients.containsKey(fromUser.getId())) {
if (users.containsKey(fromUser.getId())) {
fromUser.fireCallback(new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
new ChatMessage(toUser.getName(), message, new Date(), null, MessageColor.YELLOW, MessageType.WHISPER_TO, null)));
return true;
@ -111,6 +107,10 @@ public class ChatSession {
}
public void broadcast(String userName, String message, MessageColor color, boolean withTime, Game game, MessageType messageType, SoundToPlay soundToPlay) {
// TODO: is it called by single thread for all users?
// TODO: is it freeze on someone's connection fail/freeze?
// TODO: is it freeze on someone's connection fail/freeze with play multiple games/chats/lobby?
// TODO: send messages in another thread?!
if (!message.isEmpty()) {
Set<UUID> clientsToRemove = new HashSet<>();
ClientCallback clientCallback = new ClientCallback(ClientCallbackMethod.CHATMESSAGE, chatId,
@ -119,7 +119,7 @@ public class ChatSession {
final Lock r = lock.readLock();
r.lock();
try {
chatUserIds.addAll(clients.keySet());
chatUserIds.addAll(users.keySet());
} finally {
r.unlock();
}
@ -135,12 +135,11 @@ public class ChatSession {
final Lock w = lock.readLock();
w.lock();
try {
clients.keySet().removeAll(clientsToRemove);
users.keySet().removeAll(clientsToRemove);
} finally {
w.unlock();
}
}
}
}
@ -151,12 +150,12 @@ public class ChatSession {
return chatId;
}
public boolean hasUser(UUID userId) {
return clients.containsKey(userId);
public boolean hasUser(UUID userId, boolean useHistory) {
return useHistory ? usersHistory.contains(userId) : users.containsKey(userId);
}
public ConcurrentMap<UUID, String> getClients() {
return clients;
public ConcurrentMap<UUID, String> getUsers() {
return users;
}
public Date getCreateTime() {