From 2392b7b15546ac07f99d9653a436f03f2550c78e Mon Sep 17 00:00:00 2001 From: kennycud Date: Sun, 5 Jan 2025 13:49:31 -0800 Subject: [PATCH] system info and database connection status access --- .../org/hsqldb/jdbc/HSQLDBPoolMonitored.java | 173 ++++++++++++++++++ .../org/qortal/api/model/DatasetStatus.java | 50 +++++ .../restricted/resource/AdminResource.java | 51 +++++- .../qortal/data/system/DbConnectionInfo.java | 35 ++++ .../org/qortal/data/system/SystemInfo.java | 49 +++++ .../hsqldb/HSQLDBRepositoryFactory.java | 27 ++- .../java/org/qortal/settings/Settings.java | 11 ++ 7 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/hsqldb/jdbc/HSQLDBPoolMonitored.java create mode 100644 src/main/java/org/qortal/api/model/DatasetStatus.java create mode 100644 src/main/java/org/qortal/data/system/DbConnectionInfo.java create mode 100644 src/main/java/org/qortal/data/system/SystemInfo.java diff --git a/src/main/java/org/hsqldb/jdbc/HSQLDBPoolMonitored.java b/src/main/java/org/hsqldb/jdbc/HSQLDBPoolMonitored.java new file mode 100644 index 00000000..2037453c --- /dev/null +++ b/src/main/java/org/hsqldb/jdbc/HSQLDBPoolMonitored.java @@ -0,0 +1,173 @@ +package org.hsqldb.jdbc; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.hsqldb.jdbc.pool.JDBCPooledConnection; +import org.qortal.data.system.DbConnectionInfo; +import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; + +import javax.sql.ConnectionEvent; +import javax.sql.PooledConnection; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * Class HSQLDBPoolMonitored + * + * This class uses the same logic as HSQLDBPool. The only difference is it monitors the state of every connection + * to the database. This is used for debugging purposes only. + */ +public class HSQLDBPoolMonitored extends HSQLDBPool { + + private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepositoryFactory.class); + + private static final String EMPTY = "Empty"; + private static final String AVAILABLE = "Available"; + private static final String ALLOCATED = "Allocated"; + + private ConcurrentHashMap infoByIndex; + + public HSQLDBPoolMonitored(int poolSize) { + super(poolSize); + + this.infoByIndex = new ConcurrentHashMap<>(poolSize); + } + + /** + * Tries to retrieve a new connection using the properties that have already been + * set. + * + * @return a connection to the data source, or null if no spare connections in pool + * @exception SQLException if a database access error occurs + */ + public Connection tryConnection() throws SQLException { + for (int i = 0; i < states.length(); i++) { + if (states.compareAndSet(i, RefState.available, RefState.allocated)) { + JDBCPooledConnection pooledConnection = connections[i]; + + if (pooledConnection == null) + // Probably shutdown situation + return null; + + infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED)); + + return pooledConnection.getConnection(); + } + + if (states.compareAndSet(i, RefState.empty, RefState.allocated)) { + try { + JDBCPooledConnection pooledConnection = (JDBCPooledConnection) source.getPooledConnection(); + + if (pooledConnection == null) + // Probably shutdown situation + return null; + + pooledConnection.addConnectionEventListener(this); + pooledConnection.addStatementEventListener(this); + connections[i] = pooledConnection; + + infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED)); + + return pooledConnection.getConnection(); + } catch (SQLException e) { + states.set(i, RefState.empty); + infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY)); + } + } + } + + return null; + } + + public Connection getConnection() throws SQLException { + int var1 = 300; + if (this.source.loginTimeout != 0) { + var1 = this.source.loginTimeout * 10; + } + + if (this.closed) { + throw new SQLException("connection pool is closed"); + } else { + for(int var2 = 0; var2 < var1; ++var2) { + for(int var3 = 0; var3 < this.states.length(); ++var3) { + if (this.states.compareAndSet(var3, 1, 2)) { + infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED)); + return this.connections[var3].getConnection(); + } + + if (this.states.compareAndSet(var3, 0, 2)) { + try { + JDBCPooledConnection var4 = (JDBCPooledConnection)this.source.getPooledConnection(); + var4.addConnectionEventListener(this); + var4.addStatementEventListener(this); + this.connections[var3] = var4; + + infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), ALLOCATED)); + + return this.connections[var3].getConnection(); + } catch (SQLException var6) { + this.states.set(var3, 0); + infoByIndex.put(var3, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY)); + } + } + } + + try { + Thread.sleep(100L); + } catch (InterruptedException var5) { + } + } + + throw JDBCUtil.invalidArgument(); + } + } + + public void connectionClosed(ConnectionEvent event) { + PooledConnection connection = (PooledConnection) event.getSource(); + + for (int i = 0; i < connections.length; i++) { + if (connections[i] == connection) { + states.set(i, RefState.available); + infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), AVAILABLE)); + break; + } + } + } + + public void connectionErrorOccurred(ConnectionEvent event) { + PooledConnection connection = (PooledConnection) event.getSource(); + + for (int i = 0; i < connections.length; i++) { + if (connections[i] == connection) { + states.set(i, RefState.allocated); + connections[i] = null; + states.set(i, RefState.empty); + infoByIndex.put(i, new DbConnectionInfo(System.currentTimeMillis(), Thread.currentThread().getName(), EMPTY)); + break; + } + } + } + + public List getDbConnectionsStates() { + + return infoByIndex.values().stream() + .sorted(Comparator.comparingLong(DbConnectionInfo::getUpdated)) + .collect(Collectors.toList()); + } + + private int findConnectionIndex(ConnectionEvent connectionEvent) { + PooledConnection pooledConnection = (PooledConnection) connectionEvent.getSource(); + + for(int i = 0; i < this.connections.length; ++i) { + if (this.connections[i] == pooledConnection) { + return i; + } + } + + return -1; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/api/model/DatasetStatus.java b/src/main/java/org/qortal/api/model/DatasetStatus.java new file mode 100644 index 00000000..b587be51 --- /dev/null +++ b/src/main/java/org/qortal/api/model/DatasetStatus.java @@ -0,0 +1,50 @@ +package org.qortal.api.model; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Objects; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) +public class DatasetStatus { + + private String name; + + private long count; + + public DatasetStatus() {} + + public DatasetStatus(String name, long count) { + this.name = name; + this.count = count; + } + + public String getName() { + return name; + } + + public long getCount() { + return count; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DatasetStatus that = (DatasetStatus) o; + return count == that.count && Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, count); + } + + @Override + public String toString() { + return "DatasetStatus{" + + "name='" + name + '\'' + + ", count=" + count + + '}'; + } +} diff --git a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java index 279485bc..439904eb 100644 --- a/src/main/java/org/qortal/api/restricted/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/restricted/resource/AdminResource.java @@ -32,6 +32,7 @@ import org.qortal.controller.Synchronizer.SynchronizationResult; import org.qortal.controller.repository.BlockArchiveRebuilder; import org.qortal.data.account.MintingAccountData; import org.qortal.data.account.RewardShareData; +import org.qortal.data.system.DbConnectionInfo; import org.qortal.network.Network; import org.qortal.network.Peer; import org.qortal.network.PeerAddress; @@ -40,6 +41,7 @@ import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; import org.qortal.settings.Settings; +import org.qortal.data.system.SystemInfo; import org.qortal.utils.Base58; import org.qortal.utils.NTP; @@ -52,6 +54,7 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -1064,4 +1067,50 @@ public class AdminResource { return "true"; } -} + @GET + @Path("/systeminfo") + @Operation( + summary = "System Information", + description = "System memory usage and available processors.", + responses = { + @ApiResponse( + description = "memory usage and available processors", + content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = SystemInfo.class)) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + public SystemInfo getSystemInformation() { + + SystemInfo info + = new SystemInfo( + Runtime.getRuntime().freeMemory(), + Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), + Runtime.getRuntime().totalMemory(), + Runtime.getRuntime().maxMemory(), + Runtime.getRuntime().availableProcessors()); + + return info; + } + + @GET + @Path("/dbstates") + @Operation( + summary = "Get DB States", + description = "Get DB States", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.APPLICATION_JSON, array = @ArraySchema(schema = @Schema(implementation = DbConnectionInfo.class))) + ) + } + ) + public List getDbConnectionsStates() { + + try { + return Controller.REPOSITORY_FACTORY.getDbConnectionsStates(); + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + return new ArrayList<>(0); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/data/system/DbConnectionInfo.java b/src/main/java/org/qortal/data/system/DbConnectionInfo.java new file mode 100644 index 00000000..0e42dc20 --- /dev/null +++ b/src/main/java/org/qortal/data/system/DbConnectionInfo.java @@ -0,0 +1,35 @@ +package org.qortal.data.system; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@XmlAccessorType(XmlAccessType.FIELD) +public class DbConnectionInfo { + + private long updated; + + private String owner; + + private String state; + + public DbConnectionInfo() { + } + + public DbConnectionInfo(long timeOpened, String owner, String state) { + this.updated = timeOpened; + this.owner = owner; + this.state = state; + } + + public long getUpdated() { + return updated; + } + + public String getOwner() { + return owner; + } + + public String getState() { + return state; + } +} diff --git a/src/main/java/org/qortal/data/system/SystemInfo.java b/src/main/java/org/qortal/data/system/SystemInfo.java new file mode 100644 index 00000000..bf832194 --- /dev/null +++ b/src/main/java/org/qortal/data/system/SystemInfo.java @@ -0,0 +1,49 @@ +package org.qortal.data.system; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; + +@XmlAccessorType(XmlAccessType.FIELD) +public class SystemInfo { + + private long freeMemory; + + private long memoryInUse; + + private long totalMemory; + + private long maxMemory; + + private int availableProcessors; + + public SystemInfo() { + } + + public SystemInfo(long freeMemory, long memoryInUse, long totalMemory, long maxMemory, int availableProcessors) { + this.freeMemory = freeMemory; + this.memoryInUse = memoryInUse; + this.totalMemory = totalMemory; + this.maxMemory = maxMemory; + this.availableProcessors = availableProcessors; + } + + public long getFreeMemory() { + return freeMemory; + } + + public long getMemoryInUse() { + return memoryInUse; + } + + public long getTotalMemory() { + return totalMemory; + } + + public long getMaxMemory() { + return maxMemory; + } + + public int getAvailableProcessors() { + return availableProcessors; + } +} diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java index fdaf41a2..2ddabf8d 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepositoryFactory.java @@ -5,6 +5,8 @@ import org.apache.logging.log4j.Logger; import org.hsqldb.HsqlException; import org.hsqldb.error.ErrorCode; import org.hsqldb.jdbc.HSQLDBPool; +import org.hsqldb.jdbc.HSQLDBPoolMonitored; +import org.qortal.data.system.DbConnectionInfo; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryFactory; @@ -14,6 +16,8 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.Properties; public class HSQLDBRepositoryFactory implements RepositoryFactory { @@ -57,7 +61,13 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory { HSQLDBRepository.attemptRecovery(connectionUrl, "backup"); } - this.connectionPool = new HSQLDBPool(Settings.getInstance().getRepositoryConnectionPoolSize()); + if(Settings.getInstance().isConnectionPoolMonitorEnabled()) { + this.connectionPool = new HSQLDBPoolMonitored(Settings.getInstance().getRepositoryConnectionPoolSize()); + } + else { + this.connectionPool = new HSQLDBPool(Settings.getInstance().getRepositoryConnectionPoolSize()); + } + this.connectionPool.setUrl(this.connectionUrl); Properties properties = new Properties(); @@ -153,4 +163,19 @@ public class HSQLDBRepositoryFactory implements RepositoryFactory { return HSQLDBRepository.isDeadlockException(e); } + /** + * Get Connection States + * + * Get the database connection states, if database connection pool monitoring is enabled. + * + * @return the connection states if enabled, otherwise an empty list + */ + public List getDbConnectionsStates() { + if( Settings.getInstance().isConnectionPoolMonitorEnabled() ) { + return ((HSQLDBPoolMonitored) this.connectionPool).getDbConnectionsStates(); + } + else { + return new ArrayList<>(0); + } + } } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index deee0075..3a0d17bb 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -501,6 +501,13 @@ public class Settings { */ private boolean rewardRecordingOnly = true; + /** + * Is The Connection Monitored? + * + * Is the database connection pooled monitored? + */ + private boolean connectionPoolMonitorEnabled = false; + // Domain mapping public static class ThreadLimit { private String messageType; @@ -1322,4 +1329,8 @@ public class Settings { public boolean isRewardRecordingOnly() { return rewardRecordingOnly; } + + public boolean isConnectionPoolMonitorEnabled() { + return connectionPoolMonitorEnabled; + } }