/*
** SellaNMS
** $Id: connection_pool.cxx,v 1.17 2006/01/24 07:08:10 sella Exp $
** Copyright (c) 2001-2005 Digital Genesis Software, LLC. All Rights Reserved.
** Released under the GPL Version 2 License.
** http://www.digitalgenesis.com
*/

static const char rcsid[] = "$Id: connection_pool.cxx,v 1.17 2006/01/24 07:08:10 sella Exp $";

#include "connection_pool.h"
#include "common/util.h"
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <regex.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>

extern pthread_mutex_t __libstorage_global_mutex;
extern bool __libstorage_initialized;

ConnectionPool::ConnectionPool():debug(false),timeout(0),trimDelay(0),minSize(0),maxSize(0) {
}

ConnectionPool::ConnectionPool(ConnectionPool const &src):debug(false),timeout(0),trimDelay(0),minSize(0),maxSize(0) {
	LMSG("DEBUG: ConnectionPool copy contructor called. Deep copy is not implemented. Aborting...");
	abort();
}

ConnectionPool::~ConnectionPool() {
	/* Close all connections. */
	closeAllConnections();
}

RESULT ConnectionPool::setConfig(ConfigNode &config) {
	/* Skip out if its already configured. */
	if (isConfigured()) {
		return SUCCESS;
	}

	/* Keep an internal copy of the storage portion of the config node. */
	if (!config.assignNode("system storage", this->config) || this->config.empty()) {
		LMSG("ConnectionPool configuration is missing (system storage).");

		return FAILURE;
	}

	this->debug = this->config.isNode("pool debug");

	/* The idle timeout on a connection. */
	this->config.assignValue("pool timeout", this->timeout, LIBS_STORAGE_CONNECTIONPOOL_TIMEOUT_DEF, LIBS_STORAGE_CONNECTIONPOOL_TIMEOUT_MIN, LIBS_STORAGE_CONNECTIONPOOL_TIMEOUT_MAX);

	/* Wait at least 'trim-delay' sec before disconnecting an unused connection if over the 'min-size'. This prevents a buildup of TIME_WAIT sockets. */
	this->config.assignValue("pool trim-delay", this->trimDelay, LIBS_STORAGE_CONNECTIONPOOL_TRIMDELAY_DEF, LIBS_STORAGE_CONNECTIONPOOL_TRIMDELAY_MIN, LIBS_STORAGE_CONNECTIONPOOL_TRIMDELAY_MAX);

	/* Don't keep more than 'min-size' connections open if the connections are not assigned. */
	this->config.assignValue("pool min-size", this->minSize, LIBS_STORAGE_CONNECTIONPOOL_MINSIZE_DEF, LIBS_STORAGE_CONNECTIONPOOL_MINSIZE_MIN, LIBS_STORAGE_CONNECTIONPOOL_MINSIZE_MAX);

	/* Don't open more than 'max-size' connecitons. The maxSize may never be less than minSize. */
	this->config.assignValue("pool max-size", this->maxSize, LIBS_STORAGE_CONNECTIONPOOL_MAXSIZE_DEF, this->minSize, LIBS_STORAGE_CONNECTIONPOOL_MAXSIZE_MAX);

	/* Initialize libdbi. Returns SUCCESS if its already initialized or no errors occur. */
	if (initialize() != SUCCESS) {
		LMSG("Failed to initialize libstorage.");
		this->config.clear();

		return FAILURE;
	}

	return SUCCESS;
}

void ConnectionPool::setDebug(bool debug) {
	this->debug = debug;
}

bool ConnectionPool::isConfigured() {
	return !(this->config.empty());
}

void ConnectionPool::closeAllConnections() {
	pthread_mutex_lock(&__libstorage_global_mutex);

	/* Close all connections and empty the list. */
	for (slist<conn_pool_t>::iterator it = pool.begin(); it != pool.end(); ++it) {
		disconnect(&(*it));
	}

	pthread_mutex_unlock(&__libstorage_global_mutex);
}

