Ticket #263: Refactored ActiveConnectionMap and added group load balancing.

This commit is contained in:
James Muehlner
2013-08-08 20:49:17 -07:00
parent e715e99509
commit 8a8dce9075
6 changed files with 347 additions and 141 deletions

View File

@@ -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<Connection> {
/**
* 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<Integer, Connection> activeConnectionMap =
new HashMap<Integer, Connection>();
/**
* 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<Integer> connectionIDs) {
if(connectionIDs.isEmpty())
return null;
List<Connection> groupConnections =
new ArrayList<ActiveConnectionMap.Connection>();
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);
}
}

View File

@@ -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<Integer> activeConnectionSet = new HashSet<Integer>();
/**
* 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);
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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<MySQLConnectionGroup> 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<Integer> 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);
}
/**

View File

@@ -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<MySQLGuacamoleSocket> 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();