blob: d8bfe699ee377177f1487eb042b960e8291ee0b7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with this
* work for additional information regarding copyright ownership. The ASF
* licenses this file to You 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.apache.hugegraph.unit.cache;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import com.google.common.collect.ImmutableList;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.apache.hugegraph.HugeGraph;
import org.apache.hugegraph.backend.cache.Cache;
import org.apache.hugegraph.backend.cache.LevelCache;
import org.apache.hugegraph.backend.cache.OffheapCache;
import org.apache.hugegraph.backend.cache.RamCache;
import org.apache.hugegraph.backend.id.Id;
import org.apache.hugegraph.backend.id.IdGenerator;
import org.apache.hugegraph.testutil.Assert;
import org.apache.hugegraph.testutil.Whitebox;
import org.apache.hugegraph.unit.BaseUnitTest;
import org.apache.hugegraph.util.Blob;
import org.apache.hugegraph.util.Bytes;
public abstract class CacheTest extends BaseUnitTest {
@Before
public void setup() {
// pass
}
@After
public void teardown() throws Exception {
// pass
}
protected abstract Cache<Id, Object> newCache();
protected abstract Cache<Id, Object> newCache(long capacity);
protected abstract void checkSize(Cache<Id, Object> cache, long size,
Map<Id, Object> kvs);
protected abstract void checkNotInCache(Cache<Id, Object> cache, Id id);
protected abstract void checkInCache(Cache<Id, Object> cache, Id id);
public static class LimitMap extends LinkedHashMap<Id, Object> {
private static final long serialVersionUID = 1L;
private final int limit;
public LimitMap(int limit) {
super(limit);
this.limit = limit;
}
@Override
protected boolean removeEldestEntry(Map.Entry<Id, Object> eldest) {
return this.size() > this.limit;
}
}
public static class RamCacheTest extends CacheTest {
@Override
protected Cache<Id, Object> newCache() {
return new RamCache();
}
@Override
protected Cache<Id, Object> newCache(long capacity) {
return new RamCache(capacity);
}
@Override
protected void checkSize(Cache<Id, Object> cache, long size,
Map<Id, Object> kvs) {
Assert.assertEquals(size, cache.size());
if (kvs != null) {
for (Map.Entry<Id, Object> kv : kvs.entrySet()) {
Assert.assertEquals(kv.getValue(), cache.get(kv.getKey()));
}
}
}
@Override
protected void checkInCache(Cache<Id, Object> cache, Id id) {
Assert.assertTrue(cache.containsKey(id));
Object queue = Whitebox.getInternalState(cache, "queue");
Assert.assertThrows(RuntimeException.class, () -> {
Whitebox.invoke(queue.getClass(), new Class[]{Object.class},
"checkNotInQueue", queue, id);
}, e -> {
Assert.assertContains("should be not in", e.getMessage());
});
@SuppressWarnings("unchecked")
Map<Id, ?> map = (Map<Id, ?>) Whitebox.getInternalState(cache,
"map");
Assert.assertTrue(Whitebox.invoke(queue.getClass(),
"checkPrevNotInNext",
queue, map.get(id)));
}
@Override
protected void checkNotInCache(Cache<Id, Object> cache, Id id) {
Assert.assertFalse(cache.containsKey(id));
Object queue = Whitebox.getInternalState(cache, "queue");
Assert.assertTrue(Whitebox.invoke(queue.getClass(),
new Class[]{Object.class},
"checkNotInQueue", queue, id));
}
}
public static class OffheapCacheTest extends CacheTest {
private static final long ENTRY_SIZE = 40L;
private final HugeGraph graph = Mockito.mock(HugeGraph.class);
@Override
protected Cache<Id, Object> newCache() {
return newCache(10000L);
}
@Override
protected Cache<Id, Object> newCache(long capacity) {
return new OffheapCache(this.graph(), capacity, ENTRY_SIZE);
}
@Override
protected void checkSize(Cache<Id, Object> cache, long size,
Map<Id, Object> kvs) {
// NOTE: offheap cache is calculated based on bytes, not accurate
long apprSize = (long) (size * 1.2);
Assert.assertLte(apprSize, cache.size());
if (kvs != null) {
long matched = 0L;
for (Map.Entry<Id, Object> kv : kvs.entrySet()) {
Object value = cache.get(kv.getKey());
if (kv.getValue().equals(value)) {
matched++;
}
}
Assert.assertGt(0.8d, matched / (double) size);
}
}
@Override
protected void checkInCache(Cache<Id, Object> cache, Id id) {
Assert.assertTrue(cache.containsKey(id));
}
@Override
protected void checkNotInCache(Cache<Id, Object> cache, Id id) {
Assert.assertFalse(cache.containsKey(id));
}
protected HugeGraph graph() {
return this.graph;
}
@Test
public void testUpdateAndGetWithInvalidDataType() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.update(id, 'c');
Assert.assertNull(cache.get(id));
cache.update(id, new Object());
Assert.assertNull(cache.get(id));
cache.update(id, new byte[]{1});
Assert.assertNull(cache.get(id));
cache.update(id, "string");
Assert.assertEquals("string", cache.get(id));
}
}
public static class LevelCacheTest extends OffheapCacheTest {
@Override
protected Cache<Id, Object> newCache() {
return newCache(10000L);
}
@Override
protected Cache<Id, Object> newCache(long capacity) {
RamCache l1cache = new RamCache(capacity);
OffheapCache l2cache = (OffheapCache) super.newCache(capacity);
return new LevelCache(l1cache, l2cache);
}
@Test
@Override
public void testUpdateAndGetWithInvalidDataType() {
// LevelCache includes level-1 RamCache, which can cache any object
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.update(id, 'c');
Assert.assertEquals('c', cache.get(id));
Object obj = new Object();
cache.update(id, obj);
Assert.assertEquals(obj, cache.get(id));
byte[] bytes = new byte[]{1};
cache.update(id, bytes);
Assert.assertArrayEquals(bytes, (byte[]) cache.get(id));
cache.update(id, "string");
Assert.assertEquals("string", cache.get(id));
}
}
@Test
public void testUpdateAndGet() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
Assert.assertNull(cache.get(id));
cache.update(id, "value-1");
Assert.assertEquals("value-1", cache.get(id));
cache.update(id, "value-2");
Assert.assertEquals("value-2", cache.get(id));
}
@Test
public void testUpdateAndGetWithDataType() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
Assert.assertNull(cache.get(id));
/*
* BOOLEAN
* BYTE
* STRING
* INT
* LONG
* FLOAT
* DOUBLE
* DATE
* UUID
* BLOB
* LIST
*/
cache.update(id, true);
Assert.assertEquals(true, cache.get(id));
cache.update(id, false);
Assert.assertEquals(false, cache.get(id));
cache.update(id, (byte) 123);
Assert.assertEquals((byte) 123, cache.get(id));
cache.update(id, "string");
Assert.assertEquals("string", cache.get(id));
cache.update(id, 123);
Assert.assertEquals(123, cache.get(id));
cache.update(id, 999999999999L);
Assert.assertEquals(999999999999L, cache.get(id));
cache.update(id, 3.14f);
Assert.assertEquals(3.14f, cache.get(id));
cache.update(id, 0.123456789d);
Assert.assertEquals(0.123456789d, cache.get(id));
Date now = new Date();
cache.update(id, now);
Assert.assertEquals(now, cache.get(id));
UUID uuid = UUID.randomUUID();
cache.update(id, uuid);
Assert.assertEquals(uuid, cache.get(id));
byte[] bytes = new byte[]{1, 33, 88};
cache.update(id, Blob.wrap(bytes));
Assert.assertEquals(Blob.wrap(bytes), cache.get(id));
List<Integer> list = ImmutableList.of(1, 3, 5);
cache.update(id, list);
Assert.assertEquals(list, cache.get(id));
List<Long> list2 = ImmutableList.of(1L, 3L, 5L);
cache.update(id, list2);
Assert.assertEquals(list2, cache.get(id));
List<Double> list3 = ImmutableList.of(1.2, 3.4, 5.678);
cache.update(id, list3);
Assert.assertEquals(list3, cache.get(id));
List<Object> listAny = ImmutableList.of(1, 2L, 3.4f, 5.678d, "string");
cache.update(id, listAny);
Assert.assertEquals(listAny, cache.get(id));
}
@Test
public void testUpdateAndGetWithSameSizeAndCapacity() {
int limit = 40;
Cache<Id, Object> cache = newCache(limit);
Map<Id, Object> map = new LimitMap(limit);
for (int i = 0; i < limit; i++) {
Id id = IdGenerator.of("key-" + i);
cache.update(id, "value-" + i);
map.put(id, "value-" + i);
}
this.checkSize(cache, limit, map);
}
@Test
public void testGetOrFetch() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
Assert.assertNull(cache.get(id));
Assert.assertEquals("value-1", cache.getOrFetch(id, key -> {
return "value-1";
}));
cache.update(id, "value-2");
Assert.assertEquals("value-2", cache.getOrFetch(id, key -> {
return "value-1";
}));
}
@Test
public void testUpdateIfAbsent() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.updateIfAbsent(id, "value-1");
Assert.assertEquals("value-1", cache.get(id));
}
@Test
public void testUpdateIfAbsentWithExistKey() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.update(id, "value-1");
cache.updateIfAbsent(id, "value-2");
Assert.assertEquals("value-1", cache.get(id));
}
@Test
public void testUpdateIfPresent() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.updateIfPresent(id, "value-1");
Assert.assertEquals(null, cache.get(id));
cache.update(id, "value-1");
Assert.assertEquals("value-1", cache.get(id));
cache.updateIfPresent(id, "value-2");
Assert.assertEquals("value-2", cache.get(id));
}
@Test
public void testInvalidate() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.update(id, "value-1");
Assert.assertEquals("value-1", cache.get(id));
Assert.assertEquals(1L, cache.size());
this.checkInCache(cache, id);
cache.invalidate(id);
Assert.assertEquals(null, cache.get(id));
Assert.assertEquals(0L, cache.size());
this.checkNotInCache(cache, id);
}
@Test
public void testClear() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.update(id, "value-1");
Assert.assertEquals("value-1", cache.get(id));
id = IdGenerator.of("2");
cache.update(id, "value-2");
cache.clear();
Assert.assertEquals(null, cache.get(id));
Assert.assertEquals(0, cache.size());
}
@Test
public void testCapacity() {
Cache<Id, Object> cache = newCache(10);
Assert.assertEquals(10, cache.capacity());
cache = newCache(1024);
Assert.assertEquals(1024, cache.capacity());
cache = newCache(8);
Assert.assertEquals(8, cache.capacity());
cache = newCache(1);
Assert.assertEquals(1, cache.capacity());
cache = newCache(0);
Assert.assertEquals(0, cache.capacity());
int huge = (int) (200 * Bytes.GB);
cache = newCache(huge);
Assert.assertEquals(huge, cache.capacity());
// The min capacity is 0
cache = newCache(-1);
Assert.assertEquals(0, cache.capacity());
}
@Test
public void testSize() {
Cache<Id, Object> cache = newCache();
Id id = IdGenerator.of("1");
cache.update(id, "value-1");
id = IdGenerator.of("2");
cache.update(id, "value-2");
Assert.assertEquals(2, cache.size());
}
@Test
public void testSizeWithReachCapacity() {
int limit = 20;
Cache<Id, Object> cache = newCache(limit);
Map<Id, Object> map = new LimitMap(limit);
for (int i = 0; i < 5 * limit; i++) {
Id id = IdGenerator.of("key-" + i);
cache.update(id, "value-" + i);
map.put(id, "value-" + i);
}
this.checkSize(cache, limit, map);
}
@Test
public void testHitsAndMiss() {
Cache<Id, Object> cache = newCache();
Assert.assertEquals(false, cache.enableMetrics(true));
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(0L, cache.miss());
Id id = IdGenerator.of("1");
cache.update(id, "value-1");
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(0L, cache.miss());
cache.get(IdGenerator.of("not-exist"));
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(1L, cache.miss());
cache.get(IdGenerator.of("1"));
Assert.assertEquals(1L, cache.hits());
Assert.assertEquals(1L, cache.miss());
cache.get(IdGenerator.of("not-exist"));
Assert.assertEquals(1L, cache.hits());
Assert.assertEquals(2L, cache.miss());
cache.get(IdGenerator.of("1"));
Assert.assertEquals(2L, cache.hits());
Assert.assertEquals(2L, cache.miss());
}
@Test
public void testEnableMetrics() {
Cache<Id, Object> cache = newCache();
Assert.assertEquals(false, cache.enableMetrics(false));
Assert.assertEquals(false, cache.enableMetrics(true));
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(0L, cache.miss());
Id id = IdGenerator.of("1");
cache.update(id, "value-1");
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(0L, cache.miss());
cache.get(IdGenerator.of("not-exist"));
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(1L, cache.miss());
cache.get(IdGenerator.of("1"));
Assert.assertEquals(1L, cache.hits());
Assert.assertEquals(1L, cache.miss());
cache.get(IdGenerator.of("not-exist"));
Assert.assertEquals(1L, cache.hits());
Assert.assertEquals(2L, cache.miss());
cache.get(IdGenerator.of("1"));
Assert.assertEquals(2L, cache.hits());
Assert.assertEquals(2L, cache.miss());
Assert.assertEquals(true, cache.enableMetrics(false));
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(0L, cache.miss());
cache.get(IdGenerator.of("not-exist"));
cache.get(IdGenerator.of("1"));
Assert.assertEquals(0L, cache.hits());
Assert.assertEquals(0L, cache.miss());
}
@Test
public void testExpire() {
Cache<Id, Object> cache = newCache();
Assert.assertEquals(0L, cache.expire());
cache.expire(2000L); // 2 seconds
Assert.assertEquals(2000L, cache.expire());
cache.update(IdGenerator.of("1"), "value-1");
cache.update(IdGenerator.of("2"), "value-2");
Assert.assertEquals(2, cache.size());
waitTillNext(2);
cache.tick();
Assert.assertNull(cache.get(IdGenerator.of("1")));
Assert.assertNull(cache.get(IdGenerator.of("2")));
Assert.assertEquals(0, cache.size());
}
@Test
public void testExpireWithAddNewItem() {
Cache<Id, Object> cache = newCache();
cache.expire(2000L); // 2s
cache.update(IdGenerator.of("1"), "value-1");
cache.update(IdGenerator.of("2"), "value-2");
Assert.assertEquals(2, cache.size());
waitTillNext(1);
cache.tick();
cache.update(IdGenerator.of("3"), "value-3");
cache.tick();
Assert.assertEquals(3, cache.size());
waitTillNext(1);
cache.tick();
Assert.assertNull(cache.get(IdGenerator.of("1")));
Assert.assertNull(cache.get(IdGenerator.of("2")));
Assert.assertNotNull(cache.get(IdGenerator.of("3")));
Assert.assertEquals(1, cache.size());
waitTillNext(1);
cache.tick();
// NOTE: OffheapCache should expire item by access
Assert.assertNull(cache.get(IdGenerator.of("3")));
Assert.assertEquals(0, cache.size());
}
@Test
public void testExpireWithZeroSecond() {
Cache<Id, Object> cache = newCache();
cache.update(IdGenerator.of("1"), "value-1");
cache.update(IdGenerator.of("2"), "value-2");
Assert.assertEquals(2, cache.size());
cache.expire(0);
waitTillNext(1);
cache.tick();
Assert.assertEquals(2, cache.size());
}
private static final int THREADS_NUM = 8;
@Test
public void testMultiThreadsUpdate() {
Cache<Id, Object> cache = newCache(THREADS_NUM * 10000 * 10);
runWithThreads(THREADS_NUM, () -> {
for (int i = 0; i < 10000 * 10; i++) {
Id id = IdGenerator.of(
Thread.currentThread().getName() + "-" + i);
cache.update(id, "value-" + i);
}
});
Assert.assertEquals(THREADS_NUM * 10000 * 10, cache.size());
}
@Test
public void testMultiThreadsUpdateWithGtCapacity() {
int limit = 80;
Cache<Id, Object> cache = newCache(limit);
runWithThreads(THREADS_NUM, () -> {
int size = 10000 * 100;
for (int i = 0; i < size; i++) {
Id id = IdGenerator.of(
Thread.currentThread().getName() + "-" + i);
cache.update(id, "value-" + i);
}
});
this.checkSize(cache, limit, null);
}
@Test
public void testMultiThreadsUpdateAndCheck() {
Cache<Id, Object> cache = newCache();
runWithThreads(THREADS_NUM, () -> {
Map<Id, Object> all = new HashMap<>(1000);
for (int i = 0; i < 1000; i++) {
Id id = IdGenerator.of(Thread.currentThread().getName() +
"-" + i);
String value = "value-" + i;
cache.update(id, value);
all.put(id, value);
}
for (Map.Entry<Id, Object> entry : all.entrySet()) {
Id key = entry.getKey();
Assert.assertEquals(entry.getValue(), cache.get(key));
this.checkInCache(cache, key);
}
});
Assert.assertEquals(THREADS_NUM * 1000, cache.size());
}
@Test
public void testMultiThreadsGetWith2Items() {
Cache<Id, Object> cache = newCache(10);
Id id1 = IdGenerator.of("1");
cache.update(id1, "value-1");
Id id2 = IdGenerator.of("2");
cache.update(id2, "value-2");
Assert.assertEquals(2, cache.size());
runWithThreads(THREADS_NUM, () -> {
for (int i = 0; i < 10000 * 100; i++) {
Assert.assertEquals("value-1", cache.get(id1));
Assert.assertEquals("value-2", cache.get(id2));
Assert.assertEquals("value-1", cache.get(id1));
}
});
Assert.assertEquals(2, cache.size());
}
@Test
public void testMultiThreadsGetWith3Items() {
Cache<Id, Object> cache = newCache(10);
Id id1 = IdGenerator.of("1");
cache.update(id1, "value-1");
Id id2 = IdGenerator.of("2");
cache.update(id2, "value-2");
Id id3 = IdGenerator.of("3");
cache.update(id3, "value-3");
Assert.assertEquals(3, cache.size());
runWithThreads(THREADS_NUM, () -> {
for (int i = 0; i < 10000 * 100; i++) {
Assert.assertEquals("value-1", cache.get(id1));
Assert.assertEquals("value-2", cache.get(id2));
Assert.assertEquals("value-2", cache.get(id2));
Assert.assertEquals("value-3", cache.get(id3));
Assert.assertEquals("value-1", cache.get(id1));
}
});
Assert.assertEquals(3, cache.size());
}
@Test
public void testMultiThreadsGetAndUpdate() {
Cache<Id, Object> cache = newCache(10);
Id id1 = IdGenerator.of("1");
cache.update(id1, "value-1");
Id id2 = IdGenerator.of("2");
cache.update(id2, "value-2");
Id id3 = IdGenerator.of("3");
cache.update(id3, "value-3");
Assert.assertEquals(3, cache.size());
runWithThreads(THREADS_NUM, () -> {
for (int i = 0; i < 10000 * 100; i++) {
Assert.assertEquals("value-1", cache.get(id1));
Assert.assertEquals("value-2", cache.get(id2));
Assert.assertEquals("value-2", cache.get(id2));
Assert.assertEquals("value-3", cache.get(id3));
cache.update(id2, "value-2");
Assert.assertEquals("value-1", cache.get(id1));
}
});
Assert.assertEquals(3, cache.size());
}
@Test
public void testMultiThreadsGetAndUpdateWithGtCapacity() {
Cache<Id, Object> cache = newCache(10);
runWithThreads(THREADS_NUM, () -> {
for (int i = 0; i < 10000 * 20; i++) {
for (int k = 0; k < 15; k++) {
Id id = IdGenerator.of(k);
Object value = cache.get(id);
if (value != null) {
Assert.assertEquals("value-" + k, value);
} else {
cache.update(id, "value-" + k);
}
}
}
});
// In fact, the size may be any value(such as 43)
Assert.assertTrue(cache.size() < 10 + THREADS_NUM);
}
@Test
public void testKeyExpired() {
Cache<Id, Object> cache = newCache();
cache.expire(2000L);
Id key = IdGenerator.of("key");
cache.update(key, "value", -1000L);
waitTillNext(1);
cache.tick();
Assert.assertFalse(cache.containsKey(key));
cache.update(key, "value", -2000L);
cache.tick();
Assert.assertFalse(cache.containsKey(key));
}
}