dev: added support of client code debugging without disconnection (no more connection/pings validation in server's test mode);

server: improved disconnection logs, fixed some race conditional bugs;
This commit is contained in:
Oleg Agafonov 2024-10-09 16:03:38 +04:00
parent f0c6835d36
commit 8d6ba84556
7 changed files with 39 additions and 35 deletions

View file

@ -66,7 +66,7 @@ public class TablesPanel extends javax.swing.JPanel {
private static final Logger LOGGER = Logger.getLogger(TablesPanel.class); private static final Logger LOGGER = Logger.getLogger(TablesPanel.class);
private static final int[] DEFAULT_COLUMNS_WIDTH = {35, 150, 100, 50, 120, 180, 80, 120, 80, 60, 40, 40, 60}; private static final int[] DEFAULT_COLUMNS_WIDTH = {35, 150, 100, 50, 120, 180, 80, 120, 80, 60, 40, 40, 60};
// ping timeout (warning, must be less than SERVER_TIMEOUTS_USER_INFORM_OPPONENTS_ABOUT_DISCONNECT_AFTER_SECS) // ping timeout (warning, must be less than UserManagerImpl.USER_CONNECTION_TIMEOUTS_CHECK_SECS)
public static final int PING_SERVER_SECS = 20; public static final int PING_SERVER_SECS = 20;
// refresh timeouts for data downloads from server // refresh timeouts for data downloads from server

View file

@ -14,8 +14,8 @@ import java.util.UUID;
*/ */
public class ClientCallback implements Serializable { public class ClientCallback implements Serializable {
// for debug only: simulate bad connection on client side, use launcher's client param like -Dxmage.badconnection // for debug only: simulate bad connection on client side, use launcher's client param like -Dxmage.badConnection
private static final String SIMULATE_BAD_CONNECTION_PROP = "xmage.badconnection"; private static final String SIMULATE_BAD_CONNECTION_PROP = "xmage.badConnection";
public static final boolean SIMULATE_BAD_CONNECTION; public static final boolean SIMULATE_BAD_CONNECTION;
static { static {

View file

@ -42,6 +42,11 @@ public class SessionImpl implements Session {
private static final Logger logger = Logger.getLogger(SessionImpl.class); private static final Logger logger = Logger.getLogger(SessionImpl.class);
// connection validation on client side (jboss's ping implementation, depends on server config's leasePeriod)
// client's validation ping must be less than server's leasePeriod
private static final int SESSION_VALIDATOR_PING_PERIOD_SECS = 4;
private static final int SESSION_VALIDATOR_PING_TIMEOUT_SECS = 3;
public static final String ADMIN_NAME = "Admin"; // if you change here then change in User too public static final String ADMIN_NAME = "Admin"; // if you change here then change in User too
public static final String KEEP_MY_OLD_SESSION = "keep_my_old_session"; // for disconnects without active session lose (keep tables/games) public static final String KEEP_MY_OLD_SESSION = "keep_my_old_session"; // for disconnects without active session lose (keep tables/games)
@ -65,16 +70,11 @@ public class SessionImpl implements Session {
private static final int PING_CYCLES = 10; private static final int PING_CYCLES = 10;
private final LinkedList<Long> pingTime = new LinkedList<>(); private final LinkedList<Long> pingTime = new LinkedList<>();
private String lastPingInfo = ""; private String lastPingInfo = "";
private static boolean debugMode = false;
private boolean canceled = false; private boolean canceled = false;
private boolean jsonLogActive = false; private boolean jsonLogActive = false;
private String lastError = ""; private String lastError = "";
static {
debugMode = System.getProperty("debug.mage") != null;
}
public SessionImpl(MageClient client) { public SessionImpl(MageClient client) {
this.client = client; this.client = client;
} }
@ -381,7 +381,7 @@ public class SessionImpl implements Session {
clientMetadata.put("generalizeSocketException", "true"); clientMetadata.put("generalizeSocketException", "true");
/* A remoting server also has the capability to detect when a client is no longer available. /* A remoting server also has the capability to detect when a client is no longer available.
* This is done by estabilishing a lease with the remoting clients that connect to a server. * This is done by establishing a lease with the remoting clients that connect to a server.
* On the client side, an org.jboss.remoting.LeasePinger periodically sends PING messages to * On the client side, an org.jboss.remoting.LeasePinger periodically sends PING messages to
* the server, and on the server side an org.jboss.remoting.Lease informs registered listeners * the server, and on the server side an org.jboss.remoting.Lease informs registered listeners
* if the PING doesn't arrive withing the specified timeout period. */ * if the PING doesn't arrive withing the specified timeout period. */
@ -437,15 +437,10 @@ public class SessionImpl implements Session {
clientMetadata.put(Remoting.USE_CLIENT_CONNECTION_IDENTITY, "true"); clientMetadata.put(Remoting.USE_CLIENT_CONNECTION_IDENTITY, "true");
callbackClient = new Client(clientLocator, "callback", clientMetadata); callbackClient = new Client(clientLocator, "callback", clientMetadata);
// client side connection validator (jboss's implementation of ping)
Map<String, String> listenerMetadata = new HashMap<>(); Map<String, String> listenerMetadata = new HashMap<>();
if (debugMode) { listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_PERIOD, String.valueOf(SESSION_VALIDATOR_PING_PERIOD_SECS * 1000));
// prevent client from disconnecting while debugging listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_TIMEOUT, String.valueOf(SESSION_VALIDATOR_PING_TIMEOUT_SECS * 1000));
listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_PERIOD, "1000000");
listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_TIMEOUT, "900000");
} else {
listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_PERIOD, "15000");
listenerMetadata.put(ConnectionValidator.VALIDATOR_PING_TIMEOUT, "13000");
}
callbackClient.connect(new MageClientConnectionListener(), listenerMetadata); callbackClient.connect(new MageClientConnectionListener(), listenerMetadata);
Map<String, String> callbackMetadata = new HashMap<>(); Map<String, String> callbackMetadata = new HashMap<>();
@ -589,13 +584,13 @@ public class SessionImpl implements Session {
@Override @Override
public synchronized boolean sendFeedback(String title, String type, String message, String email) { public synchronized boolean sendFeedback(String title, String type, String message, String email) {
if (isConnected()) { try {
try { if (isConnected()) {
server.serverAddFeedbackMessage(sessionId, connection.getUsername(), title, type, message, email); server.serverAddFeedbackMessage(sessionId, connection.getUsername(), title, type, message, email);
return true; return true;
} catch (MageException e) {
logger.error(e);
} }
} catch (MageException ex) {
handleMageException(ex);
} }
return false; return false;
} }
@ -1706,10 +1701,12 @@ public class SessionImpl implements Session {
@Override @Override
public void ping() { public void ping() {
try { try {
// client side connection validator (xmage's implementation)
//
// jboss uses lease mechanic for connection check but xmage needs additional data like pings stats // jboss uses lease mechanic for connection check but xmage needs additional data like pings stats
// ping must work after login only, all other actions are single call (example: register new user) // ping must work after login only, all other actions are single call (example: register new user)
// sessionId fills on connection // - sessionId fills on connection
// serverState fills on good login // - serverState fills on good login
if (!isConnected() || sessionId == null || serverState == null) { if (!isConnected() || sessionId == null || serverState == null) {
return; return;
} }

View file

@ -54,13 +54,13 @@ public final class Main {
private static final MageVersion version = new MageVersion(Main.class); private static final MageVersion version = new MageVersion(Main.class);
// Server threads: // Server threads:
// - worker threads: creates for each connection, controls by maxPoolSize; // - acceptor threads: processing new connection from a client, controlled by numAcceptThreads;
// - acceptor threads: processing requests to start a new connection, controls by numAcceptThreads; // - worker threads (server threads): processing income requests from a client, controlled by maxPoolSize;
// - backlog threads: processing waiting queue if maxPoolSize reached, controls by backlogSize; // - backlog: max size of income queue if maxPoolSize reached, controlled by backlogSize;
// Usage hints: // Usage hints:
// - if maxPoolSize reached then new clients will freeze in connection dialog until backlog queue overflow; // - if maxPoolSize reached then new clients will freeze in connection dialog until backlog queue overflow;
// - so for active server must increase maxPoolSize to big value like "max online * 10" or enable worker idle timeout // - so for active server must increase maxPoolSize to bigger value like "max online * 20"
// - worker idle time will free unused worker thread, so new client can connect; // - worker idle timeout will free unused worker thread, so new client can connect again;
private static final int SERVER_WORKER_THREAD_IDLE_TIMEOUT_SECS = 5 * 60; // no needs to config, must be enabled for all private static final int SERVER_WORKER_THREAD_IDLE_TIMEOUT_SECS = 5 * 60; // no needs to config, must be enabled for all
// arg settings can be setup by run script or IDE's program arguments like -xxx=yyy // arg settings can be setup by run script or IDE's program arguments like -xxx=yyy
@ -85,6 +85,7 @@ public final class Main {
// - fast game buttons; // - fast game buttons;
// - cheat commands; // - cheat commands;
// - no deck validation; // - no deck validation;
// - no connection validation by pings (no disconnects on IDE's debugger usage)
// - load any deck in sideboarding; // - load any deck in sideboarding;
// - simplified registration and login (no password check); // - simplified registration and login (no password check);
// - debug main menu for GUI and rendering testing (must use -debug arg for client app); // - debug main menu for GUI and rendering testing (must use -debug arg for client app);
@ -342,6 +343,8 @@ public final class Main {
@Override @Override
public void handleConnectionException(Throwable throwable, Client client) { public void handleConnectionException(Throwable throwable, Client client) {
// called on client disconnect or on failed network (depends on server config's leasePeriod)
String sessionId = client.getSessionId(); String sessionId = client.getSessionId();
Session session = managerFactory.sessionManager().getSession(sessionId).orElse(null); Session session = managerFactory.sessionManager().getSession(sessionId).orElse(null);
if (session == null) { if (session == null) {
@ -357,7 +360,7 @@ public final class Main {
} else { } else {
sessionInfo.append("[no user]"); sessionInfo.append("[no user]");
} }
sessionInfo.append(" at ").append(session.getHost()).append(", sessionId: ").append(session.getId()); sessionInfo.append(" at ").append(session.getHost());
// check disconnection reason // check disconnection reason
// lease ping is inner jboss feature to check connection status // lease ping is inner jboss feature to check connection status
@ -371,13 +374,13 @@ public final class Main {
} else if (throwable == null) { } else if (throwable == null) {
// lease timeout (ping), so server lost connection with a client // lease timeout (ping), so server lost connection with a client
// must keep tables // must keep tables
logger.info("LOST CONNECTION - " + sessionInfo); logger.info("LOST CONNECTION (bad network) - " + sessionInfo);
logger.debug("- cause: lease expired"); logger.debug("- cause: lease expired");
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true); managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true);
} else { } else {
// unknown error // unknown error
// must keep tables // must keep tables
logger.info("LOST CONNECTION - " + sessionInfo); logger.info("LOST CONNECTION (unknown) - " + sessionInfo);
logger.debug("- cause: unknown error - " + throwable); logger.debug("- cause: unknown error - " + throwable);
managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true); managerFactory.sessionManager().disconnect(client.getSessionId(), DisconnectReason.LostConnection, true);
} }
@ -396,7 +399,8 @@ public final class Main {
connector.addInvocationHandler("callback", serverInvocationHandler); // commands processing connector.addInvocationHandler("callback", serverInvocationHandler); // commands processing
// connection monitoring and errors processing // connection monitoring and errors processing
connector.setLeasePeriod(managerFactory.configSettings().getLeasePeriod()); boolean isTestMode = ((MageServerImpl) target).getServerState().isTestMode();
connector.setLeasePeriod(isTestMode ? 3600 * 1000 : managerFactory.configSettings().getLeasePeriod());
connector.addConnectionListener(new MageServerConnectionListener(managerFactory)); connector.addConnectionListener(new MageServerConnectionListener(managerFactory));
} }

View file

@ -4,6 +4,7 @@ import mage.MageException;
import mage.players.net.UserData; import mage.players.net.UserData;
import mage.server.managers.ManagerFactory; import mage.server.managers.ManagerFactory;
import mage.server.managers.SessionManager; import mage.server.managers.SessionManager;
import mage.util.ThreadUtils;
import org.apache.log4j.Logger; import org.apache.log4j.Logger;
import org.jboss.remoting.callback.InvokerCallbackHandler; import org.jboss.remoting.callback.InvokerCallbackHandler;
@ -63,8 +64,7 @@ public class SessionManagerImpl implements SessionManager {
if (session != null) { if (session != null) {
String errorMessage = session.connectUser(userName, password, restoreSessionId); String errorMessage = session.connectUser(userName, password, restoreSessionId);
if (errorMessage == null) { if (errorMessage == null) {
logger.info(userName + " connected to server by sessionId " + sessionId logger.info(userName + " connected to server" + (restoreSessionId.isEmpty() ? "" : " with restored session"));
+ (restoreSessionId.isEmpty() ? "" : ", restoreSessionId " + restoreSessionId));
if (detailsMode) { if (detailsMode) {
logger.info("- details: " + userInfo); logger.info("- details: " + userInfo);
} }
@ -153,6 +153,8 @@ public class SessionManagerImpl implements SessionManager {
} }
user.showUserMessage("Admin action", "Your session was disconnected by admin"); user.showUserMessage("Admin action", "Your session was disconnected by admin");
ThreadUtils.sleep(1000);
logger.warn(user.getName() + " disconnected by admin");
disconnect(userSessionId, DisconnectReason.DisconnectedByAdmin, true); disconnect(userSessionId, DisconnectReason.DisconnectedByAdmin, true);
admin.showUserMessage("Admin result", "User " + user.getName() + " was disconnected"); admin.showUserMessage("Admin result", "User " + user.getName() + " was disconnected");
} }

View file

@ -234,6 +234,7 @@ public class UserManagerImpl implements UserManager {
} }
if (isBadSession) { if (isBadSession) {
// full disconnect // full disconnect
logger.info(user.getName() + " disconnected due connection problems");
disconnect(user.getId(), DisconnectReason.SessionExpired); disconnect(user.getId(), DisconnectReason.SessionExpired);
} }
break; break;

View file

@ -127,7 +127,7 @@ public enum ServerMessagesUtil {
private String getServerStatsMessage() { private String getServerStatsMessage() {
long current = System.currentTimeMillis(); long current = System.currentTimeMillis();
long hours = ((current - startDate) / (1000 * 60 * 60)); long hours = startDate <= 0 ? 0 : ((current - startDate) / (1000 * 60 * 60));
String updated = new Date().toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime().format(DateTimeFormatter.ofPattern("HH:mm:ss")); String updated = new Date().toInstant().atOffset(ZoneOffset.UTC).toLocalDateTime().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
return String.format("Server uptime: %d hours; max online: %d; active games: %d of %d, tourneys: %d of %d; stats from %s", return String.format("Server uptime: %d hours; max online: %d; active games: %d of %d, tourneys: %d of %d; stats from %s",
hours, hours,