[dna-commits] DNA SVN: r1414 - in trunk/dna-jcr/src: main/resources/org/jboss/dna/jcr and 1 other directories.
dna-commits at lists.jboss.org
dna-commits at lists.jboss.org
Mon Dec 7 15:56:19 EST 2009
Author: bcarothers
Date: 2009-12-07 15:56:18 -0500 (Mon, 07 Dec 2009)
New Revision: 1414
Modified:
trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/AbstractJcrNode.java
trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/DnaLexicon.java
trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrEngine.java
trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrI18n.java
trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrRepository.java
trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrSession.java
trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/WorkspaceLockManager.java
trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/JcrI18n.properties
trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/dna_builtins.cnd
trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/JcrRepositoryTest.java
trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/WorkspaceLockManagerTest.java
Log:
DNA-541 Locking Implementation Does Not Support Timeouts
Committed a patch that implements lock expiration based on the algorithm above modified by Randall's comments. The patch includes two tests to confirm that the functionality works, but one is @Ignored because it depends on garbage collector behavior to test the results and the other is @Ignored because it has to sleep for 30 seconds to test the timeout.
The patch also addresses specific feedback from the JIRA by integrating the ScheduledExecutionService in the JcrEngine into the DnaEngine's lifecycle methods (start, shutdown, awaitTermination) and synchronizing access to JcrRepository.activeSessions
Modified: trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/AbstractJcrNode.java
===================================================================
--- trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/AbstractJcrNode.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/AbstractJcrNode.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -1409,7 +1409,7 @@
}
}
- session().workspace().lockManager().unlock(session(), lock);
+ session().workspace().lockManager().unlock(session().getExecutionContext(), lock);
session().removeLockToken(lock.getLockToken());
}
Modified: trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/DnaLexicon.java
===================================================================
--- trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/DnaLexicon.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/DnaLexicon.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -34,10 +34,12 @@
public class DnaLexicon extends org.jboss.dna.repository.DnaLexicon {
public static final Name BASE = new BasicName(Namespace.URI, "base");
+ public static final Name EXPIRATION_DATE = new BasicName(Namespace.URI, "expirationDate");
public static final Name IS_HELD_BY_SESSION = new BasicName(Namespace.URI, "isHeldBySession");
public static final Name IS_SESSION_SCOPED = new BasicName(Namespace.URI, "isSessionScoped");
public static final Name LOCK = new BasicName(Namespace.URI, "lock");
public static final Name LOCKED_UUID = new BasicName(Namespace.URI, "lockedUuid");
+ public static final Name LOCKING_SESSION = new BasicName(Namespace.URI, "lockingSession");
public static final Name LOCKS = new BasicName(Namespace.URI, "locks");
public static final Name NAMESPACE = new BasicName(Namespace.URI, "namespace");
public static final Name NODE_TYPES = new BasicName(Namespace.URI, "nodeTypes");
Modified: trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrEngine.java
===================================================================
--- trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrEngine.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrEngine.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -23,17 +23,23 @@
*/
package org.jboss.dna.jcr;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.jcr.Repository;
import javax.jcr.RepositoryException;
import net.jcip.annotations.ThreadSafe;
import org.jboss.dna.common.util.CheckArg;
+import org.jboss.dna.common.util.Logger;
import org.jboss.dna.graph.ExecutionContext;
import org.jboss.dna.graph.Graph;
import org.jboss.dna.graph.Location;
@@ -57,9 +63,19 @@
@ThreadSafe
public class JcrEngine extends DnaEngine {
+ final static int LOCK_SWEEP_INTERVAL_IN_MILLIS = 30000;
+ final static int LOCK_EXTENSION_INTERVAL_IN_MILLIS = LOCK_SWEEP_INTERVAL_IN_MILLIS * 2;
+
+ private final Logger log = Logger.getLogger(DnaEngine.class);
+
private final Map<String, JcrRepository> repositories;
private final Lock repositoriesLock;
+ /**
+ * Provides the ability to schedule lock clean-up
+ */
+ private final ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(2);
+
JcrEngine( ExecutionContext context,
DnaConfiguration.ConfigurationDefinition configuration ) {
super(context, configuration);
@@ -68,6 +84,65 @@
}
/**
+ * Clean up session-scoped locks created by session that are no longer active by iterating over the {@link JcrRepository
+ * repositories} and calling their {@link JcrRepository#cleanUpLocks() clean-up method}.
+ * <p>
+ * It should not be possible for a session to be terminated without cleaning up its locks, but this method will help clean-up
+ * dangling locks should a session terminate abnormally.
+ * </p>
+ */
+ void cleanUpLocks() {
+ Collection<JcrRepository> repos;
+
+ try {
+ // Make a copy of the repositories to minimize the time that the lock needs to be held
+ repositoriesLock.lock();
+ repos = new ArrayList<JcrRepository>(repositories.values());
+ } finally {
+ repositoriesLock.unlock();
+ }
+
+ for (JcrRepository repository : repos) {
+ try {
+ repository.cleanUpLocks();
+ } catch (Throwable t) {
+ log.error(t, JcrI18n.errorCleaningUpLocks, repository.getRepositorySourceName());
+ }
+ }
+ }
+
+ @Override
+ public void shutdown() {
+ scheduler.shutdown();
+
+ super.shutdown();
+ }
+
+ @Override
+ public boolean awaitTermination( long timeout,
+ TimeUnit unit ) throws InterruptedException {
+ if (!scheduler.awaitTermination(timeout, unit)) return false;
+
+ return super.awaitTermination(timeout, unit);
+ }
+
+ @Override
+ public void start() {
+ super.start();
+
+ final JcrEngine engine = this;
+ Runnable cleanUpTask = new Runnable() {
+
+ @Override
+ public void run() {
+ engine.cleanUpLocks();
+ }
+
+ };
+ scheduler.scheduleAtFixedRate(cleanUpTask, 0, LOCK_SWEEP_INTERVAL_IN_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ /**
* Get the {@link Repository} implementation for the named repository.
*
* @param repositoryName the name of the repository, which corresponds to the name of a configured {@link RepositorySource}
Modified: trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrI18n.java
===================================================================
--- trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrI18n.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrI18n.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -66,6 +66,9 @@
public static I18n unableToRemapUriUsingPrefixUsedInNamespaceRegistry;
public static I18n errorWhileInitializingTheNamespaceRegistry;
+ public static I18n errorCleaningUpLocks;
+ public static I18n cleaningUpLocks;
+ public static I18n cleanedUpLocks;
public static I18n invalidRelativePath;
public static I18n invalidPathParameter;
public static I18n invalidNamePattern;
Modified: trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrRepository.java
===================================================================
--- trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrRepository.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrRepository.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -33,10 +33,12 @@
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -61,6 +63,8 @@
import org.jboss.dna.graph.ExecutionContext;
import org.jboss.dna.graph.Graph;
import org.jboss.dna.graph.JaasSecurityContext;
+import org.jboss.dna.graph.Location;
+import org.jboss.dna.graph.Node;
import org.jboss.dna.graph.SecurityContext;
import org.jboss.dna.graph.Subgraph;
import org.jboss.dna.graph.connector.RepositoryConnection;
@@ -75,12 +79,16 @@
import org.jboss.dna.graph.observe.Changes;
import org.jboss.dna.graph.observe.Observable;
import org.jboss.dna.graph.observe.Observer;
+import org.jboss.dna.graph.property.DateTime;
+import org.jboss.dna.graph.property.DateTimeFactory;
import org.jboss.dna.graph.property.Name;
import org.jboss.dna.graph.property.NamespaceRegistry;
import org.jboss.dna.graph.property.Path;
import org.jboss.dna.graph.property.PathFactory;
+import org.jboss.dna.graph.property.PathNotFoundException;
import org.jboss.dna.graph.property.Property;
import org.jboss.dna.graph.property.PropertyFactory;
+import org.jboss.dna.graph.property.ValueFactory;
import org.jboss.dna.graph.property.basic.GraphNamespaceRegistry;
import org.jboss.dna.graph.query.parse.QueryParsers;
import org.jboss.dna.graph.query.parse.SqlQueryParser;
@@ -111,6 +119,8 @@
@ThreadSafe
public class JcrRepository implements Repository {
+ private static final Logger log = Logger.getLogger(JcrRepository.class);
+
/**
* A flag that controls whether the repository uses a shared repository (or workspace) for the "/jcr:system" content in all of
* the workspaces. In production, this needs to be "true" for proper JCR functionality, but in some debugging cases it can be
@@ -122,6 +132,13 @@
static final boolean WORKSPACES_SHARE_SYSTEM_BRANCH = true;
/**
+ * The user name for anonymous sessions
+ *
+ * @see Option#ANONYMOUS_USER_ROLES
+ */
+ static final String ANONYMOUS_USER_NAME = "<anonymous>";
+
+ /**
* The available options for the {@code JcrRepository}.
*/
public enum Option {
@@ -270,6 +287,9 @@
private final QueryParsers queryParsers = new QueryParsers(new SqlQueryParser(), new XPathQueryParser(),
new FullTextSearchParser());
+ // package-scoped to facilitate testing
+ final WeakHashMap<JcrSession, Object> activeSessions = new WeakHashMap<JcrSession, Object>();
+
/**
* Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
* establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
@@ -471,7 +491,7 @@
anonymousUserContext = new SecurityContext() {
public String getUserName() {
- return null;
+ return ANONYMOUS_USER_NAME;
}
public boolean hasRole( String roleName ) {
@@ -788,6 +808,11 @@
} catch (AccessControlException ace) {
throw new NoSuchWorkspaceException(JcrI18n.workspaceNameIsInvalid.text(sourceName, workspaceName));
}
+
+ synchronized (this.activeSessions) {
+ activeSessions.put(session, null);
+ }
+
return session;
}
@@ -809,6 +834,101 @@
return lockManager;
}
+ /**
+ * Marks the given session as inactive (by removing it from the {@link #activeSessions active sessions map}.
+ *
+ * @param session the session to be marked as inactive
+ */
+ void sessionLoggedOut( JcrSession session ) {
+ synchronized (this.activeSessions) {
+ this.activeSessions.remove(session);
+ }
+ }
+
+ /**
+ * Returns the set of active sessions in this repository
+ *
+ * @return the set of active sessions in this repository
+ */
+ Set<JcrSession> activeSessions() {
+ Set<JcrSession> activeSessions;
+ synchronized (this.activeSessions) {
+ activeSessions = new HashSet<JcrSession>(this.activeSessions.keySet());
+ }
+ // There can and will be elements in this set that are no longer live but haven't yet been gc'ed.
+ // Filter those out
+ for (Iterator<JcrSession> iter = activeSessions.iterator(); iter.hasNext();) {
+ JcrSession session = iter.next();
+ if (session != null && !session.isLive()) {
+ iter.remove();
+ }
+ }
+
+ return activeSessions;
+ }
+
+ /**
+ * Iterates through the list of session-scoped locks in this repository, deleting any session-scoped locks that were created
+ * by a session that is no longer active.
+ */
+ void cleanUpLocks() {
+ if (log.isTraceEnabled()) {
+ log.trace(JcrI18n.cleaningUpLocks.text());
+ }
+
+ Set<JcrSession> activeSessions = activeSessions();
+ Set<String> activeSessionIds = new HashSet<String>(activeSessions.size());
+
+ for (JcrSession activeSession : activeSessions) {
+ activeSessionIds.add(activeSession.sessionId());
+ }
+
+ Graph systemGraph = createSystemGraph(executionContext);
+ PathFactory pathFactory = executionContext.getValueFactories().getPathFactory();
+ ValueFactory<Boolean> booleanFactory = executionContext.getValueFactories().getBooleanFactory();
+ ValueFactory<String> stringFactory = executionContext.getValueFactories().getStringFactory();
+
+ DateTimeFactory dateFactory = executionContext.getValueFactories().getDateFactory();
+ DateTime now = dateFactory.create();
+ DateTime newExpirationDate = now.plusMillis(JcrEngine.LOCK_EXTENSION_INTERVAL_IN_MILLIS);
+
+ Path locksPath = pathFactory.createAbsolutePath(JcrLexicon.SYSTEM, DnaLexicon.LOCKS);
+
+ Subgraph locksGraph = null;
+ try {
+ locksGraph = systemGraph.getSubgraphOfDepth(2).at(locksPath);
+ } catch (PathNotFoundException pnfe) {
+ // It's possible for this to run before the dna:locks child node gets added to the /jcr:system node.
+ return;
+ }
+
+ for (Location lockLocation : locksGraph.getRoot().getChildren()) {
+ Node lockNode = locksGraph.getNode(lockLocation);
+
+ Boolean isSessionScoped = booleanFactory.create(lockNode.getProperty(DnaLexicon.IS_SESSION_SCOPED).getFirstValue());
+
+ if (!isSessionScoped) continue;
+ String lockingSession = stringFactory.create(lockNode.getProperty(DnaLexicon.LOCKING_SESSION).getFirstValue());
+
+ // Extend locks held by active sessions
+ if (activeSessionIds.contains(lockingSession)) {
+ systemGraph.set(DnaLexicon.EXPIRATION_DATE).on(lockLocation).to(newExpirationDate);
+ } else {
+ DateTime expirationDate = dateFactory.create(lockNode.getProperty(DnaLexicon.EXPIRATION_DATE).getFirstValue());
+ // Destroy expired locks (if it was still held by an active session, it would have been extended by now)
+ if (expirationDate.isBefore(now)) {
+ String workspaceName = stringFactory.create(lockNode.getProperty(DnaLexicon.WORKSPACE).getFirstValue());
+ WorkspaceLockManager lockManager = lockManagers.get(workspaceName);
+ lockManager.unlock(executionContext, lockManager.createLock(lockNode));
+ }
+ }
+ }
+
+ if (log.isTraceEnabled()) {
+ log.trace(JcrI18n.cleanedUpLocks.text());
+ }
+ }
+
protected class FederatedRepositoryContext implements RepositoryContext {
private final RepositoryConnectionFactory connectionFactory;
Modified: trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrSession.java
===================================================================
--- trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrSession.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/JcrSession.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -785,6 +785,7 @@
isLive = false;
this.workspace().observationManager().removeAllEventListeners();
this.workspace().lockManager().cleanLocks(this);
+ this.repository.sessionLoggedOut(this);
this.executionContext.getSecurityContext().logout();
}
Modified: trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/WorkspaceLockManager.java
===================================================================
--- trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/WorkspaceLockManager.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/java/org/jboss/dna/jcr/WorkspaceLockManager.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -16,6 +16,8 @@
import org.jboss.dna.graph.Graph;
import org.jboss.dna.graph.Location;
import org.jboss.dna.graph.connector.LockFailedException;
+import org.jboss.dna.graph.property.DateTime;
+import org.jboss.dna.graph.property.DateTimeFactory;
import org.jboss.dna.graph.property.Path;
import org.jboss.dna.graph.property.PathFactory;
import org.jboss.dna.graph.property.PathNotFoundException;
@@ -31,6 +33,7 @@
@ThreadSafe
class WorkspaceLockManager {
+ private final ExecutionContext context;
private final Path locksPath;
private final JcrRepository repository;
private final String workspaceName;
@@ -40,6 +43,7 @@
JcrRepository repository,
String workspaceName,
Path locksPath ) {
+ this.context = context;
this.repository = repository;
this.workspaceName = workspaceName;
this.locksPath = locksPath;
@@ -79,7 +83,7 @@
}
ExecutionContext sessionContext = session.getExecutionContext();
- String lockOwner = sessionContext.getSecurityContext().getUserName();
+ String lockOwner = session.getUserID();
DnaLock lock = createLock(lockOwner, lockUuid, nodeUuid, isDeep, isSessionScoped);
Graph.Batch batch = repository.createSystemGraph(sessionContext).batch();
@@ -89,11 +93,17 @@
Property lockOwnerProp = propFactory.create(JcrLexicon.LOCK_OWNER, lockOwner);
Property lockIsDeepProp = propFactory.create(JcrLexicon.LOCK_IS_DEEP, isDeep);
+ DateTimeFactory dateFactory = sessionContext.getValueFactories().getDateFactory();
+ DateTime expirationDate = dateFactory.create();
+ expirationDate = expirationDate.plusMillis(JcrEngine.LOCK_EXTENSION_INTERVAL_IN_MILLIS);
+
batch.create(pathFactory.create(locksPath, pathFactory.createSegment(lockUuid.toString())),
propFactory.create(JcrLexicon.PRIMARY_TYPE, DnaLexicon.LOCK),
propFactory.create(DnaLexicon.WORKSPACE, workspaceName),
propFactory.create(DnaLexicon.LOCKED_UUID, nodeUuid.toString()),
propFactory.create(DnaLexicon.IS_SESSION_SCOPED, isSessionScoped),
+ propFactory.create(DnaLexicon.LOCKING_SESSION, session.sessionId()),
+ propFactory.create(DnaLexicon.EXPIRATION_DATE, expirationDate),
// This gets set after the lock succeeds and the lock token gets added to the session
propFactory.create(DnaLexicon.IS_HELD_BY_SESSION, false),
lockOwnerProp,
@@ -116,6 +126,10 @@
return lock;
}
+ DnaLock createLock( org.jboss.dna.graph.Node lockNode ) {
+ return new DnaLock(lockNode);
+ }
+
/* Factory method added to facilitate mocked testing */
DnaLock createLock( String lockOwner,
UUID lockUuid,
@@ -171,7 +185,7 @@
workspaceBatch.execute();
} catch (LockFailedException lfe) {
// Attempt to lock node at the repo level failed - cancel lock
- unlock(session, lock);
+ unlock(session.getExecutionContext(), lock);
throw new RepositoryException(lfe);
}
@@ -180,22 +194,21 @@
/**
* Removes the provided lock, effectively unlocking the node to which the lock is associated.
*
- * @param session the session in which the node is being unlocked
+ * @param sessionExecutionContext the execution context of the session in which the node is being unlocked
* @param lock the lock to be removed
*/
- void unlock( JcrSession session,
+ void unlock( ExecutionContext sessionExecutionContext,
DnaLock lock ) {
try {
- ExecutionContext context = session.getExecutionContext();
- PathFactory pathFactory = context.getValueFactories().getPathFactory();
+ PathFactory pathFactory = sessionExecutionContext.getValueFactories().getPathFactory();
// Remove the lock node under the /jcr:system branch ...
- Graph.Batch batch = repository.createSystemGraph(context).batch();
+ Graph.Batch batch = repository.createSystemGraph(sessionExecutionContext).batch();
batch.delete(pathFactory.create(locksPath, pathFactory.createSegment(lock.getUuid().toString())));
batch.execute();
// Unlock the node in the repository graph ...
- unlockNodeInRepository(session, lock);
+ unlockNodeInRepository(sessionExecutionContext, lock);
workspaceLocksByNodeUuid.remove(lock.nodeUuid);
} catch (PathNotFoundException pnfe) {
@@ -220,12 +233,12 @@
* /jcr:system/dna:locks} subgraph.
* </p>
*
- * @param session the session in which the node is being unlocked
+ * @param sessionExecutionContext the execution context of the session in which the node is being unlocked
* @param lock
*/
- void unlockNodeInRepository( JcrSession session,
+ void unlockNodeInRepository( ExecutionContext sessionExecutionContext,
DnaLock lock ) {
- Graph.Batch workspaceBatch = repository.createWorkspaceGraph(this.workspaceName, session.getExecutionContext()).batch();
+ Graph.Batch workspaceBatch = repository.createWorkspaceGraph(this.workspaceName, sessionExecutionContext).batch();
workspaceBatch.remove(JcrLexicon.LOCK_OWNER, JcrLexicon.LOCK_IS_DEEP).on(lock.nodeUuid);
workspaceBatch.unlock(lock.nodeUuid);
@@ -249,9 +262,8 @@
ValueFactory<Boolean> booleanFactory = context.getValueFactories().getBooleanFactory();
PathFactory pathFactory = context.getValueFactories().getPathFactory();
- org.jboss.dna.graph.Node lockNode = repository.createSystemGraph(context)
- .getNodeAt(pathFactory.create(locksPath,
- pathFactory.createSegment(lockToken)));
+ org.jboss.dna.graph.Node lockNode = repository.createSystemGraph(context).getNodeAt(pathFactory.create(locksPath,
+ pathFactory.createSegment(lockToken)));
return booleanFactory.create(lockNode.getProperty(DnaLexicon.IS_HELD_BY_SESSION).getFirstValue());
@@ -275,9 +287,8 @@
PropertyFactory propFactory = context.getPropertyFactory();
PathFactory pathFactory = context.getValueFactories().getPathFactory();
- repository.createSystemGraph(context)
- .set(propFactory.create(DnaLexicon.IS_HELD_BY_SESSION, value))
- .on(pathFactory.create(locksPath, pathFactory.createSegment(lockToken)));
+ repository.createSystemGraph(context).set(propFactory.create(DnaLexicon.IS_HELD_BY_SESSION, value)).on(pathFactory.create(locksPath,
+ pathFactory.createSegment(lockToken)));
}
/**
@@ -340,11 +351,12 @@
* @param session the session on behalf of which the lock operation is being performed
*/
void cleanLocks( JcrSession session ) {
+ ExecutionContext context = session.getExecutionContext();
Collection<String> lockTokens = session.lockTokens();
for (String lockToken : lockTokens) {
DnaLock lock = lockFor(lockToken);
if (lock != null && lock.isSessionScoped()) {
- unlock(session, lock);
+ unlock(context, lock);
}
}
}
@@ -361,6 +373,32 @@
private final boolean deep;
private final boolean sessionScoped;
+ DnaLock( org.jboss.dna.graph.Node lockNode ) {
+ ValueFactory<String> stringFactory = context.getValueFactories().getStringFactory();
+ ValueFactory<UUID> uuidFactory = context.getValueFactories().getUuidFactory();
+ ValueFactory<Boolean> booleanFactory = context.getValueFactories().getBooleanFactory();
+
+ assert lockNode.getLocation().getPath() != null;
+
+ String lockUuidAsString = lockNode.getLocation().getPath().getLastSegment().getName().getLocalName();
+ Property lockOwnerProperty = lockNode.getProperty(JcrLexicon.LOCK_OWNER);
+ Property nodeUuidProperty = lockNode.getProperty(DnaLexicon.LOCKED_UUID);
+ Property lockIsDeepProperty = lockNode.getProperty(JcrLexicon.LOCK_IS_DEEP);
+ Property isSessionScopedProperty = lockNode.getProperty(DnaLexicon.IS_SESSION_SCOPED);
+
+ assert lockUuidAsString != null;
+ assert lockOwnerProperty != null;
+ assert nodeUuidProperty != null;
+ assert lockIsDeepProperty != null;
+ assert isSessionScopedProperty != null;
+
+ this.lockOwner = stringFactory.create(lockOwnerProperty.getFirstValue());
+ this.lockUuid = UUID.fromString(lockUuidAsString);
+ this.nodeUuid = uuidFactory.create(nodeUuidProperty.getFirstValue());
+ this.deep = booleanFactory.create(lockIsDeepProperty.getFirstValue());
+ this.sessionScoped = booleanFactory.create(isSessionScopedProperty.getFirstValue());
+ }
+
DnaLock( String lockOwner,
UUID lockUuid,
UUID nodeUuid,
Modified: trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/JcrI18n.properties
===================================================================
--- trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/JcrI18n.properties 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/JcrI18n.properties 2009-12-07 20:56:18 UTC (rev 1414)
@@ -54,6 +54,9 @@
unableToRemapUriNotRegisteredInNamespaceRegistry = Unable to remap the namespace "{1}" to prefix "{0}" because the URI is not already registered in the workspace's namespace registry
unableToRemapUriUsingPrefixUsedInNamespaceRegistry = Unable to remap the namespace "{1}" to prefix "{0}" because the prefix is already used as the prefix for the namespace "{2}" in the workspace's namespace registry
+errorCleaningUpLocks = Error while cleaning up locks for JCR repository "{0}"
+cleaningUpLocks = Lock clean up process begun
+cleanedUpLocks = Lock clean up process completed
errorWhileInitializingTheNamespaceRegistry = Error while initializing the namespace registry for workspace "{0}"
invalidRelativePath = "{0}" is not a valid relative path
invalidPathParameter = The "{1}" parameter value "{0}" was not a valid path
Modified: trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/dna_builtins.cnd
===================================================================
--- trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/dna_builtins.cnd 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/main/resources/org/jboss/dna/jcr/dna_builtins.cnd 2009-12-07 20:56:18 UTC (rev 1414)
@@ -46,6 +46,8 @@
[dna:lock] > nt:base
- dna:lockedUuid (string) protected ignore
- jcr:lockOwner (string) protected ignore
+- dna:lockingSession (string) protected ignore
+- dna:expirationDate (date) protected ignore
- dna:sessionScope (boolean) protected ignore
- jcr:isDeep (boolean) protected ignore
- dna:isHeldBySession (boolean) protected ignore
Modified: trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/JcrRepositoryTest.java
===================================================================
--- trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/JcrRepositoryTest.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/JcrRepositoryTest.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -26,7 +26,6 @@
import static org.hamcrest.collection.IsArrayContaining.hasItemInArray;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.notNullValue;
-import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertThat;
import java.security.AccessControlContext;
import java.security.AccessController;
@@ -51,10 +50,12 @@
import org.jboss.dna.graph.connector.RepositorySourceException;
import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource;
import org.jboss.dna.graph.observe.MockObservable;
+import org.jboss.dna.jcr.JcrRepository.Option;
import org.jboss.security.config.IDTrustConfiguration;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.junit.Ignore;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -192,12 +193,12 @@
testDescriptorKeys(repository);
testDescriptorValues(repository);
}
-
+
@Test
public void shouldProvideObserver() {
assertThat(this.repository.getObserver(), is(notNullValue()));
}
-
+
@Test
public void shouldProvideRepositoryObservable() {
assertThat(this.repository.getRepositoryObservable(), is(notNullValue()));
@@ -205,7 +206,8 @@
@Test
public void shouldHaveDefaultOptionsWhenNotOverridden() {
- JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors, null);
+ JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors,
+ null);
assertThat(repository.getOptions().get(JcrRepository.Option.PROJECT_NODE_TYPES),
is(JcrRepository.DefaultOption.PROJECT_NODE_TYPES));
}
@@ -258,7 +260,7 @@
session = (JcrSession)repository.login();
assertThat(session, is(notNullValue()));
- assertThat(session.getUserID(), is(nullValue()));
+ assertThat(session.getUserID(), is(JcrRepository.ANONYMOUS_USER_NAME));
}
@@ -483,4 +485,76 @@
assertThat(repository.getDescriptor(Repository.SPEC_NAME_DESC), is(JcrI18n.SPEC_NAME_DESC.text()));
assertThat(repository.getDescriptor(Repository.SPEC_VERSION_DESC), is("1.0"));
}
+
+ @Ignore( "GC behavior is non-deterministic from the application's POV - this test _will_ occasionally fail" )
+ @Test
+ public void shouldAllowManySessionLoginsAndLogouts() throws Exception {
+ // Use a different repository that supports anonymous logins to make this test cleaner
+ Map<Option, String> options = new HashMap<Option, String>();
+ options.put(JcrRepository.Option.ANONYMOUS_USER_ROLES, JcrSession.DNA_ADMIN_PERMISSION);
+ JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors,
+ options);
+
+ Session session;
+
+ for (int i = 0; i < 10000; i++) {
+ session = repository.login();
+ session.logout();
+ }
+
+ session = repository.login();
+ session = null;
+
+ // Give the gc a chance to run
+ System.gc();
+ Thread.sleep(100);
+
+ assertThat(repository.activeSessions().size(), is(0));
+ }
+
+ @Ignore( "This test normally sleeps for 30 seconds" )
+ @Test
+ public void shouldCleanUpLocksFromDeadSessions() throws Exception {
+ // Use a different repository that supports anonymous logins to make this test cleaner
+ Map<Option, String> options = new HashMap<Option, String>();
+ options.put(JcrRepository.Option.ANONYMOUS_USER_ROLES, JcrSession.DNA_ADMIN_PERMISSION);
+ JcrRepository repository = new JcrRepository(context, connectionFactory, sourceName, new MockObservable(), descriptors,
+ options);
+
+ String lockedNodeName = "lockedNode";
+ JcrSession locker = (JcrSession)repository.login();
+
+ // Create a node to lock
+ javax.jcr.Node lockedNode = locker.getRootNode().addNode(lockedNodeName);
+ lockedNode.addMixin("mix:lockable");
+ locker.save();
+
+ // Create a session-scoped lock (not deep)
+ lockedNode.lock(false, true);
+ assertThat(lockedNode.isLocked(), is(true));
+
+ Session reader = repository.login();
+ javax.jcr.Node readerNode = (javax.jcr.Node)reader.getItem("/" + lockedNodeName);
+ assertThat(readerNode.isLocked(), is(true));
+
+ // No locks should have changed yet.
+ repository.cleanUpLocks();
+ assertThat(lockedNode.isLocked(), is(true));
+ assertThat(readerNode.isLocked(), is(true));
+
+ /*
+ * Simulate the GC cleaning up the session and it being purged from the activeSessions() map.
+ * This can't really be tested in a consistent way due to a lack of specificity around when
+ * the garbage collector runs. The @Ignored test above does cause a GC sweep on by computer and
+ * confirms that the code works in principle. A different chicken dance may be required to
+ * fully test this on a different computer.
+ */
+ repository.activeSessions.remove(locker);
+ Thread.sleep(JcrEngine.LOCK_EXTENSION_INTERVAL_IN_MILLIS + 100);
+
+ // The locker thread should be inactive and the lock cleaned up
+ repository.cleanUpLocks();
+ assertThat(readerNode.isLocked(), is(false));
+ }
+
}
Modified: trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/WorkspaceLockManagerTest.java
===================================================================
--- trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/WorkspaceLockManagerTest.java 2009-12-07 15:19:53 UTC (rev 1413)
+++ trunk/dna-jcr/src/test/java/org/jboss/dna/jcr/WorkspaceLockManagerTest.java 2009-12-07 20:56:18 UTC (rev 1414)
@@ -151,9 +151,7 @@
@Test
public void shouldCreateLockRequestWhenUnlockingNode() {
DnaLock lock = workspaceLockManager.createLock("testOwner", UUID.randomUUID(), validUuid, false, false);
- JcrSession session = mock(JcrSession.class);
- stub(session.getExecutionContext()).toReturn(context);
- workspaceLockManager.unlockNodeInRepository(session, lock);
+ workspaceLockManager.unlockNodeInRepository(context, lock);
assertNextRequestIsUnlock(validLocation);
}
More information about the dna-commits
mailing list