-
Notifications
You must be signed in to change notification settings - Fork 683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
make ResourceManager's interaction with JVM customizable with the HeapUsageProvider SPI #7456
make ResourceManager's interaction with JVM customizable with the HeapUsageProvider SPI #7456
Conversation
if (builder.length() > 1) { | ||
builder.setLength(builder.length() - 2); | ||
private static HeapUsageMonitor findHeapUsageMonitor() { | ||
for (HeapUsageMonitor heapUsageMonitor : heapUsageMonitorLoader) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the implementation's constructor throws an exception, the service loader's hasNext()
and next()
will throw. If that happens, is it okay for the HeapUsageMonitor
constructor to throw, or do you want to catch it and fall back to the default heap usage monitor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I think we want to fail with an exception since this indicates they tried to specify a HeapUsageMonitor and its constructor failed. An exception thrown here will cause the cache initialization to fail which seems good.
stats are no longer used to detect memory changes. Instead it consistently uses notification from JMX and a polling thread. The polling thread used to be used only if stats were disabled. Also the polling thread now runs in the InternalResourceManager's scheduledExecutor instead of a new executor being created. The default number of threads in that executor has changed from 1 to 2. removed unneeded static fields from the HeapUsageProvider impl. Added a ServerLoader search for the HeapUsageProvider The TenuredHeapConsumptionMonitor is now part of the MemoryPoolMXBeanHeapUsageProvider internals instead of it being create in the InternalResourceManager.
de3c28a
to
4a5ae0f
Compare
…L system property since "usageThreshold" is no longer used it is no longer required "collectionUsageThreshold" support is now optional
On general comment before I dig in a bit deeper - it looks like this will only work correctly if there is only 1 implementation of this interface on the classpath? It might be better to have a way for a user to choose which implementation of HeapUsageMonitor they want - or perhaps even for the HeapUsageMonitor to itself decide if it is relevant for the current JVM settings? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given this adds a new public API, there should probably be a docs review of the wording and content of the javadocs on it. Also, it would be good to have some test coverage showing that a user can supply a non-default HeapUsageProvider
and have it be picked up and used by Geode.
try { | ||
emitter.removeNotificationListener( | ||
InternalResourceManager.getInternalResourceManager(cache).getHeapMonitor()); | ||
InternalResourceManager.getInternalResourceManager(cache).getHeapMonitor() | ||
.getHeapUsageMonitor()); | ||
assertTrue("Expected that the resource manager was not registered", false); | ||
} catch (ListenerNotFoundException ignored) { | ||
} catch (ListenerNotFoundException expected) { | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you're modifying this class, it would be much better to have this (and other similar code in this class) be an assertThatThrownBy()
rather than a try/catch with an "assert that true is false" to force the test to fail if we don't throw.
final int MAX_RESOURCE_MANAGER_EXE_THREADS = | ||
Integer.getInteger(GeodeGlossary.GEMFIRE_PREFIX + "resource.manager.threads", 1); | ||
private final int MAX_RESOURCE_MANAGER_EXE_THREADS = | ||
Integer.getInteger(GeodeGlossary.GEMFIRE_PREFIX + "resource.manager.threads", 2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this default value being increased?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The poller thread used to create its own ScheduledPoolExecutor and now it uses this existing one on the InternalResourceManager. I changed the default number of threads from 1 to 2 to decrease the changes that the poller would be stuck behind other operations that use this executor. We could go back to having a dedicated executor.
geode-core/src/main/java/org/apache/geode/cache/control/HeapUsageProvider.java
Outdated
Show resolved
Hide resolved
...re/src/distributedTest/java/org/apache/geode/cache/management/MemoryThresholdsDUnitTest.java
Show resolved
Hide resolved
...grationTest/java/org/apache/geode/internal/statistics/GemFireStatSamplerIntegrationTest.java
Show resolved
Hide resolved
private final Supplier<List<MemoryPoolMXBean>> memoryPoolSupplier; | ||
private final Supplier<NotificationEmitter> notificationEmitterSupplier; | ||
private final TenuredHeapConsumptionMonitor tenuredHeapConsumptionMonitor; | ||
// JVM MXBean used to report changes in heap memory usage | ||
private final MemoryPoolMXBean tenuredMemoryPoolMXBean; | ||
// Calculated value for the amount of JVM tenured heap memory available. | ||
private final long tenuredPoolMaxMemory; | ||
|
||
private volatile LongConsumer heapUsageListener = disabledHeapUsageListener; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just personal preference, but could these fields be moved to be above the private static methods please? That seems to be the approach we're following elsewhere in the codebase.
@upthewaterspout Geode's ResourceManager is a singleton and it will only have one instance of HeapUsageProvider. Geode has an implementation of HeapUsageProvider on the class path but it is not a service. Geode's instance handles all the JVMs and GCs we support. The only reason for defining a service that implements HeapUsageProvider is if you want to replace geode's built in one. So at that point you will have two on the class path and only the service one will be used. |
@Immutable | ||
private static final ServiceLoader<HeapUsageProvider> heapUsageMonitorLoader = | ||
ServiceLoader.load(HeapUsageProvider.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a novel approach to service loading. Why would we have the ServiceLoading on static initialization of the class but the looking up of the implementation only at construction time of the HeapMemoryMonitor? Given that you limit it to the first implementation found AND on construction time of the HeapMemoryMonitor, I believe all this code could be done in the same location. i.e in the findHeapUsageMonitor method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still not sure we should even use the ServiceLoader for this since it supports multiple instances and we only want one. Would a system property that they set to the class name of their implementation be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a ServiceLoader works perfectly well. Having an SPI can lead to potentially having multiple on the classpath. A thought could be that one could introduce the idea of "loadWithIdentifier"... Similarly to how SSL works. There are multiple implementations on the classpath and one needs to specify which one to load. I think this could support us rather well, if we want to support different GC algorithms. Thoughts?
public static MemoryPoolMXBean getTenuredMemoryPoolMXBean() { | ||
if (tenuredMemoryPoolMXBean != null) { | ||
return tenuredMemoryPoolMXBean; | ||
private static HeapUsageProvider findHeapUsageMonitor() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we possibly change this to a "getHeapUsageMonitor"... It is not really a "find" method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really like "get" either since to me that makes it seem like it is returning a value that already exists. How about "createHeapUsageProvider"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we take a leaf out of the ServiceLoaders and call it "load"...
|
||
HeapMemoryMonitor(final InternalResourceManager resourceManager, final InternalCache cache, | ||
final ResourceManagerStats stats, | ||
TenuredHeapConsumptionMonitor tenuredHeapConsumptionMonitor) { | ||
final ResourceManagerStats stats) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would we not pass in the correct HeapUsageProvider as a parameter to the constructor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should have a constructor that takes a HeapUsageProvider for unit testing this class.
Is that all you are after?
I struggle with shifting the knowledge of how to create an instance of HeapUsageProvider to the class that wants to create a HeapMemoryMonitor. The only class that needs to interact with the HeapUsageProvider is the HeapMemoryMonitor so it seems like it would be the class that should know how to create one. Or we need to introduce another class that is a HeapUsageProviderFactory. @kohlmu-pivotal Do you have any advice on what would be best?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I was asking why there is no constructor that takes a HeapUsageProvider.
Also, I think having a HeapUsageProviderFactory is a good idea. One could even make that Factory package-private. That will then help with "containing" it within a "context"
geode-core/src/main/java/org/apache/geode/internal/cache/control/HeapMemoryMonitor.java
Show resolved
Hide resolved
geode-core/src/main/java/org/apache/geode/internal/cache/control/HeapMemoryMonitor.java
Show resolved
Hide resolved
geode-core/src/main/java/org/apache/geode/internal/cache/control/HeapMemoryMonitor.java
Outdated
Show resolved
Hide resolved
The biggest change is to no longer use the ServiceLoader. Instead users specificy their HeapUsageProvider class with a system property.
private static HeapUsageProvider createHeapUsageProvider() { | ||
Optional<String> propertyValue = getProductStringProperty(HEAP_USAGE_PROVIDER_CLASS_NAME); | ||
if (propertyValue.isPresent()) { | ||
String className = propertyValue.get(); | ||
Class<?> clazz; | ||
try { | ||
clazz = ClassPathLoader.getLatest().forName(className); | ||
} catch (Exception ex) { | ||
throw new IllegalArgumentException("Could not load class \"" + className + | ||
"\" for system property \"geode." + HEAP_USAGE_PROVIDER_CLASS_NAME + "\"", ex); | ||
} | ||
Object instance; | ||
try { | ||
instance = clazz.newInstance(); | ||
} catch (Exception ex) { | ||
throw new IllegalArgumentException("Could not create an instance of class \"" + className + | ||
"\" for system property \"geode." + HEAP_USAGE_PROVIDER_CLASS_NAME + | ||
"\". Make sure it has a public zero-arg constructor.", ex); | ||
} | ||
if (!(instance instanceof HeapUsageProvider)) { | ||
throw new IllegalArgumentException("The class \"" + className + | ||
"\" for system property \"geode." + HEAP_USAGE_PROVIDER_CLASS_NAME + | ||
"\" is not an instance of HeapUsageProvider."); | ||
} | ||
return (HeapUsageProvider) instance; | ||
} else { | ||
return new MemoryPoolMXBeanHeapUsageProvider(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two issues with this approach:
- What happened to the idea of a Factory?
- What happened to the idea of Service loading?
Generally we should avoid doing anything directly Classloader and reflection based work like this. Directly interacting with the Classloader just introduces unwarranted complexity. I cannot see a real benefit of doing this compared to ServiceLoader. But I can think of a few downsides.
Any chance this can be improved by creating a Factory and passing in the HeapUsageProvider into the HeapMemoryMonitor, at construction time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The benefit is that ServiceLoader was not designed to load a singleton. If we used it then the user needs to not only implement the class but also define it as a service (one more file to edit) and we have to add some way for them to specify which of the services that implement HeapUsageProvider they want to use. So that ends up being a sys prop they need to define to signal the provider they want. It might also mean adding something like a "getName" to the interface. So these two solutions have much in common for the user (i.e. implement a class with a zero-arg constructor, get it on the class path, designate your class with a sys prop) but the service solution has the one additional thing to do (i.e. define it as a service). So from the user's point of view the service solution is more complex.
For the implementation geode already has the code to do this that it uses for other classes the user can plugin so it was very easy to implement and this code is well tested. I also liked that it was easy to see all the possible failure conditions and give an exception message that will give our users meaningful context about what went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code could easily be moved into a factory
geode-core/src/main/java/org/apache/geode/internal/cache/control/HeapMemoryMonitor.java
Show resolved
Hide resolved
public NotificationListener getHeapUsageMonitor() { | ||
return (MemoryPoolMXBeanHeapUsageProvider) heapUsageMonitor; | ||
public NotificationListener getHeapUsageProvider() { | ||
return (MemoryPoolMXBeanHeapUsageProvider) heapUsageProvider; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this not be cast to a NotificationListener?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method only exists to get an existing test: MemoryMonitorIntegrationTest to work.
But I think getHeapUsageProvider will go away in the near future. This level of detail should be tested by a unit test on MemoryPoolMXBeanHeapUsageProvider not this integration test.
Overall this Draft PR proves that it is possible to have a pluggable implementation of a resource manager implementation depending on architecture and JDK. But overall I think there needs to be some work around the design of the solution. Going forward I expect some form of RFC to describe the new feature, it's impacts and design. |
@@ -332,12 +331,7 @@ public void setLowMemory(final ScheduledThreadPoolExecutor executor, | |||
final boolean isLowMemory, | |||
final long usedBytes, | |||
final InternalCache cache) { | |||
if (cache.isQueryMonitorDisabledForLowMemory()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this be new behavior that we can set the low memory even if the query monitor is disabled?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to clean some of the dependencies the InternalResourceManager had with the InternalCache and thought it was safe to remove this call because I thought if it was disabled then getQueryMonitor would return null. But I was wrong. Even though it was disabled for low memory monitoring it could still exist because of MAX_QUERY_EXECUTION_TIME being > 0. And making this change broke ResourceManagerWithQueryMonitorDUnitTest. I just pushed a new revision that fixes this test but instead of always asking the cache if it is disabled, QueryMonitor is now told at construction time.
…rDUnitTest it is now fixed
added close method to HeapUsageProvider removed IllegalStateException from HeapUsageProvider
This PR introduces a new external API interface: HeapUsageProvider.
Users can create a class that implements the HeapUsageProvider and configure it as a standard Java service (see java.util.ServiceLoader).
The ResourceManager will use the HeapUsageProvider to enforce it's eviction-threshold and critical-threshold.
The HeapUsageProvider provides geode with two pieces of information:
The HeapUsageProvider must return a reasonable value when getMaxMemory and getBytesUsed are called. It can also optionally implement notifications that it sends to a listener when it detects a change in used memory.
Geode will look for a service that implements HeapUsageProvider during Cache creation. If it finds one and it throws an exception during construction then this will cause the geode Cache to fail its initialization with the thrown exception. If it does not find one then it will use its own default implementation: MemoryPoolMXBeanHeapUsageProvider. This provider works the same as previous releases of geode.
For all changes:
Is there a JIRA ticket associated with this PR? Is it referenced in the commit message?
Has your PR been rebased against the latest commit within the target branch (typically
develop
)?Is your initial contribution a single, squashed commit?
Does
gradlew build
run cleanly?Have you written or updated unit tests to verify your changes?
If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under ASF 2.0?