20

I have a method that calls a SQLServer function to perform a free text search against a table. That function will occasionally on the first call result in a SQLException: "Word breaking timed out for the full-text query string". So typically I want to retry that request because it will succeed on subsequent requests. What is good style for structuring the retry logic. At the moment I have the following:

var retryCount = 0;
var results = new List<UserSummaryDto>();
using (var ctx = new UsersDataContext(ConfigurationManager.ConnectionStrings[CONNECTION_STRING_KEY].ConnectionString))
{
    for (; ; )
    {
        try
        {
            results = ctx.SearchPhoneList(value, maxRows)
                         .Select(user => user.ToDto())
                         .ToList();
            break;
        }
        catch (SqlException)
        {
            retryCount++;
            if (retryCount > MAX_RETRY) throw;
        }
    }
}

return results;
David Clarke
  • 12,888
  • 9
  • 86
  • 116
  • 1
    Actually, its conceptually simple and should work. Although I'd caution against the exteme use of Var. Sometimes an int is just an int. – MAW74656 Jan 27 '11 at 22:00
  • `On Error Resume Next` in Visual Basic was conceptually simple - that didn't make it a good idea. – MusiGenesis Jan 28 '11 at 00:12
  • @MAW74656: I voted your comment up on the basis of the `var` remark. IMO, the compiler should disallow its use where it conceals the object type, as it does here. – MusiGenesis Jan 28 '11 at 00:14
  • 5
    Just habit, I like to explicitly initialise my variables. Plus I use ReSharper which will suggest replacing declared types with "var" and tell me that "int i = 0;" doesn't need to be initialised to zero because it is by default. Cautioning against the extreme use of var may be a little extreme. – David Clarke Jan 28 '11 at 00:46
  • 1
    Just an update on the `var` comments. There isn't anything in the above code that conceals the object type. `retryCount` is clearly `int`, `results` is a generic `List`, `ctx` is a data context. I did recently have a situation where a variable was initialised by a method where the returned object was not obvious e.g. `var someVar = MyInitialiser();`. Under those circumstances a `var` declaration conceals the type of the variable and should be declared explicitly. – David Clarke May 06 '15 at 20:51

8 Answers8

17

I'd change the exception handling to only retry on certain errors:

  • 1204, 1205 deadlocks
  • -2 timeout
  • -1 connection broken

These are the basic "retryable" errors

catch (SqlException ex)
{
    if !(ex.Number == 1205 || ex.Number == 1204 || ... )
    {
        throw
    }
    retryCount++;
    if (retryCount > MAX_RETRY) throw;
}

Edit, I clean forgot about waits so you don't hammer the SQL box:

  • Add a 500 ms wait on deadlock
  • Add a 5 sec delay on timeout

Edit 2:

I'm a Developer DBA, don't do much C#. My answer was to correct exception processing for the calls...

gbn
  • 422,506
  • 82
  • 585
  • 676
  • @jani: Your link may improve the quality of c# (I'm a developer DBA: c# isn't a core skill for me) but it doesn't help with trapping database errors nicely to work out if a retry is possible. – gbn Jan 27 '11 at 21:05
  • 1
    This is OK for the purpose. The one thing I'd do is group the error codes in an array and use .Contains(ex.Number); makes it a little more compact. – KeithS Jan 27 '11 at 21:06
  • @Keith and what if your message contains two of them? relying on the error message returned by db like this, is not a good idea IMHO. – Jahan Zinedine Jan 27 '11 at 21:16
  • I think retrying in your store procedure would be much more better than retry in application level, it's more fast, less error prone, and doesn't need a round trip between the App and DB. – Jahan Zinedine Jan 27 '11 at 21:21
  • 2
    @Jani - What? An exception will be for a single error, usually the first one encountered by the engine when processing a command. And we're not relying on the message, but on the number, which is unique to a type of error and the ONLY way you can tell one AdoException or SqlException from another. – KeithS Jan 27 '11 at 22:15
  • @KeithS So that would be Ok, and I was wrong cause I thought that you check the text message not the error number. – Jahan Zinedine Jan 28 '11 at 08:23
  • @JahanZinedine http://www.parashift.com/c++-faq-lite/exceptions.html#faq-17.2 not found – Kiquenet May 24 '18 at 19:10
  • More ***Error codes*** for _retry_ https://github.com/marinoscar/CommonHelpers/blob/master/SqlErrorCodes.cs – Kiquenet May 24 '18 at 21:21
