/*
* Copyright 2007 Glencoe Software, Inc. All rights reserved.
* Use is subject to license terms supplied in LICENSE.txt
*/
package ome.server.utests.sessions;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CyclicBarrier;
import junit.framework.TestCase;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import ome.conditions.RemovedSessionException;
import ome.conditions.SessionException;
import ome.conditions.SessionTimeoutException;
import ome.model.internal.Permissions;
import ome.model.meta.ExperimenterGroup;
import ome.model.meta.Session;
import ome.services.sessions.SessionCallback;
import ome.services.sessions.SessionContext;
import ome.services.sessions.SessionContextImpl;
import ome.services.sessions.events.UserGroupUpdateEvent;
import ome.services.sessions.state.SessionCache.StaleCacheListener;
import ome.services.sessions.state.SessionCache;
import ome.services.sessions.stats.NullSessionStats;
import ome.system.OmeroContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
* @author Josh Moore, josh at glencoesoftware.com
* @since 3.0-Beta2
*/
@Test(groups = "sessions")
public class SessionCacheTest extends TestCase {
OmeroContext ctx = new OmeroContext(
new String[] { "classpath:ome/services/messaging.xml" });
SessionCache cache;
final boolean[] called = new boolean[2];
@BeforeMethod
public void setup() throws Exception {
initCache();
called[0] = called[0] = false;
}
private void initCache() {
cache = new SessionCache();
cache.setApplicationContext(ctx);
cache.setStaleCacheListener(new NoOpStaleCacheListener());
cache.setCacheManager(CacheManager.getInstance());
// Waiting a second to let the fresh cache cool down.
while (cache.getLastUpdated() == System.currentTimeMillis()) {
try {
Thread.sleep(10L);
} catch (InterruptedException e) {
// ok
}
}
}
List<Long> ids = Arrays.asList(1L);
List<String> roles = Arrays.asList("");
public void testSimpleTimeout() throws Exception {
initCache();
called[0] = false;
final Session s = sess();
s.setTimeToIdle(1L);
cache.putSession(s.getUuid(), sc(s));
cache.addSessionCallback(s.getUuid(), new SessionCallback() {
public void close() {
called[0] = true;
}
public String getName() {
return null;
}
public Object getObject() {
return null;
}
public void join(String session) {
}
});
Thread.sleep(2000L);
throwsSessionTimeout(s.getUuid());
throwsSessionTimeout(s.getUuid());
cache.updateEvent(new UserGroupUpdateEvent(this));
cache.setStaleCacheListener(new NullStaleCacheListener());
cache.doUpdate(); // Now it will be removed and the event published.
assertTrue(called[0]);
}
public void testPutNonSerializable() {
initCache();
final Session s = sess();
int size = cache.getIds().size();
cache.putSession(s.getUuid(), sc(s));
assertTrue(cache.getIds().size() == (size + 1));
}
// Now the cache still needs an update
final boolean done[] = new boolean[] { false, false };
class TryUpdate extends Thread {
int i;
Exception ex;
CyclicBarrier barrier = new CyclicBarrier(2);
TryUpdate(int i) {
this(i, "try-thread");
}
TryUpdate(int i, String name) {
super(name);
this.i = i;
}
@Override
public void run() {
try {
barrier.await();
cache.getSessionContext(this.getName());
} catch (Exception e) {
this.ex = e;
}
done[i] = true;
}
}
public void testOneThreadSuccessfulThenOtherReturns() throws Exception {
final Session s = sess();
cache.putSession(s.getUuid(), sc(s));
done[0] = done[1] = false;
final int[] count = new int[] { 0 };
cache.setStaleCacheListener(new StaleCacheListener() {
public void prepareReload() {
// noop
}
public SessionContext reload(SessionContext context) {
try {
Thread.sleep(1000L);
if (context.getSession().getUuid().equals(s.getUuid())) {
count[0]++;
}
} catch (InterruptedException e) {
}
return context;
}
});
cache.updateEvent(new UserGroupUpdateEvent(this)); // Locks & blocks
TryUpdate t1 = new TryUpdate(0, "0-try-thread"), t2 = new TryUpdate(1,
"1-try-thread");
t1.start();
t2.start();
t1.barrier.await();
t2.barrier.await();
cache.doUpdate(); // This should release the lock
t1.join();
t2.join();
assertTrue(done[0]);
assertTrue(done[1]);
assertEquals(1, count[0]);
}
@Test
public void testInMemoryAndOnDiskAreProperlyDisposed() {
initCache();
Ehcache inmemory, ondisk;
try {
inmemory = cache.inMemoryCache("doesnotexist");
fail("should fail");
} catch (SessionException se) {
// ok
}
// Create a session and attach in-memory info to it
final Session s = sess();
cache.putSession(s.getUuid(), sc(s));
inmemory = cache.inMemoryCache(s.getUuid());
ondisk = cache.onDiskCache(s.getUuid());
inmemory.put(new Element("a", "b"));
ondisk.put(new Element("c", "d"));
cache.removeSession(s.getUuid());
// Now recreate the same session and cache should be gone.
cache.putSession(s.getUuid(), sc(s));
inmemory = cache.inMemoryCache(s.getUuid());
assertFalse(inmemory.isKeyInCache("a"));
ondisk = cache.onDiskCache(s.getUuid());
assertFalse(ondisk.isKeyInCache("c"));
}
@Test
public void testMessageShouldBeRaisedOnRemoveSession() throws Exception {
class Listener implements ApplicationListener {
boolean called = false;
public void onApplicationEvent(ApplicationEvent arg0) {
called = true;
}
}
String uuid = UUID.randomUUID().toString();
Listener listener = new Listener();
ApplicationEventMulticaster multicaster = mc();
multicaster.addApplicationListener(listener);
Session s = sess();
cache.putSession(uuid, sc(s));
cache.removeSession(uuid);
assertTrue(listener.called);
}
@Test
public void testMessageShouldBeRaisedOnTimeout() throws Exception {
class Listener implements ApplicationListener {
boolean called = false;
public void onApplicationEvent(ApplicationEvent arg0) {
called = true;
}
}
String uuid = UUID.randomUUID().toString();
Listener listener = new Listener();
ApplicationEventMulticaster multicaster = mc();
multicaster.addApplicationListener(listener);
Session s = sess();
s.setTimeToLive(1L);
cache.putSession(uuid, sc(s));
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1000L) {
Thread.sleep(200L);
}
throwsSessionTimeout(uuid);
throwsSessionTimeout(uuid);
throwsSessionTimeout(uuid);
// doUpdate() is no longer called automatically
// so multiple calls to getSession will throw SessTimeout
// until the background thread runs
cache.updateEvent(new UserGroupUpdateEvent(this));
cache.setStaleCacheListener(new NullStaleCacheListener());
cache.doUpdate();
assertTrue(listener.called);
throwsRemovedSession(uuid);
}
@Test
public void testMessageShouldBeRaisedOnUpdateNeeded() throws Exception {
class Listener implements ApplicationListener {
boolean called = false;
public void onApplicationEvent(ApplicationEvent arg0) {
called = true;
}
}
String uuid = UUID.randomUUID().toString();
Listener listener = new Listener();
ApplicationEventMulticaster multicaster = mc();
multicaster.addApplicationListener(listener);
Session s = sess();
s.setTimeToLive(1L);
cache.putSession(uuid, sc(s));
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 1000L) {
Thread.sleep(200L);
}
cache.updateEvent(new UserGroupUpdateEvent(this));
cache.setStaleCacheListener(new NullStaleCacheListener());
cache.doUpdate();
assertTrue(listener.called);
}
@Test(timeOut=10000)
public void testGetSessionDoesUpdateTheTimestamp() throws Exception {
final Session s = sess();
s.setTimeToIdle(5 * 100L);
cache.putSession(s.getUuid(), sc(s));
for (int i = 0; i < 10; i++) {
Thread.sleep(1 * 100L);
try {
cache.getSessionContext(s.getUuid());
} catch (RemovedSessionException rse) {
fail("Removed session on loop " + i);
}
}
}
/**
* For {@link #testGetSessionDoesUpdateTheTimestamp()} we changed from
* cache.putQuiet(new Element) to cache.put(new Element) but we want to make
* REAL sure that timeouts are still in effect.
*/
@Test
public void testSessionsTimeoutDispiteTheNewElementPutCall()
throws Exception {
initCache();
StaleCacheListener stale = new NoOpStaleCacheListener();
cache.setStaleCacheListener(stale);
called[0] = false;
final Session s1 = sess(); // One to keep alive
final Session s2 = sess(); // One to let die
s1.setTimeToIdle(5 * 100L);
s2.setTimeToIdle(5 * 100L);
cache.putSession(s1.getUuid(), sc(s1));
cache.putSession(s2.getUuid(), sc(s2));
for (int i = 0; i < 10; i++) {
Thread.sleep(1 * 100L);
try {
cache.getSessionContext(s1.getUuid());
} catch (RemovedSessionException rse) {
fail("Removed session on loop " + i);
} catch (SessionTimeoutException ste) {
fail("Session timeout on loop " + i);
}
}
// Make sure that clean up happened.
cache.updateEvent(new UserGroupUpdateEvent(this));
cache.doUpdate();
try {
cache.getSessionContext(s2.getUuid());
fail("Should fail for " + s2.getUuid());
} catch (RemovedSessionException rse) {
// ok.
} catch (SessionTimeoutException ste) {
// ok.
}
}
/**
* Note: the listener logic was removed from the cache. The new semantics
* of when things should be cleaned up needs to be removed along with the
* methodIn() methodOut() test.
* @throws Exception
*/
@Test(groups = "broken")
public void testExpiredSessionRemainsInCacheTilCleanup() throws Exception {
initCache();
cache.setStaleCacheListener(new NoOpStaleCacheListener());
Session s = sess();
s.setTimeToIdle(1L);
cache.putSession(s.getUuid(), sc(s));
Thread.sleep(2L);
try {
cache.getSessionContext(s.getUuid());
fail("should time out");
} catch (SessionException se) {
// Good.
}
Cache internal = CacheManager.getInstance().getCache("SessionCache");
assertTrue(internal.isKeyInCache(s.getUuid()));
}
// Helpers
// ====================
Session sess() {
Session s = new Session();
s.setStarted(new Timestamp(System.currentTimeMillis()));
s.setTimeToIdle(0L);
s.setTimeToLive(0L);
s.setUuid(UUID.randomUUID().toString());
ExperimenterGroup g = new ExperimenterGroup();
g.getDetails().setPermissions(Permissions.PRIVATE);
s.getDetails().setGroup(g);
return s;
}
SessionContext sc(Session s) {
return new SessionContextImpl(s, Collections.singletonList(1L),
Collections.singletonList(1L), Collections.singletonList(""),
new NullSessionStats(), null);
}
private ApplicationEventMulticaster mc() {
ApplicationEventMulticaster multicaster = (ApplicationEventMulticaster) ctx
.getBean("applicationEventMulticaster");
return multicaster;
}
private void throwsSessionTimeout(String uuid) {
try {
cache.getSessionContext(uuid);
fail("Should throw");
} catch (SessionTimeoutException ste) {
// ok;
}
}
private void throwsRemovedSession(String uuid) {
try {
cache.getSessionContext(uuid);
fail("Should throw");
} catch (RemovedSessionException rse) {
// ok;
}
}
private final class NoOpStaleCacheListener implements StaleCacheListener {
boolean called = false;
public void prepareReload() {
// noop
}
public SessionContext reload(SessionContext context) {
called = true;
return context;
}
}
private final class NullStaleCacheListener implements StaleCacheListener {
boolean called = false;
public void prepareReload() {
// noop
}
public SessionContext reload(SessionContext context) {
called = true;
return null;
}
}
private final class ThrowsStaleCacheListener implements StaleCacheListener {
public void prepareReload() {
// noop.
}
public SessionContext reload(SessionContext context) {
throw new RuntimeException();
}
}
}