/**
* Copyright (c) 2009-2011 VMware, Inc. All Rights Reserved.
*
* Licensed 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 com.springsource.insight.plugin.jdbc;
import java.io.Serializable;
import java.sql.Connection;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.logging.Level;
import org.aspectj.lang.JoinPoint;
import com.springsource.insight.collection.OperationCollectionUtil;
import com.springsource.insight.intercept.operation.Operation;
import com.springsource.insight.intercept.operation.OperationFields;
import com.springsource.insight.intercept.plugin.CollectionSettingName;
import com.springsource.insight.intercept.plugin.CollectionSettingsRegistry;
import com.springsource.insight.intercept.plugin.CollectionSettingsUpdateListener;
import com.springsource.insight.util.logging.AbstractLoggingClass;
/**
* A rather simplistic LRU cache that tracks {@link Connection}-s created
* by a JDBC {@link java.sql.Driver} so that we can mark the {@link Connection#close()}
* operation with the URL that was to open it.
*/
class ConnectionsTracker extends AbstractLoggingClass implements CollectionSettingsUpdateListener {
/**
* Default initial LRU capacity
*/
static final int DEFAULT_CAPACITY = 100;
/**
* Default logging {@link Level} for tracker
*/
static final Level DEFAULT_LEVEL = Level.OFF;
private volatile int maxCapacity = DEFAULT_CAPACITY;
private volatile Level logLevel = DEFAULT_LEVEL;
/**
* The tracked connections {@link Map} - key={@link CacheKey}
* (consists of the class name and identity hash) and value=the connection
* URL used when connection was opened
*/
private final Map<CacheKey, String> trackedMap =
Collections.synchronizedMap(new LinkedHashMap<CacheKey, String>() {
private static final long serialVersionUID = 1L;
@Override
protected boolean removeEldestEntry(Map.Entry<CacheKey, String> entry) {
return size() > getMaxCapacity();
}
});
private static final ConnectionsTracker INSTANCE = new ConnectionsTracker();
protected static final CollectionSettingName MAX_TRACKED_CONNECTIONS_SETTING =
new CollectionSettingName("max.tracked.connections", "jdbc", "Controls the number of concurrently tracked connections (default=" + DEFAULT_CAPACITY + ")");
protected static final CollectionSettingName CONNECTION_TRACKING_LOGGING_SETTING =
new CollectionSettingName("connections.tracking.loglevel", "jdbc", "One of the java.util.logging.Level values (default=" + DEFAULT_LEVEL + ")");
// register a collection setting update listener and register the initial defaults
static {
CollectionSettingsRegistry registry = CollectionSettingsRegistry.getInstance();
registry.addListener(INSTANCE);
}
private ConnectionsTracker() {
super();
}
public int getMaxCapacity() {
return maxCapacity;
}
/**
* @param conn The created {@link Connection}
* @param op The {@link Operation} containing the URL in its {@link OperationFields#CONNECTION_URL}
* attribute
* @return The previous assigned URL to the connection - <code>null</code>
* if none
*/
String startTracking(Connection conn, Operation op) {
return startTracking(conn, op.get(OperationFields.CONNECTION_URL, String.class));
}
/**
* @param conn The created {@link Connection}
* @param url The used URL to create the connection
* @return The previous assigned URL to the connection - <code>null</code>
* if none
*/
String startTracking(Connection conn, String url) {
CacheKey key = new CacheKey(conn);
String prev = trackedMap.put(key, (url == null) ? "" : url);
if ((logLevel != null) && (!Level.OFF.equals(logLevel)) && _logger.isLoggable(logLevel)) {
_logger.log(logLevel, "startTracking(" + key + ")[" + url + "] => " + prev);
}
return prev;
}
/**
* @param conn The {@link Connection}
* @return The URL used when {@link #startTracking(Connection, String)}
* was called - <code>null</code> if connection not tracked
*/
String stopTracking(Connection conn) {
CacheKey key = new CacheKey(conn);
String url = trackedMap.remove(key);
if ((logLevel != null) && (!Level.OFF.equals(logLevel)) && _logger.isLoggable(logLevel)) {
_logger.log(logLevel, "stopTracking(" + key + ") => " + url);
}
return url;
}
/**
* Checks if a {@link Connection} is currently being tracked
*
* @param conn The connection instance
* @return The URL of the tracked connection - <code>null</code> if not tracked
*/
String checkTrackingState(Connection conn) {
return trackedMap.get(new CacheKey(conn));
}
Set<String> getTrackedURLs() {
if (trackedMap.isEmpty()) {
return Collections.emptySet();
}
return new TreeSet<String>(trackedMap.values());
}
int getNumTrackedConnections() {
return trackedMap.size();
}
/**
* @return A {@link Map} where key=URL, value=a {@link Collection} of all
* the {@link CacheKey}-s currently tracking this URL
*/
Map<String, Collection<CacheKey>> getTrackedConnections() {
if (trackedMap.isEmpty()) {
return Collections.emptyMap();
}
Map<String, Collection<CacheKey>> result = new TreeMap<String, Collection<CacheKey>>();
synchronized (trackedMap) {
for (Map.Entry<CacheKey, String> ce : trackedMap.entrySet()) {
CacheKey key = ce.getKey();
String url = ce.getValue();
Collection<CacheKey> keyList = result.get(url);
if (keyList == null) {
keyList = new TreeSet<CacheKey>();
result.put(url, keyList);
}
keyList.add(key);
}
}
return result;
}
void clear() {
trackedMap.clear();
}
public void incrementalUpdate(CollectionSettingName name, Serializable value) {
if (MAX_TRACKED_CONNECTIONS_SETTING.equals(name)) {
int newCapacity = CollectionSettingsRegistry.getIntegerSettingValue(value);
if (newCapacity <= 0) {
throw new IllegalArgumentException("Non-positive capacity N/A: " + value);
}
int oldCapacity = maxCapacity;
maxCapacity = newCapacity;
_logger.info("incrementalUpdate(" + name + ") " + oldCapacity + " => " + maxCapacity);
} else if (CONNECTION_TRACKING_LOGGING_SETTING.equals(name)) {
Level oldLevel = logLevel;
logLevel = CollectionSettingsRegistry.getLogLevelSetting(value);
_logger.info("incrementalUpdate(" + name + ") " + oldLevel + " => " + logLevel);
} else if (_logger.isLoggable(Level.FINE)) {
_logger.fine("incrementalUpdate(" + name + ")[" + value + "] ignored");
}
}
static Operation createOperation(JoinPoint jp, String url, String action) {
return createOperation(jp.getStaticPart(), url, action);
}
static Operation createOperation(JoinPoint.StaticPart staticPart, String url, String action) {
return new Operation()
.type(JdbcDriverExternalResourceAnalyzer.TYPE)
.sourceCodeLocation(OperationCollectionUtil.getSourceCodeLocation(staticPart))
.label("JDBC connection " + action)
.put(OperationFields.METHOD_NAME, action)
.put(OperationFields.CONNECTION_URL, (url == null) ? "" : url)
;
}
static ConnectionsTracker getInstance() {
return INSTANCE;
}
static class CacheKey implements Serializable, Comparable<CacheKey> {
private static final long serialVersionUID = -470721146773085523L;
private final String name;
private final int hashValue;
CacheKey(Connection conn) {
if (conn == null) {
throw new IllegalStateException("No connection");
}
name = conn.getClass().getName();
hashValue = System.identityHashCode(conn);
}
public int compareTo(CacheKey o) {
if (o == null) {
return (-1);
}
if (o == this) {
return 0;
}
int nRes = name.compareTo(o.name);
if (nRes != 0) {
return nRes;
}
if ((nRes = hashValue - o.hashValue) != 0) {
return nRes;
}
return 0;
}
@Override
public int hashCode() {
return hashValue;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (getClass() != obj.getClass()) {
return false;
}
CacheKey other = (CacheKey) obj;
if (name.equals(other.name) && (hashValue == other.hashValue)) {
return true;
}
return false;
}
@Override
public String toString() {
return name + "@" + hashValue;
}
}
}