12

Thanks for all the feedback. I'm answering this myself so I can incorporate elements from the answers given. Please let me know if I've missed something. My method becomes:

var results = new List<UserSummaryDto>();
Retry<UsersDataContext>(ctx => results = ctx.SearchPhoneList(value, maxRows)
                                            .Select(user => user.ToDto())
                                            .ToList());
return results;

And I've refactored the original method for reuse. Still lots of levels of nesting. It also relies on there being a default constructor for the data context which may be too restrictive. @Martin, I considered including your PreserveStackTrace method but in this case I don't think it really adds enough value - good to know for future reference thanks:

private const int MAX_RETRY = 2;
private const double LONG_WAIT_SECONDS = 5;
private const double SHORT_WAIT_SECONDS = 0.5;
private static readonly TimeSpan longWait = TimeSpan.FromSeconds(LONG_WAIT_SECONDS);
private static readonly TimeSpan shortWait = TimeSpan.FromSeconds(SHORT_WAIT_SECONDS);
private enum RetryableSqlErrors
{
    Timeout = -2,
    NoLock = 1204,
    Deadlock = 1205,
    WordbreakerTimeout = 30053,
}

private void Retry<T>(Action<T> retryAction) where T : DataContext, new()
{
    var retryCount = 0;
    using (var ctx = new T())
    {
        for (;;)
        {
            try
            {
                retryAction(ctx);
                break;
            }
            catch (SqlException ex)
                when (ex.Number == (int) RetryableSqlErrors.Timeout &&
                      retryCount < MAX_RETRY)
            {
                Thread.Sleep(longWait);
            }
            catch (SqlException ex)
                when (Enum.IsDefined(typeof(RetryableSqlErrors), ex.Number) &&
                      retryCount < MAX_RETRY)
            {
                Thread.Sleep(shortWait);
            }
            retryCount++;
        }
    }
}
David Clarke
  • 12,888
  • 9
  • 86
  • 116
  • More ***RetryableSqlErrors*** https://github.com/marinoscar/CommonHelpers/blob/master/SqlErrorCodes.cs ? – Kiquenet May 24 '18 at 21:20
8

My enum of retryables for sql looks like this:

SqlConnectionBroken = -1,
SqlTimeout = -2,
SqlOutOfMemory = 701,
SqlOutOfLocks = 1204,
SqlDeadlockVictim = 1205,
SqlLockRequestTimeout = 1222,
SqlTimeoutWaitingForMemoryResource = 8645,
SqlLowMemoryCondition = 8651,
SqlWordbreakerTimeout = 30053
David Clarke
  • 12,888
  • 9
  • 86
  • 116
jimasp
  • 962
  • 9
  • 26
  • More ***error codes*** for retry: https://github.com/marinoscar/CommonHelpers/blob/master/SqlErrorCodes.cs – Kiquenet May 24 '18 at 19:35
7

It's not good style, but sometimes you have to do it, because you simply can't change existing code and have to deal with it.

I am using the following generic method for this scenario. Note the PreserveStackTrace() method, which can sometimes be very helpful in a re-throw scenario.

public static void RetryBeforeThrow<T>(Action action, int retries, int timeout) where T : Exception
{
    if (action == null)
        throw new ArgumentNullException("action", string.Format("Argument '{0}' cannot be null.", "action"));

    int tries = 1;

    do
    {
        try
        {
            action();
            return;
        }
        catch (T ex)
        {
            if (retries <= 0)
            {
                PreserveStackTrace(ex);
                throw;
            }

            Thread.Sleep(timeout);
        }
    }
    while (tries++ < retries);
}

/// <summary>
/// Sets a flag on an <see cref="T:System.Exception"/> so that all the stack trace information is preserved 
/// when the exception is re-thrown.
/// </summary>
/// <remarks>This is useful because "throw" removes information, such as the original stack frame.</remarks>
/// <see href="http://weblogs.asp.net/fmarguerie/archive/2008/01/02/rethrowing-exceptions-and-preserving-the-full-call-stack-trace.aspx"/>
public static void PreserveStackTrace(Exception ex)
{
    MethodInfo preserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic);
    preserveStackTrace.Invoke(ex, null);
}

You would call it like that:

