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

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

There is a newer version: 21.16.1.0
Show newest version
/*
 * 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.common.Timestamp;
import org.tentackle.dbms.rmi.RemoteDbSessionImpl;
import org.tentackle.io.ReconnectedException;
import org.tentackle.log.Logger;
import org.tentackle.log.Logger.Level;
import org.tentackle.log.MappedDiagnosticContext;
import org.tentackle.session.PersistenceException;
import org.tentackle.session.SessionClosedException;
import org.tentackle.session.TransactionIsolation;
import org.tentackle.session.TransactionWritability;
import org.tentackle.sql.Backend;

import java.lang.ref.Cleaner;
import java.lang.ref.Cleaner.Cleanable;
import java.lang.ref.WeakReference;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Pattern;

/**
 * A jdbc connection managed by the ConnectionManager.
* * The connection provides some additional features * such as a prepared statement cache * and translates SQLExceptions to PersistenceExceptions. * The ConnectionManager is responsible to attach and detach * the connection to a session. * * @author harald */ public class ManagedConnection { /** * Minimum milliseconds a connection remains attached before being logged.
* 0 to disable, -1 to log every detach. *

* For analysis and debugging purposes only. */ private static long logMinAttachMillis; /** * Flag to turn on statement statistics. (default is turned off) */ private static boolean collectStatistics; /** * Optional periodical log of statistics in milliseconds. (default is off) *
* If turned on, the statistics will be logged and cleared every logStatisticsMillis milliseconds. */ private static long logStatisticsMillis; /** * Optional filter to log only statements for certain MappedDiagnosticContexts.
* If set, statements are only collected for logging if the MDC is valid and the pattern matches the toString-value * of the MDC. */ private static Pattern mdcFilter; /** * Optionally enable statement checks for resultSetType and resultSetConcurrency. */ private static boolean checkStatementResultSetParameters; /** * Logging level to log nested selects.
* When enabled, selects running in parallel on the same connection will be logged. * This is usually means, that from within a loop processing a result set, a method is invoked, * that in turn triggers another select. */ private static Level logLevelForParallelOpenResultSets; /** * Gets the minimum milliseconds a connection remains attached before being logged. * * @return the minimum milliseconds */ public static long getLogMinAttachMillis() { return logMinAttachMillis; } /** * Sets the minimum milliseconds a connection remains attached before being logged. * * @param millis milliseconds, 0 to disable, -1 to log every detach */ public static void setLogMinAttachMillis(long millis) { logMinAttachMillis = millis; } /** * Returns whether to collect statement statistics. * * @return true if statistics enabled */ public static boolean isCollectingStatistics() { return collectStatistics; } /** * Sets a flag whether to collect statement statistics. * * @param collect true to count invocations and execution times */ public static void setCollectingStatistics(boolean collect) { collectStatistics = collect; } /** * Gets the optional periodical log of statistics in milliseconds. * * @return the period in milliseconds, 0 if disabled (default) */ public static long getLogStatisticsMillis() { return logStatisticsMillis; } /** * Sets the optional periodical log of statistics in milliseconds.
* If turned on, the statistics will be logged and cleared every logStatisticsMillis milliseconds. * * @param millis the period in milliseconds, 0 to disable */ public static void setLogStatisticsMillis(long millis) { ManagedConnection.logStatisticsMillis = millis; } /** * Gets the MDC filter. * * @return the filter, null if none */ public static Pattern getMdcFilter() { return mdcFilter; } /** * Sets Optional filter to log only statements for certain MappedDiagnosticContexts.
* If set, statements are only collected for logging if the MDC is valid and the pattern matches the toString-value * of the MDC. * * @param filter the filter, null to clear */ public static void setMdcFilter(Pattern filter) { mdcFilter = filter; } /** * Returns whether to check that requested resulttype and concurrency matches the prepared statement reused. * * @return true if check is enabled */ public static boolean isCheckingStatementResultSetParameters() { return checkStatementResultSetParameters; } /** * Sets whether to check that requested resulttype and concurrency matches the prepared statement reused. * * @param check true to enable check */ public static void setCheckingStatementResultSetParameters(boolean check) { checkStatementResultSetParameters = check; } /** * Gets the logging level to log nested selects. * * @return the level, null if disabled */ public static Level getLogLevelForParallelOpenResultSets() { return logLevelForParallelOpenResultSets; } /** * Sets the logging level to log nested selects.
* If enabled, selects running in parallel on the same connection will be logged. * This is usually means, that from within a loop processing a result set, a method is invoked, * that in turn triggers another select. * * @param level the logging level, null to disable */ public static void setLogLevelForParallelOpenResultSets(Level level) { logLevelForParallelOpenResultSets = level; } private static final Logger LOGGER = Logger.get(ManagedConnection.class); private static final AtomicLong INSTANCE_COUNTER = new AtomicLong(); private static final Cleaner CLEANER = Cleaner.create(); // instead of deprecated finalize() /** * Holds the resources to be cleaned up.
* Notice that ManagedConnections are referenced by their ConnectionManager. * As a consequence, they will be cleaned up when the ConnectionManager is cleaned up. */ private static class Resources implements Runnable { private Connection connection; // the wrapped connection, null if closed private String name; // the name of the managed connection private boolean dead; // connection is dead (comlink error detected) private ManagedConnection me; // != null if programmatic cleanup Resources(Connection connection) { this.connection = connection; } @Override public void run() { try { Connection c = connection; if (c != null) { if (me == null) { // application lost the reference and did not close it before LOGGER.warning("closing unreferenced connection {0}", name); } try { c.close(); } catch (SQLException | RuntimeException sx) { if (!dead) { LOGGER.severe("low level close failed for " + name + " -> marked closed", sx); } } finally { connection = null; } } } finally { me = null; } } } private final Resources resources; // resources to be cleaned up private final Cleanable cleanable; // to clean up the connection private final ConnectionManager manager; // the manager that created this connection private final Backend backend; // the backend private final String backendId; // a unique ID to identify the connection at the backend private final long instanceNumber; // unique instance number (for logging only) private final long establishedSince; // connection established establishedSince... (epochal [ms]) private final int defaultTxIsolation; // the initial/default transaction isolation level private final boolean defaultReadOnly; // the initial/default read-only mode private int txIsolation; // the current transaction isolation level private boolean readOnly; // the current read-only mode private Db db; // currently attached Db, null = free connection private long expireAt; // connection shutdown at (epochal [ms]), 0 = forever private long attachedSince; // attached since when... private long detachedSince; // detached since when... private long lastVerified; // last connection verification private int index; // connection index given by connection manager, -1 not used private int attachCount; // 0 = not attached, else number of times db is attached private int maxCountForClearWarnings; // trigger when to clearWarning() on a connection (0 = disabled) private int counterForClearWarnings; // current counter private StatementHistory currentStatement; // statement history while not in transaction private long connectionInactivityTimeoutMs; // optional database connection inactivity timeout private boolean connectionKeepAliveEnabled; // optional periodical dummy select to prevent premature close of idle connection private final List statementHistory; // statement history while in transaction private final Map preparedStatements; // all prepared statements for this connection (stmtId:statement) private final Set> openResultSets; // pending open result sets (thread safe bec. of WeakRefs) private final Set> runningStatements; // running statements (thread safe bec. of WeakRefs) /** * Creates a managed connection. * * @param manager the connection manager * @param backend the backend * @param connection the low level JDBC connection */ public ManagedConnection(ConnectionManager manager, Backend backend, Connection connection) { this.manager = Objects.requireNonNull(manager, "manager"); this.backend = Objects.requireNonNull(backend, "backend"); backendId = backend.getBackendId(Objects.requireNonNull(connection, "connection")); instanceNumber = INSTANCE_COUNTER.incrementAndGet(); establishedSince = System.currentTimeMillis(); detachedSince = establishedSince; index = -1; statementHistory = new ArrayList<>(); preparedStatements = new ConcurrentHashMap<>(); // use a thread-safe set due to concurrent cleanup of weak references in getOpenResultSets and cancelRunningStatements openResultSets = ConcurrentHashMap.newKeySet(); runningStatements = ConcurrentHashMap.newKeySet(); resources = new Resources(connection); resources.name = toString(); // cannot be a constructor arg because of chicken egg cleanable = CLEANER.register(this, resources); defaultTxIsolation = txIsolation = getTransactionIsolation(); defaultReadOnly = readOnly = isReadOnly(); ManagedConnectionMonitor.getInstance().registerManagedConnection(this); } /** * Gets the connection manager * * @return the manager */ public ConnectionManager getManager() { return manager; } /** * Gets the backend. * * @return the backend */ public Backend getBackend() { return backend; } /** * Gets the low level connection. * * @return the physical connection, never null * @throws PersistenceException if connection is closed */ public Connection getConnection() { Connection c = resources.connection; if (c == null) { throw new PersistenceException("connection is closed"); } return c; } /** * Gets the optional connection inactivity timeout. * * @return the timeout in [ms] */ public long getConnectionInactivityTimeoutMs() { return connectionInactivityTimeoutMs; } /** * Sets the optional connection inactivity timeout. * * @param connectionInactivityTimeoutMs the timeout in [ms] */ public void setConnectionInactivityTimeoutMs(long connectionInactivityTimeoutMs) { this.connectionInactivityTimeoutMs = connectionInactivityTimeoutMs; } /** * Returns whether dummy selects are sent periodically to the backend. * * @return true if idle connections kept alive */ public boolean isConnectionKeepAliveEnabled() { return connectionKeepAliveEnabled; } /** * Sets the optional flag to send dummy selects periodically to the backend. * * @param connectionKeepAliveEnabled true to keep idle connections alive */ public void setConnectionKeepAliveEnabled(boolean connectionKeepAliveEnabled) { this.connectionKeepAliveEnabled = connectionKeepAliveEnabled; } /** * Gets the epochal time in ms when this connection was verified. * * @return the last verification, null if never been verified at all */ public long getLastVerified() { return lastVerified; } /** * Gets the epochal time when this connection was established. * * @return the time establishedSince in ms */ public long getEstablishedSince() { return establishedSince; } /** * Sets the epochal time when this connection should be closed, if unused. * * @param expireAt the time in [ms], 0 = forever */ public void setExpireAt(long expireAt) { this.expireAt = expireAt; } /** * Gets the epochal time when this connection should be closed, if unused. * * @return the time in [ms], 0 = forever */ public long getExpireAt() { return expireAt; } /** * Gets the epochal time when this connection was attached. * * @return the epochal time of last attach, 0 if not attached */ public long getAttachedSince() { return attachedSince; } /** * Gets the epochal time when this connection was detached. *

* Note that newly created connections get their detach time initialized * from the current system time. * * @return the epochal time of last detach, 0 if attached */ public long getDetachedSince() { return detachedSince; } /** * Sets the connection index. * Connection managers use that to manage connection lists. * * @param index the connection index */ public void setIndex(int index) { this.index = index; } /** * Gets the connection index. * * @return the connection index. */ public int getIndex() { return index; } /** * Marks a connection being dead. *

* Marking connections dead allows connection managers like * {@link MpxConnectionManager} to re-open connections before * being attached next time. Notice that not all connection managers * honour the dead-flag (makes only sense in servers, anyway). * * @param dead true if marked dead, false not dead */ public void setDead(boolean dead) { if (dead && !resources.dead) { LOGGER.severe("managed connection {0}: *** MARKED DEAD ***", this); } resources.dead = dead; } /** * Returns whether connection is marked dead * @return true if dead */ public boolean isDead() { return resources.dead; } /** * Returns whether timeout is enabled and connection wasn't use for a long time and needs verification. * * @return return true if {@link #verifyConnection()} should be invoked before use */ public boolean isConnectionVerificationNecessary() { return isConnectionVerificationNecessary(System.currentTimeMillis()); } /** * Returns whether timeout is enabled and connection wasn't use for a long time and needs verification. * * @param currentTimeMillis the current epochal time in milliseconds * @return return true if {@link #verifyConnection()} should be invoked before use */ public boolean isConnectionVerificationNecessary(long currentTimeMillis) { return connectionInactivityTimeoutMs > 0 && detachedSince > 0 && currentTimeMillis - detachedSince >= connectionInactivityTimeoutMs; } /** * Checks whether connection is still valid.

* Usually implemented by the backend via a "SELECT 1" query.
* If the check fails the connection is marked dead. * * @return true if connection still valid, false if invalid * @see Backend#getDummySelect() */ public boolean verifyConnection() { try (Statement stmt = getConnection().createStatement()) { // Can be invoked at any time, even if another statement is currently running // on this connection (submitted from another thread) since JDBC-drivers are thread-safe these days. // However, the invocation may be blocked until the other statement finishes. stmt.executeQuery(getBackend().getDummySelect()); lastVerified = System.currentTimeMillis(); return true; } // stmt.close closes the result set as well catch (RuntimeException | SQLException ex) { setDead(true); return false; } } /** * Attaches a connection to a session. * Connections must be attached before it can be used by statements * or starting a tx. * The method must only be invoked by a connection manager! * * @param db the logical Db to attach */ public void attachSession(Db db) { Objects.requireNonNull(db, "db"); if (this.db != null) { if (this.db == db) { if (db.getConnection() != this) { throw new PersistenceException(db, "db lost current connection " + this + ", count=" + attachCount); } attachCount++; } else { throw new PersistenceException(db, "connection " + this + " already attached to " + this.db); } } else { if (attachCount != 0) { throw new PersistenceException(db, "attach count of unattached connection " + this + " is not 0, but " + attachCount); } this.db = db; db.setConnection(this); attachCount = 1; attachedSince = System.currentTimeMillis(); detachedSince = 0; } LOGGER.finer("{0} attached to {1}, count={2}", db, this, attachCount); } /** * Checks whether a session is attached to this connection. * * @return true if attached, else false. */ public boolean isAttached() { return db != null; } /** * Returns whether a transaction is running. * * @return true if connection is attached to a session in transaction */ public boolean isTxRunning() { return isAttached() && db.isTxRunning(); } /** * Returns whether connection has expired.
* Expired connections should be closed and reopened. * * @return true if expired */ public boolean isExpired() { return expireAt > 0 && expireAt < detachedSince; } /** * Detaches a session from a connection. * Connections must be detached before they can be used by another Db. * The method must only be invoked by a connection manager! * * @param db the db to detach */ public void detachSession(Db db) { Objects.requireNonNull(db, "db"); if (this.db != db) { throw new PersistenceException(db, "connection " + this + " not attached to " + db + " (instead attached to " + this.db + ")"); } LOGGER.finer("{0} detached from {1}, count={2}", db, this, attachCount); attachCount--; if (attachCount == 0) { detachedSince = System.currentTimeMillis(); if (logMinAttachMillis != 0 && detachedSince - attachedSince > logMinAttachMillis) { if (isMDCValid()) { LOGGER.warning("attached for {0}ms: {1}", () -> detachedSince - attachedSince, this::toDiagnosticString); } } else if (isMDCValid()) { LOGGER.fine(this::toDiagnosticString); } this.db = null; db.setConnection(null); attachedSince = 0; if (collectStatistics) { if (logStatisticsMillis > 0 && detachedSince - StatementStatistics.getSince() > logStatisticsMillis) { // log and clear statistics StatementStatistics.log(Logger.Level.INFO, true); DbTransactionFactory.getInstance().logStatistics(Logger.Level.INFO, true); } // collect all statements if (isMDCValid()) { for (StatementHistory history: statementHistory) { StatementStatistics.collect(history); } if (currentStatement != null) { StatementStatistics.collect(currentStatement); } } } statementHistory.clear(); currentStatement = null; restoreTransactionDefaults(); } else { if (db.getConnection() != this) { throw new PersistenceException(db, "db lost current connection " + this + ", count=" + attachCount++); } if (attachCount < 0) { throw new PersistenceException(db, "connection " + this + " detached too often, attachcount = " + attachCount++); } } } /** * Gets tha attached session. * * @return the session, null if not attached */ public Db getSession() { return db; } /** * Gets the unique instance number. * * @return the instance number */ public long getInstanceNumber() { return instanceNumber; } /** * Gets the backend id. * * @return the unique id */ public String getBackendId() { return backendId; } /** * Gets the connection name. * * @return the unique name */ public String getName() { StringBuilder buf = new StringBuilder(); buf.append(instanceNumber); if (backendId != null) { buf.append('/').append(backendId); } return buf.toString(); } @Override public String toString() { StringBuilder buf = new StringBuilder(); Connection con = resources.connection; // local variable because could be closed between test and use if (con == null) { buf.append(""); } else { buf.append(con); } buf.append('[').append(getName()); if (index >= 0) { buf.append('/').append(index); } buf.append(']'); if (attachCount > 0) { buf.append(", attached ").append(attachCount).append(" since ").append(new Timestamp(attachedSince)); } if (resources.dead) { buf.append(", *DEAD*"); } return buf.toString(); } /** * Checks whether the mapped diagnostic context is valid for statistics logging. * * @return true if loggable * @see #mdcFilter */ protected boolean isMDCValid() { if (mdcFilter == null) { return true; } MappedDiagnosticContext mdc = LOGGER.getMappedDiagnosticContext(); return mdc != null && mdc.matchesPattern(mdcFilter); } /** * Logs the execution start of a statement during a transaction.
* The execution start is the current epochal time. * The execution end must be set via {@link StatementHistory#end()} * * @param statement the statement * @param sql optional SQL string if statement does not provide it * @return the history with an execution start of now and execution end not set, never null */ StatementHistory logStatementHistory(StatementWrapper statement, String sql) { StatementHistory history = new StatementHistory(statement, sql); if (isTxRunning()) { statementHistory.add(history); } else { currentStatement = history; } return history; } /** * Gets the statement history.
* * @return the statements executed during last attachment */ public List getStatementHistory() { return statementHistory; } /** * Adds a pending result set. * * @param rs the result set */ public void addResultSet(ResultSetWrapper rs) { openResultSets.add(new WeakReference<>(rs)); if (logLevelForParallelOpenResultSets != null && openResultSets.size() > 1 && LOGGER.isLoggable(logLevelForParallelOpenResultSets)) { Collection parallelResultStatements = getOpenResultSets(); if (!parallelResultStatements.isEmpty()) { int count = 0; StringBuilder buf = new StringBuilder(); buf.append("parallel queries running on ").append(this).append(":"); for (ResultSetWrapper prs: parallelResultStatements) { StatementWrapper stmt = prs.getStatement(); if (stmt != null && !stmt.isParallelOk()) { buf.append('\n').append(prs); count++; } } if (count > 1) { LOGGER.log(logLevelForParallelOpenResultSets, buf.toString(), null); } } } } /** * Removes a pending result set. * * @param rs the result set */ public void removeResultSet(ResultSetWrapper rs) { for (Iterator> iter = openResultSets.iterator(); iter.hasNext(); ) { WeakReference ref = iter.next(); ResultSetWrapper refRs = ref.get(); // if closed or unreferenced or the db to remove if (refRs == null || refRs == rs) { // == is okay here iter.remove(); } } } /** * Gets all open result sets for this connection. * * @return the result sets, never null */ public Collection getOpenResultSets() { List rsList = new ArrayList<>(); for (Iterator> iter = openResultSets.iterator(); iter.hasNext(); ) { WeakReference ref = iter.next(); ResultSetWrapper refRs = ref.get(); if (refRs != null) { rsList.add(refRs); } else { iter.remove(); } } return rsList; } /** * Creates a diagnostic string. * * @return the connection's status info */ public String toDiagnosticString() { StringBuilder buf = new StringBuilder(toString()); buf.append(": ") .append(manager) .append('[') .append(index) .append("] valid ") .append(new Date(establishedSince)) .append(" - "); if (expireAt != 0) { buf.append(new Date(expireAt)); } else { buf.append(" "); } if (readOnly != defaultReadOnly) { buf.append(", ").append(TransactionWritability.valueOf(!readOnly)); } if (txIsolation != defaultTxIsolation) { buf.append(", ").append(TransactionIsolation.valueOf(txIsolation)); } Db lDb = db; // may be closed in the meantime... if (lDb != null) { buf.append("\n -> ").append(lDb.getName()); if (lDb.getTxName() != null) { buf.append('/').append(lDb.getTxName()); } Thread ownerThread = lDb.getOwnerThread(); if (ownerThread != null) { buf.append(", ").append(lDb.getOwnerThread()); } for (RemoteDbSessionImpl session: RemoteDbSessionImpl.getOpenSessions()) { if (session.getSession() == lDb) { // this is my session! buf.append(", ").append(session); } } } try { if (!statementHistory.isEmpty() || currentStatement != null) { buf.append("\n\n Statements:"); try { buf.append(logStatementHistory(statementHistory)); } catch (ConcurrentModificationException cmx) { buf.append(" (history may be incomplete because connection is still in use by other threads)"); buf.append(logStatementHistory(new ArrayList<>(statementHistory))); // try on copy } StatementHistory stmt = currentStatement; if (stmt != null) { buf.append("\n "); buf.append(stmt); } buf.append("\n"); } } catch (RuntimeException rex) { LOGGER.warning("cannot dump statement history", rex); } try { // dump all open result sets (threadsafe) Collection rsList = getOpenResultSets(); if (!rsList.isEmpty()) { buf.append("\n\n Resultsets:"); for (ResultSetWrapper rs: rsList) { buf.append("\n "); buf.append(rs); } buf.append("\n"); } } catch (RuntimeException rex) { LOGGER.warning("cannot dump open result sets", rex); } return buf.toString(); } private StringBuilder logStatementHistory(Collection statementHistory) { StringBuilder buf = new StringBuilder(); for (StatementHistory stmt : statementHistory) { buf.append("\n ").append(stmt); } return buf; } /** * asserts that a connection is attached */ private void assertAttached() { if (db == null) { throw new PersistenceException("connection " + this + " not attached to any Db"); } } /** * Gets the current transaction isolation level. * * @return the isolation level * @see Connection#getTransactionIsolation() */ public int getTransactionIsolation() { try { return getConnection().getTransactionIsolation(); } catch (SQLException sx) { throw createFromSqlException("getting transaction isolation level failed", sx); } } /** * Sets the transaction isolation level. * * @param txIsolation the isolation level * @see Connection#setTransactionIsolation(int) */ public void setTransactionIsolation(int txIsolation) { try { getConnection().setTransactionIsolation(txIsolation); this.txIsolation = txIsolation; } catch (SQLException sx) { throw createFromSqlException("setting transaction isolation level to " + txIsolation + " failed", sx); } } /** * Gets the read-only mode. * * @return true if read-only * @see Connection#isReadOnly() */ public boolean isReadOnly() { try { return getConnection().isReadOnly(); } catch (SQLException sx) { throw createFromSqlException("getting read-only mode failed", sx); } } /** * Sets the read-only mode. * * @param readOnly true if read-only * @see Connection#setReadOnly(boolean) */ public void setReadOnly(boolean readOnly) { try { getConnection().setReadOnly(readOnly); this.readOnly = readOnly; } catch (SQLException sx) { throw createFromSqlException("setting read-only mode to " + readOnly + " failed", sx); } } /** * Sets the autocommit feature. *

* Restores the default transaction isolation level and read-only mode, if set to true and isolation or read-only mode was changed. * * @param autoCommit true to enable autocommit, false to disable. */ public void setAutoCommit(boolean autoCommit) { assertAttached(); try { getConnection().setAutoCommit(autoCommit); if (autoCommit) { restoreTransactionDefaults(); } } catch (SQLException ex) { throw createFromSqlException("setting autocommit to " + autoCommit + " failed", ex); } } /** * Gets the autocommit value. * * @return the autocommit value. */ public boolean getAutoCommit() { try { return getConnection().getAutoCommit(); } catch (SQLException ex) { throw createFromSqlException("getting autocommit failed", ex); } } /** * Performs a commit. */ public void commit() { assertAttached(); try { getConnection().commit(); } catch (SQLException ex) { throw createFromSqlException("commit failed", ex); } } /** * Performs a rollback. * * @param withLog true if log via INFO */ public void rollback(boolean withLog) { assertAttached(); try { if (withLog) { LOGGER.info(this::toDiagnosticString); } getConnection().rollback(); // unmark all statements marked ready unmarkPreparedStatements(); } catch (SQLException ex) { throw createFromSqlException("rollback failed: " + toDiagnosticString(), ex); } } /** * Creates an unnamed savepoint in the current transaction. * * @return the savepoint */ public Savepoint setSavepoint() { assertAttached(); try { return getConnection().setSavepoint(); } catch (SQLException ex) { throw createFromSqlException("setting unnamed savepoint failed: " + toDiagnosticString(), ex); } } /** * Creates a savepoint with the given name in the current transaction. * * @param name the savepoint name * * @return the savepoint */ public Savepoint setSavepoint(String name) { assertAttached(); try { return getConnection().setSavepoint(name); } catch (SQLException ex) { throw createFromSqlException("setting savepoint '" + name + "' failed: " + toDiagnosticString(), ex); } } /** * Undoes all changes made after the given Savepoint object was set. * * @param savepoint the savepoint */ public void rollback(Savepoint savepoint) { assertAttached(); try { getConnection().rollback(savepoint); } catch (SQLException ex) { throw createFromSqlException("rollback of savepoint '" + savepoint + "' failed: " + toDiagnosticString(), ex); } } /** * Removes the specified Savepoint and subsequent Savepoint objects from the current * transaction. * * @param savepoint the savepoint to be removed */ public void releaseSavepoint(Savepoint savepoint) { assertAttached(); if (db.getBackend().isReleaseSavepointSupported()) { try { getConnection().releaseSavepoint(savepoint); } catch (SQLException ex) { throw createFromSqlException("release of savepoint '" + savepoint + "' failed: " + toDiagnosticString(), ex); } } } /** * Remembers a statement as running. * * @param statement the statement */ public void addRunningStatement(StatementWrapper statement) { runningStatements.add(new WeakReference<>(statement)); } /** * Removes a statement from the running statements. * * @param statement the statement */ public void removeRunningStatement(StatementWrapper statement) { // usually there is no more than one running statement at a time, so the loop is fast for (Iterator> iter = runningStatements.iterator(); iter.hasNext(); ) { WeakReference ref = iter.next(); StatementWrapper stmt = ref.get(); if (stmt == null || stmt == statement) { iter.remove(); } } } /** * Cancels the(all) currently running statement(s).
* May be invoked from any thread. */ public void cancelRunningStatements() { for (Iterator> iter = runningStatements.iterator(); iter.hasNext(); ) { WeakReference ref = iter.next(); StatementWrapper stmt = ref.get(); if (stmt != null && stmt.isRunning()) { try { stmt.cancel(); } catch (RuntimeException ex) { // just log LOGGER.warning("canceling statement failed: " + stmt, ex); } } iter.remove(); } } /** * Unmark all prepared statements that are marked ready. */ private void unmarkPreparedStatements() { // unmark all statements marked ready for (PreparedStatementWrapper stmt: preparedStatements.values()) { if (stmt != null && stmt.isMarkedReady()) { try { stmt.consume(); } catch (RuntimeException ex) { // just log (if not cancelled) if (!stmt.isCancelled()) { LOGGER.warning("unmarking statement failed: " + stmt, ex); } } } } } /** * Reads all warnings, logs and clears them. */ public void logAndClearWarnings() { try { // log connection warning SQLWarning warning = getConnection().getWarnings(); while (warning != null) { LOGGER.warning(warning.getMessage()); warning = warning.getNextWarning(); } getConnection().clearWarnings(); // release memory used by warnings // log prepared statement warnings for (PreparedStatementWrapper stmt: preparedStatements.values()) { if (!stmt.isClosed()) { // if not closed warning = stmt.getStatement().getWarnings(); while (warning != null) { LOGGER.warning(warning.getMessage()); warning = warning.getNextWarning(); } stmt.getStatement().clearWarnings(); } } // as this statement is called when there are no more running statements // we ignore the unprepared running statements. } catch (SQLException e) { if (!resources.dead) { throw createFromSqlException("reading warnings failed", e); } } } /** * Sets the countForClearWarnings trigger. * * @param max the maxcount, 0 = app must eat the warnings */ public void setMaxCountForClearWarnings(int max) { maxCountForClearWarnings = max; } /** * Gets the current setting for clearWarnings() trigger. * * @return the maxcount, 0 = app must eat the warnings */ public int getMaxCountForClearWarnings() { return maxCountForClearWarnings; } /** * Increments a counter and empties the warnings on the connection * and all prepared statements if a trigger value is reached. * This is necessary for apps that don't * clearWarnings() on their own, otherwise filling up memory might occur. */ public void countForClearWarnings() { if (maxCountForClearWarnings > 0) { counterForClearWarnings++; if (counterForClearWarnings >= maxCountForClearWarnings) { logAndClearWarnings(); counterForClearWarnings = 0; } } } /** * Forces the connection to be detached from the session.
* The method is used from connection managers only, hence package scope. */ void forceDetached() { if (isAttached()) { // database is still attached LOGGER.warning("forcing detach of connection {0}, attached to {1}", this, db); // close pending statements closePreparedStatements(true); if (!isClosed()) { try { if (!getAutoCommit()) { // in transaction!!! rollback LOGGER.severe("rolling back pending transaction for {0}", db); rollback(true); } } catch (PersistenceException pex) { if (!resources.dead) { LOGGER.severe("connection is broken", pex); } closeImpl(); } } if (db != null) { // if not already detached via closePreparedStatements(true) db.setConnection(null); db = null; } attachCount = 0; statementHistory.clear(); currentStatement = null; openResultSets.clear(); restoreTransactionDefaults(); } // else already detached } /** * Closes the connection.
* Closing an already closed connection is allowed. */ public void close() { if (!isClosed()) { try { // rollback before close to prevent an implicit commit (JDBC implementation specific behaviour) if (!getConnection().getAutoCommit()) { LOGGER.warning("closing connection while in transaction:\n{0}", this::toDiagnosticString); getConnection().rollback(); } } catch (SQLException ex) { if (!resources.dead) { LOGGER.warning("low level rollback failed: {0}", ex.getMessage()); } } logAndClearWarnings(); forceDetached(); closePreparedStatements(false); closeImpl(); } } /** * Closes the connection. */ private void closeImpl() { resources.me = this; cleanable.clean(); } /** * Gets the connection's closed state. * * @return true if connection is closed */ public boolean isClosed() { return resources.connection == null; } /** * Closes prepared statements. * * @param onlyMarkedReady true if close only pending statements, false if all */ public void closePreparedStatements(boolean onlyMarkedReady) { // close all statements for (Iterator iter = preparedStatements.values().iterator(); iter.hasNext(); ) { PreparedStatementWrapper stmt = iter.next(); if (!stmt.isClosed() && // if not already closed (!onlyMarkedReady || stmt.isMarkedReady())) { // or all or only pending try { stmt.close(); } catch (RuntimeException ex) { // just log, we need to close them all if (!resources.dead) { LOGGER.warning("closing statement failed: " + stmt, ex); } } } if (onlyMarkedReady) { iter.remove(); } } if (!onlyMarkedReady) { preparedStatements.clear(); } } /** * Removes a closed prepared statement. * * @param st the statement */ public void removePreparedStatement(PreparedStatementWrapper st) { if (!st.isClosed()) { throw new PersistenceException(db, "statement still open: " + st); } StatementKey key = st.getStatementKey(); if (key != null) { // if not a one-shot preparedStatements.remove(st.getStatementKey()); } } /** * Creates a non-prepared statement.
* * One-shot statements (i.e. non-prepared statements) must attach the db as soon as they * are instantiated. The db is detached after executeUpdate or after executeQuery when * its result set is closed. * * @param resultSetType a result set type; one of * ResultSet.TYPE_FORWARD_ONLY, * ResultSet.TYPE_SCROLL_INSENSITIVE, or * ResultSet.TYPE_SCROLL_SENSITIVE * @param resultSetConcurrency a concurrency type; one of * ResultSet.CONCUR_READ_ONLY or * ResultSet.CONCUR_UPDATABLE * @return a new Statement object that will generate * ResultSet objects with the given type and * concurrency */ public StatementWrapper createStatement (int resultSetType, int resultSetConcurrency) { try { Statement stmt = getConnection().createStatement(resultSetType, resultSetConcurrency); return new StatementWrapper(this, stmt); } catch (SQLException ex) { throw createFromSqlException("creating statement failed for " + this, ex); } } /** * Creates a prepared statement. * * @param statementKey the statement key, null if one-shot * @param sql the sql code * @param resultSetType the result set type * @param resultSetConcurrency the result set concurrency * @return the statement */ public PreparedStatementWrapper createPreparedStatement(StatementKey statementKey, String sql, int resultSetType, int resultSetConcurrency) { try { sql = backend.optimizeSql(sql); return new PreparedStatementWrapper( this, getConnection().prepareStatement(sql, resultSetType, resultSetConcurrency), statementKey, sql); } catch (SQLException e) { throw createFromSqlException("creating prepared statement failed", e); } } /** * Gets a prepared statement.
* * The statement will be reused if already prepared. * Else it will be prepared according to the statement definition in BackendStatement. * * @param statementKey the unique statement key * @param alwaysPrepare true if always do a physical prepare * @param resultSetType one of ResultSet.TYPE_... * @param resultSetConcurrency one of ResultSet.CONCUR_... * @param sqlSupplier the SQL supplier * @return the prepared statement for this connection */ public PreparedStatementWrapper getPreparedStatement (StatementKey statementKey, boolean alwaysPrepare, int resultSetType, int resultSetConcurrency, SqlSupplier sqlSupplier) { if (statementKey == null) { throw new PersistenceException("statement key required"); } assertAttached(); PreparedStatementWrapper prepStmt = preparedStatements.get(statementKey); if (prepStmt == null || prepStmt.isClosed() || alwaysPrepare) { // we need to prepare it prepStmt = createPreparedStatement(statementKey, sqlSupplier.get(backend).toString(), resultSetType, resultSetConcurrency); preparedStatements.put(statementKey, prepStmt); LOGGER.finer("statement {0} prepared on {1}", prepStmt, this); } else { // already created: check that parameters are the same if (checkStatementResultSetParameters) { checkStatement(prepStmt, resultSetType, resultSetConcurrency); } LOGGER.finer("statement {0} re-used on {1}", prepStmt, this); } return prepStmt; } /** * Checks for a dead communications link.
* If the link is down, for example the database server closed it, * the connection is marked dead. * * @param ex the sql exception * @return true if connection is dead */ public boolean checkForDeadLink(SQLException ex) { if (!isDead() && backend.isCommunicationLinkException(ex)) { // some severe comlink error, probably closed by server setDead(true); } return isDead(); } /** * Creates a {@link PersistenceException} or {@link SessionClosedException} depending on the SQL error.
* If the session was reconnected, a {@link ReconnectedException} is returned. * * @param message the message * @param sx the SQL exception * @return the persistence exception */ public PersistenceException createFromSqlException(String message, SQLException sx) { PersistenceException persistenceException; if (checkForDeadLink(sx)) { persistenceException = new SessionClosedException(db, message, sx); Db ldb = db; if (ldb != null && ldb.optionallyReconnect()) { throw new ReconnectedException(persistenceException); } } else { persistenceException = new PersistenceException(db, message, sx); } return persistenceException; } /** * Check that requested resulttype and concurrency matches the prepared statement to reuse. * * @param prepStmt the prepared statement * @param resultSetType the requested result set type * @param resultSetConcurrency the requested result set concurrency */ private void checkStatement(PreparedStatementWrapper prepStmt, int resultSetType, int resultSetConcurrency) { Statement stmt = prepStmt.getStatement(); try { if (stmt.getResultSetType() != resultSetType) { throw new PersistenceException("wrong requested resultset type: " + resultSetType + " != statement: " + stmt.getResultSetType() + " for '" + prepStmt.getSql() + "'"); } if (stmt.getResultSetConcurrency() != resultSetConcurrency) { throw new PersistenceException("wrong requested resultset concurrency: " + resultSetConcurrency + " != statement: " + stmt.getResultSetConcurrency() + " for '" + prepStmt.getSql() + "'"); } } catch (SQLException sqx) { throw createFromSqlException("cannot determine prepared statement configuration for '" + prepStmt.getSql() + "'", sqx); } } private void restoreTransactionDefaults() { if (txIsolation != defaultTxIsolation) { setTransactionIsolation(defaultTxIsolation); } if (readOnly != defaultReadOnly) { setReadOnly(defaultReadOnly); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy