/* * Copyright (C) 2012 Facebook, Inc. * * 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 com.facebook.concurrency; import com.facebook.collections.Pair; import com.facebook.testing.MockExecutor; import org.joda.time.DateTimeUtils; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; public class TestExpiringConcurrentCache { private ExpiringConcurrentCache<String, ReapableString, RuntimeException> legacyCache; private static final String KEY = "key"; private ReapableString reapableValue1; private String value1; private BlockingValueProducer<ReapableString, RuntimeException> producer; private ConcurrentCacheTestHelper<String, ReapableString> testHelper; private MockExecutor mockExecutor; private ExpiringConcurrentCache<String, String, RuntimeException> cache; private List<Pair<String,String>> evicted; @BeforeMethod(alwaysRun = true) public void setUp() throws Exception { value1 = "value1"; reapableValue1 = new ReapableString(value1); producer = new BlockingValueProducer<ReapableString, RuntimeException>(reapableValue1); mockExecutor = new MockExecutor(); // arbitrary time for now DateTimeUtils.setCurrentMillisFixed(0); legacyCache = ExpiringConcurrentCache.createWithReapableValue( new ValueFactory<String, ReapableString, RuntimeException>() { @Override public ReapableString create(String input) throws RuntimeException { return producer.call(); } }, 30, TimeUnit.MILLISECONDS, RuntimeExceptionHandler.INSTANCE, mockExecutor ); evicted = new ArrayList<Pair<String, String>>(); cache = new ExpiringConcurrentCache<String, String, RuntimeException>( new ValueFactory<String, String, RuntimeException>() { @Override public String create(String input) throws RuntimeException { return producer.call().toString(); } }, 30, TimeUnit.MILLISECONDS, new EvictionListener<String, String>() { @Override public void evicted(String key, String value) { evicted.add(new Pair<String, String>(key, value)); } }, RuntimeExceptionHandler.INSTANCE, mockExecutor ); testHelper = new ConcurrentCacheTestHelper<String, ReapableString>( legacyCache ); } @Test(groups = "fast") public void testExpiration() throws Exception { // add a value to the cache Assert.assertEquals(legacyCache.get(KEY), reapableValue1); Assert.assertEquals(producer.getCalledCount(), 1); // advance time to close to the expiration DateTimeUtils.setCurrentMillisFixed(29); // still in cache Assert.assertTrue(legacyCache.getIfPresent(KEY) != null, "key should be in cache"); // now 30ms passed, should expire DateTimeUtils.setCurrentMillisFixed(30); Assert.assertFalse(legacyCache.getIfPresent(KEY) != null, "key should NOT be in cache"); mockExecutor.drain(); // and value1.shutdown was called Assert.assertEquals(reapableValue1.getShutdownCalled(), 1); } @Test(groups = "fast") public void testEvictionListener() throws Exception { // add a value to the cache Assert.assertEquals(cache.get(KEY), value1); Assert.assertEquals(producer.getCalledCount(), 1); DateTimeUtils.setCurrentMillisFixed(30); cache.prune(); // key is removed from the cache Assert.assertEquals(cache.size(), 0); // and this will execute the eviction callback mockExecutor.drain(); Assert.assertEquals(evicted.size(), 1); } @AfterMethod(alwaysRun = true) public void tearDown() throws Exception { DateTimeUtils.setCurrentMillisSystem(); } private static class ReapableString implements Reapable<RuntimeException>{ private final String value; private final AtomicLong shutdownCalled = new AtomicLong(0); private ReapableString(String value) { this.value = value; } @Override public void shutdown() throws RuntimeException { shutdownCalled.incrementAndGet(); } public long getShutdownCalled() { return shutdownCalled.get(); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final ReapableString that = (ReapableString) o; if (value != null ? !value.equals(that.value) : that.value != null) { return false; } return true; } @Override public int hashCode() { return value != null ? value.hashCode() : 0; } @Override public String toString() { return value; } } }