From 8a8dce9075a496a70d0b6bd9789658691e2a5847 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 8 Aug 2013 20:49:17 -0700 Subject: [PATCH] Ticket #263: Refactored ActiveConnectionMap and added group load balancing. --- .../net/auth/mysql/ActiveConnectionMap.java | 283 ++++++++++++++++++ .../net/auth/mysql/ActiveConnectionSet.java | 121 -------- .../mysql/MySQLAuthenticationProvider.java | 4 +- .../net/auth/mysql/MySQLGuacamoleSocket.java | 4 +- .../mysql/service/ConnectionGroupService.java | 62 +++- .../auth/mysql/service/ConnectionService.java | 14 +- 6 files changed, 347 insertions(+), 141 deletions(-) create mode 100644 extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionMap.java delete mode 100644 extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionSet.java diff --git a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionMap.java b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionMap.java new file mode 100644 index 000000000..b0ed80c67 --- /dev/null +++ b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionMap.java @@ -0,0 +1,283 @@ + +package net.sourceforge.guacamole.net.auth.mysql; + +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * http://www.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is guacamole-auth-mysql. + * + * The Initial Developer of the Original Code is + * James Muehlner. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +import com.google.inject.Inject; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import net.sourceforge.guacamole.GuacamoleException; +import net.sourceforge.guacamole.net.auth.mysql.dao.ConnectionHistoryMapper; +import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionHistory; + +/** + * Represents the map of currently active Connections to the count of the number + * of current users. Whenever a socket is opened, the connection count should be + * incremented, and whenever a socket is closed, the connection count should be + * decremented. + * + * @author James Muehlner + */ +public class ActiveConnectionMap { + + /** + * Represents the count of users currently using a MySQL connection. + */ + public class Connection implements Comparable { + + /** + * The ID of the MySQL connection that this Connection represents. + */ + private int connectionID; + + /** + * The number of users currently using this connection. + */ + private int currentUserCount; + + /** + * Returns the ID of the MySQL connection that this Connection + * represents. + * + * @return the ID of the MySQL connection that this Connection + * represents. + */ + public int getConnectionID() { + return connectionID; + } + + /** + * Returns the number of users currently using this connection. + * + * @return the number of users currently using this connection. + */ + public int getCurrentUserCount() { + return currentUserCount; + } + + /** + * Set the current user count for this connection. + * + * @param currentUserCount The new user count for this Connection. + */ + public void setCurrentUserCount(int currentUserCount) { + this.currentUserCount = currentUserCount; + } + + /** + * Create a new Connection for the given connectionID with a zero + * current user count. + * + * @param connectionID The ID of the MySQL connection that this + * Connection represents. + */ + public Connection(int connectionID) { + this.connectionID = connectionID; + this.currentUserCount = 0; + } + + @Override + public int compareTo(Connection other) { + // Sort only based on current user count + return this.currentUserCount - other.currentUserCount; + } + } + + /** + * DAO for accessing connection history. + */ + @Inject + private ConnectionHistoryMapper connectionHistoryDAO; + + /** + * Map of all the connections that are currently active the + * count of current users. + */ + private Map activeConnectionMap = + new HashMap(); + + /** + * Returns the ID of the connection with the lowest number of current + * active users, if found. + * + * @param connectionIDs + * + * @return The ID of the connection with the lowest number of current + * active users, if found. + */ + public Integer getLeastUsedConnection(Collection connectionIDs) { + + if(connectionIDs.isEmpty()) + return null; + + List groupConnections = + new ArrayList(); + + for(Integer connectionID : connectionIDs) { + Connection connection = activeConnectionMap.get(connectionID); + + // Create the Connection if it does not exist + if(connection == null) { + connection = new Connection(connectionID); + activeConnectionMap.put(connectionID, connection); + } + + groupConnections.add(connection); + } + + // Sort the Connections into decending order + Collections.sort(groupConnections); + + if(!groupConnections.isEmpty()) + return groupConnections.get(0).getConnectionID(); + + return null; + } + + /** + * Returns the count of currently active users for the given connectionID. + * @return the count of currently active users for the given connectionID. + */ + public int getCurrentUserCount(int connectionID) { + Connection connection = activeConnectionMap.get(connectionID); + + if(connection == null) + return 0; + + return connection.getCurrentUserCount(); + } + + /** + * Decrement the current user count for this Connection. + * + * @param connectionID The ID of the MySQL connection that this + * Connection represents. + * + * @throws GuacamoleException If the connection is not found. + */ + private void decrementUserCount(int connectionID) + throws GuacamoleException { + Connection connection = activeConnectionMap.get(connectionID); + + if(connection == null) + throw new GuacamoleException + ("Connection to decrement does not exist."); + + // Decrement the current user count + connection.setCurrentUserCount(connection.getCurrentUserCount() - 1); + } + + /** + * Increment the current user count for this Connection. + * + * @param connectionID The ID of the MySQL connection that this + * Connection represents. + * + * @throws GuacamoleException If the connection is not found. + */ + private void incrementUserCount(int connectionID) { + Connection connection = activeConnectionMap.get(connectionID); + + // If the Connection does not exist, it should be created + if(connection == null) { + connection = new Connection(connectionID); + activeConnectionMap.put(connectionID, connection); + } + + // Increment the current user count + connection.setCurrentUserCount(connection.getCurrentUserCount() + 1); + } + + /** + * Check if a connection is currently in use. + * @param connectionID The connection to check the status of. + * @return true if the connection is currently in use. + */ + public boolean isActive(int connectionID) { + return getCurrentUserCount(connectionID) > 0; + } + + /** + * Set a connection as open. + * @param connectionID The ID of the connection that is being opened. + * @param userID The ID of the user who is opening the connection. + * @return The ID of the history record created for this open connection. + */ + public int openConnection(int connectionID, int userID) { + + // Create the connection history record + ConnectionHistory connectionHistory = new ConnectionHistory(); + connectionHistory.setConnection_id(connectionID); + connectionHistory.setUser_id(userID); + connectionHistory.setStart_date(new Date()); + connectionHistoryDAO.insert(connectionHistory); + + // Increment the user count + incrementUserCount(connectionID); + + return connectionHistory.getHistory_id(); + } + + /** + * Set a connection as closed. + * @param connectionID The ID of the connection that is being opened. + * @param historyID The ID of the history record about the open connection. + * @throws GuacamoleException If the open connection history is not found. + */ + public void closeConnection(int connectionID, int historyID) + throws GuacamoleException { + + // Get the existing history record + ConnectionHistory connectionHistory = + connectionHistoryDAO.selectByPrimaryKey(historyID); + + if(connectionHistory == null) + throw new GuacamoleException("History record not found."); + + // Update the connection history record to mark that it is now closed + connectionHistory.setEnd_date(new Date()); + connectionHistoryDAO.updateByPrimaryKey(connectionHistory); + + // Decrement the user count. + decrementUserCount(connectionID); + } +} diff --git a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionSet.java b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionSet.java deleted file mode 100644 index 4e5ba4937..000000000 --- a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/ActiveConnectionSet.java +++ /dev/null @@ -1,121 +0,0 @@ - -package net.sourceforge.guacamole.net.auth.mysql; - -/* ***** BEGIN LICENSE BLOCK ***** - * Version: MPL 1.1/GPL 2.0/LGPL 2.1 - * - * The contents of this file are subject to the Mozilla Public License Version - * 1.1 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * http://www.mozilla.org/MPL/ - * - * Software distributed under the License is distributed on an "AS IS" basis, - * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - * for the specific language governing rights and limitations under the - * License. - * - * The Original Code is guacamole-auth-mysql. - * - * The Initial Developer of the Original Code is - * James Muehlner. - * Portions created by the Initial Developer are Copyright (C) 2010 - * the Initial Developer. All Rights Reserved. - * - * Contributor(s): - * - * Alternatively, the contents of this file may be used under the terms of - * either the GNU General Public License Version 2 or later (the "GPL"), or - * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), - * in which case the provisions of the GPL or the LGPL are applicable instead - * of those above. If you wish to allow use of your version of this file only - * under the terms of either the GPL or the LGPL, and not to allow others to - * use your version of this file under the terms of the MPL, indicate your - * decision by deleting the provisions above and replace them with the notice - * and other provisions required by the GPL or the LGPL. If you do not delete - * the provisions above, a recipient may use your version of this file under - * the terms of any one of the MPL, the GPL or the LGPL. - * - * ***** END LICENSE BLOCK ***** */ - -import com.google.inject.Inject; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; -import net.sourceforge.guacamole.GuacamoleException; -import net.sourceforge.guacamole.net.auth.mysql.dao.ConnectionHistoryMapper; -import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionHistory; - -/** - * Represents the set of currently active Connections. Whenever a socket is - * opened, the connection ID should be added to this set, and whenever a socket - * is closed, the connection ID should be removed from this set. - * - * @author James Muehlner - */ -public class ActiveConnectionSet { - - /** - * DAO for accessing connection history. - */ - @Inject - private ConnectionHistoryMapper connectionHistoryDAO; - - /** - * Set of all the connections that are currently active. - */ - private Set activeConnectionSet = new HashSet(); - - /** - * Check if a connection is currently in use. - * @param connectionID The connection to check the status of. - * @return true if the connection is currently in use. - */ - public boolean isActive(int connectionID) { - return activeConnectionSet.contains(connectionID); - } - - /** - * Set a connection as open. - * @param connectionID The ID of the connection that is being opened. - * @param userID The ID of the user who is opening the connection. - * @return The ID of the history record created for this open connection. - */ - public int openConnection(int connectionID, int userID) { - - // Create the connection history record - ConnectionHistory connectionHistory = new ConnectionHistory(); - connectionHistory.setConnection_id(connectionID); - connectionHistory.setUser_id(userID); - connectionHistory.setStart_date(new Date()); - connectionHistoryDAO.insert(connectionHistory); - - // Mark the connection as active - activeConnectionSet.add(connectionID); - - return connectionHistory.getHistory_id(); - } - - /** - * Set a connection as closed. - * @param connectionID The ID of the connection that is being opened. - * @param historyID The ID of the history record about the open connection. - * @throws GuacamoleException If the open connection history is not found. - */ - public void closeConnection(int connectionID, int historyID) - throws GuacamoleException { - - // Get the existing history record - ConnectionHistory connectionHistory = - connectionHistoryDAO.selectByPrimaryKey(historyID); - - if(connectionHistory == null) - throw new GuacamoleException("History record not found."); - - // Update the connection history record to mark that it is now closed - connectionHistory.setEnd_date(new Date()); - connectionHistoryDAO.updateByPrimaryKey(connectionHistory); - - // Remove the connection from the set of active connections. - activeConnectionSet.remove(connectionID); - } -} diff --git a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java index 0bdcdc9e9..25b31276a 100644 --- a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java +++ b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java @@ -81,7 +81,7 @@ public class MySQLAuthenticationProvider implements AuthenticationProvider { /** * Set of all active connections. */ - private ActiveConnectionSet activeConnectionSet = new ActiveConnectionSet(); + private ActiveConnectionMap activeConnectionSet = new ActiveConnectionMap(); /** * Injector which will manage the object graph of this authentication @@ -175,7 +175,7 @@ public class MySQLAuthenticationProvider implements AuthenticationProvider { bind(ConnectionService.class); bind(ConnectionGroupService.class); bind(UserService.class); - bind(ActiveConnectionSet.class).toInstance(activeConnectionSet); + bind(ActiveConnectionMap.class).toInstance(activeConnectionSet); } } // end of mybatis module diff --git a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLGuacamoleSocket.java b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLGuacamoleSocket.java index 8b2b6aade..f63a4a030 100644 --- a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLGuacamoleSocket.java +++ b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLGuacamoleSocket.java @@ -50,10 +50,10 @@ import net.sourceforge.guacamole.net.GuacamoleSocket; public class MySQLGuacamoleSocket implements GuacamoleSocket { /** - * Injected ActiveConnectionSet which will contain all active connections. + * Injected ActiveConnectionMap which will contain all active connections. */ @Inject - private ActiveConnectionSet activeConnectionSet; + private ActiveConnectionMap activeConnectionSet; /** * The wrapped socket. diff --git a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionGroupService.java b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionGroupService.java index 96a95276f..4c53f5e15 100644 --- a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionGroupService.java +++ b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionGroupService.java @@ -37,24 +37,25 @@ package net.sourceforge.guacamole.net.auth.mysql.service; * * ***** END LICENSE BLOCK ***** */ -import com.google.common.collect.Lists; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; +import net.sourceforge.guacamole.GuacamoleClientException; +import net.sourceforge.guacamole.GuacamoleException; import net.sourceforge.guacamole.net.GuacamoleSocket; +import net.sourceforge.guacamole.net.auth.mysql.ActiveConnectionMap; +import net.sourceforge.guacamole.net.auth.mysql.MySQLConnection; import net.sourceforge.guacamole.net.auth.mysql.MySQLConnectionGroup; import net.sourceforge.guacamole.net.auth.mysql.MySQLConstants; import net.sourceforge.guacamole.net.auth.mysql.dao.ConnectionGroupMapper; import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionGroup; import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionGroupExample; import net.sourceforge.guacamole.net.auth.mysql.model.ConnectionGroupExample.Criteria; +import net.sourceforge.guacamole.net.auth.mysql.properties.MySQLGuacamoleProperties; +import net.sourceforge.guacamole.properties.GuacamoleProperties; import net.sourceforge.guacamole.protocol.GuacamoleClientInformation; /** @@ -65,6 +66,12 @@ import net.sourceforge.guacamole.protocol.GuacamoleClientInformation; */ public class ConnectionGroupService { + /** + * Service for managing connections. + */ + @Inject + private ConnectionService connectionService; + /** * DAO for accessing connection groups. */ @@ -77,6 +84,11 @@ public class ConnectionGroupService { @Inject private Provider mysqlConnectionGroupProvider; + /** + * The map of all active connections. + */ + @Inject + private ActiveConnectionMap activeConnectionMap; /** @@ -157,9 +169,45 @@ public class ConnectionGroupService { return toMySQLConnectionGroup(connectionGroup, userID); } + + /** + * Connect to the connection within the given group with the lowest number + * of currently active users. + * + * @param connection The group to load balance across. + * @param info The information to use when performing the connection + * handshake. + * @param userID The ID of the user who is connecting to the socket. + * @return The connected socket. + * @throws GuacamoleException If an error occurs while connecting the + * socket. + */ public GuacamoleSocket connect(MySQLConnectionGroup group, - GuacamoleClientInformation info, int userID) { - throw new UnsupportedOperationException("Not yet implemented"); + GuacamoleClientInformation info, int userID) throws GuacamoleException { + + // Get all connections in the group. + List connectionIDs = connectionService.getAllConnectionIDs + (group.getConnectionGroupID()); + + // Get the least used connection. + Integer leastUsedConnectionID = + activeConnectionMap.getLeastUsedConnection(connectionIDs); + + if(leastUsedConnectionID == null) + throw new GuacamoleException("No connections found in group."); + + if(GuacamoleProperties.getProperty( + MySQLGuacamoleProperties.MYSQL_DISALLOW_SIMULTANEOUS_CONNECTIONS, false) + && activeConnectionMap.isActive(leastUsedConnectionID)) + throw new GuacamoleClientException + ("Cannot connect. All connections are in use."); + + // Get the connection + MySQLConnection connection = connectionService + .retrieveConnection(leastUsedConnectionID, userID); + + // Connect to the connection + return connectionService.connect(connection, info, userID); } /** diff --git a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionService.java b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionService.java index e20ae736d..1ac8f5597 100644 --- a/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionService.java +++ b/extensions/guacamole-auth-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/service/ConnectionService.java @@ -37,14 +37,10 @@ package net.sourceforge.guacamole.net.auth.mysql.service; * * ***** END LICENSE BLOCK ***** */ -import com.google.common.collect.Lists; import com.google.inject.Inject; import com.google.inject.Provider; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -54,7 +50,7 @@ import net.sourceforge.guacamole.GuacamoleException; import net.sourceforge.guacamole.net.GuacamoleSocket; import net.sourceforge.guacamole.net.InetGuacamoleSocket; import net.sourceforge.guacamole.net.SSLGuacamoleSocket; -import net.sourceforge.guacamole.net.auth.mysql.ActiveConnectionSet; +import net.sourceforge.guacamole.net.auth.mysql.ActiveConnectionMap; import net.sourceforge.guacamole.net.auth.mysql.MySQLConnection; import net.sourceforge.guacamole.net.auth.mysql.MySQLConnectionRecord; import net.sourceforge.guacamole.net.auth.mysql.MySQLGuacamoleSocket; @@ -114,10 +110,10 @@ public class ConnectionService { private Provider mySQLGuacamoleSocketProvider; /** - * Set of all currently active connections. + * Map of all currently active connections. */ @Inject - private ActiveConnectionSet activeConnectionSet; + private ActiveConnectionMap activeConnectionMap; /** * Service managing users. @@ -340,7 +336,7 @@ public class ConnectionService { // connections are not allowed, disallow connection if(GuacamoleProperties.getProperty( MySQLGuacamoleProperties.MYSQL_DISALLOW_SIMULTANEOUS_CONNECTIONS, false) - && activeConnectionSet.isActive(connection.getConnectionID())) + && activeConnectionMap.isActive(connection.getConnectionID())) throw new GuacamoleClientException("Cannot connect. This connection is in use."); // Get guacd connection information @@ -361,7 +357,7 @@ public class ConnectionService { ); // Mark this connection as active - int historyID = activeConnectionSet.openConnection(connection.getConnectionID(), userID); + int historyID = activeConnectionMap.openConnection(connection.getConnectionID(), userID); // Return new MySQLGuacamoleSocket MySQLGuacamoleSocket mySQLGuacamoleSocket = mySQLGuacamoleSocketProvider.get();