RE-using JMXConnectors as much as possible.

This commit is contained in:
Kris De Volder
2018-07-13 15:20:17 -07:00
parent 1354e1f3fc
commit c1c46a3709
8 changed files with 239 additions and 64 deletions

View File

@@ -14,7 +14,6 @@ import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.PlatformManagedObject;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
@@ -40,10 +39,15 @@ import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansMod
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.Boot1xRequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMappingsParser20;
import org.springframework.ide.vscode.commons.util.MemoizingDisposableSupplier;
import org.springframework.ide.vscode.commons.util.ExceptionUtil;
import org.springframework.ide.vscode.commons.util.FuctionWithException;
import org.springframework.ide.vscode.commons.util.FunctionWithException;
import org.springframework.ide.vscode.commons.util.StringUtil;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@@ -60,14 +64,14 @@ public abstract class AbstractSpringBootApp implements SpringBootApp {
private String jmxMbeanActuatorDomain;
// NOTE: Gson-based serialisation replaces the old Jackson ObjectMapper. Not sure if this makes a difference in the long run, but to retain the same output that Jackson Object Mapper
// was generating during serialisatino, some configuration in Gson is required, as the default behaviour of Gson is different than Object Mapper.
// was generating during serialisation, some configuration in Gson is required, as the default behaviour of Gson is different than Object Mapper.
// Namely: Object Mapper does not escape Html, whereas Gson does by default (for example
// '=' in Gson appears as '\u003d')
protected final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.create();
protected abstract JMXServiceURL getJmxUrl() throws MalformedURLException;
protected abstract String getJmxUrl();
@Override
public abstract Properties getSystemProperties() throws Exception;
@@ -79,11 +83,42 @@ public abstract class AbstractSpringBootApp implements SpringBootApp {
@Override
public abstract boolean isSpringBootApp();
protected <T> T withJmxConnector(FunctionWithException<JMXConnector, T> doit) throws Exception {
JMXServiceURL serviceUrl = getJmxUrl();
try (JMXConnector jmxConnector = JMXConnectorFactory.connect(serviceUrl, null)) {
return doit.apply(jmxConnector);
private final MemoizingDisposableSupplier<JMXConnector> jmxConnector = new MemoizingDisposableSupplier<JMXConnector>(
//creating jmx connector:
() -> {
String url = getJmxUrl();
logger.info("Creating JMX connector: "+url);
try {
return JMXConnectorFactory.connect(new JMXServiceURL(url), null);
} catch (Exception e) {
logger.info("Creating JMX connector failed: {}", ExceptionUtil.getMessage(e));
throw e;
}
},
//disposing jmx connector:
(connector) -> {
try {
logger.info("Disposing JMX connector: "+getJmxUrl());
connector.close();
} catch (IOException e) {
//ignore
}
}
);
protected <T> T withJmxConnector(FunctionWithException<JMXConnector, T> doit) throws Exception {
try {
return doit.apply(jmxConnector.get());
} catch (Exception e) {
logger.info("Evicting JMX connector {} because of error: {}", getJmxUrl(), ExceptionUtil.getMessage(e));
jmxConnector.evict();
throw e;
}
}
@Override
public void dispose() {
jmxConnector.dispose();
}
@Override
@@ -247,27 +282,28 @@ public abstract class AbstractSpringBootApp implements SpringBootApp {
protected Object getActuatorDataFromAttribute(ObjectName objectName, String attribute) throws Exception {
if (objectName != null) {
try {
return withJmxConnector(jmxConnector -> {
return withJmxConnector(jmxConnector -> {
try {
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
return connection.getAttribute(objectName, "Data");
});
} catch (InstanceNotFoundException e) {
}
} catch (InstanceNotFoundException e) {
return null;
}
});
}
return null;
}
protected Object getActuatorDataFromOperation(ObjectName objectName, String operation) throws Exception {
if (objectName != null) {
try {
return withJmxConnector(jmxConnector -> {
return withJmxConnector(jmxConnector -> {
try {
MBeanServerConnection connection = jmxConnector.getMBeanServerConnection();
return connection.invoke(objectName, operation, null, null);
});
}
catch (InstanceNotFoundException e) {
}
} catch (InstanceNotFoundException e) {
return null;
}
});
}
return null;
}
@@ -312,11 +348,6 @@ public abstract class AbstractSpringBootApp implements SpringBootApp {
@Override
public String getJavaCommand() throws Exception {
Properties props = getSystemProperties();
System.err.println(">>>>> sysprops");
for (Entry<Object, Object> e : props.entrySet()) {
System.err.println(e.getKey() +"="+e.getValue());
}
System.err.println("<<<< sysprops");
return (String) props.get("sun.java.command");
}
@@ -324,8 +355,12 @@ public abstract class AbstractSpringBootApp implements SpringBootApp {
public String getHost() throws Exception {
//TODO: different implementation for cf apps with locally tunnelled
// jmx connection?
JMXServiceURL serviceUrl = getJmxUrl();
return serviceUrl.getHost();
try {
JMXServiceURL serviceUrl = new JMXServiceURL(getJmxUrl());
return serviceUrl.getHost();
} catch (Exception e) {
return "Unknown host";
}
}
@Override

View File

@@ -11,19 +11,14 @@
package org.springframework.ide.vscode.commons.boot.app.cli;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.Properties;
import javax.management.remote.JMXServiceURL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.util.CollectorUtil;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
@@ -42,7 +37,24 @@ public class LocalSpringBootApp extends AbstractSpringBootApp {
private Boolean isSpringBootApp;
private final Supplier<String> jmxConnect = Suppliers.memoize(() -> {
private static LocalSpringBootAppCache cache = new LocalSpringBootAppCache();
public static Collection<SpringBootApp> getAllRunningJavaApps() throws Exception {
return cache.getAllRunningJavaApps();
}
public static Collection<SpringBootApp> getAllRunningSpringApps() throws Exception {
return getAllRunningJavaApps().stream().filter(SpringBootApp::isSpringBootApp).collect(CollectorUtil.toImmutableList());
}
public LocalSpringBootApp(VirtualMachineDescriptor vmd) throws AttachNotSupportedException, IOException {
this.vm = VirtualMachine.attach(vmd);
this.vmd = vmd;
}
@Override
protected String getJmxUrl() {
String address = null;
try {
address = vm.getAgentProperties().getProperty(LOCAL_CONNECTOR_ADDRESS);
@@ -57,22 +69,10 @@ public class LocalSpringBootApp extends AbstractSpringBootApp {
}
}
return address;
});
private static LocalSpringBootAppCache cache = new LocalSpringBootAppCache();
public static Collection<SpringBootApp> getAllRunningJavaApps() throws Exception {
return cache.getAllRunningJavaApps();
}
public static Collection<SpringBootApp> getAllRunningSpringApps() throws Exception {
return getAllRunningJavaApps().stream().filter(SpringBootApp::isSpringBootApp).collect(CollectorUtil.toImmutableList());
}
public LocalSpringBootApp(VirtualMachineDescriptor vmd) throws AttachNotSupportedException, IOException {
this.vmd = vmd;
this.vm = VirtualMachine.attach(vmd);
}
// Supplier<String> jmxConnectUrl = Suppliers.memoize(() -> {
// });
@Override
public String getProcessID() {
@@ -122,11 +122,6 @@ public class LocalSpringBootApp extends AbstractSpringBootApp {
return props.containsKey(key);
}
@Override
protected JMXServiceURL getJmxUrl() throws MalformedURLException {
return new JMXServiceURL(jmxConnect.get());
}
protected boolean contains(String[] cpElements, String element) {
for (String cpElement : cpElements) {
if (cpElement.contains(element)) {
@@ -162,6 +157,7 @@ public class LocalSpringBootApp extends AbstractSpringBootApp {
System.out.println("}");
}
@Override
public void dispose() {
if (vm!=null) {
logger.info("SpringBootApp disposed: "+this);
@@ -174,5 +170,6 @@ public class LocalSpringBootApp extends AbstractSpringBootApp {
if (vmd!=null) {
vmd = null;
}
super.dispose();
}
}

View File

@@ -12,13 +12,10 @@ package org.springframework.ide.vscode.commons.boot.app.cli;
import java.io.IOException;
import java.lang.management.RuntimeMXBean;
import java.net.MalformedURLException;
import java.time.Duration;
import java.util.Map.Entry;
import java.util.Properties;
import javax.management.remote.JMXServiceURL;
import org.springframework.ide.vscode.commons.util.MemoizingProxy;
public class RemoteSpringBootApp extends AbstractSpringBootApp {
@@ -30,8 +27,8 @@ public class RemoteSpringBootApp extends AbstractSpringBootApp {
}
@Override
protected JMXServiceURL getJmxUrl() throws MalformedURLException {
return new JMXServiceURL(jmxUrl);
protected String getJmxUrl() {
return jmxUrl;
}
@Override

View File

@@ -18,7 +18,9 @@ import java.util.Properties;
import org.springframework.ide.vscode.commons.boot.app.cli.livebean.LiveBeansModel;
import org.springframework.ide.vscode.commons.boot.app.cli.requestmappings.RequestMapping;
public interface SpringBootApp {
import reactor.core.Disposable;
public interface SpringBootApp extends Disposable {
String[] getClasspath() throws Exception;
String getJavaCommand() throws Exception;

View File

@@ -118,7 +118,10 @@ public class ExceptionUtil {
);
}
public static RuntimeException unchecked(Exception e) {
public static RuntimeException unchecked(Throwable e) {
if (e instanceof RuntimeException) {
return (RuntimeException)e;
}
return new RuntimeException(e);
}

View File

@@ -0,0 +1,138 @@
/*******************************************************************************
* Copyright (c) 2018 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.commons.util;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.FinalizableReference;
import com.google.common.base.Supplier;
import reactor.core.Disposable;
/**
* Wraps a Callable with memoization and logic that allows
* proper cleanup of the memoized value.
* <p>
* Both real results and thrown exceptions are memoized.
*/
public class MemoizingDisposableSupplier<T> implements Supplier<T>, Disposable {
private static Logger logger = LoggerFactory.getLogger(MemoizingDisposableSupplier.class);
private T value;
private Throwable failure;
private Callable<T> computer;
private Long lastComputed;
private Long expireExceptionsAfter = null;
private Consumer<T> disposeWith;
public MemoizingDisposableSupplier(Callable<T> computer, Consumer<T> disposeWith) {
this.computer = computer;
this.disposeWith = disposeWith;
}
public synchronized void evict() {
T oldValue = value;
if (oldValue!=null) {
disposeWith.accept(oldValue);
}
value = null;
failure = null;
lastComputed = null;
}
@Override
public void dispose() {
boolean shouldDispose;
synchronized (this) {
shouldDispose = computer!=null;
computer = null;
}
if (shouldDispose) {
if (disposeWith!=null && value!=null) {
disposeWith.accept(value);
}
value = null;
failure = null;
lastComputed = null;
disposeWith = null;
}
}
@Override
public synchronized T get() {
Assert.isLegal(!isDisposed());
if (shouldCompute()) {
lastComputed = System.currentTimeMillis();
T oldValue = value;
if (oldValue instanceof Disposable) {
((Disposable) oldValue).dispose();
}
try {
value = computer.call();
failure = null;
} catch (Throwable e) {
value = null;
failure = e;
}
}
if (failure!=null) {
throw ExceptionUtil.unchecked(failure);
} else {
return value;
}
}
@Override
public boolean isDisposed() {
return computer==null;
}
private boolean shouldCompute() {
if (lastComputed==null) {
//Never computed
return true;
} else {
//Computed before... should check expiration
if (failure!=null) {
// cached result is a exception
return expireExceptionsAfter!=null &&
System.currentTimeMillis() - lastComputed >= expireExceptionsAfter;
} else {
// cached result is a normal value
return false; //for now normal values never expire.
}
}
}
public MemoizingDisposableSupplier<T> expireExceptions(Duration after) {
this.expireExceptionsAfter = after.toMillis();
return this;
}
@Override
protected void finalize() throws Throwable {
//TODO: consider removing this method when we have confidence we have no leaks that need cleaning up.
// Using finalizers is expensive so should be avoided. This is only here as a temporary fail-safe.
// Proper cleanup should be implemented and logger.error below should never be reached.
if (!isDisposed()) {
logger.error("Leaked Disposable detected: "+this);
this.dispose();
}
}
}

View File

@@ -225,17 +225,17 @@ public class BootJavaHoverProvider implements HoverHandler {
private Hover provideHoverForAnnotation(ASTNode exactNode, Annotation annotation, int offset, TextDocument doc, IJavaProject project) {
ITypeBinding type = annotation.resolveTypeBinding();
if (type != null) {
logger.info("Hover requested for "+type.getName());
logger.debug("Hover requested for "+type.getName());
SpringBootApp[] runningApps = getRunningSpringApps(project);
if (runningApps.length > 0) {
for (HoverProvider provider : this.hoverProviders.get(type)) {
Hover hover = provider.provideHover(exactNode, annotation, type, offset, doc, project, runningApps);
if (hover!=null) {
logger.info("Hover found: "+hover);
logger.debug("Hover found: "+hover);
//TODO: compose multiple hovers somehow instead of just returning the first one?
return hover;
}
logger.info("NO Hover!");
logger.debug("NO Hover!");
}
//Only reaching here if we didn't get a hover.
if (!hasActuatorDependency(project)) {

View File

@@ -14,6 +14,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.slf4j.Logger;
@@ -47,12 +48,14 @@ public class RemoteRunningAppsProvider implements RunningAppProvider {
Set<String> urls = settings.getStringSet("boot-java", "remote-apps");
{ //Remove obsolete apps...
Iterator<String> keys = remoteAppByUrl.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
Iterator<Entry<String, SpringBootApp>> entries = remoteAppByUrl.entrySet().iterator();
while (entries.hasNext()) {
Entry<String, SpringBootApp> entry = entries.next();
String key = entry.getKey();
if (!urls.contains(key)) {
logger.debug("Removing RemoteSpringBootApp: "+key);
keys.remove();
entries.remove();
entry.getValue().dispose();
}
}
}