keeping JMX connection open and react to connection lost event + integrating remote apps into the regular connect/disconnect command handling

This commit is contained in:
Martin Lippert
2019-09-11 12:47:48 +02:00
parent 96e5b1d1a4
commit 66c95991a9
10 changed files with 225 additions and 115 deletions

View File

@@ -16,6 +16,7 @@ import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jface.resource.ImageDescriptor;
@@ -94,8 +95,14 @@ public class LiveProcessCommandElement extends QuickAccessElement {
commandParams.setArguments(arguments);
CompletableFuture.allOf(usedLanguageServers.stream().map(ls ->
ls.getWorkspaceService().executeCommand(commandParams)).toArray(CompletableFuture[]::new)).join();
try {
CompletableFuture.allOf(usedLanguageServers.stream().map(ls ->
ls.getWorkspaceService().executeCommand(commandParams)).toArray(CompletableFuture[]::new)).get(2, TimeUnit.SECONDS);
}
catch (Exception e) {
// TODO: better exception handling
e.printStackTrace();
}
}
}

View File

@@ -15,6 +15,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
@@ -59,9 +60,16 @@ public class LiveProcessCommandsQuickAccessProvider implements IQuickAccessCompu
commandParams.setCommand(LiveProcessCommandElement.COMMAND_LIST_PROCESSES);
List<QuickAccessElement> res = Collections.synchronizedList(new ArrayList<>());
CompletableFuture.allOf(usedLanguageServers.stream().map(ls ->
ls.getWorkspaceService().executeCommand(commandParams).thenAcceptAsync(commandResult ->
createCommandItems(res, commandResult))).toArray(CompletableFuture[]::new)).join();
try {
CompletableFuture.allOf(usedLanguageServers.stream().map(ls ->
ls.getWorkspaceService().executeCommand(commandParams).thenAcceptAsync(commandResult ->
createCommandItems(res, commandResult))).toArray(CompletableFuture[]::new)).get(2000, TimeUnit.MILLISECONDS);
}
catch (Exception e) {
// TODO: better error handling
e.printStackTrace();
}
return res.toArray(new QuickAccessElement[res.size()]);
}

View File

