diff --git a/core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplMultithreadedTests.java b/core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplMultithreadedTests.java new file mode 100644 index 0000000000..c55de6defe --- /dev/null +++ b/core/src/test/java/org/acegisecurity/concurrent/SessionRegistryImplMultithreadedTests.java @@ -0,0 +1,213 @@ +/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.acegisecurity.concurrent; + +import junit.framework.TestCase; + +import java.util.Set; +import java.util.Collections; +import java.util.HashSet; +import java.util.Random; + +/** + * Tests concurrency access to SessionRegistryImpl. + * + * @author Luke Taylor + * @version $Id$ + */ +public class SessionRegistryImplMultithreadedTests extends TestCase { + private static final Random rnd = new Random(); + private static boolean errorOccurred; + + protected void setUp() throws Exception { + errorOccurred = false; + } + + /** + * Reproduces the NPE mentioned in SEC-484 where a sessionId is removed from + * the set of sessions before it is removed from the list of sessions for a principal. + * getAllSessions(principal, false) then finds the sessionId in the principal's session list + * but reads null for the SessionInformation with the same Id. + * Note that this is not guaranteed to produce the error but is a good testing point. Increasing the number + * of sessions makes a failure more likely, but slows the test considerably. + * Inserting temporary sleep statements in SessionRegistryClassImpl will also help. + */ + public void testConcurrencyOfReadAndRemoveIsSafe() { + Object principal = "Joe Principal"; + SessionRegistryImpl sessionregistry = new SessionRegistryImpl(); + Set sessions = Collections.synchronizedSet(new HashSet()); + // Register some sessions + for (int i = 0; i < 50; i++) { + String sessionId = Integer.toString(i); + sessions.add(sessionId); + sessionregistry.registerNewSession(sessionId, principal); + } + + // Pile of readers to hammer the getAllSessions method. + for (int i=0; i < 10; i++) { + Thread reader = new Thread(new SessionRegistryReader(principal, sessionregistry)); + reader.start(); + } + + Thread remover = new Thread(new SessionRemover("remover", sessionregistry, sessions)); + + remover.start(); + + while(remover.isAlive()) { + pause(250); + } + + assertFalse("Thread errors detected; review log output for details", errorOccurred); + } + + public void testConcurrentRemovalIsSafe() { + Object principal = "Some principal object"; + SessionRegistryImpl sessionregistry = new SessionRegistryImpl(); + // The session list (effectivelly the containers sessions). + Set sessions = Collections.synchronizedSet(new HashSet()); + Thread registerer = new Thread(new SessionRegisterer(principal, sessionregistry, 100, sessions)); + + registerer.start(); + + int nRemovers = 4; + + SessionRemover[] removers = new SessionRemover[nRemovers]; + Thread[] removerThreads = new Thread[nRemovers]; + + for (int i = 0; i < removers.length; i++) { + removers[i] = new SessionRemover("remover" + i, sessionregistry, sessions); + removerThreads[i] = new Thread(removers[i], "remover" + i); + removerThreads[i].start(); + } + + while (stillRunning(removerThreads)) { + pause(500); + } + } + + private boolean stillRunning(Thread[] threads) { + for (int i = 0; i < threads.length; i++) { + if (threads[i].isAlive()) { + return true; + } + } + + return false; + } + + private static class SessionRegisterer implements Runnable { + private SessionRegistry sessionregistry; + private int nIterations; + private Set sessionList; + private Object principal; + + public SessionRegisterer(Object principal, SessionRegistry sessionregistry, int nIterations, Set sessionList) { + this.sessionregistry = sessionregistry; + this.nIterations = nIterations; + this.sessionList = sessionList; + this.principal = principal; + } + + public void run() { + for (int i=0; i < nIterations && !errorOccurred; i++) { + String sessionId = Integer.toString(i); + sessionList.add(sessionId); + try { + sessionregistry.registerNewSession(sessionId,principal); + pause(20); + Thread.yield(); + } catch(Exception e) { + e.printStackTrace(); + errorOccurred = true; + } + } + } + } + + private static class SessionRegistryReader implements Runnable { + private SessionRegistry sessionRegistry; + private Object principal; + + public SessionRegistryReader(Object principal, SessionRegistry sessionregistry) { + this.sessionRegistry = sessionregistry; + this.principal = principal; + } + + public void run() { + while (!errorOccurred) { + try { + sessionRegistry.getAllSessions(principal, false); + sessionRegistry.getAllPrincipals(); + sessionRegistry.getAllSessions(principal, true); + Thread.yield(); + } catch (Exception e) { + e.printStackTrace(); + errorOccurred = true; + } + } + } + } + + private static class SessionRemover implements Runnable { + private SessionRegistry sessionregistry; + private Set sessionList; + private String name; + + public SessionRemover(String name, SessionRegistry sessionregistry, Set sessionList) { + this.name = name; + this.sessionregistry = sessionregistry; + this.sessionList = sessionList; + } + + public void run() { + boolean finished = false; + + while (!finished && !errorOccurred) { + if (sessionList.isEmpty()) { + finished = true; + // List of sessions appears to be empty but give it a chance to fill up again + System.out.println(name + ": Session list empty. Waiting."); + pause(500); + } + + Object[] sessions = sessionList.toArray(); + + if (sessions.length > 0) { + finished = false; + String sessionId = (String) sessions[0]; + System.out.println(name + ": removing " + sessionId); + try { + sessionregistry.removeSessionInformation(sessionId); + + pause(rnd.nextInt(100)); + + sessionList.remove(sessionId); + Thread.yield(); + } catch (Exception e) { + e.printStackTrace(); + errorOccurred = true; + } + } + } + } + } + + private static void pause(int length) { + try { + Thread.sleep(length); + } catch (InterruptedException ignore) {} + } +}