RESULT ConnectionPool::initialize() {
	int err = 1;
	string str;

	assert(!config.empty());

	pthread_mutex_lock(&__libstorage_global_mutex);
	if (!__libstorage_initialized) {
		LMSG("ConnectionPool is being initialized...");
		if (!config.assignValue("driver-directory", str)) {
			/* Use the default (DBI_DRIVER_DIR) driver directory. */
			err = dbi_initialize(NULL);
		} else {
			/* Use the specified driver directory to initialize available libdbi drivers. */
			err = dbi_initialize(str.c_str());
			if (err < 1) {
				LMSG("Specified driver-directory '%s' was unusable, trying default.", str.c_str());
				err = dbi_initialize(NULL);
			}
		}

		if (err < 0) {
			LMSG("Call to dbi_initialize() returned in error. Specify a valid driver-directory and/or verify libdbi was compiled with support for the MySQL driver.");
			return FAILURE;
		} else if (err == 0) {
			LMSG("No libdbi drivers are available. Specify a valid driver-directory or reinstall libdbi.");
			return FAILURE;
		}

		LMSG("libdbi initialized with %d drivers available.", err);
		__libstorage_initialized = true;
	}
	pthread_mutex_unlock(&__libstorage_global_mutex);

	return SUCCESS;
}

RESULT ConnectionPool::requestConnection(conn_pool_t *& connection, bool exclusive) {
	RESULT ret_val = FAILURE;

	conn_pool_t new_conn;

	pthread_mutex_lock(&__libstorage_global_mutex);

	connection = NULL;

	/* Attempt to find an existing unused connection for the caller. */
	for (slist<conn_pool_t>::iterator it = pool.begin(); it != pool.end(); ++it) {
		if (it->users == 0) {
			assert(it->mutex != NULL); /* Should always be the case. */

			/* Reconnect if connection is failed or stale. */
			if (it->failed || (unsigned long) (time(NULL) - it->last) > this->timeout) {
				if (it->failed) {
					LMSG("ConnectionPool found a failed unused connection for the caller. Attempting to reconnect it. (%lu/%lu conn)", pool.size(), this->maxSize);
				} else if (this->debug) {
					LMSG("ConnectionPool found a stale unused connection for the caller. Reconnecting it. (%lu/%lu conn)", pool.size(), this->maxSize);
				}

				disconnect(&(*it));
				ret_val = connect(&(*it));
			} else {
				if (this->debug) {
					LMSG("ConnectionPool found an unused connection for the caller. (%lu/%lu conn)", pool.size(), this->maxSize);
				}

				ret_val = SUCCESS;
			}

			if (ret_val == SUCCESS) {
				/* Configure connection structure. */
				it->users = 1;
				it->exclusive = exclusive;
				it->last = time(NULL);
				it->failed = false;

				/* Assign conneciton. */
				connection = &(*it);
			}

			pthread_mutex_unlock(&__libstorage_global_mutex);
			return ret_val;
		}
	}

	/* Is it OK to create a new connection? */
	if (pool.size() < this->maxSize) {
		/* Pool is not at maxSize, creating a new one. */
		if ((ret_val = connect(&new_conn)) == SUCCESS) {
			pool.push_front(new_conn);
			connection = &(*(pool.begin()));
			connection->users = 1;
			connection->failed = false;
		}

		if (this->debug) {
			LMSG("ConnectionPool creating new connection for caller. (%lu/%lu conn)", pool.size(), this->maxSize);
		}
	} else {
		/* Pool is at max. Find the least used non-exclusive connection. */
		unsigned int nonexclusives = 0;
		unsigned int lowest_users = 65000;
		for (slist<conn_pool_t>::iterator it = pool.begin(); it != pool.end(); ++it) {
			if (it->exclusive == false) {
				nonexclusives++;
				lowest_users = (lowest_users > it->users) ? it->users : lowest_users;
			}
		}

		/* Were there any non-exclusives to use? If not, use one of them. */
		if (nonexclusives > 0) {
			/* Assign the lowest used non-exclusive (that isn't failed). */
			for (slist<conn_pool_t>::iterator it = pool.begin(); it != pool.end(); ++it) {
				if (!it->failed && it->exclusive == false && it->users <= lowest_users) {
					ret_val = SUCCESS;
					it->users++;
					connection = &(*it);

					if (this->debug) {
						LMSG("ConnectionPool at max (%lu) with non-exclusives available. Connection now has %lu users.", this->maxSize, it->users);
					}

					break;
				}
			}
		} else {
			/* There are only exclusives. Assign the lowest used one (that isn't failed). */
			for (slist<conn_pool_t>::iterator it = pool.begin(); it != pool.end(); ++it) {
				if (!it->failed && it->users <= lowest_users) {
					ret_val = SUCCESS;
					it->users++;
					connection = &(*it);

					if (this->debug) {
						LMSG("ConnectionPool at max (%lu) with only exclusive connections. Exclusive connection now has %lu users, since the pool is too small.", this->maxSize, it->users);
					}

					break;
				}
			}

		}
	}

	if (connection == NULL) {
		LMSG("ConnectionPool failed to provide a connection. (Usually connectivity related)");
	}

	pthread_mutex_unlock(&__libstorage_global_mutex);

	return ret_val;
}

