GUAC-1101: Implement reserved concurrency policy.

This commit is contained in:
Michael Jumper
2015-03-01 21:08:01 -08:00
parent a824ef14ea
commit 3f22026c9e

View File

@@ -24,9 +24,12 @@ package org.glyptodon.guacamole.auth.jdbc.socket;
import com.google.inject.Singleton;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser;
import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.GuacamoleResourceConflictException;
/**
@@ -43,17 +46,138 @@ import org.glyptodon.guacamole.GuacamoleException;
public class ReservedGuacamoleSocketService
extends AbstractGuacamoleSocketService {
/**
* An arbitrary number of reservations associated with a specific user.
* Initially, each Reservation instance represents exactly one reservation,
* but future calls to acquire() may increase this value. Once the
* reservation count is reduced to zero by calls to release(), a
* Reservation instance is empty and cannot be reused. It must be discarded
* and replaced with a fresh Reservation.
*
* This is necessary as each Reservation will be stored within a Map, and
* the effect of acquire() must be deterministic. If Reservations could be
* reused, the internal count could potentially increase after being
* removed from the map, resulting in a successful acquire() that really
* should have failed.
*/
private static class Reservation {
/**
* The username of the user associated with this reservation.
*/
private final String username;
/**
* The number of reservations effectively present under the associated
* username.
*/
private int count = 1;
/**
* Creates a new reservation which tracks the overall number of
* reservations for a given user.
* @param username
*/
public Reservation(String username) {
this.username = username;
}
/**
* Attempts to acquire a new reservation under the given username. If
* this reservation is for a different user, or the reservation has
* expired, this will fail.
*
* @param username
* The username of the user to acquire the reservation for.
*
* @return
* true if the reservation was successful, false otherwise.
*/
public boolean acquire(String username) {
// Acquire always fails if for the wrong user
if (!this.username.equals(username))
return false;
// Determine success/failure based on count
synchronized (this) {
// If already expired, no further reservations are allowed
if (count == 0)
return false;
// Otherwise, add another reservation, report success
count++;
return true;
}
}
/**
* Releases a previous reservation. The result of calling this function
* without a previous matching call to acquire is undefined.
*
* @return
* true if the last reservation has been released and this
* reservation is now empty, false otherwise.
*/
public boolean release() {
synchronized (this) {
// Reduce reservation count
count--;
// Empty if no reservations remain
return count == 0;
}
}
}
/**
* Map of connection identifier to associated reservations.
*/
private final ConcurrentMap<String, Reservation> reservations =
new ConcurrentHashMap<String, Reservation>();
@Override
protected ModeledConnection acquire(AuthenticatedUser user,
List<ModeledConnection> connections) throws GuacamoleException {
// STUB
throw new UnsupportedOperationException("STUB");
String username = user.getUser().getIdentifier();
// Return the first successfully-reserved connection
for (ModeledConnection connection : connections) {
String identifier = connection.getIdentifier();
// Attempt to reserve connection, return if successful
Reservation reservation = reservations.putIfAbsent(identifier, new Reservation(username));
if (reservation == null || reservation.acquire(username))
return connection;
}
// Already in use
throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use.");
}
@Override
protected void release(AuthenticatedUser user, ModeledConnection connection) {
// STUB
throw new UnsupportedOperationException("STUB");
String identifier = connection.getIdentifier();
// Retrieve active reservation (which must exist)
Reservation reservation = reservations.get(identifier);
assert(reservation != null);
// Release reservation, remove from map if empty
if (reservation.release())
reservations.remove(identifier);
}
}