/* vi:set ts=3 sw=3 sts=3 noexpandtab: */
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
#region static class MySqlPoolManager [singleton for managing MySqlPool's]
///
/// Singleton used by MySqlConnection objects when
/// they need to attach themselves to a pool.
///
/// Also used by MySqlPool objects to tell the manager
/// when a pool wants to destroy itself.
///
internal static class MySqlPoolManager {
// Connection pools are mapped to their DSN strings in this hash table.
private static Dictionary Pools = new Dictionary();
// A lock is held when creating and destroying pools.
private static object poolLock = new object();
///
/// Return new or existing pool if Pooling enabled.
///
internal static MySqlPool GetPool(DsnParameters p) {
MySqlPool pool = null;
if (p.pooling) {
lock (poolLock) {
if (! Pools.ContainsKey(p.original)) {
// Performance counter eligible: one pool created.
Pools[p.original] = new MySqlPool(p);
}
pool = Pools[p.original];
}
}
return pool;
}
///
/// Remove an unused pool.
///
internal static void NukePool(string original) {
lock (poolLock) {
// Performance counter eligible: one pool destroyed.
Pools.Remove(original);
}
}
}
#endregion
#region class MySqlPool [maintains a pool of drivers]
///
/// Maintains a set of MySqlDriver objects corresponding to a given DSN.
///
/// A MySqlDriver is fetched from the pool by calling Catch(). After use,
/// the MySqlDriver must be returned to the pool by calling Release().
///
/// When an in-use Driver is released back to the pool, the server state
/// for the underlying server connection is first cleaned. Then the object
/// is returned to the pool on the very top of the list of free drivers.
/// Thus the next Catch() call will most likely get a recently used driver.
///
/// After being released, if a MySqlDriver sits in the pool for long enough
/// since the last executed statement, a ping to the server will occur over
/// the MySQL wire protocol. If the ping is successful, the driver's
/// activity counter is reset. If the ping is not successful, the driver
/// will either remove itself from the pool, or deliver an exception
/// describing the problem to the application (see below).
///
/// While a driver is pinging the server, it is not removed from the
/// pool of available drivers. The application can call Catch() and
/// (in theory) hit a pinging driver. When that happens, the app is
/// forced to wait for the ping to complete, which can take up to the
/// amount of time specified for the connection timeout. If the ping
/// fails, an exception describing the problem is delivered to the app,
/// just as would happen if the MySqlDriver was a newly constructed
/// instance trying to reach the database server.
///
/// The above will rarely happen in real life, for reasons noted below.
///
/// Applications with low levels of activity will rarely fetch a connection
/// that is performing a ping. The default ping interval is 10 seconds,
/// and a ping takes on the order of milliseconds to perform, so it is
/// simply extremely unlikely.
///
/// Applications with a high level of activity causes the pool to rarely
/// if ever perform server pings, because drivers are fetched from the
/// pool so quickly after being released that the time since the last
/// driver activity rarely or never exceeds the ping interval.
///
/// To avoid excessive client and server load, the pool is not prepopulated
/// to the minimum pool size. Instead, drivers are added as necessary.
/// Unused drivers are not purged beyond the minimum pool size, however.
///
/// If an entire pool goes unused for a long time, it shuts itself down.
/// Tearing down long unused connection pools has two purposes.
///
/// It saves resources, which can be important if the DSNs used to
/// connect changes often. This could be caused by a mechanism such
/// as automatically rotating passwords, or could just be a natural
/// high ratio of possible versus concurrently sustainable connection
/// pools.
///
/// It is also an important security measure, because it releases
/// pool memory containing login names and passwords for previously
/// used connections, so that the memory can be scrubbed by the memory
/// manager. This is important if users authenticate in a manner where
/// the database password is derived from the user's login credentials,
/// such that keeping these in RAM forever even after the user has logged
/// out can be avoided.
///
internal class MySqlPool {
// Simple FIFO linked list of the drivers in this pool.
protected volatile PooledMySqlDriver freeTop = null;
protected object free = new Object();
// Timer and thread used to destroy inactive pools.
private MySqlEventTimer lastRetrieval = new MySqlEventTimer();
private MySqlThread activityWatchdog;
// Unique pool parameters.
private DsnParameters dsn;
// Internal status.
private volatile int poolSize = 0;
private volatile int poolFree = 0;
public MySqlPool(DsnParameters dsn) {
this.dsn = dsn;
StartActivityWatchdog();
}
///
/// Catch() is invoked to get a live, squiggly MySQL driver from the pool.
///
///
///
/// If the pool has already reached full capacity,
/// an OverflowException is thrown.
///
public PooledMySqlDriver Catch() {
PooledMySqlDriver mine = null;
lock (free) {
// Grab the most recently released driver.
mine = freeTop;
if (freeTop != null) freeTop = freeTop.next;
}
if (mine == null) {
// Create a new driver if none available.
mine = new PooledMySqlDriver(this, dsn);
} else {
// Clear the now unused link to next free connection.
mine.next = null;
}
mine.Bind();
// Performance counter eligible: one driver caught.
return mine;
}
///
/// Release() is invoked via the application to return
/// a previously fetched MySQL driver to the pool.
///
public void Release(MySqlDriver dirty) {
// Reset driver state in the background.
// The driver is responsible for re-adding
// itself to the pool if successful.
if (dirty is PooledMySqlDriver) {
// Performance counter eligible: one driver released.
PooledMySqlDriver driver = ((PooledMySqlDriver) dirty);
driver.UnBind();
}
}
///
/// New drivers internally call Register() when they're constructed.
///
///
///
/// If the pool has already reached full capacity,
/// an OverflowException is thrown and ultimately
/// propagated via Catch() to the application.
///
internal void Register() {
const string abortMsg = "The connection pool has reached it's maximum size.";
lock (free) {
if (poolSize >= dsn.poolMax) throw new OverflowException(abortMsg);
poolSize++;
}
}
///
/// Aging drivers internally call UnRegister()
/// when they wish to die of boredom.
///
internal bool UnRegister(PooledMySqlDriver driver) {
PooledMySqlDriver current, prev;
lock (free) {
// Traverse the pool, looking for the driver.
current = freeTop;
prev = null;
while (current != null && current != driver) {
prev = current;
current = current.next;
}
if (current == driver) {
// Found! Unlink the driver.
if (prev == null) freeTop = current.next;
else prev.next = current.next;
poolFree--;
}
}
// Return false if the driver has already been caught
// (end of list was reached).
return current != null;
}
///
/// Drivers internally call BellyUp() when they croak.
///
internal void BellyUp() {
lock (free) {
poolSize--;
}
}
///
/// Released drivers internally call Swim() to place themselves
/// at the top of the pool's free list immediately after they've
/// finished cleaning themselves up.
///
internal void Swim(PooledMySqlDriver fresh) {
lock (free) {
// Insert fresh entries at top of list.
fresh.next = freeTop;
freeTop = fresh;
poolFree++;
}
}
private void StartActivityWatchdog() {
lastRetrieval.Reset();
MySqlThread.WorkerCode watchdog = delegate(ManualResetEvent ready) {
while (true) {
try {
ready.Set();
// Wake up half a second after the pool life timer reaches
// it's maximum, given that no connection has occurred
// in the mean time. Alternatively, wake up every half
// second if we're waiting for in-use connections to be
// returned.
int nextWake = lastRetrieval.NextEvent(dsn.poolLifetime) * 1000 + 500;
Thread.Sleep(nextWake);
// When awoken, check if the pool has been used in the meantime.
if (lastRetrieval.HasTranspired(dsn.poolLifetime)) {
// Make sure there are no unreleased drivers.
lock (free) {
// Are we good to go?
if (poolSize == poolFree) {
// Dereference pool from the pool manager.
MySqlPoolManager.NukePool(dsn.original);
// Change minimum pool size to zero to force
// remaining free drivers to make away with themselves.
dsn.poolMin = 0;
// Exit the maintenance thread.
// After this, there should be no running threads
// with references to the pool manager, except for
// the associated drivers themselves, which should
// be exiting shortly. Garbage collection now
// kicks in and takes all the objects away.
break;
}
}
}
} catch (ThreadInterruptedException) {
// For good measure - currently unused.
}
}
};
activityWatchdog = new MySqlThread("Pool watchdog " + dsn.InstanceName);
activityWatchdog.Run(watchdog);
}
}
#endregion
#region public class MySqlConnection [contains the public API]
///
/// The MySqlConnection class which can be instantiated by applications.
///
/// Calls the MySqlPoolManager if necessary, to create or join a pool of
/// connections. An appropriate MySqlDriver is then grabbed from the pool.
///
/// If pooling is disabled in the connection string, MySqlConnection
/// creates it's own MySqlDriver to communicate with the database server.
///
public class MySqlConnection {
private DsnParameters dsn;
private MySqlPool pool;
private MySqlDriver driver;
public MySqlConnection(string connectionString) {
// Performance counter eligible: one connection shim opened.
this.dsn = new DsnParameters(connectionString);
pool = MySqlPoolManager.GetPool(dsn);
if (pool != null) driver = pool.Catch();
else driver = new MySqlDriver(dsn);
}
public void Close() {
// Performance counter eligible: one connection shim closed.
if (pool != null) {
pool.Release(driver);
driver = null;
} else {
driver.SafeClose();
driver = null;
}
}
public void Execute(string query) {
driver.Execute(query);
}
// Unimplemented: all MySqlDriver wrappers.
}
#endregion
#region class MySqlDriver [performs server communication]
internal class MySqlDriver {
// Used in Open() for establishing a connection to database server.
protected DsnParameters dsn;
// Cache of the server version retrieved right after connecting.
protected int version = 10000;
// The server connection id.
protected int connectionId = 12345;
public MySqlDriver(DsnParameters dsn) {
this.dsn = dsn;
Open();
}
internal void Open() {
// Low-level: unimplemented.
// Performance counter eligible: one driver connected.
version = (int) Execute("SELECT VERSION()");
// Above: pseudo code (actually needs a version string parser).
}
internal void SafeClose() {
// Low-level: unimplemented.
// Performance counter eligible: one driver disconnected.
}
public int ServerVersion {
get {
return version;
}
}
public virtual object Execute(string sql) {
// Low-level: unimplemented.
return 54321;
}
public virtual void Ping() {
// Low-level: unimplemented.
}
}
#endregion
#region class PooledMySqlDriver [handles a driver in a pool]
internal class PooledMySqlDriver : MySqlDriver {
// Used to implement the linked list of free drivers.
internal volatile PooledMySqlDriver next = null;
// Pool to which this driver belongs.
private MySqlPool pool;
// Used to avoid race condition when pinging/catching free drivers.
private AutoResetEvent idle = new AutoResetEvent(true);
// Used for bookkeeping by maintenance thread.
private volatile bool isDirty = false;
private MySqlEventTimer lastCaught = new MySqlEventTimer();
private MySqlEventTimer lastActivity = new MySqlEventTimer();
private volatile Exception pingProblem = null;
// Per-driver maintenance thread, most often suspended.
private MySqlThread maintenanceThread;
internal PooledMySqlDriver(MySqlPool pool, DsnParameters dsn) : base(dsn) {
this.pool = pool;
// Just connected, clear the activity counter.
lastActivity.Reset();
// Just connected, prevent premature ejection from pool.
lastCaught.Reset();
// Register with pool.
pool.Register();
// After registering, start the maintenance thread.
StartMaintenanceThread();
}
public override object Execute(string sql) {
// Simple wrapper that also clears activity counter.
object data = base.Execute(sql);
lastActivity.Reset();
return data;
}
public override void Ping() {
// Simple wrapper that also clears activity counter.
base.Ping();
lastActivity.Reset();
}
internal void Bind() {
// When a connection is caught by the pool, this method
// is called to grab the activity lock which ensures that
// any background pings have finished executing.
idle.WaitOne();
if (pingProblem != null) throw pingProblem;
}
internal void UnBind() {
// When a connection is released by the application, this
// method is called to release the activity lock and start
// the background cleaner.
idle.Set();
// Set dirty flag and poke maintenance thread.
isDirty = true;
maintenanceThread.Interrupt();
}
private void StartMaintenanceThread() {
// Calculate the maximum delay between loops.
int delay = Math.Min(dsn.pingSweepRerun, dsn.unusedSweepRerun);
if (dsn.pingSweepRerun < 1) delay = dsn.unusedSweepRerun;
if (dsn.unusedSweepRerun < 1) delay = dsn.pingSweepRerun;
if (delay < 1) return;
// Activate half a second after the next event is supposed to transpire.
delay = delay * 1000 + 500;
MySqlThread.WorkerCode maintenance = delegate(ManualResetEvent ready) {
while (true) {
try {
ready.Set();
// Sleep first. When the driver is initially created,
// allows the creator to grab the activity lock before we do.
Thread.Sleep(delay);
// Wait till noone is using the driver.
idle.WaitOne();
// Check if the driver is dirty and needs cleaning.
if (isDirty) {
if (dsn.forceReuse) {
// Cleaning was explicitly disabled, just go back in.
isDirty = false;
pool.Swim(this);
} else {
try {
if (ServerVersion > 99999) {
// Once the server supports this, we're ready.
Execute("FLUSH CONNECTION");
} else {
// Reconnect to clean state.
SafeClose();
Open();
}
// OK, good to go back in.
isDirty = false;
pool.Swim(this);
} catch (Exception) {
// Failed to clean state; instead of going
// back into the pool, tell it we've croaked.
SafeClose();
pool.BellyUp();
break;
}
}
}
// Check if the driver has been lingering unused for too long.
if ((! isDirty) && lastCaught.HasTranspired(dsn.maxUnusedAge)) {
// First remove from pool, to prevent race condition with
// an outsider that grabs the driver from the pool.
bool removed = pool.UnRegister(this);
if (removed) {
// Tear down connection, tell the pool we're gone,
// and at last exit the maintenance thread.
SafeClose();
pool.BellyUp();
break;
} else {
// The driver has already been caught.
// Guess there was really no need to go away after all!
}
}
// Check if it's time to make sure the connection's alive.
if ((! isDirty) && lastActivity.HasTranspired(dsn.pingInterval)) {
// Run a MySQL server ping.
try {
Ping();
} catch (Exception e) {
// Store any problem for later consumption.
pingProblem = e;
}
// In case of a problem, remove driver from the pool.
bool removed = pool.UnRegister(this);
if (removed) {
// Tear down connection as nicely as possible,
// tell the pool we've quit, and at last exit
// the maintenance thread.
SafeClose();
pool.BellyUp();
break;
} else {
// The driver has already been caught.
// Rest of the matter is handled in Bind().
}
}
// Clear the activity flag/lock.
idle.Set();
} catch (ThreadInterruptedException) {
// Allow wake-ups when cleaning is needed.
// If thread is interrupted while not sleeping,
// it is postponed to the next sleep, and one
// eventless extra round might be paced through.
}
}
};
maintenanceThread = new MySqlThread("MySqlDriver maintenance " + connectionId);
maintenanceThread.Run(maintenance);
}
}
#endregion
#region class DsnParameters [parameter parser and container]
///
/// DSN parser and parameter container class:
/// - Can parse a DSN string (unimplemented).
/// - Acts as a store for the resulting parameters.
/// - Contains default settings for stuff not specified in the DSN string.
///
internal class DsnParameters {
internal string original;
internal bool pooling = true;
internal int poolMin = 0, poolMax = 100;
internal int maxUnusedAge = 300, unusedSweepRerun = 30;
internal int pingInterval = 10, pingSweepRerun = 1;
internal bool forceReuse = false;
internal int poolLifetime = 900;
// Included for completeness, the following parameters
// are not used in this prototype / example.
internal string host = null, user = null, password = null;
internal bool useMySqlPrepStmtMarker = false;
internal int connectTimeout = 10;
internal int defaultCommandTimeout = 600;
///
/// Parse DSN string in "original" and apply on top of defaults above.
///
private static void ParseOriginal() {
// Unimplemented.
}
public DsnParameters(string dsn) {
original = dsn;
ParseOriginal();
}
///
/// Useful for per-pool, per-driver and per-connection
/// performance counter instance names.
///
public string InstanceName {
get {
return user + "@" + host;
}
}
}
#endregion
#region class MySqlEventTimer [helps keep track of elapsed time]
///
/// Uses a monotonically increasing value to implement an event timer.
/// This prevents a problem that could occur if DateTime.Now was
/// used, such as manual time changes or automatic time changes (DST)
/// causing time-dependent code to malfunction.
///
/// The timer must be reset at least once every 50 days, otherwise it
/// becomes imprecise on account of the underlying value wrapping.
///
/// The longest time you can wait for an event to transpire is also
/// just below 50 days, corresponding to uint.MaxValue milliseconds.
///
internal class MySqlEventTimer {
private volatile uint value;
public MySqlEventTimer() {
Reset();
}
public void Reset() {
value = (uint) Environment.TickCount;
}
public uint Elapsed {
get {
// Handles overflows.
uint now = (uint) Environment.TickCount;
uint elapsed = now < value ? uint.MaxValue - value + now : now - value;
return elapsed / 1000;
}
}
///
/// Has an event transpired?
///
public bool HasTranspired(int seconds) {
return Elapsed >= seconds;
}
///
/// How many seconds till a given event transpires?
///
/// Returns 0 if the event has already come and gone.
public int NextEvent(int seconds) {
long when = (long) seconds - (long) Elapsed;
if (when < 0) when = 0;
return (int) when;
}
}
#endregion
#region class MySqlThread [helps in writing clean code]
///
/// Helper class used to maintain clean and simple source code
/// locality given a threading implementation in C# that does
/// not exactly facilitate this (Thread is a sealed class;
/// compare Java).
///
/// Implemented as a simple wrapper around Thread
/// that executes delegated code passed to it.
///
/// Exceptions must be handled by the passed code.
/// If an unexpected exception occurs, the runtime
/// will terminate the application.
///
internal class MySqlThread {
// Use this delegate when creating a worker code block.
public delegate void WorkerCode(ManualResetEvent ready);
// Use this property to access the internal Thread.
private Thread wrappedThread;
// Used to coordinate when it's appropriate to use Interrupt().
private ManualResetEvent codeReady = new ManualResetEvent(false);
// Contains code to be executed by this thread.
private volatile WorkerCode code = null;
// Used internally as the main thread method.
private void codeWrapper() {
code(codeReady);
}
///
/// Creates a ready-to-run thread.
///
public MySqlThread(string threadName) {
ThreadStart ts = new ThreadStart(codeWrapper);
wrappedThread = new Thread(ts);
// To test automatic driver and pool shutdown,
// remove the IsBackground line and set low timeouts.
wrappedThread.IsBackground = true;
wrappedThread.Name = threadName;
}
///
/// Schedule code to run on this thread.
///
public void Run(WorkerCode code) {
this.code = code;
wrappedThread.Start();
}
///
/// Interrupt the next Thread.Sleep in the currently running code.
///
public void Interrupt() {
// Allow the code to exit any OS-specific WaitSleepJoin states first, then interrupt.
codeReady.WaitOne();
wrappedThread.Interrupt();
}
}
#endregion