Background
In a web project I'm working on just now, users can share and edit documents. Because of the potential for people overwriting other people's edits, or even deleting a document whilst someone else is working on it, we use a locking mechanism to ensure that only one person can edit the document at a time. The lock is acquired when someone starts editing, and is released when any of these conditions are true:- The user logs out.
- The user session expires.
- The user saves and closes the document.
Our project uses the Apache Shiro security library, a very versatile library that can be used in web and non-web projects, and has good support for testing. Up till now, though, all our tests ran in a single thread, with the result that only one user could be logged in at a time.
For our integration tests we needed to have:
- Several users logged on simultaneously, simulating concurrent web sessions.
- One user active at a time, whilst the other users wait.
- Active session management, so that session expiration and logout listeners would be triggered after session lifecycle events.
Solution
In this solution, we build on a mechanism discussed in a StackOverflow post about sequencing of threads. All the code discussed here is available on Github as a Maven project at https://github.com/otter606/shiro-multithread-junit. Just import into your IDE and runmvn test
to run.
Setting up Shiro.
Shiro provides a TestUtils class in its documentation. Our unit test class can extend this, or include it as a helper class. In our example, for ease of explanation, we'll extend from it.First of all, we'll just initialise a regular SecurityManager, using a configuration file, shiro.ini at the root of the classpath - this is a standard approach to initialising Shiro.
@BeforeClass
public static void readShiroConfiguration() {
Factory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
log.setLevel(Level.INFO);
}
The shiro.ini file is very simple, it just defines three users and their passwords:
[users]
user1 = password1
user2 = password2
user3 = password3
The code to login a user, and bind to a particular thread is boiler-plate code that we can put in a utility method.
private Subject doLogin(User user) {
Subject subjectUnderTest = new Subject.Builder(SecurityUtils.getSecurityManager())
.buildSubject();
subjectUnderTest.login(new UsernamePasswordToken(user.getUsername(), user.getPassword()));
setSubject(subjectUnderTest);
return subjectUnderTest;
}
So, at the moment we just have some code to login a User - useful, but nothing new. Calling this method multiple times in the same thread is likely to lead to strange results, so now we need to set up a mechanism to run multiple threads in JUnit, where each thread performs actions for a particular user, in a sequenced manner, so that the other threads pause when one thread is active. In this manner, we can test a use case like:
- User1 logs in and accesses a resource R for editing.
- User2 logs in and and also requests to access R, but is rejected, as user 1 holds a lock,
- User1 closes resource R (but remains logged in).
- User2 now successfully accesses R.
- User1 logs out.
- User2 logs out.
Invokables and CountDown latches.
To run the test, we'll use a simple interface, Invokable, in which to define callback functions that perform a single step in the use case. public interface Invokable {
void invoke() throws Exception;
}
And let's define an array of these - 6 long - to hold each action:
Invokable[] invokables = new Invokable[6];
invokables[0] = new Invokable() {
// annotate these inner class methods with @Test
@Test
public void invoke() throws Exception {
log.info("logging in user1");
doLogin(u1);
log.info(" user1 doing some actions..");
log.info(" user1 pausing but still logged in.");
}
};
invokables[1] = new Invokable() {
public void invoke() throws Exception {
log.info("logging in user2");
doLogin(u2);
log.info(" user2 doing some actions..");
// some action
log.info(" user2 pausing but still logged in.");
}
};
//.. + 4 more Invokables defined for subsequent steps.
Now, we'll set up a mechanism to sequence the execution of these Invokables in different threads using a CountDown Latch mechanism. Here's how we'll call it:
Map config = new TreeMap<>();
// these are the array indices of the Invokable [].
config.put("t1", new Integer[] { 0, 3 });
config.put("t2", new Integer[] { 1, 5, });
config.put("t3", new Integer[] { 2, 4 });
SequencedRunnableRunner runner = new SequencedRunnableRunner(config, invokables);
runner.runSequence();
In the code above, we define that we want to run 3 threads, and specify the indices of the Invokable [] that will run in each thread. I.e., we want Invokable[0] to run in thread t1, then invokable 1 to run in thread t2, etc.,
What happens under the hood in runSequence is as follows. We define an array of CountDownLatch objects. Each Invokable will wait on a CountDownLatch, and each latch will be counted down by the completion of its predecessor:
public void runSequence() throws InterruptedException {
// Lock l = new ReentrantLock(true);
CountDownLatch[] conditions = new CountDownLatch[actions.length];
for (int i = 0; i < actions.length; i++) {
// each latch will be counted down by the action of its predecessor
conditions[i] = new CountDownLatch(1);
}
Thread[] threads = new Thread[nameToSequence.size()];
int i = 0;
for (String name : nameToSequence.keySet()) {
threads[i] = new Thread(new SequencedRunnable(name, conditions, actions,
nameToSequence.get(name)));
i++;
}
for (Thread t : threads) {
t.start();
}
try {
// tell the thread waiting for the first latch to wake up.
conditions[0].countDown();
} finally {
// l.unlock();
}
// wait for all threads to finish before leaving the test
for (Thread t : threads) {
t.join();
}
}
In SequenceRunnable, we run an Invokable, and count down the next latch in the sequence:
public void run() {
try {
for (int i = 0; i < sequence.length; i++) {
int toWaitForIndx = sequence[i];
try {
log.debug(name + ": waiting for event " + toWaitForIndx);
toWaitFor[toWaitForIndx].await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(name + ": invoking action " + toWaitForIndx);
actions[toWaitForIndx].invoke();
if (toWaitForIndx < toWaitFor.length - 1) {
log.debug(name + "counting down for next latch " + (toWaitForIndx + 1));
toWaitFor[++toWaitForIndx].countDown();
} else
log.debug(name + " executed last invokable!");
}
} catch (Exception e) {
e.printStackTrace();
}
}
That's it! Using this setup, we can login multiple users simultaneously in concurrent sessions, and perform actions for any user in a guaranteed order, thus being able to test thoroughly functionality that is affected by resource contention or locking.