ResetAbandonedTimer.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tomcat.jdbc.pool.interceptor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Wrapper;
import javax.management.ObjectName;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.PooledConnection;
import org.apache.tomcat.jdbc.pool.jmx.JmxUtil;
import org.apache.tomcat.jdbc.pool.util.MessageFactory;
/**
* Class that resets the abandoned timer on any activity on the Connection, on any query execution and on any read of a
* {@link java.sql.ResultSet} returned by a statement obtained from the pooled connection, whether the operation
* completed successfully or failed. Iterating a large result set therefore keeps the connection alive through the fetch
* phase, not only until the query returns. Each reset happens when an individual call completes, so a single call that
* itself blocks on the database for longer than
* {@link org.apache.tomcat.jdbc.pool.PoolConfiguration#setRemoveAbandonedTimeout(int)} still gets the connection
* abandoned. The statement facade is always used, so a result set handle that outlives its borrow is sealed off before
* it can go on refreshing the abandoned timer of whoever holds that physical connection next. The handles this
* interceptor hands out release what they wrap - the statement layer, the physical result set - once a close has been
* forwarded to it, and answer any later call from the closed contract, as the statement facade's own handles do; a
* handle the application keeps beyond its close therefore stops keeping the physical objects below it reachable. This
* interceptor is useful for when you have a
* {@link org.apache.tomcat.jdbc.pool.PoolConfiguration#setRemoveAbandonedTimeout(int)} that is fairly low, and you
* want to reset the abandoned time each time any operation on the connection is performed This is useful for batch
* processing programs that use connections for extensive amount of times.
*/
public class ResetAbandonedTimer extends AbstractQueryReport implements ResetAbandonedTimerMBean {
private static final Log log = LogFactory.getLog(ResetAbandonedTimer.class);
private PooledConnection pcon;
private ObjectName oname = null;
/**
* Default constructor.
*/
public ResetAbandonedTimer() {
}
@Override
public void reset(ConnectionPool parent, PooledConnection con) {
super.reset(parent, con);
if (con == null) {
this.pcon = null;
if (oname != null) {
JmxUtil.unregisterJmx(oname);
oname = null;
}
} else {
this.pcon = con;
if (oname == null) {
String keyprop = ",JdbcInterceptor=" + getClass().getSimpleName();
oname = JmxUtil.registerJmx(pcon.getObjectName(), keyprop, this);
}
}
}
@Override
public boolean resetTimer() {
boolean result = false;
if (pcon != null) {
pcon.setTimestamp(System.currentTimeMillis());
result = true;
}
return result;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return super.invoke(proxy, method, args);
} finally {
// The operation was attempted on the connection, which counts
// as activity even when it failed
resetTimer();
}
}
/**
* Wraps the query reporting statement proxy so that the result sets handed to the application reset the abandoned
* timer while they are read. Statements that this interceptor does not proxy are passed through unchanged.
*/
@Override
public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
Object result = super.createStatement(proxy, method, args, statement, time);
if (result == statement) {
// Not one of the statement types we proxy, or the proxy
// creation above failed - nothing to wrap
return result;
}
try {
String name = method.getName();
Class<?> type = Statement.class;
int index = CREATE_STATEMENT_IDX;
if (compare(PREPARE_STATEMENT, name)) {
type = PreparedStatement.class;
index = PREPARE_STATEMENT_IDX;
} else if (compare(PREPARE_CALL, name)) {
type = CallableStatement.class;
index = PREPARE_CALL_IDX;
}
return getConstructor(index, type).newInstance(new Object[] { new TimerStatementProxy(result) });
} catch (Exception x) {
log.warn(MessageFactory.get("resetAbandonedTimer.unableToCreateTimerStatementProxy",
getClass().getName()), x);
}
return result;
}
@Override
protected void prepareCall(String query, long time) {
resetTimer();
}
@Override
protected void prepareStatement(String sql, long time) {
resetTimer();
}
@Override
public void closeInvoked() {
resetTimer();
}
@Override
protected String reportFailedQuery(String query, Object[] args, String name, long start, long delta,
Throwable t) {
// The timer only needs to know that the execution was attempted, so
// it takes no interest in the start or the duration it reports
resetTimer();
return super.reportFailedQuery(query, args, name, start, delta, t);
}
@Override
protected String reportQuery(String query, Object[] args, String name, long start, long delta) {
resetTimer();
return super.reportQuery(query, args, name, start, delta);
}
@Override
protected String reportSlowQuery(String query, Object[] args, String name, long start, long delta) {
resetTimer();
return super.reportSlowQuery(query, args, name, start, delta);
}
/**
* Proxy for a statement that turns the result sets the statement hands out into {@link TimerResultSetProxy}
* instances, whatever the method that returned one was called. Every other call is passed straight through to the
* wrapped statement, and so is what the {@link java.sql.Wrapper} methods hand back: a driver object reached through
* {@code unwrap()} stays the driver's own object, even one that implements {@link java.sql.ResultSet}.
*/
private final class TimerStatementProxy implements InvocationHandler {
/**
* The statement proxy created by {@code AbstractQueryReport}, nulled
* once a close has been forwarded so that a handle the application
* keeps beyond its close stops pinning the statement layer beneath it.
* A close can come from a thread other than the one invoking on the
* handle - the pool's statement finalizer closes the statements it
* tracks from whichever thread closes the connection - so the field is
* read by more threads than the one that nulls it and, as in the
* facade's and the query report proxy, must be volatile to reach them
* at all.
*/
private volatile Object delegate;
TimerStatementProxy(Object delegate) {
this.delegate = delegate;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Handle identity on this proxy, like the statement facade
// does, rather than on the object below it: the application
// must be able to rely on its own handles
if (compare(TOSTRING_VAL, method)) {
return toString();
}
if (compare(EQUALS_VAL, method)) {
if (args[0] == null || !Proxy.isProxyClass(args[0].getClass())) {
return Boolean.FALSE;
}
return Boolean.valueOf(this.equals(Proxy.getInvocationHandler(args[0])));
}
if (compare(HASHCODE_VAL, method)) {
return Integer.valueOf(this.hashCode());
}
// Read the delegate once for this call: the close that nulls it
// can come from another thread, and the guards and the forward
// must agree on the one value this call read. A call that finds
// the delegate gone is answered from the closed contract below -
// the same answers the query report proxy beneath gives for its
// own closed state - rather than by invoking on a null receiver:
// that is a NullPointerException, where the closed contract owes
// the application a SQLException. The window this cannot close is
// the race itself: a call that read the delegate while it was
// still live goes to it, an outcome indistinguishable from having
// simply outrun the close.
final Object target = delegate;
final boolean close = compare(CLOSE_VAL, method);
// Allow close to be called multiple times, from either thread
if (close && target == null) {
return null;
}
if (compare(ISCLOSED_VAL, method) && target == null) {
return Boolean.TRUE;
}
if (target == null) {
throw new SQLException(MessageFactory.get("resetAbandonedTimer.statementClosed"));
}
Object result;
try {
// Execute the call on the delegate this call read as live:
// the snapshot keeps a concurrent close from replacing the
// receiver with null between the guards and the call
result = method.invoke(target, args);
} catch (InvocationTargetException x) {
Throwable cause = x.getCause();
if (cause == null) {
throw x;
}
throw cause;
}
// Perform close cleanup: the handle kept beyond this close now
// holds nothing that can reach the statement beneath it
if (close) {
delegate = null;
}
// Wrap every result set the call hands back, whatever the method
// that returned it was called, on the same rule the statement
// facade uses: a decision made on the method name only sees the
// three calls the standard API names, and misses the result sets
// declared to come back as an Object - a cursor OUT-parameter
// reaches the application through CallableStatement.getObject(...),
// and iterating it keeps a cursor open on this connection just as
// an executeQuery one does. The Wrapper methods are kept out of
// this for the facade's reason: unwrap() is the documented way to
// reach the driver's own object, and a driver's cursor type is
// such an object, so what it hands back stays the driver's object
// even where that object implements ResultSet.
if (result instanceof ResultSet && method.getDeclaringClass() != Wrapper.class) {
try {
return getConstructor(RESULTSET_IDX, ResultSet.class)
.newInstance(new Object[] { new TimerResultSetProxy(result, proxy) });
} catch (Exception x) {
// The application just does not get the timer reset
// while reading this result set
log.warn(MessageFactory.get("resetAbandonedTimer.unableToCreateTimerResultSetProxy",
ResetAbandonedTimer.this.getClass().getName()), x);
}
}
return result;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public String toString() {
return ResetAbandonedTimer.this.getClass().getName() + "[Proxy=" + hashCode() + "; Delegate=" + delegate +
']';
}
}
/**
* Proxy for a result set that resets the abandoned timer on every read, so that an application iterating a result
* set - or doing its per-row work between the reads - counts as active and is not abandoned mid-iteration.
*/
private final class TimerResultSetProxy implements InvocationHandler {
/**
* The result set being read, nulled once a close has been forwarded so
* that a handle the application keeps beyond its close stops pinning
* the physical result set. Read by other threads than the one that
* nulls it - a close can come from a foreign thread, as in the
* statement proxy - so, as in the facade and the query report proxy,
* volatile.
*/
private volatile Object delegate;
/**
* The statement handle in use by this interceptor, returned by {@link java.sql.ResultSet#getStatement()}. It
* keeps the timer reset like the handle the application holds, but it is not necessarily that same outermost
* handle: the statement facade substitutes its own handle from outside this interceptor, and any interceptor
* configured around this one may too. Nulled along with {@link #delegate} once a close has been forwarded, so
* that the handle of a closed result set stops pinning the statement layer either; read in the same snapshot as
* the delegate, so it is volatile for the same reason.
*/
private volatile Object statement;
TimerResultSetProxy(Object delegate, Object statement) {
this.delegate = delegate;
this.statement = statement;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Handle identity on this proxy, like the statement facade
// does, rather than on the object below it: the application
// must be able to rely on its own handles
if (compare(TOSTRING_VAL, method)) {
return toString();
}
if (compare(EQUALS_VAL, method)) {
if (args[0] == null || !Proxy.isProxyClass(args[0].getClass())) {
return Boolean.FALSE;
}
return Boolean.valueOf(this.equals(Proxy.getInvocationHandler(args[0])));
}
if (compare(HASHCODE_VAL, method)) {
return Integer.valueOf(this.hashCode());
}
try {
// Read both fields once for this call: the close that nulls
// them can come from another thread, and the guards and the
// forward must agree on the one value this call read. The
// close nulls the delegate before the statement, so reading
// the statement first keeps the snapshot coherent: a call that
// read the delegate as live cannot have missed the statement.
final Object stmt = statement;
final Object target = delegate;
final boolean close = compare(CLOSE_VAL, method);
// Allow close to be called multiple times, from either thread
if (close && target == null) {
return null;
}
if (compare(ISCLOSED_VAL, method) && target == null) {
return Boolean.TRUE;
}
if (target == null) {
// Answered from the closed contract, as the facade's own
// result set handle does, rather than by invoking on a
// null receiver: that is a NullPointerException, where the
// closed contract owes the application a SQLException
throw new SQLException(MessageFactory.get("resetAbandonedTimer.resultSetClosed"));
}
if (compare(GET_STATEMENT, method)) {
return stmt;
}
Object result;
try {
// Invoke on the delegate this call read as live: the
// snapshot keeps a concurrent close from replacing the
// receiver with null between the guards and the call
result = method.invoke(target, args);
} catch (InvocationTargetException x) {
Throwable cause = x.getCause();
if (cause == null) {
throw x;
}
throw cause;
}
// Perform close cleanup: the handle kept beyond this close now
// holds nothing that can reach the cursor or the statement
if (close) {
delegate = null;
statement = null;
}
return result;
} finally {
// Reading the result set is activity on the connection,
// even when the read failed. The guards above sit inside
// this block, so a read refused on a handle whose delegate
// has been released counts as activity too, like every
// other attempted operation on this connection.
resetTimer();
}
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public String toString() {
return ResetAbandonedTimer.this.getClass().getName() + "[Proxy=" + hashCode() + "; Delegate=" + delegate +
']';
}
}
}