/* 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