From 3f22026c9eebe2491c1afe5382015bccbb97f853 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 Mar 2015 21:08:01 -0800 Subject: [PATCH] GUAC-1101: Implement reserved concurrency policy. --- .../ReservedGuacamoleSocketService.java | 132 +++++++++++++++++- 1 file changed, 128 insertions(+), 4 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java index 050e40c17..8cab0b814 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java @@ -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 reservations = + new ConcurrentHashMap(); + @Override protected ModeledConnection acquire(AuthenticatedUser user, List 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); + } }