Search code examples
c#sql-serverdocker-composedbupfluent-docker

C# + DockerCompose - Wait for MS SQL Server Docker container to be up before trying to connect


I am trying to create integration tests for my microservices similar to Spotify's approach.

I am still working on how to spin up and seed the database. Currently I have a .NET Core 2.0 project with FluentDocker v2.2.15 and DbUp 4.1.0.

I use FluentDocker to call DockerCompose and start my services, including the SQL Server container

var hosts = new Hosts().Discover();
var dockerHost = hosts.FirstOrDefault(x => x.IsNative) ?? hosts.FirstOrDefault(x => x.Name == "default");

if (dockerHost == null)
{
    return;
}

var composeFile = Args["composeFile"];

var result = dockerHost.Host.ComposeUp(composeFile: composeFile);

and then I use DbUp to run my scripts and seed the database.

var connectionString = Args["connectionString"];
var scriptsPath = Args["scriptsPath"];

EnsureDatabase.For.SqlDatabase(connectionString);

var upgradeEngine = DeployChanges.To.SqlDatabase(connectionString).WithScriptsFromFileSystem(scriptsPath).Build();

var result = upgradeEngine.PerformUpgrade();

I can run this successfully when I give SQL Server enough time to start, for example, when debugging. However, if I run this at full speed, then DbUp tries to connect to SQL Server when it isn't ready yet. FluentDocker has a WaitForPort method but it doesn't seem to work with DockerCompose API.

I would like to know if there is a way to wait for SQL Server's port 1433 to be responsive before running the scripts (excluding non-deterministic tactics such as await Task.Delay) or if there are alternative libraries that allow me to have this kind of control.

Thanks


Solution

  • You can use WaitForPort, WaitForProcess, WaitForHttp, or custom lambda Wait functions on compose in FluentDocker v2.6.2. For example:

    Given the docker-compose file:

    version: '3.3'
    services:
      db:
        image: mysql:5.7
        volumes:
        - db_data:/var/lib/mysql
        restart: always
        environment:
          MYSQL_ROOT_PASSWORD: somewordpress
          MYSQL_DATABASE: wordpress
          MYSQL_USER: wordpress
          MYSQL_PASSWORD: wordpress
    
      wordpress:
        depends_on:
        - db
        image: wordpress:latest
        ports:
        - "8000:80"
        restart: always
        environment:
          WORDPRESS_DB_HOST: db:3306
          WORDPRESS_DB_USER: wordpress
          WORDPRESS_DB_PASSWORD: wordpress
    volumes:
      db_data:
    

    The file specifies that wordpress depends on db and thus the db is started first and then the wordpress container is instantiated. However to ensure that within the using clause that the wordpress web is running you need to use HTTP to determine just that. You can use the Fluent API to instantiate and wait for the service to start like this.

      var file = Path.Combine(Directory.GetCurrentDirectory(),
        (TemplateString) "Resources/ComposeTests/WordPress/docker-compose.yml");
    
    using (new Builder()
                .UseContainer()
                .UseCompose()
                .FromFile(file)
                .RemoveOrphans()
                .Wait("wordpress", (service, cnt) => {
                    if (cnt > 60) throw new FluentDockerException("Failed to wait for wordpress service");
    
                    var res = HttpExtensions.DoRequest("http://localhost:8000/wp-admin/install.php").Result;            
                    return (res.Code == HttpStatusCode.OK && 
                            res.Body.IndexOf("https://wordpress.org/", StringComparison.Ordinal) != -1) ? 0 : 500;
                  })
                .Build().Start())
      {
        // Since we have waited - this shall now always work.       
        var installPage = await "http://localhost:8000/wp-admin/install.php".Wget();
        Assert.IsTrue(installPage.IndexOf("https://wordpress.org/", StringComparison.Ordinal) != -1);
      }
    

    (The above sample is a more cumbersome WaitForHttp but using custom lambda to illustrate the point).

    In this way you could even use a db connection and query a table before continuation. Return values above zero is the time to wait until next time to test. Zero and below will end the wait successfully. An exception will terminate the wait (and fail).

    The example above uses the FluentAPI syntax, but you can add manually a hook onto compose container and use the extensions by yourself.

    Cheers, Mario