From b2c3f222252243b1ee079aba27922ba1a40d050a Mon Sep 17 00:00:00 2001 From: Chris Dennis Date: Fri, 18 Mar 2016 14:35:38 -0400 Subject: [PATCH] Issue #50 Implement offheap-resource provider --- offheap-resource/pom.xml | 6 + .../offheapresource/OffHeapResource.java | 93 ++++++++++++++ .../OffHeapResourceIdentifier.java | 56 ++++++++ .../OffHeapResourcesProvider.java | 98 +++++++++++--- .../offheapresource/PhysicalMemory.java | 82 ++++++++++++ .../OffHeapResourceIdentifierTest.java | 53 ++++++++ .../offheapresource/OffHeapResourceTest.java | 91 +++++++++++++ .../OffHeapResourcesProviderTest.java | 120 ++++++++++++++++++ 8 files changed, 582 insertions(+), 17 deletions(-) create mode 100644 offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResource.java create mode 100644 offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourceIdentifier.java create mode 100644 offheap-resource/src/main/java/org/terracotta/offheapresource/PhysicalMemory.java create mode 100644 offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceIdentifierTest.java create mode 100644 offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceTest.java create mode 100644 offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourcesProviderTest.java diff --git a/offheap-resource/pom.xml b/offheap-resource/pom.xml index f0482ff71d..9011ddad39 100644 --- a/offheap-resource/pom.xml +++ b/offheap-resource/pom.xml @@ -12,6 +12,12 @@ offheap-resource + + + org.slf4j + slf4j-api + 1.7.18 + org.terracotta.internal tc-config-parser diff --git a/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResource.java b/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResource.java new file mode 100644 index 0000000000..bafe4ac843 --- /dev/null +++ b/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResource.java @@ -0,0 +1,93 @@ +/* + * The contents of this file are subject to the Terracotta Public 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://terracotta.org/legal/terracotta-public-license. + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for + * the specific language governing rights and limitations under the License. + * + * The Covered Software is OffHeap Resource. + * + * The Initial Developer of the Covered Software is + * Terracotta, Inc., a Software AG company + */ + +package org.terracotta.offheapresource; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Represents an offheap resource, providing a reservation system that can be + * used to control the combined memory usage of participating consumers. + *

