views:

118

answers:

1

We require programmatic access to a SQL Server Express service as part of our application. Depending on what the user is trying to do, we may have to attach a database, detach a database, back one up, etc. Sometimes the service might not be started before we attempt these operations. So we need to ensure the service is started. Here is where we are running into problems. Apparently the ServiceController.WaitForStatus(ServiceControllerStatus.Running) returns prematurely for SQL Server Express. What is really puzzling is that the master database seems to be immediately available, but not other databases. Here is a console application to demonstrate what I am talking about:

namespace ServiceTest
{
    using System;
    using System.Data.SqlClient;
    using System.Diagnostics;
    using System.ServiceProcess;
    using System.Threading;

    class Program
    {
        private static readonly ServiceController controller = new ServiceController("MSSQL$SQLEXPRESS");
        private static readonly Stopwatch stopWatch = new Stopwatch();

        static void Main(string[] args)
        {
            stopWatch.Start();

            EnsureStop();
            Start();
            OpenAndClose("master");

            EnsureStop();
            Start();
            OpenAndClose("AdventureWorksLT");

            Console.ReadLine();
        }

        private static void EnsureStop()
        {
            Console.WriteLine("EnsureStop enter, {0:N0}", stopWatch.ElapsedMilliseconds);

            if (controller.Status != ServiceControllerStatus.Stopped)
            {
                controller.Stop();
                controller.WaitForStatus(ServiceControllerStatus.Stopped);
                Thread.Sleep(5000); // really, really make sure it stopped ... this has a problem too.
            }

            Console.WriteLine("EnsureStop exit, {0:N0}", stopWatch.ElapsedMilliseconds);
        }

        private static void Start()
        {
            Console.WriteLine("Start enter, {0:N0}", stopWatch.ElapsedMilliseconds);
            controller.Start();
            controller.WaitForStatus(ServiceControllerStatus.Running);
            // Thread.Sleep(5000); 
            Console.WriteLine("Start exit, {0:N0}", stopWatch.ElapsedMilliseconds);
        }

        private static void OpenAndClose(string database)
        {
            Console.WriteLine("OpenAndClose enter, {0:N0}", stopWatch.ElapsedMilliseconds);
            var connection = new SqlConnection(string.Format(@"Data Source=.\SQLEXPRESS;initial catalog={0};integrated security=SSPI", database));
            connection.Open();
            connection.Close();
            Console.WriteLine("OpenAndClose exit, {0:N0}", stopWatch.ElapsedMilliseconds);
        }
    }
}

On my machine, this will consistently fail as written. Notice that the connection to "master" has no problems; only the connection to the other database. (You can reverse the order of the connections to verify this.) If you uncomment the Thread.Sleep in the Start() method, it will work fine.

Obviously I want to avoid an arbitrary Thread.Sleep(). Besides the rank code smell, what arbitary value would I put there? The only thing we can think of is to put some dummy connections to our target database in a while loop, catching the SqlException thrown and trying again until it works. But I'm thinking there must be a more elegant solution out there to know when the service is really ready to be used. Any ideas?

EDIT: Based on feedback provided below, I added a check on the status of the database. However, it is still failing. It looks like even the state is not reliable. Here is the function I am calling before OpenAndClose(string):

private static void WaitForOnline(string database)
{
    Console.WriteLine("WaitForOnline start, {0:N0}", stopWatch.ElapsedMilliseconds);

    using (var connection = new SqlConnection(string.Format(@"Data Source=.\SQLEXPRESS;initial catal
    using (var command = connection.CreateCommand())
    {
        connection.Open();

        try
        {
            command.CommandText = "SELECT [state] FROM sys.databases WHERE [name] = @DatabaseName";
            command.Parameters.AddWithValue("@DatabaseName", database);

            byte databaseState = (byte)command.ExecuteScalar();
            Console.WriteLine("databaseState = {0}", databaseState);
            while (databaseState != OnlineState)
            {
                Thread.Sleep(500);
                databaseState = (byte)command.ExecuteScalar();
                Console.WriteLine("databaseState = {0}", databaseState);
            }
        }
        finally
        {
            connection.Close();
        }
    }

    Console.WriteLine("WaitForOnline exit, {0:N0}", stopWatch.ElapsedMilliseconds);
}

I found another discussion dealing with a similar problem. Apparently the solution is to check the sys.database_files of the database in question. But that, of course, is a chicken-and-egg problem. Any other ideas?

+1  A: 

Service start != database start.

Service is started when the SQL Server process is running and responded to the SCM that is 'alive'. After that the server will start putting user databases online. As part of this process, it runs the recovery process on each database, to ensure transactional consistency. Recovery of a database can last anywhere from microseconds to whole days, it depends on the ammount of log to be redone and the speed of the disk(s).

After the SCM returns that the service is running, you should connect to 'master' and check your database status in sys.databases. Only when the status is ONLINE can you proceed to open it.

Remus Rusanu
I added the check for ONLINE. It is still failing. :(I editted my original post to include the check, as well as a comment about sys_database_files I saw on other discussion.
Dave
So you are able to connect to master after service start up, you are checking the target database status and is ONLINE, and yet when you try to open a connection that specifies Initial Catalog the target db, it still fails. Is there any error in the ERRORLOG? I'm trying to understand if ADO.Net connection pooling can interfere with this scenario (it shouldn't, but one never knows...). If even after the db is ONLINE the conenciton still throws error, I don't see any solution other than the try/catch loop until it actualy succeeds (yuck).
Remus Rusanu