/*****************************************************************
* 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.cayenne.datasource;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.sql.DataSource;
import org.apache.cayenne.CayenneRuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A non-blocking {@link DataSource} with a pool of connections.
*
* @since 4.0
*/
public class UnmanagedPoolingDataSource implements PoolingDataSource {
// An old hack that fixes Sybase problems with autocommit. Used idea from
// Jonas org.objectweb.jonas.jdbc_xa.ConnectionImpl
// (http://www.objectweb.org/jonas/).
//
// If problem is not the one that can be fixed by this patch, original
// exception is rethrown. If exception occurs when fixing the problem, new
// exception is thrown.
//
static void sybaseAutoCommitPatch(Connection c, SQLException e, boolean autoCommit) throws SQLException {
String s = e.getMessage().toLowerCase();
if (s.contains("set chained command not allowed")) {
// TODO: doing 'commit' here is extremely dangerous... we need to
// get a hold of Sybase instance and verify whether this issue is
// still there, and fix it differently (and perhaps generically) by
// calling 'rollback' on connections (can we do it when getting
// connection from the pool? returning it to the pool?)
c.commit();
c.setAutoCommit(autoCommit); // Shouldn't fail now.
} else {
throw e;
}
}
/**
* An exception indicating that a connection request waiting in the queue
* timed out and was unable to obtain a connection.
*/
public static class ConnectionUnavailableException extends SQLException {
private static final long serialVersionUID = 1063973806941023165L;
public ConnectionUnavailableException(String message) {
super(message);
}
}
/**
* Defines a maximum time in milliseconds that a connection request could
* wait in the connection queue. After this period expires, an exception
* will be thrown in the calling method.
*/
public static final int MAX_QUEUE_WAIT_DEFAULT = 20000;
private static Logger LOGGER = LoggerFactory.getLogger(UnmanagedPoolingDataSource.class);
private DataSource nonPoolingDataSource;
private long maxQueueWaitTime;
private Map<PoolAwareConnection, Object> pool;
private Semaphore poolCap;
private BlockingQueue<PoolAwareConnection> available;
private int maxIdleConnections;
private int minConnections;
private int maxConnections;
private String validationQuery;
static int maxIdleConnections(int min, int max) {
return min == max ? min : min + (int) Math.ceil((max - min) / 2d);
}
public UnmanagedPoolingDataSource(DataSource nonPoolingDataSource, PoolingDataSourceParameters parameters) {
int minConnections = parameters.getMinConnections();
int maxConnections = parameters.getMaxConnections();
// sanity check
if (minConnections < 0) {
throw new IllegalArgumentException("Negative min connections: " + minConnections);
}
if (maxConnections < 0) {
throw new IllegalArgumentException("Negative max connections: " + maxConnections);
}
if (minConnections > maxConnections) {
throw new IllegalArgumentException("Min connections (" + minConnections
+ ") is greater than max connections (" + maxConnections + ")");
}
this.nonPoolingDataSource = nonPoolingDataSource;
this.maxQueueWaitTime = parameters.getMaxQueueWaitTime();
this.validationQuery = parameters.getValidationQuery();
this.minConnections = minConnections;
this.maxConnections = maxConnections;
this.pool = new ConcurrentHashMap<PoolAwareConnection, Object>((int) (maxConnections / 0.75));
this.available = new ArrayBlockingQueue<PoolAwareConnection>(maxConnections);
this.poolCap = new Semaphore(maxConnections);
this.maxIdleConnections = maxIdleConnections(minConnections, maxConnections);
// grow pool to min connections
try {
for (int i = 0; i < minConnections; i++) {
PoolAwareConnection c = createUnchecked();
reclaim(c);
}
} catch (BadValidationQueryException e) {
throw new CayenneRuntimeException("Bad validation query: " + validationQuery, e);
} catch (SQLException e) {
LOGGER.info("Error creating new connection when starting connection pool, ignoring", e);
}
}
int poolSize() {
return pool.size();
}
int availableSize() {
return available.size();
}
int canExpandSize() {
return poolCap.availablePermits();
}
@Override
public void close() {
// expecting surrounding environment to block new requests for
// connections before calling this method. Still previously unchecked
// connections may be returned. I.e. "pool" will not grow during
// shutdown, which is the only thing that we need
for (PoolAwareConnection c : pool.keySet()) {
retire(c);
}
available.clear();
pool = Collections.emptyMap();
}
void managePool() {
// do not grow or shrink abruptly ... open or close 1 connection on
// each call
if (available.size() < minConnections) {
try {
PoolAwareConnection c = createUnchecked();
if (c != null) {
reclaim(c);
}
} catch (SQLException e) {
LOGGER.info("Error creating new connection when managing connection pool, ignoring", e);
}
} else if (available.size() > maxIdleConnections) {
PoolAwareConnection c = uncheckNonBlocking(false);
if (c != null) {
retire(c);
}
}
}
/**
* Closes the connection and removes it from the pool. The connection must
* be an unchecked connection.
*/
void retire(PoolAwareConnection connection) {
pool.remove(connection);
poolCap.release();
try {
connection.getConnection().close();
} catch (SQLException e) {
// ignore?
}
}
/**
* Returns connection back to the pool if possible. The connection must be
* an unchecked connection.
*/
void reclaim(PoolAwareConnection connection) {
// TODO: rollback any in-process tx?
// the queue may overflow potentially and we won't be able to add the
// object
if (!available.offer(connection)) {
retire(connection);
}
}
PoolAwareConnection uncheckNonBlocking(boolean validate) {
PoolAwareConnection c = available.poll();
return validate ? validateUnchecked(c) : c;
}
PoolAwareConnection uncheckBlocking(boolean validate) {
PoolAwareConnection c;
try {
c = available.poll(maxQueueWaitTime, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
return null;
}
return validate ? validateUnchecked(c) : c;
}
PoolAwareConnection validateUnchecked(PoolAwareConnection c) {
if (c == null || c.validate()) {
return c;
}
// this will recursively validate all connections that exist in the pool
// until a valid one is found or a pool is exhausted
retire(c);
return validateUnchecked(available.poll());
}
PoolAwareConnection createUnchecked() throws SQLException {
if (!poolCap.tryAcquire()) {
return null;
}
PoolAwareConnection c;
try {
c = createWrapped();
} catch (SQLException e) {
poolCap.release();
throw e;
}
pool.put(c, 1);
// even though we got a fresh connection, let's still validate it...
// This will provide consistent behavior between cached and uncached
// connections in respect to invalid validation queries
if (!c.validate()) {
throw new BadValidationQueryException(
"Can't validate a fresh connection. Likely validation query is wrong: " + validationQuery);
}
return c;
}
PoolAwareConnection createWrapped() throws SQLException {
return new PoolAwareConnection(this, createUnwrapped(), validationQuery);
}
/**
* Creates a new connection.
*/
Connection createUnwrapped() throws SQLException {
return nonPoolingDataSource.getConnection();
}
/**
* Updates connection state to a default state.
*/
Connection resetState(Connection c) throws SQLException {
// TODO: tx isolation level?
if (!c.getAutoCommit()) {
try {
c.setAutoCommit(true);
} catch (SQLException e) {
UnmanagedPoolingDataSource.sybaseAutoCommitPatch(c, e, true);
}
}
c.clearWarnings();
return c;
}
@Override
public Connection getConnection() throws SQLException {
// strategy for getting a connection -
// 1. quick peek for available connections
// 2. create new one
// 3. wait for a user to return connection
PoolAwareConnection c;
c = uncheckNonBlocking(true);
if (c != null) {
return resetState(c);
}
c = createUnchecked();
if (c != null) {
return resetState(c);
}
c = uncheckBlocking(true);
if (c != null) {
return resetState(c);
}
int poolSize = poolSize();
int canGrow = poolCap.availablePermits();
throw new ConnectionUnavailableException("Can't obtain connection. Request to pool timed out. Total pool size: "
+ poolSize + ", can expand by: " + canGrow);
}
@Override
public Connection getConnection(String userName, String password) throws SQLException {
throw new UnsupportedOperationException(
"Connections for a specific user are not supported by the pooled DataSource");
}
@Override
public int getLoginTimeout() throws java.sql.SQLException {
return nonPoolingDataSource.getLoginTimeout();
}
@Override
public void setLoginTimeout(int seconds) throws java.sql.SQLException {
nonPoolingDataSource.setLoginTimeout(seconds);
}
@Override
public PrintWriter getLogWriter() throws java.sql.SQLException {
return nonPoolingDataSource.getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws java.sql.SQLException {
nonPoolingDataSource.setLogWriter(out);
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return (UnmanagedPoolingDataSource.class.equals(iface)) ? true : nonPoolingDataSource.isWrapperFor(iface);
}
@SuppressWarnings("unchecked")
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return UnmanagedPoolingDataSource.class.equals(iface) ? (T) this : nonPoolingDataSource.unwrap(iface);
}
@Override
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
return nonPoolingDataSource.getParentLogger();
}
String getValidationQuery() {
return validationQuery;
}
long getMaxQueueWaitTime() {
return maxQueueWaitTime;
}
int getMaxIdleConnections() {
return maxIdleConnections;
}
int getMinConnections() {
return minConnections;
}
int getMaxConnections() {
return maxConnections;
}
}