org.tentackle.dbms.DefaultConnectionManager Maven / Gradle / Ivy
Show all versions of tentackle-database Show documentation
/**
* Tentackle - http://www.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 java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Random;
import org.tentackle.log.Logger;
import org.tentackle.log.LoggerFactory;
import org.tentackle.misc.FormatHelper;
import org.tentackle.session.PersistenceException;
import org.tentackle.sql.BackendInfo;
/**
* 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 implememt a M:N mapping, see the {@link MpxConnectionManager}.
*
* @author harald
*/
public class DefaultConnectionManager implements ConnectionManager {
/**
* logger for this class.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultConnectionManager.class);
/** name of the connection manager. */
final protected String name;
/** initial size. */
final protected int iniSize;
/** offset for connection IDs. */
final protected int idOffset;
/** min hours to close connections. */
final protected int minMinutes;
/** max hours to close connections. */
final protected int maxMinutes;
/** randomizer. */
final protected Random random;
/** maximum number of connections, 0 = unlimited. */
final protected 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 shutdown
private Thread timeoutThread; // thread to check for connections attached too long (default null)
/**
* Creates a new connection manager.
*
* @param name the name of the connection manager
* @param iniSize the initial iniSize of the db table
* @param maxSize the maximum number of connections, 0 = unlimited (dangerous!)
* @param idOffset the offset for connection 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) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("missing 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.name = name;
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;
}
}
/**
* 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 dbserver
* by opening connections excessively. The usual application holds 2 connections and temporarily
* 1 or 2 more. If you need more, change the connection manager in Db.
* Within 12 to 36h (approx. once a day), close and reopen connections
* (allows updates of the database servers'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(name + " Attach Timeout Thread");
timeoutThread.start();
}
}
else {
if (timeoutThread != null) {
timeoutThread.interrupt();
timeoutThread = null;
}
}
}
@Override
public synchronized int getMaxSessions() {
return maxDbSize;
}
@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) {
ManagedConnection con = createConnection(session.getBackendInfo());
synchronized(this) {
int id = addDb(session);
/**
* because we add the connections in the same order as the db (1:1 mapping), the
* index returned after adding the conncetion must be the same as for the db.
*/
if (addConnection(con) != id) {
con.close();
removeDb(id);
throw new PersistenceException(this + ": db- and connection-list out of sync");
}
id += idOffset;
LOGGER.info("{0}: assigned {1} to connection {2}, id={3}", this, session, con, id);
return id;
}
}
@Override
public synchronized void logout(Db session) {
assertSessionBelongsToMe(session);
int index = getIndexFromSessionId(session);
removeDb(index);
try {
ManagedConnection con = removeConnection(index);
LOGGER.info("{0}: released {1} from connection {2}, id={3}", this, session, con, session.getSessionId());
con.close(); // physically close the removed connection
}
finally {
session.clearSessionId();
}
}
@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 = getIndexFromSessionId(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 = getIndexFromSessionId(session);
ManagedConnection con = conList[index];
con.detachSession(session);
}
@Override
public synchronized void forceDetach(Db session) {
assertSessionBelongsToMe(session);
int index = getIndexFromSessionId(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) {
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();
}
}
}
/**
* Adds a Db to the list.
*
* @param db the db to add
* @return the index of db in the dblist
*/
protected int addDb(Db db) {
if (freeDbCount == 0) {
// no more free Db entries: double the list size
if (maxDbSize > 0 && dbSize >= maxDbSize) {
throw new PersistenceException(db, this + ": max. number of Db instances exceeded (" + maxDbSize + ")");
}
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 Db from the list.
*
* @param index the index of db in the dblist
*/
protected void removeDb(int index) {
freeDbList[freeDbCount++] = index;
}
/**
* Adds a connection to the list.
*
* @param con the connection to add
* @return the index of connection in the conlist
*/
protected int addConnection(ManagedConnection con) {
if (freeConCount == 0) {
// no more free connection entries: double the list size
if (maxConSize > 0 && conList.length >= maxConSize) {
throw new PersistenceException(this + ": max. number of connections exceeded (" + maxConSize + ")");
}
// 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 conlist
* @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 getIndexFromSessionId(Db session) {
int id = session.getSessionId();
if (id <= 0) {
throw new PersistenceException(session, this + ": session already closed");
}
int ndx = id - idOffset;
if (ndx < 0 || ndx >= dbSize) {
throw new PersistenceException(session, this + ": invalid connection id=" + id +
", 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");
}
ManagedConnection con;
try {
con = new ManagedConnection(this, backendInfo.getBackend(), backendInfo.connect());
}
catch (SQLException e) {
throw new PersistenceException(this + ": creating connection failed", e);
}
if (!con.getAutoCommit()) {
con.close();
throw new PersistenceException(this + ": connection " + con + " is not in autoCommit mode");
}
con.setMaxCountForClearWarnings(maxCountForClearWarnings);
if (minMinutes > 0) {
con.setExpireAt(con.getEstablishedSince() +
(minMinutes * 60 + random.nextInt((maxMinutes - minMinutes) * 60)) * 1000L);
}
LOGGER.info("{0}: open connection {1}, valid until {2}",
this, con, FormatHelper.formatTimestamp(new Date(con.getExpireAt())));
return con;
}
/**
* 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 = getIndexFromSessionId(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.formatTimestamp(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.
*/
private class AttachTimeoutThread extends Thread {
public AttachTimeoutThread(String name) {
super(name);
setDaemon(true);
}
@Override
public void run() {
LOGGER.info("{0} started with timeout={1}ms", getName(), attachTimeout);
long timeout;
while (!shutdownRequested && (timeout = attachTimeout) > 0) {
try {
sleep(timeout);
// check for attached connections
long curTime = System.currentTimeMillis();
ManagedConnection[] connections;
synchronized (DefaultConnectionManager.this) {
connections = conList.clone(); // fast array copy!
}
for (ManagedConnection con : connections) {
if (con != null) {
if (con.isAttached() && (curTime - con.getAttachedSince()) > timeout) {
Db db = con.getSession();
if (db != null) { // still unsyncd..
synchronized (DefaultConnectionManager.this) { // attach/detach is syncd on me
long attachedMillis = curTime - con.getAttachedSince();
if (db.getConnection() == con && con.isAttached() &&
attachedMillis > timeout) { // double check within syncd 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);
}
}
}
}
}
}
}
}
catch (InterruptedException iex) {
// sleep interrupted -> check termination conditions above
}
}
LOGGER.info("{0} stopped", getName());
}
}
}