+ * Reservation and release calls perform no allocations, and therefore rely on + * the cooperation of callers to achieve control over the 'real' resource usage. + */ +public class OffHeapResource { + + private final AtomicLong remaining; + + /** + * Creates a resource of the given initial size. + * + * @param size size of the resource + * @throws IllegalArgumentException if the size is negative + */ + OffHeapResource(long size) throws IllegalArgumentException { + if (size < 0) { + throw new IllegalArgumentException("Resource size cannot be negative"); + } else { + this.remaining = new AtomicLong(size); + } + } + + /** + * Reserves the given amount of this resource. + *

+ * This method performs no allocation. It is simply a reservation + * that the consumer agrees to bind by the result of. A {@code false} return + * should mean the caller refrains from performing any associated allocation. + * + * @param size reservation size + * @return {@code true} if the reservation succeeded + * @throws IllegalArgumentException if the reservation size is negative + */ + public boolean reserve(long size) throws IllegalArgumentException { + if (size < 0) { + throw new IllegalArgumentException("Reservation size cannot be negative"); + } else { + for (long current = remaining.get(); current >= size; current = remaining.get()) { + if (remaining.compareAndSet(current, current - size)) { + return true; + } + } + return false; + } + } + + /** + * Releases the given amount of resource back to this pool. + * + * @param size release size + * @throws IllegalArgumentException if the release size is negative + */ + public void release(long size) throws IllegalArgumentException { + if (size < 0) { + throw new IllegalArgumentException("Released size cannot be negative"); + } else { + remaining.addAndGet(size); + } + } + + /** + * Returns the size of the remaining resource that can be reserved. + * + * @return the remaining resource size + */ + public long available() { + return remaining.get(); + } +} diff --git a/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourceIdentifier.java b/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourceIdentifier.java new file mode 100644 index 0000000000..64ca428e7e --- /dev/null +++ b/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourceIdentifier.java @@ -0,0 +1,56 @@ +/* + * The contents of this file are subject to the Terracotta Public 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://terracotta.org/legal/terracotta-public-license. + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for + * the specific language governing rights and limitations under the License. + * + * The Covered Software is OffHeap Resource. + * + * The Initial Developer of the Covered Software is + * Terracotta, Inc., a Software AG company + */ + +package org.terracotta.offheapresource; + +import org.terracotta.entity.ServiceConfiguration; + +/** + * + * @author cdennis + */ +public final class OffHeapResourceIdentifier implements ServiceConfiguration { + + private final String name; + + public static OffHeapResourceIdentifier identifier(String name) { + return new OffHeapResourceIdentifier(name); + } + + private OffHeapResourceIdentifier(String name) { + if (name == null) { + throw new NullPointerException("Name cannot be null"); + } else { + this.name = name; + } + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof OffHeapResourceIdentifier) && name.equals(((OffHeapResourceIdentifier) obj).name); + } + + @Override + public Class getServiceType() { + return OffHeapResource.class; + } +} diff --git a/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourcesProvider.java b/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourcesProvider.java index 280ac7fc5d..f4273132f2 100644 --- a/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourcesProvider.java +++ b/offheap-resource/src/main/java/org/terracotta/offheapresource/OffHeapResourcesProvider.java @@ -1,45 +1,109 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * The contents of this file are subject to the Terracotta Public 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://terracotta.org/legal/terracotta-public-license. + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for + * the specific language governing rights and limitations under the License. + * + * The Covered Software is OffHeap Resource. + * + * The Initial Developer of the Covered Software is + * Terracotta, Inc., a Software AG company */ + package org.terracotta.offheapresource; -import java.io.IOException; +import java.math.BigInteger; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.terracotta.entity.ServiceConfiguration; import org.terracotta.entity.ServiceProvider; -import org.terracotta.entity.ServiceProviderCleanupException; import org.terracotta.entity.ServiceProviderConfiguration; +import org.terracotta.offheapresource.config.MemoryUnit; +import org.terracotta.offheapresource.config.ResourceType; /** - * - * @author cdennis + * A provider of {@link OffHeapResource} instances. + *

+ * This service allows for the configuration of a multitude of virtual offheap + * resource pools from which participating entities can reserve space. This + * allows for the partitioning and control of memory usage by entities + * consuming this service. */ -class OffHeapResourcesProvider implements ServiceProvider { +public class OffHeapResourcesProvider implements ServiceProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(OffHeapResourcesProvider.class); + + private final Map resources = new HashMap(); @Override - public boolean initialize(ServiceProviderConfiguration configuration) { - throw new UnsupportedOperationException(); + public synchronized boolean initialize(ServiceProviderConfiguration unknownConfig) { + if (unknownConfig instanceof OffHeapResourcesConfiguration) { + OffHeapResourcesConfiguration configuration = (OffHeapResourcesConfiguration) unknownConfig; + if (resources.isEmpty()) { + long totalSize = 0; + for (ResourceType r : configuration.getResources()) { + long size = convert(r.getValue(), r.getUnit()).longValueExact(); + totalSize += size; + resources.put(OffHeapResourceIdentifier.identifier(r.getName()), new OffHeapResource(size)); + } + Long physicalMemory = PhysicalMemory.totalPhysicalMemory(); + if (physicalMemory != null && totalSize > physicalMemory) { + LOGGER.warn("More offheap configured than there is physical memory [{} > {}]", totalSize, physicalMemory); + } + return true; + } else { + throw new IllegalStateException("Resources already initialized"); + } + } else { + return false; + } } @Override - public T getService(long consumerID, ServiceConfiguration configuration) { - throw new UnsupportedOperationException(); + public T getService(long consumerID, ServiceConfiguration unknownConfiguration) { + if (unknownConfiguration instanceof OffHeapResourceIdentifier) { + OffHeapResourceIdentifier identifier = (OffHeapResourceIdentifier) unknownConfiguration; + return (T) identifier.getServiceType().cast(resources.get(identifier)); + } else { + throw new IllegalArgumentException("Unexpected configuration type " + unknownConfiguration.getClass()); + } } @Override public Collection> getProvidedServiceTypes() { - throw new UnsupportedOperationException(); + return Collections.>singleton(OffHeapResource.class); } @Override - public void close() throws IOException { - throw new UnsupportedOperationException(); + public void close() { + clear(); } @Override - public void clear() throws ServiceProviderCleanupException { - throw new UnsupportedOperationException(); + public void clear() { + resources.clear(); + } + + private static BigInteger convert(BigInteger value, MemoryUnit unit) { + switch (unit) { + case B: return value.shiftLeft(0); + case K_B: return value.shiftLeft(10); + case MB: return value.shiftLeft(20); + case GB: return value.shiftLeft(30); + case TB: return value.shiftLeft(40); + case PB: return value.shiftLeft(50); + } + throw new IllegalArgumentException("Unknown unit " + unit); } } diff --git a/offheap-resource/src/main/java/org/terracotta/offheapresource/PhysicalMemory.java b/offheap-resource/src/main/java/org/terracotta/offheapresource/PhysicalMemory.java new file mode 100644 index 0000000000..3ca8a9a06d --- /dev/null +++ b/offheap-resource/src/main/java/org/terracotta/offheapresource/PhysicalMemory.java @@ -0,0 +1,82 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.terracotta.offheapresource; + +import java.lang.management.ManagementFactory; +import java.lang.management.OperatingSystemMXBean; +import java.lang.reflect.InvocationTargetException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author cdennis + */ +class PhysicalMemory { + + private static final Logger LOGGER = LoggerFactory.getLogger(PhysicalMemory.class); + private static final OperatingSystemMXBean OS_BEAN = ManagementFactory.getOperatingSystemMXBean(); + + public static Long totalPhysicalMemory() { + return getAttribute("getTotalPhysicalMemorySize"); + } + + public static Long freePhysicalMemory() { + return getAttribute("getFreePhysicalMemorySize"); + } + + public static Long totalSwapSpace() { + return getAttribute("getTotalSwapSpaceSize"); + } + + public static Long freeSwapSpace() { + return getAttribute("getFreeSwapSpaceSize"); + } + + public static Long ourCommittedVirtualMemory() { + return getAttribute("getCommittedVirtualMemorySize"); + } + + private static T getAttribute(String name) { + LOGGER.trace("Bean lookup for {}", name); + for (Class s = OS_BEAN.getClass(); s != null; s = s.getSuperclass()) { + try { + T result = (T) s.getMethod(name).invoke(OS_BEAN); + LOGGER.trace("Bean lookup successful using {}, got {}", s, result); + return result; + } catch (SecurityException e) { + LOGGER.trace("Bean lookup failed on {}", s, e); + } catch (NoSuchMethodException e) { + LOGGER.trace("Bean lookup failed on {}", s, e); + } catch (IllegalAccessException e) { + LOGGER.trace("Bean lookup failed on {}", s, e); + } catch (IllegalArgumentException e) { + LOGGER.trace("Bean lookup failed on {}", s, e); + } catch (InvocationTargetException e) { + LOGGER.trace("Bean lookup failed on {}", s, e); + } + } + for (Class i : OS_BEAN.getClass().getInterfaces()) { + try { + T result = (T) i.getMethod(name).invoke(OS_BEAN); + LOGGER.trace("Bean lookup successful using {}, got {}", i, result); + return result; + } catch (SecurityException e) { + LOGGER.trace("Bean lookup failed on {}", i, e); + } catch (NoSuchMethodException e) { + LOGGER.trace("Bean lookup failed on {}", i, e); + } catch (IllegalAccessException e) { + LOGGER.trace("Bean lookup failed on {}", i, e); + } catch (IllegalArgumentException e) { + LOGGER.trace("Bean lookup failed on {}", i, e); + } catch (InvocationTargetException e) { + LOGGER.trace("Bean lookup failed on {}", i, e); + } + } + LOGGER.trace("Returning null for {}", name); + return null; + } +} diff --git a/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceIdentifierTest.java b/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceIdentifierTest.java new file mode 100644 index 0000000000..662b28b41f --- /dev/null +++ b/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceIdentifierTest.java @@ -0,0 +1,53 @@ +/* + * The contents of this file are subject to the Terracotta Public 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://terracotta.org/legal/terracotta-public-license. + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for + * the specific language governing rights and limitations under the License. + * + * The Covered Software is OffHeap Resource. + * + * The Initial Developer of the Covered Software is + * Terracotta, Inc., a Software AG company + */ +package org.terracotta.offheapresource; + +import static org.hamcrest.core.Is.is; +import org.junit.Test; + +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +public class OffHeapResourceIdentifierTest { + + @Test + public void testNullName() { + try { + OffHeapResourceIdentifier.identifier(null); + fail("Expected NullPointerException"); + } catch (NullPointerException e) { + //expected + } + } + + @Test + public void testServiceType() { + assertThat(OffHeapResourceIdentifier.identifier("foo").getServiceType(), equalTo(OffHeapResource.class)); + } + + @Test + public void testEquals() { + assertThat(OffHeapResourceIdentifier.identifier("foo").equals(OffHeapResourceIdentifier.identifier("foo")), is(true)); + assertThat(OffHeapResourceIdentifier.identifier("foo").equals(OffHeapResourceIdentifier.identifier("bar")), is(false)); + } + + @Test + public void testHashcode() { + assertThat(OffHeapResourceIdentifier.identifier("foo").hashCode(), is(OffHeapResourceIdentifier.identifier("foo").hashCode())); + } +} diff --git a/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceTest.java b/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceTest.java new file mode 100644 index 0000000000..325517b250 --- /dev/null +++ b/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourceTest.java @@ -0,0 +1,91 @@ +/* + * The contents of this file are subject to the Terracotta Public 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://terracotta.org/legal/terracotta-public-license. + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for + * the specific language governing rights and limitations under the License. + * + * The Covered Software is OffHeap Resource. + * + * The Initial Developer of the Covered Software is + * Terracotta, Inc., a Software AG company + */ + +package org.terracotta.offheapresource; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import org.junit.Test; + +public class OffHeapResourceTest { + + @Test + public void testNegativeResourceSize() { + try { + new OffHeapResource(-1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + //expected; + } + } + + @Test + public void testZeroSizeResourceIsUseless() { + OffHeapResource ohr = new OffHeapResource(0); + assertThat(ohr.reserve(1), is(false)); + assertThat(ohr.available(), is(0L)); + } + + @Test + public void testAllocationReducesSize() { + OffHeapResource ohr = new OffHeapResource(20); + assertThat(ohr.available(), is(20L)); + assertThat(ohr.reserve(10), is(true)); + assertThat(ohr.available(), is(10L)); + } + + @Test + public void testNegativeAllocationFails() { + OffHeapResource ohr = new OffHeapResource(20); + try { + ohr.reserve(-1); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + //expected + } + } + + @Test + public void testAllocationWhenExhaustedFails() { + OffHeapResource ohr = new OffHeapResource(20); + ohr.reserve(20); + assertThat(ohr.reserve(1), is(false)); + assertThat(ohr.available(), is(0L)); + } + + @Test + public void testFreeIncreasesSize() { + OffHeapResource ohr = new OffHeapResource(20); + ohr.reserve(20); + assertThat(ohr.available(), is(0L)); + ohr.release(10); + assertThat(ohr.available(), is(10L)); + } + + @Test + public void testNegativeFreeFails() { + OffHeapResource ohr = new OffHeapResource(20); + ohr.reserve(10); + try { + ohr.release(-10); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + //expected + } + } +} diff --git a/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourcesProviderTest.java b/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourcesProviderTest.java new file mode 100644 index 0000000000..831ff6dc48 --- /dev/null +++ b/offheap-resource/src/test/java/org/terracotta/offheapresource/OffHeapResourcesProviderTest.java @@ -0,0 +1,120 @@ +/* + * The contents of this file are subject to the Terracotta Public 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://terracotta.org/legal/terracotta-public-license. + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for + * the specific language governing rights and limitations under the License. + * + * The Covered Software is OffHeap Resource. + * + * The Initial Developer of the Covered Software is + * Terracotta, Inc., a Software AG company + */ + +package org.terracotta.offheapresource; + +import java.math.BigInteger; +import java.util.Collections; +import org.junit.Test; +import org.terracotta.entity.ServiceProvider; +import org.terracotta.entity.ServiceProviderConfiguration; + +import org.terracotta.offheapresource.config.MemoryUnit; +import org.terracotta.offheapresource.config.ResourceType; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsNull.notNullValue; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class OffHeapResourcesProviderTest { + + @Test + public void testInitializeWithWrongConfig() { + OffHeapResourcesProvider provider = new OffHeapResourcesProvider(); + + assertThat(provider.initialize(new ServiceProviderConfiguration() { + @Override + public Class getServiceProviderType() { + return OffHeapResourcesProvider.class; + } + }), is(false)); + } + + @Test + public void testInitializeWithValidConfig() { + ResourceType resourceConfig = mock(ResourceType.class); + when(resourceConfig.getName()).thenReturn("foo"); + when(resourceConfig.getUnit()).thenReturn(MemoryUnit.MB); + when(resourceConfig.getValue()).thenReturn(BigInteger.valueOf(2)); + + OffHeapResourcesConfiguration config = mock(OffHeapResourcesConfiguration.class); + when(config.getResources()).thenReturn(Collections.singleton(resourceConfig)); + + OffHeapResourcesProvider provider = new OffHeapResourcesProvider(); + provider.initialize(config); + + assertThat(provider.getService(42L, OffHeapResourceIdentifier.identifier("foo")), notNullValue()); + assertThat(provider.getService(42L, OffHeapResourceIdentifier.identifier("foo")).available(), is(2L * 1024 * 1024)); + } + + @Test + public void testDoubleInitialize() { + ResourceType resourceConfig = mock(ResourceType.class); + when(resourceConfig.getName()).thenReturn("foo"); + when(resourceConfig.getUnit()).thenReturn(MemoryUnit.MB); + when(resourceConfig.getValue()).thenReturn(BigInteger.valueOf(2)); + + OffHeapResourcesConfiguration config = mock(OffHeapResourcesConfiguration.class); + when(config.getResources()).thenReturn(Collections.singleton(resourceConfig)); + + OffHeapResourcesProvider provider = new OffHeapResourcesProvider(); + provider.initialize(config); + try { + provider.initialize(config); + fail("Expected IllegalStateException"); + } catch (IllegalStateException e) { + //expected + } + } + + @Test + public void testNullReturnOnInvalidResource() { + ResourceType resourceConfig = mock(ResourceType.class); + when(resourceConfig.getName()).thenReturn("foo"); + when(resourceConfig.getUnit()).thenReturn(MemoryUnit.MB); + when(resourceConfig.getValue()).thenReturn(BigInteger.valueOf(2)); + + OffHeapResourcesConfiguration config = mock(OffHeapResourcesConfiguration.class); + when(config.getResources()).thenReturn(Collections.singleton(resourceConfig)); + + OffHeapResourcesProvider provider = new OffHeapResourcesProvider(); + provider.initialize(config); + + assertThat(provider.getService(42L, OffHeapResourceIdentifier.identifier("bar")), nullValue()); + } + + + @Test + public void testNullReturnAfterClose() { + ResourceType resourceConfig = mock(ResourceType.class); + when(resourceConfig.getName()).thenReturn("foo"); + when(resourceConfig.getUnit()).thenReturn(MemoryUnit.MB); + when(resourceConfig.getValue()).thenReturn(BigInteger.valueOf(2)); + + OffHeapResourcesConfiguration config = mock(OffHeapResourcesConfiguration.class); + when(config.getResources()).thenReturn(Collections.singleton(resourceConfig)); + + OffHeapResourcesProvider provider = new OffHeapResourcesProvider(); + provider.initialize(config); + provider.close(); + assertThat(provider.getService(42L, OffHeapResourceIdentifier.identifier("foo")), nullValue()); + } +}