Bug #115135 EntityFrameworkCore+MySQL hangs when `Min Pool Size=10` only on ubuntu/linux
Submitted: 27 May 8:54 Modified: 27 May 10:02
Reporter: José Javier Rodríguez Zas Email Updates:
Status: Analyzing Impact on me:
None 
Category:Connector / NET Severity:S3 (Non-critical)
Version:mysql 8.0.35 & MySql.EFCore 8.0.2 OS:Ubuntu (22.04)
Assigned to: MySQL Verification Team CPU Architecture:x86 (x86_64 )
Tags: dotnet, DotNet Connector, EFCore, Ubuntu dotnet

[27 May 8:54] José Javier Rodríguez Zas
Description:
I am currently using net8.0 (though I have also tested this on net6.0), along with the latest versions of EntityFrameworkCore and MySql.EntityFrameworkCore and MySQL 8.0.35 (both on docker and installed on host machine). My connection string includes "Min Pool Size=10". The application runs smoothly on various windows platforms: Windows machine, an Ubuntu VM on Windows, a Docker Compose on a Windows machine, and even on Azure. However, when I attempt to run the same application on Ubuntu 22.04 machine, or a Docker Compose on an Ubuntu 22.04 machine, it hangs while trying to establish a connection to the database

When I remove the "Min Pool Size=10" from the connection string, the application runs without any issues in all tested environments. Despite increasing the timeout in the connection string, the application continues to stall if "Min Pool Size > 0" is present.

This is the exception:

app  | Unhandled exception. System.AggregateException: One or more errors occurred. (Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.)
app  |  ---> MySql.Data.MySqlClient.MySqlException (0x80004005): Timeout expired.  The timeout period elapsed prior to completion of the operation or the server is not responding.
app  |  ---> System.TimeoutException: The operation has timed out.
app  |    at MySql.Data.Common.StreamCreator.<>c.<GetTcpStreamAsync>b__8_1()
app  |    at System.Threading.CancellationTokenSource.Invoke(Delegate d, Object state, CancellationTokenSource source)
app  |    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
app  | --- End of stack trace from previous location ---
app  |    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
app  |    at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
app  |    --- End of inner exception stack trace ---
app  |    at System.Threading.CancellationTokenSource.ExecuteCallbackHandlers(Boolean throwOnFirstException)
app  |    at System.Threading.TimerQueueTimer.Fire(Boolean isThreadPool)
app  |    at System.Threading.TimerQueue.FireNextTimers()
app  |    at System.Threading.ThreadPoolWorkQueue.Dispatch()
app  |    at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()

How to repeat:
My original application is an ASPNET Core API, but I was able to replicate the issue using a simple console application:

TestApp:

static async Task Main(string[] args)
{
    var envCnx = Environment.GetEnvironmentVariable("CONNECTION_STRING");

    IDbContextFactory dbContextFactory = new AppDbContextFactory(envCnx);

    using var db = await dbContextFactory.CreateDbContextAsync();

    await db.Database.MigrateAsync();

    var name = "User 1";

    if (!await db.Set<UserInfo>().AnyAsync(x => x.Name == name))
    {
        db.Add(new UserInfo() { Id = Guid.NewGuid(), Name = name, CreatedOn = DateTime.Now });

        await db.SaveChangesAsync();
    }
}
AppDbContextFactory:

public interface IDbContextFactory : IDbContextFactory<DbContext>
{
}

public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>, IDbContextFactory<AppDbContext>, IDbContextFactory
{
    private readonly string _cnx;

    public AppDbContextFactory(string cnx)
    {
        _cnx = cnx;
    }

    public AppDbContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        string cnx = args.Length > 0 ? args[0] : this._cnx;

#if DEBUG
        optionsBuilder.EnableSensitiveDataLogging();
        optionsBuilder.EnableDetailedErrors();
#endif
        optionsBuilder.UseMySql(cnx, ServerVersion.AutoDetect(cnx), mySqlOpts =>
        {
            mySqlOpts.UseNewtonsoftJson();
            mySqlOpts.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
        })
#if DEBUG
            .EnableSensitiveDataLogging()
            .EnableDetailedErrors()
#endif
        ;

        return new AppDbContext(optionsBuilder.Options);
    }

    public AppDbContext GetContext()
    {
        return CreateDbContext([_cnx]);
    }

    public AppDbContext CreateDbContext()
    {
        return GetContext();
    }

    DbContext IDbContextFactory<DbContext>.CreateDbContext()
    {
        return GetContext();
    }
}
AppDbContext:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<UserInfo>().HasKey(x => x.Id);
        modelBuilder.Entity<UserInfo>().HasIndex(x => x.Id).IsUnique();
        modelBuilder.Entity<UserInfo>().Property(t => t.Id).ValueGeneratedOnAdd();
        modelBuilder.Entity<UserInfo>().Property(x => x.Timestamp).IsRowVersion();
        modelBuilder.Entity<UserInfo>().HasIndex(x => x.Name);
        modelBuilder.Entity<UserInfo>().ToTable("users");
    }
}
UserInfo:

public class UserInfo
{
    [JsonConstructor]
    public UserInfo() { }

    public Guid Id { get; set; }
    public string Name { get; set; }

    public DateTimeOffset CreatedOn { get; set; }

    public DateTime Timestamp { get; set; }
}
Dockerfile:

#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
USER app
WORKDIR /app

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["ConsoleTestApp/ConsoleTestApp.csproj", "ConsoleTestApp/"]
RUN dotnet restore "./ConsoleTestApp/ConsoleTestApp.csproj"
COPY . .
WORKDIR "/src/ConsoleTestApp"
RUN dotnet build "./ConsoleTestApp.csproj" -c $BUILD_CONFIGURATION -o /app/build

FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./ConsoleTestApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ConsoleTestApp.dll"]
docker-compose.yml

version: "3.3"
name: "test-app"

services:

  app:
    image: console-app:latest
    container_name: console-app
    depends_on:
      - mysql
    environment:
      # CONNECTION_STRING: server=mysql,3306;user=root;password=2YSCK+fktPRGOi5omfRjCAP_am9+3r;database=test_db
      CONNECTION_STRING: server=mysql,3306;user=root;password=2YSCK+fktPRGOi5omfRjCAP_am9+3r;database=test_db;Min Pool Size=10;Connect Timeout=600

  mysql:
    container_name: db
    image: mysql:8.0.35
    cap_add:
      - SYS_NICE
    restart: always
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: 2YSCK+fktPRGOi5omfRjCAP_am9+3r
      MYSQL_DATABASE: test_db
project structure:

.
├── ConsoleTestApp
│   ├── ConsoleTestApp.csproj
│   ├── Data
│   │   ├── AppDbContext.cs
│   │   ├── AppDbContextFactory.cs
│   │   ├── Migrations
│   │   │   ├── 20240523164903_InitialMigration.Designer.cs
│   │   │   ├── 20240523164903_InitialMigration.cs
│   │   │   └── AppDbContextModelSnapshot.cs
│   │   └── UserInfo.cs
│   ├── Program.cs
│   └── Properties
│       └── launchSettings.json
├── ConsoleTestApp.sln
├── Dockerfile
└── docker-compose.yml