/* * Copyright Terracotta, 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 org.ehcache.clustered.server.offheap; import java.nio.ByteBuffer; import java.util.Random; import org.ehcache.clustered.common.internal.store.Chain; import org.ehcache.clustered.common.internal.store.Element; import org.ehcache.clustered.server.KeySegmentMapper; import org.ehcache.clustered.server.store.ChainBuilder; import org.ehcache.clustered.server.store.ElementBuilder; import org.ehcache.clustered.common.internal.store.ServerStore; import org.ehcache.clustered.server.store.ServerStoreTest; import org.junit.Test; import org.mockito.Matchers; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.terracotta.offheapstore.buffersource.OffHeapBufferSource; import org.terracotta.offheapstore.exceptions.OversizeMappingException; import org.terracotta.offheapstore.paging.UnlimitedPageSource; import org.terracotta.offheapstore.paging.UpfrontAllocatingPageSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.core.Is.is; import org.junit.Assert; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import static org.terracotta.offheapstore.util.MemoryUnit.GIGABYTES; import static org.terracotta.offheapstore.util.MemoryUnit.KILOBYTES; import static org.terracotta.offheapstore.util.MemoryUnit.MEGABYTES; public class OffHeapServerStoreTest extends ServerStoreTest { private static final KeySegmentMapper DEFAULT_MAPPER = new KeySegmentMapper(16); @SuppressWarnings("unchecked") private OffHeapChainMap<Object> getOffHeapChainMapMock() { return mock(OffHeapChainMap.class); } @Override public ServerStore newStore() { return new OffHeapServerStore(new UnlimitedPageSource(new OffHeapBufferSource()), DEFAULT_MAPPER); } @Override public ChainBuilder newChainBuilder() { return new ChainBuilder() { @Override public Chain build(Element... elements) { ByteBuffer[] buffers = new ByteBuffer[elements.length]; for (int i = 0; i < buffers.length; i++) { buffers[i] = elements[i].getPayload(); } return OffHeapChainMap.chain(buffers); } }; } @Override public ElementBuilder newElementBuilder() { return new ElementBuilder() { @Override public Element build(final ByteBuffer payLoad) { return new Element() { @Override public ByteBuffer getPayload() { return payLoad; } }; } }; } @Test public void testGetMaxSize() { assertThat(OffHeapServerStore.getMaxSize(MEGABYTES.toBytes(2)), is(64L)); assertThat(OffHeapServerStore.getMaxSize(MEGABYTES.toBytes(4)), is(128L)); assertThat(OffHeapServerStore.getMaxSize(MEGABYTES.toBytes(16)), is(512L)); assertThat(OffHeapServerStore.getMaxSize(MEGABYTES.toBytes(64)), is(2048L)); assertThat(OffHeapServerStore.getMaxSize(MEGABYTES.toBytes(128)), is(4096L)); assertThat(OffHeapServerStore.getMaxSize(MEGABYTES.toBytes(256)), is(8192L)); assertThat(OffHeapServerStore.getMaxSize(MEGABYTES.toBytes(512)), is(8192L)); assertThat(OffHeapServerStore.getMaxSize(GIGABYTES.toBytes(2)), is(8192L)); } @Test public void test_append_doesNotConsumeBuffer_evenWhenOversizeMappingException() throws Exception { OffHeapServerStore store = (OffHeapServerStore) spy(newStore()); final OffHeapChainMap<Object> offHeapChainMap = getOffHeapChainMapMock(); doThrow(OversizeMappingException.class).when(offHeapChainMap).append(any(Object.class), any(ByteBuffer.class)); when(store.segmentFor(anyLong())).then(new Answer<Object>() { int invocations = 0; @Override public Object answer(InvocationOnMock invocation) throws Throwable { if (invocations++ < 10) { return offHeapChainMap; } else { return invocation.callRealMethod(); } } }); when(store.handleOversizeMappingException(anyLong())).thenReturn(true); ByteBuffer payload = createPayload(1L); store.append(1L, payload); assertThat(payload.remaining(), is(8)); } @Test public void test_getAndAppend_doesNotConsumeBuffer_evenWhenOversizeMappingException() throws Exception { OffHeapServerStore store = (OffHeapServerStore) spy(newStore()); final OffHeapChainMap<Object> offHeapChainMap = getOffHeapChainMapMock(); doThrow(OversizeMappingException.class).when(offHeapChainMap).getAndAppend(any(), any(ByteBuffer.class)); when(store.segmentFor(anyLong())).then(new Answer<Object>() { int invocations = 0; @Override public Object answer(InvocationOnMock invocation) throws Throwable { if (invocations++ < 10) { return offHeapChainMap; } else { return invocation.callRealMethod(); } } }); when(store.handleOversizeMappingException(anyLong())).thenReturn(true); ByteBuffer payload = createPayload(1L); store.getAndAppend(1L, payload); assertThat(payload.remaining(), is(8)); Chain expected = newChainBuilder().build(newElementBuilder().build(payload), newElementBuilder().build(payload)); Chain update = newChainBuilder().build(newElementBuilder().build(payload)); store.replaceAtHead(1L, expected, update); assertThat(payload.remaining(), is(8)); } @Test public void test_replaceAtHead_doesNotConsumeBuffer_evenWhenOversizeMappingException() throws Exception { OffHeapServerStore store = (OffHeapServerStore) spy(newStore()); final OffHeapChainMap<Object> offHeapChainMap = getOffHeapChainMapMock(); doThrow(OversizeMappingException.class).when(offHeapChainMap).replaceAtHead(any(), any(Chain.class), any(Chain.class)); when(store.segmentFor(anyLong())).then(new Answer<Object>() { int invocations = 0; @Override public Object answer(InvocationOnMock invocation) throws Throwable { if (invocations++ < 10) { return offHeapChainMap; } else { return invocation.callRealMethod(); } } }); when(store.handleOversizeMappingException(anyLong())).thenReturn(true); ByteBuffer payload = createPayload(1L); Chain expected = newChainBuilder().build(newElementBuilder().build(payload), newElementBuilder().build(payload)); Chain update = newChainBuilder().build(newElementBuilder().build(payload)); store.replaceAtHead(1L, expected, update); assertThat(payload.remaining(), is(8)); } @Test public void testCrossSegmentShrinking() { long seed = System.nanoTime(); Random random = new Random(seed); try { OffHeapServerStore store = new OffHeapServerStore(new UpfrontAllocatingPageSource(new OffHeapBufferSource(), MEGABYTES.toBytes(1L), MEGABYTES.toBytes(1)), DEFAULT_MAPPER); ByteBuffer smallValue = ByteBuffer.allocate(1024); for (int i = 0; i < 10000; i++) { try { store.getAndAppend(random.nextInt(500), smallValue.duplicate()); } catch (OversizeMappingException e) { //ignore } } ByteBuffer largeValue = ByteBuffer.allocate(100 * 1024); for (int i = 0; i < 10000; i++) { try { store.getAndAppend(random.nextInt(500), largeValue.duplicate()); } catch (OversizeMappingException e) { //ignore } } } catch (Throwable t) { throw (AssertionError) new AssertionError("Failed with seed " + seed).initCause(t); } } @Test public void testServerSideUsageStats() { long maxBytes = MEGABYTES.toBytes(1); OffHeapServerStore store = new OffHeapServerStore(new UpfrontAllocatingPageSource(new OffHeapBufferSource(), maxBytes, MEGABYTES.toBytes(1)), new KeySegmentMapper(16)); int oneKb = 1024; long smallLoopCount = 5; ByteBuffer smallValue = ByteBuffer.allocate(oneKb); for (long i = 0; i < smallLoopCount; i++) { store.getAndAppend(i, smallValue.duplicate()); } Assert.assertThat(store.getAllocatedMemory(),lessThanOrEqualTo(maxBytes)); Assert.assertThat(store.getAllocatedMemory(),greaterThanOrEqualTo(smallLoopCount * oneKb)); Assert.assertThat(store.getAllocatedMemory(),greaterThanOrEqualTo(store.getOccupiedMemory())); //asserts above already guarantee that occupiedMemory <= maxBytes and that occupiedMemory <= allocatedMemory Assert.assertThat(store.getOccupiedMemory(),greaterThanOrEqualTo(smallLoopCount * oneKb)); Assert.assertThat(store.getSize(), is(smallLoopCount)); int multiplier = 100; long largeLoopCount = 5 + smallLoopCount; ByteBuffer largeValue = ByteBuffer.allocate(multiplier * oneKb); for (long i = smallLoopCount; i < largeLoopCount; i++) { store.getAndAppend(i, largeValue.duplicate()); } Assert.assertThat(store.getAllocatedMemory(),lessThanOrEqualTo(maxBytes)); Assert.assertThat(store.getAllocatedMemory(),greaterThanOrEqualTo( (smallLoopCount * oneKb) + ( (largeLoopCount - smallLoopCount) * oneKb * multiplier) )); Assert.assertThat(store.getAllocatedMemory(),greaterThanOrEqualTo(store.getOccupiedMemory())); //asserts above already guarantee that occupiedMemory <= maxBytes and that occupiedMemory <= allocatedMemory Assert.assertThat(store.getOccupiedMemory(),greaterThanOrEqualTo(smallLoopCount * oneKb)); Assert.assertThat(store.getSize(), is(smallLoopCount + (largeLoopCount - smallLoopCount))); } }