@Override public void onRemoval(RemovalNotification<Serializable, Session> notification) { Serializable key = notification.getKey(); Session session = notification.getValue(); if (notification.getCause() == RemovalCause.EXPIRED) { // time out cause session expired. logger.info("session for {} expired.", session.getId()); } else { // logout cause session be removed. logger.info("session for {} stoped.", session.getId()); } Object attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if (attribute instanceof PrincipalCollection) { PrincipalCollection collection = (PrincipalCollection) attribute; for (Object object : collection) { if (object instanceof ShiroPrincipal) { ShiroPrincipal shiroPrincipal = (ShiroPrincipal) object; UsrSession userSession = shiroPrincipal.getSession(); userSession.setLastAccessTime(new Timestamp(session.getLastAccessTime().getTime())); userSession.setStopTime(new Timestamp(System.currentTimeMillis())); UsrSession merge = userSessionDao.update(userSession); shiroPrincipal.setSession(merge); } } } }
@GwtIncompatible("removalListener") public void testRemovalNotification_clear() throws InterruptedException { // If a clear() happens while a computation is pending, we should not get a removal // notification. final AtomicBoolean shouldWait = new AtomicBoolean(false); final CountDownLatch computingLatch = new CountDownLatch(1); CacheLoader<String, String> computingFunction = new CacheLoader<String, String>() { @Override public String load(String key) throws InterruptedException { if (shouldWait.get()) { computingLatch.await(); } return key; } }; QueuingRemovalListener<String, String> listener = queuingRemovalListener(); final LoadingCache<String, String> cache = CacheBuilder.newBuilder() .concurrencyLevel(1) .removalListener(listener) .build(computingFunction); // seed the map, so its segment's count > 0 cache.getUnchecked("a"); shouldWait.set(true); final CountDownLatch computationStarted = new CountDownLatch(1); final CountDownLatch computationComplete = new CountDownLatch(1); new Thread( new Runnable() { @Override public void run() { computationStarted.countDown(); cache.getUnchecked("b"); computationComplete.countDown(); } }) .start(); // wait for the computingEntry to be created computationStarted.await(); cache.invalidateAll(); // let the computation proceed computingLatch.countDown(); // don't check cache.size() until we know the get("b") call is complete computationComplete.await(); // At this point, the listener should be holding the seed value (a -> a), and the map should // contain the computed value (b -> b), since the clear() happened before the computation // completed. assertEquals(1, listener.size()); RemovalNotification<String, String> notification = listener.remove(); assertEquals("a", notification.getKey()); assertEquals("a", notification.getValue()); assertEquals(1, cache.size()); assertEquals("b", cache.getUnchecked("b")); }
@Override public void onRemoval(RemovalNotification<String, OutputStream> notification) { try { log.debug("Closing {}", notification.getKey()); notification.getValue().close(); } catch (Throwable throwable) { log.warn("Unable to close logfile for {}", notification.getKey(), throwable); } }
@Override public void onRemoval(RemovalNotification<TypedDirectionNamedPath, Notification> event) { switch (event.getCause()) { case SIZE: { LOG.warn( String.format( "Evicted notification '%s' with %d events", event.getKey(), event.getValue().getCount())); break; } default: { break; } } }
@Override public void onRemoval( RemovalNotification<String, InstallFuture> removalNotification) { switch (removalNotification.getCause()) { case EXPLICIT: break; case REPLACED: case COLLECTED: case EXPIRED: case SIZE: SettableFuture<Boolean> value = removalNotification.getValue().future; if (value != null) { // May have been GCed value.set(false); } break; } }
@Override public void onRemoval(RemovalNotification<Serializable, WebSession> notification) { Serializable key = notification.getKey(); WebSession session = notification.getValue(); if (notification.getCause() == RemovalCause.EXPIRED) { // time out cause session expired. logger.info("session for {} expired.", session.getId()); } else { // logout cause session be removed. logger.info("session for {} stoped.", session.getId()); } session.stop(); UsrSession userSession = session.getValue(); userSession.setLastAccessTime(new Timestamp(session.getLastAccessTime().getTime())); userSession.setStopTime(new Timestamp(session.getStopTimestamp().getTime())); userSessionDao.update(userSession); }
@Override public synchronized void onRemoval(RemovalNotification<String, Session> removal) { removal.getValue().close(); ; logger.debug("Closed connection to {}.", removal.getKey().toString()); }
/** * Calls get() repeatedly from many different threads, and tests that all of the removed entries * (removed because of size limits or expiration) trigger appropriate removal notifications. */ @GwtIncompatible("removalListener") public void testRemovalNotification_get_basher() throws InterruptedException { int nTasks = 3000; int nThreads = 100; final int getsPerTask = 1000; final int nUniqueKeys = 10000; final Random random = new Random(); // Randoms.insecureRandom(); QueuingRemovalListener<String, String> removalListener = queuingRemovalListener(); final AtomicInteger computeCount = new AtomicInteger(); final AtomicInteger exceptionCount = new AtomicInteger(); final AtomicInteger computeNullCount = new AtomicInteger(); CacheLoader<String, String> countingIdentityLoader = new CacheLoader<String, String>() { @Override public String load(String key) throws InterruptedException { int behavior = random.nextInt(4); if (behavior == 0) { // throw an exception exceptionCount.incrementAndGet(); throw new RuntimeException("fake exception for test"); } else if (behavior == 1) { // return null computeNullCount.incrementAndGet(); return null; } else if (behavior == 2) { // slight delay before returning Thread.sleep(5); computeCount.incrementAndGet(); return key; } else { computeCount.incrementAndGet(); return key; } } }; final LoadingCache<String, String> cache = CacheBuilder.newBuilder() .concurrencyLevel(2) .expireAfterWrite(100, TimeUnit.MILLISECONDS) .removalListener(removalListener) .maximumSize(5000) .build(countingIdentityLoader); ExecutorService threadPool = Executors.newFixedThreadPool(nThreads); for (int i = 0; i < nTasks; i++) { threadPool.submit( new Runnable() { @Override public void run() { for (int j = 0; j < getsPerTask; j++) { try { cache.getUnchecked("key" + random.nextInt(nUniqueKeys)); } catch (RuntimeException e) { } } } }); } threadPool.shutdown(); threadPool.awaitTermination(300, TimeUnit.SECONDS); // Since we're not doing any more cache operations, and the cache only expires/evicts when doing // other operations, the cache and the removal queue won't change from this point on. // Verify that each received removal notification was valid for (RemovalNotification<String, String> notification : removalListener) { assertEquals("Invalid removal notification", notification.getKey(), notification.getValue()); } CacheStats stats = cache.stats(); assertEquals(removalListener.size(), stats.evictionCount()); assertEquals(computeCount.get(), stats.loadSuccessCount()); assertEquals(exceptionCount.get() + computeNullCount.get(), stats.loadExceptionCount()); // each computed value is still in the cache, or was passed to the removal listener assertEquals(computeCount.get(), cache.size() + removalListener.size()); }
/** * This is a less carefully-controlled version of {@link #testRemovalNotification_clear} - this is * a black-box test that tries to create lots of different thread-interleavings, and asserts that * each computation is affected by a call to {@code clear()} (and therefore gets passed to the * removal listener), or else is not affected by the {@code clear()} (and therefore exists in the * cache afterward). */ @GwtIncompatible("removalListener") public void testRemovalNotification_clear_basher() throws InterruptedException { // If a clear() happens close to the end of computation, one of two things should happen: // - computation ends first: the removal listener is called, and the cache does not contain the // key/value pair // - clear() happens first: the removal listener is not called, and the cache contains the pair AtomicBoolean computationShouldWait = new AtomicBoolean(); CountDownLatch computationLatch = new CountDownLatch(1); QueuingRemovalListener<String, String> listener = queuingRemovalListener(); final LoadingCache<String, String> cache = CacheBuilder.newBuilder() .removalListener(listener) .concurrencyLevel(20) .build(new DelayingIdentityLoader<String>(computationShouldWait, computationLatch)); int nThreads = 100; int nTasks = 1000; int nSeededEntries = 100; Set<String> expectedKeys = Sets.newHashSetWithExpectedSize(nTasks + nSeededEntries); // seed the map, so its segments have a count>0; otherwise, clear() won't visit the in-progress // entries for (int i = 0; i < nSeededEntries; i++) { String s = "b" + i; cache.getUnchecked(s); expectedKeys.add(s); } computationShouldWait.set(true); final AtomicInteger computedCount = new AtomicInteger(); ExecutorService threadPool = Executors.newFixedThreadPool(nThreads); final CountDownLatch tasksFinished = new CountDownLatch(nTasks); for (int i = 0; i < nTasks; i++) { final String s = "a" + i; threadPool.submit( new Runnable() { @Override public void run() { cache.getUnchecked(s); computedCount.incrementAndGet(); tasksFinished.countDown(); } }); expectedKeys.add(s); } computationLatch.countDown(); // let some computations complete while (computedCount.get() < nThreads) { Thread.yield(); } cache.invalidateAll(); tasksFinished.await(); // Check all of the removal notifications we received: they should have had correctly-associated // keys and values. (An earlier bug saw removal notifications for in-progress computations, // which had real keys with null values.) Map<String, String> removalNotifications = Maps.newHashMap(); for (RemovalNotification<String, String> notification : listener) { removalNotifications.put(notification.getKey(), notification.getValue()); assertEquals( "Unexpected key/value pair passed to removalListener", notification.getKey(), notification.getValue()); } // All of the seed values should have been visible, so we should have gotten removal // notifications for all of them. for (int i = 0; i < nSeededEntries; i++) { assertEquals("b" + i, removalNotifications.get("b" + i)); } // Each of the values added to the map should either still be there, or have seen a removal // notification. assertEquals(expectedKeys, Sets.union(cache.asMap().keySet(), removalNotifications.keySet())); assertTrue(Sets.intersection(cache.asMap().keySet(), removalNotifications.keySet()).isEmpty()); }