@@ -107,7 +107,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private final ProjectBasedPropertyIndexProvider adHocPropertyIndexProvider;
private final SpringProcessLiveDataProvider liveDataProvider;
private final SpringProcessConnectorService liveDataConnector;
private final SpringProcessConnectorService liveDataService;
private final SpringLiveChangeDetectionWatchdog liveChangeDetectionWatchdog;
private final ProjectObserver projectObserver;
@@ -153,47 +153,36 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
workspaceService.onWorkspaceSymbol(new BootJavaWorkspaceSymbolHandler(indexer,
new LiveAppURLSymbolProvider(runningAppProvider)));
//
// live data component wiring
//
// central live data components (to coordinate live data flow)
liveDataProvider = new SpringProcessLiveDataProvider();
liveDataService = new SpringProcessConnectorService(liveDataProvider);
// connect the live data provider with the hovers (for data extraction and live updates)
hoverProvider = createHoverHandler(projectFinder, sourceLinks, liveDataProvider);
new SpringProcessLiveHoverUpdater(server, hoverProvider, projectFinder, liveDataProvider);
liveDataConnector = new SpringProcessConnectorService();
new SpringProcessConnectorRemote(server, liveDataConnector, liveDataProvider);
// deal with locally running processes and their connections
SpringProcessConnectorLocal liveDataLocalProcessConnector = new SpringProcessConnectorLocal(liveDataService, projectObserver);
// deal with configured remote connections
SpringProcessConnectorRemote liveDataRemoteProcessConnector = new SpringProcessConnectorRemote(server, liveDataService);
// create and handle commands
new SpringProcessCommandHandler(server, liveDataService, liveDataLocalProcessConnector, liveDataRemoteProcessConnector);
// track locally running processes and automatically connect to them if configured to do so
liveProcessTracker = new SpringProcessTracker(liveDataLocalProcessConnector, serverParams.watchDogInterval);
SpringProcessConnectorLocal liveHoverLocalProcessConnector = new SpringProcessConnectorLocal(liveDataConnector, liveDataProvider, projectObserver);
liveProcessTracker = new SpringProcessTracker(liveHoverLocalProcessConnector, serverParams.watchDogInterval);
//
//
//
new SpringProcessCommandHandler(server, liveDataConnector, liveHoverLocalProcessConnector);
// liveHoverWatchdog = new SpringLiveHoverWatchdog(server, hoverProvider, runningAppProvider,
// projectFinder, projectObserver, serverParams.watchDogInterval);
// documents.onDidChangeContent(params -> {
// TextDocument doc = params.getDocument();
// if (getInterestingLanguages().contains(doc.getLanguageId())) {
// if (testHightlighter != null) {
// getClient()
// .highlight(new HighlightParams(params.getDocument().getId(), testHightlighter.apply(doc)));
// } else {
// try {
// liveHoverWatchdog.watchDocument(doc.getUri());
// liveHoverWatchdog.update(doc.getUri());
// } catch (Throwable t) {
// log.error("", t);
// }
// }
// }
// });
// documents.onDidClose(doc -> {
// if (testHightlighter != null) {
// getClient().highlight(new HighlightParams(doc.getId(), testHightlighter.apply(doc)));
// } else {
// liveHoverWatchdog.unwatchDocument(doc.getUri());
// }
// });
liveChangeDetectionWatchdog = new SpringLiveChangeDetectionWatchdog(
this,
server,

View File

@@ -18,6 +18,7 @@ import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.eclipse.lsp4j.ExecuteCommandParams;
import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessConnectorRemote.RemoteBootAppData;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import com.google.gson.JsonElement;
@@ -35,10 +36,13 @@ public class SpringProcessCommandHandler {
private final SpringProcessConnectorService connectorService;
private final SpringProcessConnectorLocal localProcessConnector;
private final SpringProcessConnectorRemote remoteProcessConnector;
public SpringProcessCommandHandler(SimpleLanguageServer server, SpringProcessConnectorService connectorService, SpringProcessConnectorLocal localProcessConnector) {
public SpringProcessCommandHandler(SimpleLanguageServer server, SpringProcessConnectorService connectorService,
SpringProcessConnectorLocal localProcessConnector, SpringProcessConnectorRemote remoteProcessConnector) {
this.connectorService = connectorService;
this.localProcessConnector = localProcessConnector;
this.remoteProcessConnector = remoteProcessConnector;
server.onCommand(COMMAND_LIST_PROCESSES, (params) -> {
return getProcessCommands();
@@ -61,6 +65,8 @@ public class SpringProcessCommandHandler {
private CompletableFuture<Object> connect(ExecuteCommandParams params) {
String processKey = getProcessKey(params);
if (processKey != null) {
// try local processes
SpringProcessDescriptor[] processes = localProcessConnector.getProcesses(false, SpringProcessStatus.REGULAR);
for (SpringProcessDescriptor process : processes) {
if (process.getProcessKey().equals(processKey)) {
@@ -68,6 +74,16 @@ public class SpringProcessCommandHandler {
return CompletableFuture.completedFuture(null);
}
}
// try remote processes
RemoteBootAppData[] remoteProcesses = remoteProcessConnector.getProcesses();
for (RemoteBootAppData remoteProcess : remoteProcesses) {
String key = SpringProcessConnectorRemote.getProcessKey(remoteProcess);
if (processKey.equals(key)) {
remoteProcessConnector.connectProcess(remoteProcess);
return CompletableFuture.completedFuture(null);
}
}
}
return CompletableFuture.completedFuture(null);
@@ -108,13 +124,11 @@ public class SpringProcessCommandHandler {
refreshCommand.setProcessKey(processKey);
result.add(refreshCommand);
if (localProcessConnector.isLocalProcess(process.getProcessKey())) {
LiveProcessCommand disconnectCommand = new LiveProcessCommand();
disconnectCommand.setAction(COMMAND_DISCONNECT);
disconnectCommand.setLabel(label);
disconnectCommand.setProcessKey(processKey);
result.add(disconnectCommand);
}
LiveProcessCommand disconnectCommand = new LiveProcessCommand();
disconnectCommand.setAction(COMMAND_DISCONNECT);
disconnectCommand.setLabel(label);
disconnectCommand.setProcessKey(processKey);
result.add(disconnectCommand);
alreadyConnected.add(processKey);
}
@@ -136,6 +150,23 @@ public class SpringProcessCommandHandler {
}
}
// other available remote processes
RemoteBootAppData[] remoteProcesses = remoteProcessConnector.getProcesses();
for (RemoteBootAppData remoteProcess : remoteProcesses) {
String processKey = SpringProcessConnectorRemote.getProcessKey(remoteProcess);
if (!alreadyConnected.contains(processKey)) {
String label = "remote process: " + remoteProcess.getJmxurl();
String action = COMMAND_CONNECT;
LiveProcessCommand command = new LiveProcessCommand();
command.setAction(action);
command.setLabel(label);
command.setProcessKey(processKey);
result.add(command);
}
}
return CompletableFuture.completedFuture((Object[]) result.toArray(new Object[result.size()]));
}

View File

@@ -19,7 +19,7 @@ public interface SpringProcessConnector {
String getLabel();
void connect() throws Exception;
void refresh() throws Exception;
SpringProcessLiveData refresh() throws Exception;
void disconnect() throws Exception;
void addConnectorChangeListener(SpringProcessConnectionChangeListener listener);

View File

@@ -67,14 +67,11 @@ public class SpringProcessConnectorLocal {
private final Set<SpringProcessDescriptor> processes;
private final SpringProcessConnectorService processConnectorService;
private final SpringProcessLiveDataProvider liveDataProvider;
public SpringProcessConnectorLocal(SpringProcessConnectorService processConnector, SpringProcessLiveDataProvider liveDataProvider,
ProjectObserver projectObserver) {
public SpringProcessConnectorLocal(SpringProcessConnectorService processConnector, ProjectObserver projectObserver) {
this.projects = Collections.synchronizedCollection(new HashSet<>());
this.processes = Collections.synchronizedSet(new HashSet<>());
this.liveDataProvider = liveDataProvider;
this.processConnectorService = processConnector;
projectObserver.addListener(new ProjectObserver.Listener() {
@@ -253,7 +250,7 @@ public class SpringProcessConnectorLocal {
String urlScheme = "http";
SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(
liveDataProvider, descriptor.getProcessKey(), jmxAddress, urlScheme, processID, processName, null, null);
descriptor.getProcessKey(), jmxAddress, urlScheme, processID, processName, null, null);
this.processConnectorService.connectProcess(descriptor.getProcessKey(), connector);
}

View File

@@ -10,9 +10,14 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.livehover.v2;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.remote.JMXConnectionNotification;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;
@@ -27,22 +32,31 @@ public class SpringProcessConnectorOverJMX implements SpringProcessConnector {
private static final Logger log = LoggerFactory.getLogger(SpringProcessConnectorOverJMX.class);
private final SpringProcessLiveDataProvider liveDataProvider;
private static final String JMX_CLIENT_CONNECTION_CHECK_PERIOD_PROPERTY_KEY = "jmx.remote.x.client.connection.check.period";
private static final long JMX_HEARTBEAT_INTERVAL = 1000;
private final String processKey;
private final String jmxURL;
private final String urlScheme;
private final String processID;
private final String processName;
private final String host;
private final String port;
// not final, might be updated with data from JMX process, if not initially set
private String processID;
private String processName;
private String host;
private final List<SpringProcessConnectionChangeListener> listeners;
public SpringProcessConnectorOverJMX(SpringProcessLiveDataProvider liveDataProvider, String processKey, String jmxURL,
private JMXConnector jmxConnection;
private JMXServiceURL jmxServiceURL;
private final NotificationListener notificationListener;
public SpringProcessConnectorOverJMX(String processKey, String jmxURL,
String urlScheme, String processID, String processName, String host, String port) {
this.liveDataProvider = liveDataProvider;
this.processKey = processKey;
this.jmxURL = jmxURL;
this.urlScheme = urlScheme;
this.processID = processID;
@@ -50,7 +64,28 @@ public class SpringProcessConnectorOverJMX implements SpringProcessConnector {
this.host = host;
this.port = port;
this.jmxConnection = null;
this.jmxServiceURL = null;
this.listeners = new CopyOnWriteArrayList<>();
this.notificationListener = new NotificationListener() {
@Override
public void handleNotification(Notification notification, Object handback) {
String notificationType = notification.getType();
if (JMXConnectionNotification.CLOSED.equals(notificationType)) {
try {
jmxConnection.removeConnectionNotificationListener(notificationListener);
jmxConnection = null;
}
catch (Exception e) {
log.error("exception while reacting to connection close of: " + jmxURL, e);
}
announceConnectionClosed();
}
}
};
}
@Override
@@ -65,40 +100,44 @@ public class SpringProcessConnectorOverJMX implements SpringProcessConnector {
@Override
public void connect() throws Exception {
jmxServiceURL = new JMXServiceURL(jmxURL);
Map<String, Object> environment = new HashMap<>();
environment.put(JMX_CLIENT_CONNECTION_CHECK_PERIOD_PROPERTY_KEY, new Long(JMX_HEARTBEAT_INTERVAL));
jmxConnection = JMXConnectorFactory.connect(jmxServiceURL, environment);
jmxConnection.addConnectionNotificationListener(notificationListener, null, null);
}
@Override
public void refresh() throws Exception {
public SpringProcessLiveData refresh() throws Exception {
log.info("try to open JMX connection to: " + jmxURL);
JMXConnector jmxConnector = null;
try {
SpringProcessLiveDataExtractorOverJMX springJMXConnector = new SpringProcessLiveDataExtractorOverJMX();
JMXServiceURL jmxServiceURL = new JMXServiceURL(jmxURL);
jmxConnector = JMXConnectorFactory.connect(jmxServiceURL, null);
String hostName = host != null ? host : jmxServiceURL.getHost();
log.info("retrieve live data from: " + jmxURL);
SpringProcessLiveData liveData = springJMXConnector.retrieveLiveData(jmxConnector, processID, processName, urlScheme, hostName, null, port);
if (liveData != null && liveData.getBeans() != null && !liveData.getBeans().isEmpty()) {
this.liveDataProvider.add(processKey, liveData);
return;
if (jmxConnection != null) {
try {
SpringProcessLiveDataExtractorOverJMX springJMXConnector = new SpringProcessLiveDataExtractorOverJMX();
if (this.host == null) {
this.host = jmxServiceURL.getHost();
}
log.info("retrieve live data from: " + jmxURL);
SpringProcessLiveData liveData = springJMXConnector.retrieveLiveData(jmxConnection, processID, processName, urlScheme, host, null, port);
if (this.processID == null) {
this.processID = liveData.getProcessID();
}
if (this.processName == null) {
this.processName = liveData.getProcessName();
}
if (liveData != null && liveData.getBeans() != null && !liveData.getBeans().isEmpty()) {
return liveData;
}
}
}
catch (Exception e) {
log.error("exception while connecting to jmx: " + jmxURL, e);
}
finally {
if (jmxConnector != null) {
try {
log.info("close JMX connection to: " + jmxURL);
jmxConnector.close();
}
catch (Exception e) {
log.error("error closing the JMX connection for: " + jmxURL, e);
}
catch (Exception e) {
log.error("exception while connecting to jmx: " + jmxURL, e);
}
}
@@ -107,7 +146,18 @@ public class SpringProcessConnectorOverJMX implements SpringProcessConnector {
@Override
public void disconnect() throws Exception {
this.liveDataProvider.remove(processKey);
try {
if (jmxConnection != null) {
log.info("close JMX connection to: " + jmxURL);
jmxConnection.removeConnectionNotificationListener(notificationListener);
jmxConnection.close();
jmxConnection = null;
}
}
catch (Exception e) {
log.error("error closing the JMX connection for: " + jmxURL, e);
}
}
@Override
@@ -126,6 +176,4 @@ public class SpringProcessConnectorOverJMX implements SpringProcessConnector {
}
}
}

View File

@@ -141,12 +141,9 @@ public class SpringProcessConnectorRemote {
*/
private final Map<RemoteBootAppData, String> remoteAppInstances;
private final SpringProcessConnectorService processConnectorService;
private final SpringProcessLiveDataProvider liveDataProvider;
public SpringProcessConnectorRemote(SimpleLanguageServer server, SpringProcessConnectorService processConnector,
SpringProcessLiveDataProvider liveDataProvider) {
public SpringProcessConnectorRemote(SimpleLanguageServer server, SpringProcessConnectorService processConnector) {
this.processConnectorService = processConnector;
this.liveDataProvider = liveDataProvider;
this.remoteAppInstances = new HashMap<>();
server.getWorkspaceService().onDidChangeConfiguraton(this::handleSettings);
@@ -181,26 +178,34 @@ public class SpringProcessConnectorRemote {
for (RemoteBootAppData data : newAppData) {
remoteAppInstances.computeIfAbsent(data, (_appData) -> {
logger.info("Creating RemoteStringBootApp: " + _appData);
String processKey = getProcessKey(_appData);
String processID = null;
String processName = null;
String jmxURL = _appData.getJmxurl();
String host = _appData.getHost();
String port = _appData.getPort();
String urlScheme = _appData.getUrlScheme();
// boolean keepChecking = _appData.isKeepChecking();
SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(liveDataProvider, processKey, jmxURL, urlScheme, processID, processName, host, port);
processConnectorService.connectProcess(processKey, connector);
connectProcess(_appData);
return processKey;
});
}
}
private static String getProcessKey(RemoteBootAppData appData) {
return "remote";
public static String getProcessKey(RemoteBootAppData appData) {
return "remote process - " + appData.getJmxurl();
}
public void connectProcess(RemoteBootAppData remoteProcess) {
String processKey = getProcessKey(remoteProcess);
String processID = null;
String processName = null;
String jmxURL = remoteProcess.getJmxurl();
String host = remoteProcess.getHost();
String port = remoteProcess.getPort();
String urlScheme = remoteProcess.getUrlScheme();
// boolean keepChecking = _appData.isKeepChecking();
SpringProcessConnectorOverJMX connector = new SpringProcessConnectorOverJMX(processKey, jmxURL, urlScheme, processID, processName, host, port);
processConnectorService.connectProcess(processKey, connector);
}
public RemoteBootAppData[] getProcesses() {
Set<RemoteBootAppData> remoteApps = this.remoteAppInstances.keySet();
return (RemoteBootAppData[]) remoteApps.toArray(new RemoteBootAppData[remoteApps.size()]);
}
}

View File

@@ -28,14 +28,26 @@ public class SpringProcessConnectorService {
private static final int RETRY_MAX_NO = 10;
private static final int RETRY_DELAY_IN_SECONDS = 3;
private final SpringProcessLiveDataProvider liveDataProvider;
private final ScheduledThreadPoolExecutor scheduler;
private final ConcurrentMap<String, SpringProcessConnector> connectors;
private final ConcurrentMap<String, Boolean> connectedSuccess;
private final SpringProcessConnectionChangeListener connectorListener;
public SpringProcessConnectorService() {
public SpringProcessConnectorService(SpringProcessLiveDataProvider liveDataProvider) {
this.liveDataProvider = liveDataProvider;
this.scheduler = new ScheduledThreadPoolExecutor(10);
this.connectors = new ConcurrentHashMap<>();
this.connectedSuccess = new ConcurrentHashMap<>();
this.connectorListener = new SpringProcessConnectionChangeListener() {
@Override
public void connectionClosed(String processKey) {
disconnectProcess(processKey);
}
};
}
public void connectProcess(String processKey, SpringProcessConnector connector) {
@@ -43,6 +55,8 @@ public class SpringProcessConnectorService {
this.connectors.put(processKey, connector);
this.connectedSuccess.put(processKey, false);
connector.addConnectorChangeListener(connectorListener);
try {
scheduleConnect(processKey, connector, 0, TimeUnit.SECONDS, 0);
@@ -64,6 +78,8 @@ public class SpringProcessConnectorService {
public void disconnectProcess(String processKey) {
log.info("disconnect from process: " + processKey);
this.liveDataProvider.remove(processKey);
SpringProcessConnector connector = this.connectors.remove(processKey);
this.connectedSuccess.put(processKey, false);
@@ -90,6 +106,7 @@ public class SpringProcessConnectorService {
this.scheduler.schedule(() -> {
try {
connector.connect();
refreshProcess(processKey);
}
catch (Exception e) {
log.info("problem occured during process connect", e);
@@ -98,7 +115,6 @@ public class SpringProcessConnectorService {
scheduleConnect(processKey, connector, RETRY_DELAY_IN_SECONDS, TimeUnit.SECONDS, retryNo + 1);
}
}
refreshProcess(processKey);
}, delay, unit);
}
@@ -124,14 +140,20 @@ public class SpringProcessConnectorService {
this.scheduler.schedule(() -> {
try {
connector.refresh();
this.connectedSuccess.put(processKey, true);
SpringProcessLiveData newLiveData = connector.refresh();
if (newLiveData != null) {
if (!this.liveDataProvider.add(processKey, newLiveData)) {
this.liveDataProvider.update(processKey, newLiveData);
}
this.connectedSuccess.put(processKey, true);
}
}
catch (Exception e) {
log.info("problem occured during process live data refresh", e);
if (retryNo < RETRY_MAX_NO) {
scheduleRefresh(processKey, connector, 3, TimeUnit.SECONDS, retryNo + 1);
scheduleRefresh(processKey, connector, RETRY_DELAY_IN_SECONDS, TimeUnit.SECONDS, retryNo + 1);
}
else {
disconnectProcess(processKey);

View File

@@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.livehover.v2;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
@@ -68,7 +69,9 @@ public class SpringProcessLiveHoverUpdater {
});
liveDataProvider.addLiveDataChangeListener(event -> {
update();
CompletableFuture.runAsync(() -> {
update();
});
});
}