RESULT ConnectionPool::releaseConnection(conn_pool_t *& connection) {
	RESULT ret_val = SUCCESS;

	pthread_mutex_lock(&__libstorage_global_mutex);

	assert(connection != NULL);
	assert(connection->mutex != NULL);
	assert(connection->users > 0);

	/* Remove this user. */
	connection->users--;

	/* Trim open connections down the minSize, if possible. */
	slist<conn_pool_t>::iterator it = pool.begin();
	while (it != pool.end() && this->minSize < pool.size()) {
		/* If this connection has no users and its older than the trim-delay, loose it. */
		if (it->users < 1 && (unsigned long) (time(NULL) - it->last) > this->trimDelay) {
			/* Disconnect and remove from pool. */
			disconnect(&(*it));
			it = pool.erase(it);
		} else {
			/* Move to the next element */
			++it;
		}
	}

//	pool.reverse();

	connection = NULL;

	pthread_mutex_unlock(&__libstorage_global_mutex);

	return ret_val;
}

RESULT ConnectionPool::connect(conn_pool_t *connections) {
	RESULT ret_val = SUCCESS;
	char *err_msg;
	string str;
	ConfigNode node;
	dbi_driver driver = NULL;
	dbi_conn connection = NULL;
	vector<string> v;
	vector<string>::iterator it;

	/* Object must be initialized prior to connecting. */
	assert(__libstorage_initialized == true);

	/* Get our storage entries. */
	if (!config.assignValues("", v)) {
		LMSG("No storage entries listed within 'system storage'.");

		return FAILURE;
	}

	/* There are already active connections. */
	if (!connections->list.empty()) {
		LMSG("Call to ConnectionPool::connect() when there are already open connections.");

		return FAILURE;
	}

	/* Attempt to get a connection to each storage entry. */
	for (it = v.begin(); it != v.end(); ++it) {
		/* Ignore keywords under 'storage'. */
		if (!it->compare("driver-directory") || !it->compare("pool") || !it->compare("debug")) {
			continue;
		}

		if (config.assignNode(*it, node)) {
			if (node.assignValue("driver", str)) {
				if ((connection = dbi_conn_new(str.c_str())) == NULL) {
					LMSG("ConnectionPool failed to allocate a connection to specified driver type '%s'.", str.c_str());

					continue;
				}
			} else if ((connection = dbi_conn_new("mysql")) == NULL) {
				LMSG("ConnectionPool failed to allocate a default (mysql) driver type.");

				continue;
			}

			/* Configured connections. */
			if (node.assignValue("server", str)) {
				dbi_conn_set_option(connection, "host", (char*) str.c_str());
			} else {
				dbi_conn_set_option(connection, "host", "localhost");
			}

			if (node.assignValue("port", str)) {
				dbi_conn_set_option(connection, "port", (char*) str.c_str());
			}

			if (node.assignValue("username", str)) {
				dbi_conn_set_option(connection, "username", (char*) str.c_str());
			}

			if (node.assignValue("password", str)) {
				dbi_conn_set_option(connection, "password", (char*) str.c_str());
			}

			if (node.assignValue("database", str)) {
				dbi_conn_set_option(connection, "dbname", (char*) str.c_str());
			} else {
				dbi_conn_set_option(connection, "dbname", "sella_nms");
			}

			if ((driver = dbi_conn_get_driver(connection)) == NULL) {
			/* MySQL Specific */
				if (!strcmp(dbi_driver_get_name(connection), "mysql")) {
					if (node.assignValue("socket", str)) {
						dbi_conn_set_option(connection, "mysql_unix_socket", (char*) str.c_str());
					}
					if (node.isNode("compression")) {
						dbi_conn_set_option_numeric(connection, "mysql_compression", 1);
					}
				/* Postgre Specific */
				} else if (!strcmp(dbi_driver_get_name(connection), "pgsql")) {
					if (node.assignValue("tty", str)) {
						dbi_conn_set_option(connection, "pgsql_tty", (char*) str.c_str());
					}
					if (node.assignValue("options", str)) {
						dbi_conn_set_option(connection, "pgsql_options", (char*) str.c_str());
					}
				}
			}

			if (this->debug) {
				LMSG("Attempting to connect to %s", dbi_conn_get_option(connection, "host"));
			}

			/* Establish connection. */
			if (dbi_conn_connect(connection) != 0) {
				LMSG("ConnectionPool failed to connect to database '%s' on '%s' using username '%s'.", dbi_conn_get_option(connection, "dbname"), dbi_conn_get_option(connection, "host"), dbi_conn_get_option(connection, "username"));
				dbi_conn_error(connection, (const char**) &err_msg);
				LMSG("libdbi: %s", err_msg);

				dbi_conn_close(connection);
			} else {
				if (this->debug) {
					LMSG("Database connection established with %s", dbi_conn_get_option(connection, "host"));
				}
				/* Add our new connection. */
				connections->list.push_front(connection);
			}

			connection = NULL;
		}
	}

	/* Put them in the correct order. */
	connections->list.reverse();

	if (connections->list.empty()) {
		ret_val = FAILURE;
	} else {
		/* Finish setting up the structure */
		connections->last = time(NULL);
		connections->exclusive = false;
		connections->users = 0;
		connections->mutex = new pthread_mutex_t;
		if (connections->mutex) {
			pthread_mutex_init(connections->mutex, NULL);
		} else {
			ret_val = FAILURE;
		}
	}

	return ret_val;
}

void ConnectionPool::disconnect(conn_pool_t *connections) {
	assert(connections->users == 0);

	/* Nothing to disconnect. */
	if (connections->mutex == NULL) {
		return;
	}

	/* Loop through all connectsions, and close them. */
	for (slist<dbi_conn>::iterator it = connections->list.begin(); it != connections->list.end(); ++it) {
		if (*it != NULL) {
			dbi_conn_close(*it);
			*it = NULL;
		}
	}

	/* Cleanup structure */
	connections->list.clear();
	connections->exclusive = false;
	connections->last = 0;
	connections->users = 0;

	pthread_mutex_destroy(connections->mutex);
	SAFE_DELETE(connections->mutex);
}

void ConnectionPool::printOptions(dbi_conn conn) {
	const char *ptr = NULL;

	while ((ptr = dbi_conn_get_option_list(conn, ptr)) != NULL) {
		LMSG("option: %s", ptr);
	}
}

/*
** Local Variables:
** c-basic-offset: 3
** tab-width: 3
** End:
** vim:
** vim: noet ts=3 sw=3
*/