RetryBeforeThrow<SqlException>(() => MethodWhichFails(), 3, 100);
Martin Buberl
  • 45,844
  • 25
  • 100
  • 144
  • `throw` should not remove original stack frame. Only if you used `throw ex` – František Žiačik Jan 27 '11 at 20:55
  • 1
    @František Žiačik Generaly it should. Read the article on Fabrice blog in my answer and you'll see what I mean. – Martin Buberl Jan 27 '11 at 20:58
  • 3
    I tried it myself and it turns out you're right. One never stops learning. The other way to preserve the information is to wrap the exception (`throw new Exception("Message", ex)`) – František Žiačik Jan 27 '11 at 21:17
  • Better solution is [ExceptionDispatchInfo](http://msdn.microsoft.com/en-us/library/system.runtime.exceptionservices.exceptiondispatchinfo%28v=vs.110%29.aspx) view the [answer](https://stackoverflow.com/a/17091351/206730) , not use [PreserveStackTrace](https://stackoverflow.com/a/2085377/206730) ? anyways, better use [PrepForRemoting](https://stackoverflow.com/a/4557183/206730) if not `ExceptionDispatchInfo` available – Kiquenet May 25 '18 at 10:34
3

There is no good style for doing something like this. You'd be better off figuring out why the request fails the first time but succeeds the second time.

It seems possible that Sql Server has to initially compile an execution plan and then execute the query. So the first call fails because the combined times exceed your timeout property, and succeeds the second time because the execution plan is already compiled and saved.

I don't know how UsersDataContext works, but it may be the case that you have the option to Prepare the query before actually executing it.

Real Answer: If I had to do this, I would retry just once and not again, like this:

var results = new List<UserSummaryDto>();
using (var ctx = new 
    UsersDataContext(ConfigurationManager.ConnectionStrings[CONNECTION_STRING_KEY].ConnectionString))
{
        try
        {
            results = ctx.SearchPhoneList(value, maxRows)
                         .Select(user => user.ToDto())
                         .ToList();
            break;
        }
        catch (SqlException)
        {
            try
            {
                results = ctx.SearchPhoneList(value, maxRows)
                         .Select(user => user.ToDto())
                         .ToList();
                break;
            }
            catch (SqlException)
            {
                // set return value, or indicate failure to user however
            }
        }
    }
}

return results;

While I might trust you to not abuse the retry process, you'd be tempting your successor to increase the retry count as a quick fix.

MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
  • 1
    I was going to say this, but was too chicken. If timeouts are a big problem, and getting the system to where it can stand up without having to do retries, I would consider smoothing out the requests into a queue and then responding asynchronously. – Chris B. Behrens Jan 27 '11 at 21:11
  • 4
    I disagree: some errors in SQL server are inherently retryable. If you hit the server during index rebuild for example, a wait and retry is harmless and avoids unnecessary support or logging – gbn Jan 27 '11 at 21:22
  • Figuring out why the query fails is a good idea but isn't particularly defensive. As I indicated in my original question, the function is doing a free text search and the exception reflects that. I have used the Sql Server Configuration Manager to automatically start the FullText filter service but I can still get the exception on the first request. This is on my development machine using a local install of Sql Server. – David Clarke Jan 27 '11 at 21:45
  • 1
    @gbn and David Clarke: if you guys are comfortable being responsible for an app that fails like this, then more power to you. I wouldn't be at all comfortable with it, and would put it at the top of my "must fix this" list. – MusiGenesis Jan 27 '11 at 23:07
  • 1
    @MusiGenesis if you've ever been involved with enterprise-level development you might take a more defensive approach to coding. Yes it is good to identify why something is failing but it doesn't help the poor user whose app starts crashing because someone introduced a new stored proc into the database that locks the requested rows. If you allow a retry then there is at least a possibility for the user to continue their work, albeit more slowly. – David Clarke Jan 27 '11 at 23:37
  • @David Clarke: I've been involved with enterprise-level development for my entire career, but that means nothing in this case. Whether large or small, any company that treats a catch-and-retry "solution" like this as anything other than a desperate, emergency fix is begging for more trouble. Let me know what enterprises you've worked for, and I'll go ahead and sell their stock. – MusiGenesis Jan 28 '11 at 00:10
  • 2
    @MusiGenesis clearly we disagree. To me this is neither a desperate emergency fix or a solution, it is a defensive measure to allow for some flexibility when dealing with an enterprise system that isn't under my sole control. There are a variety of issues that can occur on a production database that shouldn't result in the user's application failing. With respect to your last comment, you'll be concerned to know that I've worked previously for a number of banks - perhaps you should consider keeping your money under your mattress ;-) – David Clarke Jan 28 '11 at 00:31
  • @David: to be fair, I would do the same thing as you if I faced the same situation and couldn't find a real fix. I just wouldn't admit to it on StackOverflow. :) – MusiGenesis Jan 28 '11 at 00:39
  • @MusiGenesis thanks for the udpate to your answer but now you have implicitly approved copy-paste coding, breaking the DRY principle and which my less knowledgeable successor is likely to repeat in order to add a 2nd or 3rd retry. – David Clarke Jan 28 '11 at 01:02
  • 1
    @David: that's good stuff. :) I was going to delete this post to punish myself for getting drawn into another round of nerd wrestling, but I think I'll leave it up as a warning to future generations. – MusiGenesis Jan 28 '11 at 01:13
  • 1
    Out of curiosity, how would you handle a deadlock given you can't eliminate them? – gbn Jan 28 '11 at 04:50
  • @gbn - selectively denormalize, if I think that's likely to be the only problem, or go whole hog with an OLAP implementation. Or if I was feeling lazy, throw hardware at it to try and decrease the deadlock window. – Chris B. Behrens Jan 28 '11 at 15:54
  • 1
    @Chris B. Behrens: interesting approaches. I prefer to keep my app running more simply in line with what MSDN says... http://msdn.microsoft.com/en-us/library/ms177453.aspx – gbn Jan 28 '11 at 16:38
  • 1
    @gbn - yeah...my beef with that analysis is that it is totally agnostic to the cause of the deadlock. It may be that blindly resubmitting the query makes the deadlock problem worse. As with every db question, the ultimate answer is "there is no single answer - it depends on the circumstances." – Chris B. Behrens Jan 28 '11 at 16:41
  • @Chris B. Behrens: I've never seen a recurring, predictable *SQL Server* deadlock outside of my own test scripts. – gbn Jan 28 '11 at 16:47
  • @gbn - absolutely. Which is kind of my point...you can't know at runtime (at least with a naive implementation) that it's the smart thing to do to immediately retry. – Chris B. Behrens Jan 28 '11 at 16:50
1

I think annotating a method with an aspect specifying the retry count would result in more structured code, although it needs some infrastructure coding.

peterh
  • 11,875
  • 18
  • 85
  • 108
Jahan Zinedine
  • 14,616
  • 5
  • 46
  • 70
0

You can simply use SqlConnectionStringBuilder properties to sql connection retry.

var conBuilder = new SqlConnectionStringBuilder("Server=.;Database=xxxx;Trusted_Connection=True;MultipleActiveResultSets=true"); conBuilder.ConnectTimeout = 90; conBuilder.ConnectRetryInterval = 15; conBuilder.ConnectRetryCount = 6;

Note:- Required .Net 4.5 or later.

Neeraj Singh
  • 151
  • 2
  • 6
-7

Pull the relevant code out into its own method, then use recursion.

Pseudo-code:

try
{
    doDatabaseCall();
}
catch (exception e)
{
    //Check exception object to confirm its the error you've been experiencing as opposed to the server being offline.
    doDatabaseCall();
}
Gavin
  • 8,204
  • 3
  • 32
  • 42
MAW74656
  • 3,449
  • 21
  • 71
  • 118
  • And if the server is offline for 2 hours you propose to keep trying...? – gbn Jan 27 '11 at 21:28
  • @gbn- No, of course not. We're only giving him concepts here, not writing his entire code. See my edit. – MAW74656 Jan 27 '11 at 21:56
  • Maybe some combination of his counter and my abstraction would be best. – MAW74656 Jan 27 '11 at 22:03
  • 2
    I don't see any recursion in your code. Anyway, it doesn't seem to me like a good idea to use recursion for this. – František Žiačik Jan 27 '11 at 23:00
  • Ok, put the try-catch inside the doDatabaseCall() and then its recursive. Neater code too. Doesn't really matter what you call it, the effect is the same. And use a counter (global variable?) to keep track of the number of retries and limit them within the catch block. – MAW74656 Jan 31 '11 at 18:13
  • 1
    I marked this as low quality, and a moderator declined my flag. SO is broken. – Mitch Wheat Oct 11 '18 at 01:59