Savoir quand réessayer ou échouer lors de l'appel de SQL Server à partir de C #?

J'ai une application C # qui récupère des données de SQL Server hébergé dans un environnement un peu floconneux. Je ne peux rien faire pour régler les problèmes environnementaux, je dois donc les traiter aussi gracieusement que possible.

Pour ce faire, je veux réessayer les opérations qui sont le résultat de défaillances d'infrastructure telles que des problèmes de réseau, des servers SQL qui sont hors ligne parce qu'ils sont en train d'être redémarrés, des timeouts d'attente de requêtes, etc. En même time, je ne veux pas pour réessayer des requêtes si elles ont échoué pour des erreurs logiques. Je veux juste que ceux-ci fassent l'exception jusqu'au client.

Ma question est la suivante: quelle est la meilleure façon de distinguer entre les problèmes environnementaux (connections perdues, timeouts d'attente) et d'autres types d'exceptions (des choses comme des erreurs logiques qui se seraient produites même si l'environnement était stable).

Y a-t-il un motif couramment utilisé en C # pour traiter des choses comme ça? Par exemple, existe-t-il une propriété que je peux vérifier sur l'object SqlConnection pour détecter les connections ayant échoué? Si non, quelle est la meilleure façon d'aborder ce problème?

Pour ce que ça vaut, mon code n'a rien de spécial:

