skip to Main Content

I’ve made a TCP server for a turn-based game in C#. The server uses MySql for the database and uses stored procedures for all database interactions. All database interactions are contained within a repository layer which is injected into a service layer. These services are then injected throughout the server. The database interactions currently include Inserts/Selects/Updates for the following

  • Logging
  • Account System
  • Match history
  • Player Stats

I need the server to scale to thousands of concurrent clients. I began load testing and quickly hit a wall with the MySql error ‘Too Many Connections’. At any given time, the server may need to call X amount of stored procedures, but there is a limit on how many connections I can have open at any given time.

What I’ve considered:

  • The only solution I’ve thought of so far is to have some kind of event system queue where you can add a database request to a queue, the queue fires off these requests based on a max connection number and an event gets fired when each request is complete containing the return data. However, I’ve not done this before so I don’t have a clear idea on implementation.

What answer I’m hoping to get:

A solution on how I could scale the server to handle X amount of database calls when there is a limited number of available connections. Ideally with a written example in C# or pseudo code.

2

Answers


  1. Chosen as BEST ANSWER

    As AndrewR suggested, I had a look at connection pools and I ended up writing my own. Not sure if it's particiularly efficient but I've tested it with a load of 10,000 users (bots) and it seems to be working. Here it is:

    NOTES

    • This solution is coupled with MySqlConnection. You can replace with this with any data access class of your choosing
    • The max connections the pool will handle is determined by the parameter passed into InstantiateConnectionsAsync.

    Interfaces:

    public interface IDbConnection : IDisposable
    {
        int Id { get; set; }
        MySqlConnection Connector { get; }
    
        void Connect(int id);
        void Disconnect();
        void Release();
    }
    
    public interface IDbConnectionPool : IDisposable
    {
        Task<IDbConnection> GetConnectionAsync();
    
        void ReleaseConnection(IDbConnection connection);
    }
    

    DbConnection.cs

    using MySqlConnector;
    using System.Timers;
    public class DbConnection : IDbConnection
    {
        public int Id { get; set; }
        public MySqlConnection Connector { get; private set; }
    
        private readonly Config config;
        private System.Timers.Timer timeout { get; set; }
        private readonly object connectionLock = new();
    
        public DbConnection(
            Config config)
        {
            this.config = config;
            Id = -1;
            timeout = new System.Timers.Timer();
            timeout.Elapsed += TimeoutElapsed;
            timeout.Interval = config.Database.OpenConnectionTimeout;
            timeout.AutoReset = false;
        }
    
        public void Connect(int id)
        {       
            timeout?.Stop();
            lock (connectionLock)
            {
                Connector ??= new MySqlConnection();
                if (Connector.State != System.Data.ConnectionState.Open)
                {
                    Disconnect();
                    Connector = new MySqlConnection(config.Database.DbConnectionString);
                    Connector.Open();
                }
                Id = id;
            }                                           
        }
    
        public void Release()
        {
            timeout?.Start();
            Id = -1;
        }
    
        private void TimeoutElapsed(object sender, ElapsedEventArgs e)
        {
            Disconnect();
        }
    
        public void Disconnect()
        {
            lock (connectionLock)
            {
                if (Id < 0)
                {
                    Connector?.Close();
                    Connector?.Dispose();
                }               
            }               
        }
    
        public void Dispose()
        {
            timeout?.Stop();
            timeout?.Dispose();
            Connector?.Close();
            Connector?.Dispose();
            Connector = null;
        }
    }
    

    DbConnectionPool.cs

    using MySqlConnector;
    public class DbConnectionPool : IDbConnectionPool
    {
        private readonly Config config;
        private Queue<int> waiting;
        private readonly object idLock = new();
        private readonly object waitingLock = new();
        private readonly object connectionsLock = new();
        private IList<IDbConnection> connections;
        private bool active;
        private int ticketId = 0;
    
        public DbConnectionPool(
            Config config)
        {
            this.config = config;
            waiting = new Queue<int>();
            connections = new List<IDbConnection>();
            InstantiateConnectionsAsync(config.Database.MaxConnections);
            active = true;
            _ = Task.Run(ConnectionHandler);
        }
    
        public async Task<IDbConnection> GetConnectionAsync()
        {
            int id;
            id = GenerateTicketId();
            Enqueue(id);
    
            while (!IsConnectionReady(id))
                await Task.Delay(config.Database.ConnectionWaitTime);
    
            return connections.FirstOrDefault(f => f.Id == id);
        }
    
        public void ReleaseConnection(IDbConnection connection)
        {
            lock (connectionsLock)
            {
                connection?.Release();
            }
        }
    
        private int GenerateTicketId()
        {
            lock (idLock)
            {
                if (ticketId >= int.MaxValue - 1)
                    return ticketId = 0;
    
                return ++ticketId;
            }           
        }
    
        private void InstantiateConnectionsAsync(int maxConnections)
        {
            int count = 0;
            while (count < maxConnections)
            {
                connections.Add(DIContainer.ServiceProvider.GetService<IDbConnection>());
                count++;
            }
        }
    
        private void Enqueue(int id)
        {
            try
            {
                lock (waitingLock)
                    waiting.Enqueue(id);
            }
            catch (Exception e)
            {
                // getting a weird/inconsistent error around here
            }
        }
    
        private void ConnectionHandler()
        {
            while (active)
            {
                if (waiting.Count < 1)
                {
                    Task.Delay(config.Database.ConnectionHandlerDelay);
                    continue;
                }
    
                IDbConnection connection = null;
                lock (connectionsLock)
                {
                    while (waiting.Count > 0 && GetAvailableConnection(ref connection) != null)
                    {
                        var id = -1;
                        lock (waitingLock)
                            id = waiting.Dequeue();
    
                        connection.Connect(id);
                        connection = null;
                    }
                }
            }
        }
    
        private IDbConnection GetAvailableConnection(ref IDbConnection connection) => connection = connections.FirstOrDefault(f => f.Id < 0);
    
        private bool IsConnectionReady(int id) => connections.Any(a => a.Id == id);     
    
        public void Dispose()
        {
            active = false;
            lock (connectionsLock)
            {
                Parallel.ForEach(connections ?? new List<IDbConnection>(), connection =>
                {
                    connection?.Dispose();
                });
                connections = null;
            }
        }
    }
    

    Create a singleton of DbConnectionPool. You can then get a connection by calling GetConnectionAsync(). Here is how calling GetConnectionAsync might look:

    public async Task<bool> SomeMethodAsync()
        {
            try
            {
                var con = await pool.GetConnectionAsync();
                using var cmd = new MySqlCommand("UpdateSomething", con.Connector)
                {
                    CommandType = CommandType.StoredProcedure
                };
    
                var result = await cmd.ExecuteNonQueryAsync() > 0;
    
                await cmd.DisposeAsync();
                pool.ReleaseConnection(con);
                return result;
            }
            catch (Exception ex)
            {
                // Do something with the exception
                return false;
            }
        }
    

  2. You are looking for a connection pool – a layer in your application which will open a fixed number of connections and allow higher layers to use those.

    Higher layer will request a connection from the pool, execute operations and release the connection back to the pool.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search