QueryTimeoutInterceptor.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.Method;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
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.PoolProperties.InterceptorProperty;
import org.apache.tomcat.jdbc.pool.PooledConnection;
import org.apache.tomcat.jdbc.pool.util.MessageFactory;
/**
* Interceptor that sets a query timeout on every created {@link Statement}.
* <p>
* The timeout is applied by the {@code createStatement} hook, which only runs for statement-creation calls that pass
* through this interceptor. When combined with {@link StatementCache}, this interceptor must therefore be listed
* <em>before</em> {@code StatementCache} in the
* {@link org.apache.tomcat.jdbc.pool.PoolConfiguration#setJdbcInterceptors(String) jdbcInterceptors} configuration. A
* statement cache hit is served by the cache without invoking the rest of the chain, and a statement recycled into the
* cache has its query timeout reset to zero, so an interceptor listed after the cache never sees the reuse and the
* cached statement executes with no timeout. Listed before the cache, the hook runs for every creation request,
* including the ones the cache serves, and the timeout is re-applied to the reused statement.
* <p>
* Whether a configured timeout actually interrupts a query is up to the driver. A driver that refuses
* {@link Statement#setQueryTimeout(int)} does not stop the statement from being handed out - the pool cannot enforce a
* timeout itself, so such a statement runs unbounded - and the refusal is reported once per physical connection rather
* than once per statement, so that the log says what is not in effect without repeating itself for every statement
* that connection creates.
*/
public class QueryTimeoutInterceptor extends AbstractCreateStatementInterceptor {
private static final Log log = LogFactory.getLog(QueryTimeoutInterceptor.class);
/**
* Query timeout value in seconds.
*/
int timeout = 1;
/**
* Whether the driver of this connection has already been reported as refusing
* {@link Statement#setQueryTimeout(int)}. The pool builds one interceptor chain per pooled connection and reuses
* that chain for every borrow of it, so this record makes a refusal an event of the physical connection rather than
* of the statement creation that happened to notice it: a driver that refuses the call refuses it for every
* statement it is asked about, and one line per creation would bury the fact that the configured protection is not
* in effect. It is cleared when the physical session is replaced, so the session that takes over reports itself.
* The hook runs on the thread that borrowed the connection, and a pooled connection is only borrowed by one thread
* at a time - the next borrower reaches this instance through the pool's idle queue, which publishes what the
* previous one stored here - so a plain field is enough, as it is for the cached state of {@link ConnectionState}.
*/
private boolean timeoutRefusalReported;
/**
* Default constructor.
*/
public QueryTimeoutInterceptor() {
super();
}
/**
* Initializes the {@code queryTimeout} property from the provided configuration map. A value that is not an
* integer is not applied and is warned about: the timeout keeps the value it has - for a fresh instance the
* one-second default - so a misconfigured pool behaves as the default and the warning says so as the bad value
* is configured. The pool configures an interceptor instance once at pool start, once per physical connection
* chain, and once when the pool stops, so the report shares the per-connection reporting volume of the driver
* refusal below.
*
* @param properties map of interceptor properties
*/
@Override
public void setProperties(Map<String, InterceptorProperty> properties) {
super.setProperties(properties);
InterceptorProperty p = properties.get("queryTimeout");
if (p == null || p.getValue() == null) {
// No property, or one with no value to read: as ever, the timeout
// stays at the default it carries, without a word about it.
return;
}
try {
timeout = Integer.parseInt(p.getValue());
} catch (NumberFormatException x) {
log.warn(MessageFactory.get("queryTimeoutInterceptor.invalidQueryTimeout",
p.getValue(), timeout));
}
}
/**
* Sets the query timeout on the created statement if it is a {@link Statement} and the timeout value is positive.
*
* @param proxy the actual proxy object
* @param method the method that was called
* @param args the arguments to the method
* @param statement the statement that the underlying connection created
* @param time elapsed time in milliseconds
*
* @return the (possibly wrapped) statement object
*/
@Override
public Object createStatement(Object proxy, Method method, Object[] args, Object statement, long time) {
if (statement instanceof Statement && timeout > 0) {
Statement s = (Statement) statement;
try {
s.setQueryTimeout(timeout);
} catch (SQLException x) {
reportTimeoutRefusal(x);
}
}
return statement;
}
/*
* Reports that the driver will not apply the configured timeout, once per physical connection.
*
* The statement is still handed out, unchanged: enforcing a timeout is something only the driver can do, so
* refusing to create a statement an application can use would trade a protection the pool cannot deliver for a
* driver that simply does not implement the call. What the pool can do is say so - and say it in a way that is
* readable, which per-statement warnings are not. The warning therefore names the timeout that is not in effect and
* says that this connection will not mention it again, so one line tells the operator both the state and the
* volume of what is missing from the log.
*/
private void reportTimeoutRefusal(SQLException x) {
if (timeoutRefusalReported) {
return;
}
timeoutRefusalReported = true;
log.warn(MessageFactory.get("queryTimeoutInterceptor.queryTimeoutRefused", timeout, x.getMessage()), x);
}
/**
* Forgets the reported refusal, as the physical connection this interceptor is attached to is gone: the session
* that replaces it gets its own refusal recorded and its own warning, the way it gets its own driver to answer the
* call.
*/
@Override
public void disconnected(ConnectionPool parent, PooledConnection con, boolean finalizing) {
timeoutRefusalReported = false;
super.disconnected(parent, con, finalizing);
}
/**
* No-op; the refusal record belongs to the physical connection, not to the borrow, so ending a borrow has nothing
* here to clean up.
*/
@Override
public void closeInvoked() {
}
}