using (SqlConnection connection = new SqlConnection(myConnectionSsortingng)) using (SqlCommand command = connection.CreateCommand()) { command.CommandText = mySelectCommand; connection.Open(); using (SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { // Do something with the returned data. } } } 

Une seule SqlException (peut) SqlException plusieurs erreurs SQL Server. Vous pouvez itérer à travers eux avec la propriété Errors . Chaque erreur est SqlError :

 foreach (SqlError error in exception.Errors) 

Chaque SqlError a une propriété de Class vous pouvez utiliser pour déterminer approximativement si vous pouvez réessayer ou non (et dans le cas où vous réessayez si vous devez recréer la connection aussi). De MSDN :

  • Class <10 est pour les erreurs dans les informations que vous avez passées (probablement) vous ne pouvez pas réessayer si vous ne corrigez pas les inputs.
  • Class de 11 à 16 sont "générées par l'user" alors probablement vous ne pouvez plus rien faire si l'user ne corrige pas d'abord ses inputs. S'il vous plaît noter que la class 16 comprend de nombreuses erreurs temporaires et la class 13 est pour les blocages (grâce à EvZ) de sorte que vous pouvez exclure ces classs si vous les manipulez une par une.
  • Class de 17 à 24 sont des erreurs matérielles / logicielles generics et vous pouvez réessayer. Lorsque la Class est 20 ou plus, vous devez recréer la connection . Notez que 22 et 23 peuvent être de sérieuses erreurs matérielles / logicielles et que vous pouvez arrêter directement d'essayer. 24 indique une erreur de support (un user doit être averti mais vous pouvez réessayer s'il ne s'agit que d'une erreur "temporaire").

Vous pouvez find une description plus détaillée de chaque class ici .

En général, si vous gérez les erreurs avec leur class, vous n'aurez pas besoin de connaître exactement chaque erreur (en utilisant la propriété error.Number ou exception.Number qui est juste un raccourci pour le premier SqlError dans cette list). Cela a l'inconvénient que vous pouvez réessayer quand ce n'est pas utile (ou l'erreur ne peut pas être récupérée). Je suggère une approche en deux étapes :

  • SELECT * FROM master.sys.messages codes d'erreur connus ( SELECT * FROM master.sys.messages codes d'erreur avec SELECT * FROM master.sys.messages ) pour voir ce que vous voulez gérer (savoir comment). Cette vue contient des messages dans toutes les langues sockets en charge, vous devrez peut-être les filterr par colonne msglangid (par exemple 1033 pour l'anglais).
  • Pour tout le rest s'appuyer sur la class d'erreur, réessayer lorsque la Class est de 13 ou supérieure à 16 (et se reconnecter si 20 ou plus).
  • Les erreurs de gravité supérieure à 21 (22, 23 et 24) sont des erreurs graves et peu d'attente ne permettra pas de résoudre ces problèmes (la database elle-même peut également être endommagée).

La stratégie pour réessayer dépend de l'erreur que vous gérez: ressources libres, attente d'exécution d'une opération en attente, action alternative, etc. En général, vous ne devez réessayer que si toutes les erreurs sont réessayables:

 bool rebuildConnection = true; // First try connection must be open for (int i=0; i < MaximumNumberOfResortinges; ++i) { try { // (Re)Create connection to SQL Server if (rebuildConnection) { if (connection != null) connection.Dispose(); // Create connection and open it... } // Perform your task // No exceptions, task has been completed break; } catch (SqlException e) { if (e.Errors.Cast<SqlError>().All(x => CanRetry(x))) { // What to do? Handle that here, also checking Number property. // For Class < 20 you may simply Thread.Sleep(DelayOnError); rebuildConnection = e.Errors .Cast<SqlError>() .Any(x => x.Class >= 20); continue; } throw; } } 

Enveloppez tout dans try / finally pour mettre correctement la connection au rebut. Avec cette fonction CanRetry() simple-faux-naïf:

 private static readonly int[] ResortingableClasses = { 13, 16, 17, 18, 19, 20, 21, 22, 24 }; private static bool CanRetry(SqlError error) { // Use this switch if you want to handle only well-known errors, // remove it if you want to always retry. A "blacklist" approach may // also work: return false when you're sure you can't recover from one // error and rely on Class for anything else. switch (error.Number) { // Handle well-known error codes, } // Handle unknown errors with severity 21 or less. 22 or more // indicates a serious error that need to be manually fixed. // 24 indicates media errors. They're serious errors (that should // be also notified) but we may retry... return ResortingableClasses.Contains(error.Class); // LINQ... } 

Quelques moyens assez rusés pour find la list des erreurs non critiques ici .

Habituellement j'insère tout ce code (passe-partout) dans une méthode (où je peux cacher toutes les choses sales faites pour créer / disposer / recréer la connection) avec cette signature:

 public static void Try( Func<SqlConnection> connectionFactory, Action<SqlCommand> performer); 

Pour être utilisé comme ceci:

 Try( () => new SqlConnection(connectionSsortingng), cmd => { cmd.CommandText = "SELECT * FROM master.sys.messages"; using (var reader = cmd.ExecuteReader()) { // Do stuff } }); 

S'il vous plaît noter que squelette (réessayer sur erreur) peut être utilisé aussi lorsque vous ne travaillez pas avec SQL Server (en fait, il peut être utilisé pour de nombreuses autres opérations comme E / S et réseau connexe, donc je suggère d'écrire une fonction générale et de le réutiliser largement).

Je ne connais pas de standard, mais voici une list d'exceptions de Sql-Server que j'ai généralement considérées comme ré-utilisables, avec un artefact DTC:

 catch (SqlException sqlEx) { canRetry = ((sqlEx.Number == 1205) // 1205 = Deadlock || (sqlEx.Number == -2) // -2 = TimeOut || (sqlEx.Number == 3989) // 3989 = New request is not allowed to start because it should come with valid transaction descriptor || (sqlEx.Number == 3965) // 3965 = The PROMOTE TRANSACTION request failed because there is no local transaction active. || (sqlEx.Number == 3919) // 3919 Cannot enlist in the transaction because the transaction has already been committed or rolled back || (sqlEx.Number == 3903)); // The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION. } 

En ce qui concerne les tentatives, suggérer que le timeout random est ajouté entre les tentatives, afin de réduire les chances de par exemple les mêmes 2 interblocages de transactions à nouveau .

Avec certaines des erreurs liées au DTC , la suppression de la connection peut être nécessaire (ou, au pire, SqlClient.SqlConnection.ClearAllPools() ). Dans le cas contraire, une connection incorrecte est renvoyée au pool.

Vous pouvez simplement utiliser les propriétés SqlConnectionSsortingngBuilder pour réessayer la connection sql.

var conBuilder = new SqlConnectionSsortingngBuilder(Configuration["Database:Connection"]); conBuilder.ConnectTimeout = 90; conBuilder.ConnectRetryInterval = 15; conBuilder.ConnectRetryCount = 6;

Note: – Requis .Net 4.5 ou plus tard.

Dans l'esprit de garder les préoccupations séparées, j'imagine trois couches logiques dans ce cas …

  1. La couche d'application, qui appelle une couche "flaky dependency handler"
  2. La couche "flaky dependency handler", qui appelle la couche d'access aux données
  3. La couche d'access aux données, qui ne sait rien de la desquamation

Toute la logique pour réessayer serait dans cette couche gestionnaire, afin de ne pas polluer la couche d'access aux données avec une logique autre que la communication avec la database. (En tant que tel, votre code d'access aux données n'a pas besoin d'être modifié et il ne devrait pas s'inquiéter de la "flakiness" s'il doit logiquement changer pour de nouvelles fonctionnalités.)

Un model de ré-essai pourrait être basé sur la capture d'exceptions spécifiques dans une boucle de countur. (Le countur est juste pour empêcher infiniment de réessayer.) Quelque chose comme ceci:

 public SomeReturnValue GetSomeData(someIdentifier) { var sortinges = 0; while (sortinges < someConfiguredMaximum) { try { sortinges++; return someDataAccessObject.GetSomeData(someIdentifier); } catch (SqlException e) { someLogger.LogError(e); // maybe wait for some number of milliseconds? make the method async if possible } } throw new CustomException("Maximum number of sortinges has been reached."); } 

Cela boucle un certain nombre de fois configuré, réessayer jusqu'à ce que cela fonctionne ou le maximum a été atteint. Après ce nombre maximal, une exception personnalisée est levée pour l'application à gérer. Vous pouvez affiner la gestion des exceptions en inspectant l' SqlException spécifique interceptée. Peut-être basé sur le message d'erreur que vous pourriez vouloir continuer avec la boucle ou lancer une CustomException .

Vous pouvez affiner davantage cette logique en attrapant d'autres types d'exceptions, en les examinant, etc. Le point principal ici est que cette responsabilité est conservée isolée d'une couche logique spécifique de l'application, aussi transparente que possible pour les autres couches. Idéalement, la couche gestionnaire et la couche d'access aux données implémentent les mêmes interfaces. De cette façon, si vous déplacez le code dans un environnement plus stable et que vous n'avez plus besoin de la couche gestionnaire, il serait sortingvial de l'enlever sans avoir besoin de modifier la couche application.

Je ne connais pas de véritable standard. Vous pouvez essayer de regarder le bloc d'application de error handling transitoires . C'est assez robuste, mais peut-être un peu trop «entreprise» pour certains users. Une autre approche pourrait consister à utiliser un cadre d'aspects pour piéger les erreurs. Ou bien le bon vieux try / catch fonctionnera.

Pour ce qui est de déterminer ce qu'il faut réessayer, vous voudrez généralement regarder l'exception. SqlException fournit beaucoup d'informations sur la source de votre problème, mais il peut être difficile de l'parsingr. J'ai rassemblé du code pour les séparer et essayer de déterminer ce qui est réessayable et ce qui ne l'est pas. Cela n'a pas été maintenu dans un certain time, donc vous devriez probablement prendre cela comme un sharepoint départ plutôt que comme un produit fini. En outre, cela était destiné à SQL Azure, donc il peut ne pas s'appliquer entièrement à votre situation (par exemple, la limitation des ressources est une fonctionnalité spécifique à Azure, IIRC).

 /// <summary> /// Helps to extract useful information from SQLExceptions, particularly in SQL Azure /// </summary> public class SqlExceptionDetails { public ResourcesThrottled SeriouslyExceededResources { get; private set; } public ResourcesThrottled SlightlyExceededResources { get; private set; } public OperationsThrottled OperationsThrottled { get; private set; } public IList<SqlErrorCode> Errors { get; private set; } public ssortingng ThrottlingMessage { get; private set; } public bool ShouldRetry { get; private set; } public bool ShouldRetryImmediately { get; private set; } private SqlExceptionDetails() { this.ShouldRetryImmediately = false; this.ShouldRetry = true; this.SeriouslyExceededResources = ResourcesThrottled.None; this.SlightlyExceededResources = ResourcesThrottled.None; this.OperationsThrottled = OperationsThrottled.None; Errors = new List<SqlErrorCode>(); } public SqlExceptionDetails(SqlException exception) :this(exception.Errors.Cast<SqlError>()) { } public SqlExceptionDetails(IEnumerable<SqlError> errors) : this() { List<ISqlError> errorWrappers = (from err in errors select new SqlErrorWrapper(err)).Cast<ISqlError>().ToList(); this.ParseErrors(errorWrappers); } public SqlExceptionDetails(IEnumerable<ISqlError> errors) : this() { ParseErrors(errors); } private void ParseErrors(IEnumerable<ISqlError> errors) { foreach (ISqlError error in errors) { SqlErrorCode code = GetSqlErrorCodeFromInt(error.Number); this.Errors.Add(code); switch (code) { case SqlErrorCode.ServerBusy: ParseServerBusyError(error); break; case SqlErrorCode.ConnectionFailed: //This is a very non-specific error, can happen for almost any reason //so we can't make any conclusions from it break; case SqlErrorCode.DatabaseUnavailable: ShouldRetryImmediately = false; break; case SqlErrorCode.EncryptionNotSupported: //this error code is sometimes sent by the client when it shouldn't be //Therefore we need to retry it, even though it seems this problem wouldn't fix itself ShouldRetry = true; ShouldRetryImmediately = true; break; case SqlErrorCode.DatabaseWorkerThreadThrottling: case SqlErrorCode.ServerWorkerThreadThrottling: ShouldRetry = true; ShouldRetryImmediately = false; break; //The following errors are probably not going to resolved in 10 seconds //They're mostly related to poor query design, broken DB configuration, or too much data case SqlErrorCode.ExceededDatabaseSizeQuota: case SqlErrorCode.TransactionRanTooLong: case SqlErrorCode.TooManyLocks: case SqlErrorCode.ExcessiveTempDBUsage: case SqlErrorCode.ExcessiveMemoryUsage: case SqlErrorCode.ExcessiveTransactionLogUsage: case SqlErrorCode.BlockedByFirewall: case SqlErrorCode.TooManyFirewallRules: case SqlErrorCode.CannotOpenServer: case SqlErrorCode.LoginFailed: case SqlErrorCode.FeatureNotSupported: case SqlErrorCode.StoredProcedureNotFound: case SqlErrorCode.SsortingngOrBinaryDataWouldBeTruncated: this.ShouldRetry = false; break; } } if (this.ShouldRetry && Errors.Count == 1) { SqlErrorCode code = this.Errors[0]; if (code == SqlErrorCode.TransientServerError) { this.ShouldRetryImmediately = true; } } if (IsResourceThrottled(ResourcesThrottled.Quota) || IsResourceThrottled(ResourcesThrottled.Disabled)) { this.ShouldRetry = false; } if (!this.ShouldRetry) { this.ShouldRetryImmediately = false; } SetThrottlingMessage(); } private void SetThrottlingMessage() { if (OperationsThrottled == Sql.OperationsThrottled.None) { ThrottlingMessage = "No throttling"; } else { ssortingng opsThrottled = OperationsThrottled.ToSsortingng(); ssortingng seriousExceeded = SeriouslyExceededResources.ToSsortingng(); ssortingng slightlyExceeded = SlightlyExceededResources.ToSsortingng(); ThrottlingMessage = "SQL Server throttling encountered. Operations throttled: " + opsThrottled + ", Resources Seriously Exceeded: " + seriousExceeded + ", Resources Slightly Exceeded: " + slightlyExceeded; } } private bool IsResourceThrottled(ResourcesThrottled resource) { return ((this.SeriouslyExceededResources & resource) > 0 || (this.SlightlyExceededResources & resource) > 0); } private SqlErrorCode GetSqlErrorCodeFromInt(int p) { switch (p) { case 40014: case 40054: case 40133: case 40506: case 40507: case 40508: case 40512: case 40516: case 40520: case 40521: case 40522: case 40523: case 40524: case 40525: case 40526: case 40527: case 40528: case 40606: case 40607: case 40636: return SqlErrorCode.FeatureNotSupported; } try { return (SqlErrorCode)p; } catch { return SqlErrorCode.Unknown; } } /// <summary> /// Parse out the reason code from a ServerBusy error. /// </summary> /// <remarks>Basic idea extracted from http://msdn.microsoft.com/en-us/library/gg491230.aspx /// </remarks> /// <param name="error"></param> private void ParseServerBusyError(ISqlError error) { int idx = error.Message.LastIndexOf("Code:"); if (idx < 0) { return; } ssortingng reasonCodeSsortingng = error.Message.Subssortingng(idx + "Code:".Length); int reasonCode; if (!int.TryParse(reasonCodeSsortingng, out reasonCode)) { return; } int opsThrottledInt = (reasonCode & 3); this.OperationsThrottled = (OperationsThrottled)(Math.Max((int)OperationsThrottled, opsThrottledInt)); int slightResourcesMask = reasonCode >> 8; int seriousResourcesMask = reasonCode >> 16; foreach (ResourcesThrottled resourceType in Enum.GetValues(typeof(ResourcesThrottled))) { if ((seriousResourcesMask & (int)resourceType) > 0) { this.SeriouslyExceededResources |= resourceType; } if ((slightResourcesMask & (int)resourceType) > 0) { this.SlightlyExceededResources |= resourceType; } } } } public interface ISqlError { int Number { get; } ssortingng Message { get; } } public class SqlErrorWrapper : ISqlError { public SqlErrorWrapper(SqlError error) { this.Number = error.Number; this.Message = error.Message; } public SqlErrorWrapper() { } public int Number { get; set; } public ssortingng Message { get; set; } } /// <summary> /// Documents some of the ErrorCodes from SQL/SQL Azure. /// I have not included all possible errors, only the ones I thought useful for modifying runtime behaviors /// </summary> /// <remarks> /// Comments come from: http://social.technet.microsoft.com/wiki/contents/articles/sql-azuree-connection-management-in-sql-azuree.aspx /// </remarks> public enum SqlErrorCode : int { /// <summary> /// We don't recognize the error code returned /// </summary> Unknown = 0, /// <summary> /// A SQL feature/function used in the query is not supported. You must fix the query before it will work. /// This is a rollup of many more-specific SQL errors /// </summary> FeatureNotSupported = 1, /// <summary> /// Probable cause is server maintenance/upgrade. Retry connection immediately. /// </summary> TransientServerError = 40197, /// <summary> /// The server is throttling one or more resources. Reasons may be available from other properties /// </summary> ServerBusy = 40501, /// <summary> /// You have reached the per-database cap on worker threads. Investigate long running transactions and reduce server load. /// http://social.technet.microsoft.com/wiki/contents/articles/1541.windows-azuree-sql-database-connection-management.aspx#Throttling_Limits /// </summary> DatabaseWorkerThreadThrottling = 10928, /// <summary> /// The per-server worker thread cap has been reached. This may be partially due to load from other databases in a shared hosting environment (eg, SQL Azure). /// You may be able to alleviate the problem by reducing long running transactions. /// http://social.technet.microsoft.com/wiki/contents/articles/1541.windows-azuree-sql-database-connection-management.aspx#Throttling_Limits /// </summary> ServerWorkerThreadThrottling = 10929, ExcessiveMemoryUsage = 40553, BlockedByFirewall = 40615, /// <summary> /// The database has reached the maximum size configured in SQL Azure /// </summary> ExceededDatabaseSizeQuota = 40544, /// <summary> /// A transaction ran for too long. This timeout seems to be 24 hours. /// </summary> /// <remarks> /// 24 hour limit taken from http://social.technet.microsoft.com/wiki/contents/articles/sql-azuree-connection-management-in-sql-azuree.aspx /// </remarks> TransactionRanTooLong = 40549, TooManyLocks = 40550, ExcessiveTempDBUsage = 40551, ExcessiveTransactionLogUsage = 40552, DatabaseUnavailable = 40613, CannotOpenServer = 40532, /// <summary> /// SQL Azure databases can have at most 128 firewall rules defined /// </summary> TooManyFirewallRules = 40611, /// <summary> /// Theoretically means the DB doesn't support encryption. However, this can be indicated incorrectly due to an error in the client library. /// Therefore, even though this seems like an error that won't fix itself, it's actually a retryable error. /// </summary> /// <remarks> /// http://social.msdn.microsoft.com/Forums/en/ssdsgetstarted/thread/e7cbe094-5b55-4b4a-8975-162d899f1d52 /// </remarks> EncryptionNotSupported = 20, /// <summary> /// User failed to connect to the database. This is probably not recoverable. /// </summary> /// <remarks> /// Some good info on more-specific debugging: http://blogs.msdn.com/b/sql_protocols/archive/2006/02/21/536201.aspx /// </remarks> LoginFailed = 18456, /// <summary> /// Failed to connect to the database. Could be due to configuration issues, network issues, bad login... hard to tell /// </summary> ConnectionFailed = 4060, /// <summary> /// Client sortinged to call a stored procedure that doesn't exist /// </summary> StoredProcedureNotFound = 2812, /// <summary> /// The data supplied is too large for the column /// </summary> SsortingngOrBinaryDataWouldBeTruncated = 8152 }