All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.dbms.DefaultConnectionManager Maven / Gradle / Ivy

/*
 * Tentackle - https://tentackle.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


package org.tentackle.dbms;

import org.tentackle.log.Logger;
import org.tentackle.misc.FormatHelper;
import org.tentackle.session.PersistenceException;
import org.tentackle.session.SessionInfo;
import org.tentackle.session.TransactionIsolation;
import org.tentackle.session.TransactionWritability;
import org.tentackle.sql.Backend;
import org.tentackle.sql.BackendInfo;

import java.lang.ref.WeakReference;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;


/**
 * The default implementation of a connection manager.
* Each session will get its own physical connection. *

* Although this manager implements a strict 1:1 mapping between sessions and connections, * it can be easily extended to implement an M:N mapping, see the {@link MpxConnectionManager}. * * @author harald */ public class DefaultConnectionManager implements ConnectionManager { private static final Logger LOGGER = Logger.get(DefaultConnectionManager.class); /** name of the connection manager. */ protected final String name; /** initial size. */ protected final int iniSize; /** offset for session IDs. */ protected final int idOffset; /** min hours to close connections. */ protected final int minMinutes; /** max hours to close connections. */ protected final int maxMinutes; /** randomizer. */ protected final Random random; /** maximum number of connections, 0 = unlimited. */ protected final int maxConSize; /** current allocation size for {@link Db}s logged in. **/ protected int dbSize; /** maximum number of {@link Db}s, 0 = unlimited. */ protected int maxDbSize; /** free list for dbList (unused entries in dbList). */ protected int[] freeDbList; /** number of entries in freeDbList. */ protected int freeDbCount; /** managed connections. */ protected ManagedConnection[] conList; /** free list for conList (unused entries in conList). */ protected int[] freeConList; /** number of entries in freeConList. */ protected int freeConCount; private int maxCountForClearWarnings = 1000; // trigger when to clearWarning() on a connection (enabled by default) private long attachTimeout; // max. attach timeout, 0 to disable check (default) private volatile boolean shutdownRequested; // true if connection manager and timeout thread should shut down private Thread timeoutThread; // thread to check for connections attached too long (default null) private boolean configurationLogged; // true if configuration logged (once) private record VersionInfo(int majorVersion, int minorVersion) { @Override public String toString() { return majorVersion + "." + minorVersion; } } private final Map versionInfoMap; // version info per URL /** * Creates a new connection manager. * * @param name the name of the connection manager * @param iniSize the initial size of the session table * @param maxSize the maximum number of connections, 0 = unlimited (dangerous!) * @param idOffset the offset for session ids (> 0) * @param minMinutes minimum minutes a connection should be used, 0 if keep connection forever * @param maxMinutes maximum minutes a connection should be used */ public DefaultConnectionManager(String name, int iniSize, int maxSize, int idOffset, int minMinutes, int maxMinutes) { this.name = Objects.requireNonNull(name, "name"); if (name.isEmpty()) { throw new IllegalArgumentException("empty name"); } if (iniSize < 1) { throw new IllegalArgumentException("initial size must be > 0"); } if (idOffset < 1) { throw new IllegalArgumentException("connection ID offset must be > 0"); } if (maxSize != 0 && maxSize < iniSize) { throw new IllegalArgumentException("maxSize must be 0 or >= iniSize"); } if (minMinutes > maxMinutes) { throw new IllegalArgumentException("maxMinutes must be >= minMinutes"); } this.iniSize = iniSize; this.idOffset = idOffset; this.maxDbSize = maxSize; // Db and Cons use the same max-setting! this.maxConSize = maxSize; this.minMinutes = minMinutes; this.maxMinutes = maxMinutes; random = new Random(); dbSize = iniSize; conList = new ManagedConnection[iniSize]; freeDbList = new int[iniSize]; freeConList = new int[iniSize]; for (int i=iniSize-1; i >= 0; i--) { freeDbList[freeDbCount++] = i; freeConList[freeConCount++] = i; conList[i] = null; } versionInfoMap = new ConcurrentHashMap<>(); } /** * Creates a connection manager. *

* With an initial size of 2, a maximum of 8 concurrent connections and an id offset of 1. * This is the default connection manager for 2-tier client applications. * The max connections will prevent ill behaving applications from tearing down the RMI server * by opening connections excessively. The usual client application holds 2 connections and temporarily * a few more.
* Within 12 to 36h (approx. once a day), close and reopen connections * (allows updates of the database server's QEPs, prepared statements cache, etc...). */ public DefaultConnectionManager() { this("default-mgr", 2, 8, 1, 720, 2160); } @Override public String getName() { return name; } @Override public String toString() { return name; } /** * Sets the countForClearWarnings trigger, 0 = app must eat the warnings! * * @param maxCountForClearWarnings the maxcount */ public void setMaxCountForClearWarnings(int maxCountForClearWarnings) { this.maxCountForClearWarnings = maxCountForClearWarnings; } /** * Gets the current setting for clearWarnings() trigger * * @return the countForClearWarnings trigger, 0 = app must eat the warnings! */ public int getMaxCountForClearWarnings() { return maxCountForClearWarnings; } /** * Gets the attach-timeout. * * @return the max. attach time, 0 if check is disabled (default) */ public synchronized long getAttachTimeout() { return attachTimeout; } /** * Sets the attach-timeout. * * @param attachTimeout the max. attach time, 0 if check is disabled (default) */ public synchronized void setAttachTimeout(long attachTimeout) { this.attachTimeout = attachTimeout; if (attachTimeout > 0) { if (timeoutThread == null) { timeoutThread = new AttachTimeoutThread(this); timeoutThread.start(); } } else { if (timeoutThread != null) { timeoutThread.interrupt(); timeoutThread = null; } } } @Override public synchronized int getMaxSessions() { return maxDbSize; } @Override public int getSessionIdOffset() { return idOffset; } @Override public synchronized int getNumSessions() { return dbSize - freeDbCount; } @Override public int getMaxConnections() { return maxConSize; } @Override public synchronized int getNumConnections() { return conList.length - freeConCount; } @Override public synchronized Collection getConnections() { Collection connections = new ArrayList<>(); for (ManagedConnection con: conList) { if (con != null) { connections.add(con); } } return connections; } @Override public int login(Db session) { BackendInfo backendInfo = session.getBackendInfo(); if (!session.isRemote() && backendInfo.getUser() == null && backendInfo.getPassword() == null) { // for direct jdbc connections via backend config: // take user/passwd from session info if missing in backend info SessionInfo sessionInfo = session.getSessionInfo(); backendInfo = new BackendInfo(backendInfo, sessionInfo.getUserName(), sessionInfo.getPassword()); } ManagedConnection con = null; int id; synchronized (this) { if (!configurationLogged) { logConfiguration(); configurationLogged = true; } id = getSessionIndex(); // throws PersistenceException if too many sessions } try { con = createConnection(backendInfo); // lengthy operation outside synchronized! synchronized (this) { /* * because we add the connections in the same order as the sessions (1:1 mapping), the * index returned after adding the connection must be the same as for the session. */ if (addConnection(con) != id) { throw new PersistenceException(this + ": session- and connection-list out of sync"); } } } catch (RuntimeException rx) { synchronized (this) { removeSessionIndex(id); } if (con != null) { con.close(); } throw rx; } id += idOffset; LOGGER.info("{0}: assigned {1} to connection {2}, id={3}", this, session.getName(), con, id); return id; } @Override public synchronized void logout(Db session) { assertSessionBelongsToMe(session); int index = getIndexFromSession(session); removeSessionIndex(index); try { ManagedConnection con = removeConnection(index); LOGGER.info("{0}: released {1} from connection {2}, id={3}", this, session.getName(), con, session.getSessionId()); con.close(); // physically close the removed connection } finally { session.clearSessionId(); } } @Override public synchronized void cleanup(int sessionId, ManagedConnection connection) { ManagedConnection con; if (sessionId > 0) { int index = getIndexFromSessionId(sessionId); removeSessionIndex(index); con = removeConnection(index); } else { con = connection; } LOGGER.info("{0}: cleanup session ID {1} from connection {2}", this, sessionId, con); con.close(); // physically close the removed connection } @Override public void attach(Db session) { /* * The happy path is synchronized only once. * Only in case of a dead or expired connection, we need a second synchronized block. * However, the lengthy close and reopen are not synchronized and therefore will not block other * threads from using the connection manager. */ ManagedConnection con = attachImpl(session); // <<-- synchronized if (con != null) { // need reopen // not synchronized -> does not block the connection manager try { con.close(); } catch (RuntimeException ex) { LOGGER.warning("closing connection failed -> ignored", ex); } // reopen the connection con = createConnection(session.getBackendInfo()); synchronized(this) { int index = getIndexFromSession(session); conList[index] = con; LOGGER.info("{0}: connection {1} reopened", this, con); con.attachSession(session); } } } @Override public synchronized void detach(Db session) { assertSessionBelongsToMe(session); int index = getIndexFromSession(session); ManagedConnection con = conList[index]; con.detachSession(session); } @Override public synchronized void forceDetach(Db session) { assertSessionBelongsToMe(session); int index = getIndexFromSession(session); ManagedConnection con = conList[index]; con.forceDetached(); } @Override public synchronized void shutdown() { shutdownRequested = true; Thread t = timeoutThread; if (t != null) { // shutdown monitoring thread t.interrupt(); try { t.join(); // wait until thread terminates } catch (InterruptedException ex) { // daemon thread is terminated via shutdown()! LOGGER.warning(this + ": stopping the timeout thread failed", ex); } } // close all connections for (int i=0; i < conList.length; i++) { if (conList[i] != null) { ManagedConnection con = removeConnection(i); LOGGER.info("{0}: close connection {1}", this, con); con.close(); } } } /** * Logs the connection managers configuration. */ protected void logConfiguration() { LOGGER.info("{0} {1}: iniSize={2}, idOffset={3}, maxSize={4}, minMinutes={5}, maxMinutes={6}", getClass().getSimpleName(), name, iniSize, idOffset, maxDbSize, minMinutes, maxMinutes); } /** * Gets the index for a new session. * * @return the index of session in the list */ protected int getSessionIndex() { if (freeDbCount == 0) { // no more free Db entries: double the list size if (maxDbSize > 0 && dbSize >= maxDbSize) { PersistenceException pex = new PersistenceException(this + ": max. number of sessions exceeded (" + maxDbSize + ")"); pex.setTemporary(true); throw pex; } int newSize = dbSize << 1; if (maxDbSize > 0 && newSize > maxDbSize) { newSize = maxDbSize; } int[] nFreeDbList = new int[newSize]; System.arraycopy(freeDbList, 0, nFreeDbList, 0, dbSize); for (int i=newSize-1; i >= dbSize; i--) { nFreeDbList[freeDbCount++] = i; nFreeDbList[i] = -1; } dbSize = newSize; freeDbList = nFreeDbList; } return freeDbList[--freeDbCount]; } /** * Removes a session index. * * @param index the index of session in the list */ protected void removeSessionIndex(int index) { freeDbList[freeDbCount++] = index; } /** * Adds a connection to the list. * * @param con the connection to add * @return the index of connection in the list */ protected int addConnection(ManagedConnection con) { if (freeConCount == 0) { // no more free connection entries: double the list size if (maxConSize > 0 && conList.length >= maxConSize) { PersistenceException pex = new PersistenceException(this + ": max. number of connections exceeded (" + maxConSize + ")"); pex.setTemporary(true); throw pex; } // no more free Db entries: double the list size int newSize = conList.length << 1; if (maxConSize > 0 && newSize > maxConSize) { newSize = maxConSize; } ManagedConnection[] nConList = new ManagedConnection[newSize]; System.arraycopy(conList, 0, nConList, 0, conList.length); int[] nFreeConList = new int[newSize]; System.arraycopy(freeConList, 0, nFreeConList, 0, conList.length); for (int i=newSize-1; i >= conList.length; i--) { nFreeConList[freeConCount++] = i; nFreeConList[i] = -1; nConList[i] = null; } conList = nConList; freeConList = nFreeConList; } int index = freeConList[--freeConCount]; conList[index] = con; con.setIndex(index); return index; } /** * Removes a connection from the list. * * @param index the index of connection in the list * @return the removed connection */ protected ManagedConnection removeConnection(int index) { ManagedConnection con = conList[index]; conList[index] = null; freeConList[freeConCount++] = index; con.setIndex(-1); return con; } /** * Gets the internal array index from the session's session ID. * * @param session the session * @return the array index */ protected int getIndexFromSession(Db session) { int sessionId = session.getSessionId(); if (sessionId <= 0) { throw new PersistenceException(session, this + ": session already closed"); } int ndx = sessionId - idOffset; if (ndx < 0 || ndx >= dbSize) { throw new PersistenceException(session, this + ": invalid connection id=" + sessionId + ", expected " + idOffset + " - " + (idOffset + dbSize - 1)); } return ndx; } /** * Gets the internal array index from the session's session ID. * * @param sessionId the session ID * @return the array index */ protected int getIndexFromSessionId(int sessionId) { if (sessionId <= 0) { throw new PersistenceException(this + ": session already closed"); } int ndx = sessionId - idOffset; if (ndx < 0 || ndx >= dbSize) { throw new PersistenceException(this + ": invalid connection id=" + sessionId + ", expected " + idOffset + " - " + (idOffset + dbSize - 1)); } return ndx; } /** * Asserts that the session belongs to this manager. * * @param session the session */ protected void assertSessionBelongsToMe(Db session) { if (session.getConnectionManager() != this) { throw new PersistenceException(session, "session " + session + " does not belong to " + this + " but to " + session.getConnectionManager()); } } /** * Creates a managed connection. * * @param backendInfo the backend info * @return the managed connection */ protected ManagedConnection createConnection(BackendInfo backendInfo) { if (!backendInfo.isConnectable()) { throw new PersistenceException(this + ": backend info " + backendInfo + " is not connectable"); } Connection con; ManagedConnection mc; VersionInfo versionInfo; try { con = backendInfo.connect(); } catch (SQLException sx) { throw new PersistenceException(this + ": connection to '" + backendInfo + "' failed", sx); } try { versionInfo = versionInfoMap.computeIfAbsent(backendInfo.getUrl(), url -> { try { DatabaseMetaData metaData = con.getMetaData(); // may be a lengthy operation -> cached per URL return new VersionInfo(metaData.getDatabaseMajorVersion(), metaData.getDatabaseMinorVersion()); } catch (SQLException sx) { throw new PersistenceException(this + ": connection metadata could not be retrieved", sx); } }); Backend backend = backendInfo.getBackend(); backend.validateVersion(versionInfo.majorVersion, versionInfo.minorVersion); mc = new ManagedConnection(this, backend, con); } catch (RuntimeException rx) { try { con.close(); } catch (SQLException sx) { LOGGER.severe("closing connection failed -> ignored", sx); } throw rx; } if (!mc.getAutoCommit()) { mc.close(); throw new PersistenceException(this + ": connection " + mc + " is not in autoCommit mode"); } mc.setMaxCountForClearWarnings(maxCountForClearWarnings); if (minMinutes > 0) { mc.setExpireAt(mc.getEstablishedSince() + (minMinutes * 60L + random.nextInt((maxMinutes - minMinutes) * 60)) * 1000L); } LOGGER.info(() -> MessageFormat.format("{0}: open connection {1}, {2} {3}, {4}, {5}, valid until {6}", this, mc, backendInfo.getBackend().getName(), versionInfo, TransactionIsolation.valueOf(mc.getTransactionIsolation()), TransactionWritability.valueOf(!mc.isReadOnly()), FormatHelper.formatTimestampFast(new Date(mc.getExpireAt())))); return mc; } /** * Attaches the session to a connection. * * @param session the session * @return null if attached, the connection if dead or expired and needs to be reopened */ private synchronized ManagedConnection attachImpl(Db session) { assertSessionBelongsToMe(session); int index = getIndexFromSession(session); ManagedConnection con = conList[index]; if (con.isDead()) { LOGGER.warning("{0}: closing ***DEAD*** connection {1}", this, con); } else if (con.isExpired()) { LOGGER.info("{0}: closing connection {1}, open since {2}", this, con, FormatHelper.formatTimestampFast(new Date(con.getEstablishedSince()))); } else if (con.isClosed()) { LOGGER.warning("{0}: connection {1} is closed", this, con); } else { con.attachSession(session); con = null; } return con; } /** * The attach-timeout thread.
* Detaches connections that have been attached for longer than attachTimeout milliseconds. * The thread is stopped as soon as the connection manager is shut down or becomes unreferenced. */ private static class AttachTimeoutThread extends Thread { private final WeakReference cmRef; private long attachTimeout; AttachTimeoutThread(DefaultConnectionManager mgr) { super(mgr.getName() + " Attach Timeout Thread"); setDaemon(true); cmRef = new WeakReference<>(mgr); attachTimeout = mgr.attachTimeout; } @Override public void run() { LOGGER.info("{0} started with timeout={1}ms", getName(), attachTimeout); for (;;) { try { sleep(attachTimeout); } catch (InterruptedException iex) { // sleep interrupted -> check termination conditions below (mgr == null) } DefaultConnectionManager mgr = cmRef.get(); if (mgr == null || mgr.shutdownRequested || mgr.attachTimeout <= 0) { break; } attachTimeout = mgr.attachTimeout; // check for attached connections long curTime = System.currentTimeMillis(); ManagedConnection[] connections; synchronized (mgr) { connections = mgr.conList.clone(); // fast array copy! } for (ManagedConnection con : connections) { if (con != null && con.isAttached() && (curTime - con.getAttachedSince()) > attachTimeout) { Db db = con.getSession(); if (db != null) { // still un-synchronized synchronized (mgr) { // attach/detach is synchronized on me long attachedMillis = curTime - con.getAttachedSince(); if (db.getConnection() == con && con.isAttached() && attachedMillis > attachTimeout) { // double check within synchronized block // timed out try { LOGGER.warning("detaching timed out connection " + con + " from " + db.getName() + " (" + attachedMillis + "ms)"); db.forceDetached(); } catch (RuntimeException rex) { LOGGER.severe("detaching " + con + " failed", rex); } } } } } } } LOGGER.info("{0} stopped", getName()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy