From 6ba275173c3171410e9cfe37e135a5e69845eb6c Mon Sep 17 00:00:00 2001 From: slominskir Date: Wed, 14 Feb 2024 14:45:03 -0500 Subject: [PATCH 1/4] Just pass null value onto client instead of empty string. --- src/main/java/org/jlab/epics2web/epics/ChannelManager.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/jlab/epics2web/epics/ChannelManager.java b/src/main/java/org/jlab/epics2web/epics/ChannelManager.java index 8721db7..86c8615 100644 --- a/src/main/java/org/jlab/epics2web/epics/ChannelManager.java +++ b/src/main/java/org/jlab/epics2web/epics/ChannelManager.java @@ -130,7 +130,9 @@ public static String getDbrValueAsString(DBR dbr) { public void addValueToJSON(JsonObjectBuilder builder, DBR dbr) { try { - if (dbr.isDOUBLE()) { + if(dbr == null) { + builder.addNull("value"); // null happens on restart? + } else if (dbr.isDOUBLE()) { double value = ((gov.aps.jca.dbr.DOUBLE) dbr).getDoubleValue()[0]; if (Double.isFinite(value)) { builder.add("value", value); From c4b7c536a159f8cb9f0b2018a25140d380718525 Mon Sep 17 00:00:00 2001 From: slominskir Date: Wed, 14 Feb 2024 15:39:49 -0500 Subject: [PATCH 2/4] Handle race condition on accessing session info --- .../jlab/epics2web/controller/WebConsole.java | 4 +- .../jlab/epics2web/epics/ChannelManager.java | 2 +- .../jlab/epics2web/websocket/SessionInfo.java | 43 +++++++++++++++++++ .../websocket/WebSocketSessionManager.java | 27 ++++++++++-- src/main/webapp/WEB-INF/views/console.jsp | 8 ++-- 5 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/jlab/epics2web/websocket/SessionInfo.java diff --git a/src/main/java/org/jlab/epics2web/controller/WebConsole.java b/src/main/java/org/jlab/epics2web/controller/WebConsole.java index 014c156..07f61b7 100644 --- a/src/main/java/org/jlab/epics2web/controller/WebConsole.java +++ b/src/main/java/org/jlab/epics2web/controller/WebConsole.java @@ -8,9 +8,9 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.websocket.Session; import org.jlab.epics2web.Application; import org.jlab.epics2web.epics.ChannelMonitor; +import org.jlab.epics2web.websocket.SessionInfo; import org.jlab.epics2web.websocket.WebSocketSessionManager; import org.jlab.epics2web.epics.ChannelManager; @@ -40,7 +40,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) Map monitorMap = channelManager.getMonitorMap(); - Map> clientMap = sessionManager.getClientMap(); + Map> clientMap = sessionManager.getClientMap(); request.setAttribute("monitorMap", monitorMap); request.setAttribute("clientMap", clientMap); diff --git a/src/main/java/org/jlab/epics2web/epics/ChannelManager.java b/src/main/java/org/jlab/epics2web/epics/ChannelManager.java index 86c8615..36ce8b5 100644 --- a/src/main/java/org/jlab/epics2web/epics/ChannelManager.java +++ b/src/main/java/org/jlab/epics2web/epics/ChannelManager.java @@ -382,7 +382,7 @@ public Map getMonitorMap() { * * @return The listener to PVs map */ - public Map> getClientMap() { + public Map> getListenerMap() { return Collections.unmodifiableMap(clientMap); } } diff --git a/src/main/java/org/jlab/epics2web/websocket/SessionInfo.java b/src/main/java/org/jlab/epics2web/websocket/SessionInfo.java new file mode 100644 index 0000000..1bc9367 --- /dev/null +++ b/src/main/java/org/jlab/epics2web/websocket/SessionInfo.java @@ -0,0 +1,43 @@ +package org.jlab.epics2web.websocket; + +/** + * This class provides a snapshot of websocket information that will not throw Exceptions if you try to interrogate it + * after the session happens to have closed. There is a race condition if you hand a list of javax.websocket.Session + * objects to a debug console for example as if the session happens to close between the time you return it and the + * time the debug console calls the userProperties method for example you get an exception. + */ +public class SessionInfo { + private String id; + private String ip; + private String name; + private String agent; + private long droppedMessageCount; + + public SessionInfo(String id, String ip, String name, String agent, long droppedMessageCount) { + this.id = id; + this.ip = ip; + this.name = name; + this.agent = agent; + this.droppedMessageCount = droppedMessageCount; + } + + public String getId() { + return id; + } + + public String getIp() { + return ip; + } + + public String getName() { + return name; + } + + public String getAgent() { + return agent; + } + + public long getDroppedMessageCount() { + return droppedMessageCount; + } +} diff --git a/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java b/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java index 5465dee..452b418 100644 --- a/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java +++ b/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java @@ -229,14 +229,33 @@ public void removePvs(Session session, Set pvSet) { * * @return The map */ - public Map> getClientMap() { - Map> pvMap = Application.channelManager.getClientMap(); - Map> clientMap = new HashMap<>(); + public Map> getClientMap() { + Map> pvMap = Application.channelManager.getListenerMap(); + Map> clientMap = new HashMap<>(); for (Session session : listenerMap.keySet()) { WebSocketSessionMonitor listener = listenerMap.get(session); Set pvSet = pvMap.get(listener); - clientMap.put(session, pvSet); + + if(session.isOpen()) { + String id = null; + + try { + id = session.getId(); + String ip = (String)session.getUserProperties().get("ip"); + String name = (String)session.getUserProperties().get("name"); + String agent = (String)session.getUserProperties().get("agent"); + AtomicLong droppedMessageCount = (AtomicLong)session.getUserProperties().get("droppedMessageCount"); + + SessionInfo info = new SessionInfo(id, ip, name, agent, droppedMessageCount.get()); + + clientMap.put(info, pvSet); + } catch(Exception e) { + // Even id may be null if closed before getId() called. Oh well. + LOGGER.log(Level.FINEST, "Session '{0}' closed while preparing info report", id); + // Ignore + } + } } return clientMap; diff --git a/src/main/webapp/WEB-INF/views/console.jsp b/src/main/webapp/WEB-INF/views/console.jsp index b8ccea6..b643a26 100644 --- a/src/main/webapp/WEB-INF/views/console.jsp +++ b/src/main/webapp/WEB-INF/views/console.jsp @@ -49,11 +49,11 @@ - - - + + + (${client.value == null ? '0' : client.value.size()}) - + From cb742474a1854ab9b32a547e6a37674aabfaa5a5 Mon Sep 17 00:00:00 2001 From: slominskir Date: Wed, 14 Feb 2024 15:40:14 -0500 Subject: [PATCH 3/4] Log session id on shutdown --- src/main/java/org/jlab/epics2web/Application.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jlab/epics2web/Application.java b/src/main/java/org/jlab/epics2web/Application.java index 59f7c29..4b16811 100644 --- a/src/main/java/org/jlab/epics2web/Application.java +++ b/src/main/java/org/jlab/epics2web/Application.java @@ -62,6 +62,7 @@ public static Future writeFromBlockingQueue(Session session) { return writerExecutor.submit(new Runnable() { @Override public void run() { + final String id = session.getId() + " / " + session.getUserProperties().get("ip"); final ArrayBlockingQueue writequeue = (ArrayBlockingQueue) session.getUserProperties().get("writequeue"); try { while (true) { @@ -72,7 +73,7 @@ public void run() { try { session.getBasicRemote().sendText(msg); } catch (IllegalStateException | IOException e) { // If session closes between time session.isOpen() and sentText(msg) then you'll get this exception. Not an issue. - LOGGER.log(Level.FINEST, "Unable to send message: ", e); + LOGGER.log(Level.FINEST, "Unable to send message to " + id, e); if (!session.isOpen()) { LOGGER.log(Level.FINEST, "Session closed after write exception; shutting down write thread"); @@ -81,12 +82,12 @@ public void run() { } } } else { - LOGGER.log(Level.FINEST, "Session closed; shutting down write thread"); + LOGGER.log(Level.FINEST, "Session {0} closed; shutting down write thread", id); break; } } } catch (InterruptedException e) { - LOGGER.log(Level.FINEST, "Shutting down writer thread as requested", e); + LOGGER.log(Level.FINEST, "Shutting down {0} writer thread as requested by InterruptException", id); } } }); From ba5bc2c38c2760003dda459117d7f5e5751cdbe6 Mon Sep 17 00:00:00 2001 From: slominskir Date: Wed, 14 Feb 2024 16:13:51 -0500 Subject: [PATCH 4/4] Log when dropped message thresholds are reached --- .../jlab/epics2web/epics/ChannelMonitor.java | 2 +- .../websocket/WebSocketSessionManager.java | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java b/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java index 21c4f5a..50d20b0 100644 --- a/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java +++ b/src/main/java/org/jlab/epics2web/epics/ChannelMonitor.java @@ -266,7 +266,7 @@ public void run() { handleRegularConnectionOrReconnect(); } } else { - LOGGER.log(Level.FINE, "Notifying clients of disconnect from channel: {0}", pv); + LOGGER.log(Level.FINEST, "Notifying clients of disconnect from channel: {0}", pv); state.set(MonitorState.DISCONNECTED); notifyPvInfoAll(false); diff --git a/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java b/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java index 452b418..7eee3df 100644 --- a/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java +++ b/src/main/java/org/jlab/epics2web/websocket/WebSocketSessionManager.java @@ -335,14 +335,17 @@ public void sendUpdate(Session session, String pv, DBR dbr) { @SuppressWarnings("unchecked") public void send(Session session, String pv, String msg) { if (session.isOpen()) { + String id = session.toString(); if (Application.WRITE_STRATEGY == WriteStrategy.ASYNC_QUEUE) { ConcurrentLinkedQueue writequeue = (ConcurrentLinkedQueue) session.getUserProperties().get("writequeue"); - - //System.out.println("Queue Size: " + writequeue.size()); + if (writequeue.size() > Application.WRITE_QUEUE_SIZE_LIMIT) { - //LOGGER.log(Level.INFO, "Dropping message: {0}", msg); AtomicLong dropCount = (AtomicLong)session.getUserProperties().get("droppedMessageCount"); - dropCount.getAndIncrement(); + long count = dropCount.getAndIncrement() + 1; // getAndIncrement is actually returning previous value, not newly updated, so we add 1. + // Limit log file output by only reporting when thresholds are reached + if(count == 1 || count == 1000 || count == 10000 || count == 100000) { + LOGGER.log(Level.FINEST, "Session {0} queue full (limit={1}); Dropping pv {2} message: {3}; total dropped: {4}", new Object[]{id, Application.WRITE_QUEUE_SIZE_LIMIT, pv, msg, count}); + } } else { writequeue.offer(msg); } @@ -355,9 +358,12 @@ public void send(Session session, String pv, String msg) { boolean success = writequeue.offer(msg); if(!success) { - //LOGGER.log(Level.INFO, "Dropping message: {0}", msg); AtomicLong dropCount = (AtomicLong)session.getUserProperties().get("droppedMessageCount"); - dropCount.getAndIncrement(); + long count = dropCount.getAndIncrement() + 1; // getAndIncrement is actually returning previous value, not newly updated, so we add 1. + // Limit log file output by only reporting when thresholds are reached + if(count == 1 || count == 1000 || count == 10000 || count == 100000) { + LOGGER.log(Level.FINEST, "Session {0} queue full (limit={1}); Dropping pv {2} message: {3}; total dropped: {4}", new Object[]{id, Application.WRITE_QUEUE_SIZE_LIMIT, pv, msg, count}); + } } } else { try {