/* * 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.springframework.security.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 */ public class SessionRegistryImplMTTests 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); } assertThat(errorOccurred).as("Thread errors detected; review log output for details").isFalse(); } 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) {} } }