/*
* Copyright 2015 Terracotta, Inc., a Software AG company.
*
* 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 org.terracotta.offheapstore.paging;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.hamcrest.core.CombinableMatcher;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.terracotta.offheapstore.buffersource.BufferSource;
import org.terracotta.offheapstore.buffersource.HeapBufferSource;
import org.terracotta.offheapstore.buffersource.OffHeapBufferSource;
import org.terracotta.offheapstore.concurrent.ConcurrentOffHeapHashMap;
import org.terracotta.offheapstore.storage.PointerSize;
import org.terracotta.offheapstore.storage.StringStorageEngine;
import org.terracotta.offheapstore.util.Factory;
import org.terracotta.offheapstore.util.MemoryUnit;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import static org.hamcrest.collection.IsArray.array;
import static org.hamcrest.core.IsAnything.anything;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.StringContains.containsString;
import static org.hamcrest.core.StringStartsWith.startsWith;
public class UpfrontAllocatingPageSourceTest {
private static final Lock LOCK = new ReentrantLock();
@Before
public void lock() {
LOCK.lock();
}
@After
public void unlock() {
LOCK.unlock();
}
@Test
public void testGetCapacity() {
UpfrontAllocatingPageSource source = new UpfrontAllocatingPageSource(new OffHeapBufferSource(), 2 * 1024, 1024);
Assert.assertEquals(2 * 1024, source.getCapacity());
}
@Test
public void testUpfrontAllocatingBufferSource() {
PageSource source = new UpfrontAllocatingPageSource(new OffHeapBufferSource(), 2 * 1024, 1024);
Page p = source.allocate(1024, false, false, null);
Assert.assertNotNull(p);
Assert.assertEquals(1024, p.size());
Assert.assertNull(source.allocate(1024 * 2, false, false, null));
for (int i = 0; i < 8; i++) {
Assert.assertEquals(128, source.allocate(128, false, false, null).size());
}
Assert.assertNull(source.allocate(1, false, false, null));
}
@Test
public void testUpfrontAllocationFreeing() {
PageSource test = new UpfrontAllocatingPageSource(new OffHeapBufferSource(), 1024, 1024);
Page p = test.allocate(512, false, false, null);
test.free(p);
}
@Test
public void testUpfrontAllocationResizing() {
Map<String, String> map = new ConcurrentOffHeapHashMap<String, String>(new UnlimitedPageSource(new OffHeapBufferSource()), new Factory<StringStorageEngine>() {
private final PageSource source = new UpfrontAllocatingPageSource(new OffHeapBufferSource(), 1024 * 1024, 128 * 1024);
@Override
public StringStorageEngine newInstance() {
return new StringStorageEngine(PointerSize.INT, source, 1);
}
}, 1, 4);
for (int i = 0; i < 100; i++) {
map.put(Integer.toString(i), "Hello World");
Assert.assertTrue(map.containsKey(Integer.toString(i)));
}
}
@Test
public void testReallyLargeUpfrontAllocation() {
try {
new UpfrontAllocatingPageSource(new OffHeapBufferSource(), Long.MAX_VALUE, MemoryUnit.GIGABYTES.toBytes(1));
Assert.fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
Assert.assertThat(e.getMessage(), CombinableMatcher.<String>either(containsString("physical memory")).or(containsString("allocate more off-heap memory")));
}
}
@Test
public void testVariableChunkSizesSuccess() {
Map<Integer, Integer> chunks = new HashMap<Integer, Integer>();
//Allocate 128k successfully
chunks.put(64 * 1024, 1);
chunks.put(32 * 1024, 1);
chunks.put(16 * 1024, 2);
TestChunkedBufferSource source = new TestChunkedBufferSource(chunks);
new UpfrontAllocatingPageSource(source, 128 * 1024, 128 * 1024, 16 * 1024);
}
@Test
public void testVariableChunkSizesFailureDueToLimitation() {
Map<Integer, Integer> chunks = new HashMap<Integer, Integer>();
//Allocate 128k successfully
chunks.put(64 * 1024, 1);
chunks.put(32 * 1024, 1);
chunks.put(16 * 1024, 2);
TestChunkedBufferSource source = new TestChunkedBufferSource(chunks);
try {
new UpfrontAllocatingPageSource(source, 128 * 1024, 128 * 1024, 32 * 1024);
Assert.fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
//expected
}
}
@Test
public void testVariableChunkSizesFailureDueToExhaustion() {
Map<Integer, Integer> chunks = new HashMap<Integer, Integer>();
//Allocate 128k successfully
chunks.put(64 * 1024, 1);
chunks.put(32 * 1024, 1);
chunks.put(16 * 1024, 1);
TestChunkedBufferSource source = new TestChunkedBufferSource(chunks);
try {
new UpfrontAllocatingPageSource(source, 128 * 1024, 128 * 1024, 16 * 1024);
Assert.fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
//expected
}
}
@Rule
public final TemporaryFolder tempFolder = new TemporaryFolder();
@Test
public void testAllocatorLogging() throws IOException {
File logLocation = tempFolder.newFolder("testAllocatorLogging");
Assert.assertThat(logLocation.listFiles(), array());
System.setProperty(UpfrontAllocatingPageSource.ALLOCATION_LOG_LOCATION, logLocation.getAbsolutePath());
try {
new UpfrontAllocatingPageSource(new HeapBufferSource(), 128 * 1024, 1024, 512);
} finally {
System.clearProperty(UpfrontAllocatingPageSource.ALLOCATION_LOG_LOCATION);
}
File[] files = logLocation.listFiles();
Assert.assertThat(files, array(anything()));
LineNumberReader reader = new LineNumberReader(new InputStreamReader(new FileInputStream(files[0]), "US-ASCII"));
try {
Assert.assertThat(reader.readLine(), startsWith("Timestamp: "));
Assert.assertThat(reader.readLine(), equalTo("Allocating: 128KB"));
Assert.assertThat(reader.readLine(), equalTo("Max Chunk: 1KB"));
Assert.assertThat(reader.readLine(), equalTo("Min Chunk: 512B"));
Assert.assertThat(reader.readLine(), equalTo("timestamp,threadid,duration,size,physfree,totalswap,freeswap,committed"));
while (true) {
String line = reader.readLine();
if (line == null) {
break;
} else {
Assert.assertThat(reader.readLine(), new TypeSafeMatcher<String>() {
@Override
protected boolean matchesSafely(String t) {
return t.matches("(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null)");
}
@Override
public void describeTo(Description description) {
description.appendText("line matching \"(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null),(?:\\d+|null)\"");
}
});
}
}
} finally {
reader.close();
}
}
@Test
public void testInterruption() throws InterruptedException {
final AtomicReference<UpfrontAllocatingPageSource> pageSourceAtomicReference = new AtomicReference<UpfrontAllocatingPageSource>();
final CountDownLatch latch = new CountDownLatch(1);
final AtomicBoolean wasInterrupted = new AtomicBoolean(false);
Thread t = new Thread() {
public void run() {
UpfrontAllocatingPageSource pageSource = new UpfrontAllocatingPageSource(new HeapBufferSource() {
private boolean firstTime = true;
@Override
public ByteBuffer allocateBuffer(int size) {
if(firstTime) {
try {
latch.countDown();
Thread.sleep(200);
} catch (InterruptedException e) {
// ignore
}
firstTime = false;
}
return super.allocateBuffer(size);
}
}, 512, 512, 512);
pageSourceAtomicReference.set(pageSource);
wasInterrupted.set(Thread.currentThread().isInterrupted());
}
};
t.start();
latch.await();
t.interrupt();
t.join();
Assert.assertThat(wasInterrupted.get(), equalTo(true));
Assert.assertThat(pageSourceAtomicReference.get().getCapacity(), equalTo(512L));
}
static class TestChunkedBufferSource implements BufferSource {
private final Map<Integer, Integer> chunks;
public TestChunkedBufferSource(Map<Integer, Integer> chunks) {
this.chunks = chunks;
}
@Override
public ByteBuffer allocateBuffer(int size) {
Integer available = chunks.get(size);
if (available == null) {
return null;
} else if (available == 1) {
chunks.remove(size);
return ByteBuffer.allocate(size);
} else if (available > 1) {
chunks.put(size, available.intValue() - 1);
return ByteBuffer.allocate(size);
} else {
return null;
}
}
}
}