Seguro que de muchos es sabido que cuando trabajamos con Sql Database es posible que alguna de nuestras operaciones pueda fallar, independientemente de si usamos EF, NH o directamente con nuestro SqlClient, debido a inestabilidades de la red o problemas de back-end [si desea explorar el porque de la realidad de estos problemas le recomiendo este enlace]. Aunque en la realidad esto no es algo ni mucho menos habitual, puede pasar, y, por lo general, sino somos conscientes de ello suele dar bastantes dolores de cabeza, por lo aleatorio de los fallos. Por supuesto, hay muchos workaround para este problema, y como no, también para los que usamos actualmente EF, en cualquier de sus versiones como por ejemplo este que comenta mi buen amigo Cesar de La Torre. en un post de hace una eternidad. Con el fin de hacer esto más sencillo para nosotros y en realidad para resolver otros posibles problema similares para este u otros motores relacionales el equipo de EF ya tiene en la rama de EF 6 nuevas funcionalidades.
Estas nuevas características se pueden concretar en los siguientes elementos:
IExecutionStrategy
Esta interfaz define el contrato para la pieza que tendrá como responsabilidad la ejecución de operaciones que potencialmente necesiten reintentarse, por ejemplo las operaciones sobre Sql Database. El contrato de esta interfaz no es nada complicado como podemos ver a continuación:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
<span class="kwrd">namespace</span> System.Data.Entity.Infrastructure { <span class="kwrd">using</span> System.Diagnostics.CodeAnalysis; <span class="kwrd">using</span> System.Threading; <span class="kwrd">using</span> System.Threading.Tasks; <span class="kwrd">public</span> <span class="kwrd">interface</span> IExecutionStrategy { <span class="rem">/// <summary></span> <span class="rem">/// Indicates whether this <see cref="IExecutionStrategy"/> might retry the execution after a failure.</span> <span class="rem">/// </summary></span> <span class="kwrd">bool</span> RetriesOnFailure { get; } <span class="rem">/// <summary></span> <span class="rem">/// Executes the specified action.</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="action">A delegate representing an executable action that doesn't return any results.</param></span> <span class="kwrd">void</span> Execute(Action action); <span class="rem">/// <summary></span> <span class="rem">/// Executes the specified function and returns the result.</span> <span class="rem">/// </summary></span> <span class="rem">/// <typeparam name="TResult">The return type of <paramref name="func"/>.</typeparam></span> <span class="rem">/// <param name="func">A delegate representing an executable action that returns the result of type <typeparamref name="TResult"/>.</param></span> <span class="rem">/// <returns>The result from the action.</returns></span> TResult Execute<TResult>(Func<TResult> func); <span class="preproc">#if</span> !NET40 <span class="rem">/// <summary></span> <span class="rem">/// Executes the specified asynchronous action.</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="taskAction">A function that returns a started task.</param></span> <span class="rem">/// <returns></span> <span class="rem">/// A task that will run to completion if the original task completes successfully (either the</span> <span class="rem">/// first time or after retrying transient failures). If the task fails with a non-transient error or</span> <span class="rem">/// the retry limit is reached, the returned task will become faulted and the exception must be observed.</span> <span class="rem">/// </returns></span> Task ExecuteAsync(Func<Task> taskFunc); <span class="rem">/// <summary></span> <span class="rem">/// Executes the specified asynchronous action.</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="taskAction">A function that returns a started task.</param></span> <span class="rem">/// <param name="cancellationToken"></span> <span class="rem">/// A cancellation token used to cancel the retry operation, but not operations that are already in flight</span> <span class="rem">/// or that already completed successfully.</span> <span class="rem">/// </param></span> <span class="rem">/// <returns></span> <span class="rem">/// A task that will run to completion if the original task completes successfully (either the</span> <span class="rem">/// first time or after retrying transient failures). If the task fails with a non-transient error or</span> <span class="rem">/// the retry limit is reached, the returned task will become faulted and the exception must be observed.</span> <span class="rem">/// </returns></span> Task ExecuteAsync(Func<Task> taskFunc, CancellationToken cancellationToken); <span class="rem">/// <summary></span> <span class="rem">/// Executes the specified asynchronous function and returns the result.</span> <span class="rem">/// </summary></span> <span class="rem">/// <typeparam name="TResult"></span> <span class="rem">/// The type parameter of the <see cref="Task{T}"/> returned by <paramref name="taskFunc"/>.</span> <span class="rem">/// </typeparam></span> <span class="rem">/// <param name="taskFunc">A function that returns a started task of type <typeparamref name="TResult"/>.</param></span> <span class="rem">/// <returns></span> <span class="rem">/// A task that will run to completion if the original task completes successfully (either the</span> <span class="rem">/// first time or after retrying transient failures). If the task fails with a non-transient error or</span> <span class="rem">/// the retry limit is reached, the returned task will become faulted and the exception must be observed.</span> <span class="rem">/// </returns></span> [SuppressMessage(<span class="str">"Microsoft.Design"</span>, <span class="str">"CA1006:DoNotNestGenericTypesInMemberSignatures"</span>)] Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> taskFunc); <span class="rem">/// <summary></span> <span class="rem">/// Executes the specified asynchronous function and returns the result.</span> <span class="rem">/// </summary></span> <span class="rem">/// <typeparam name="TResult"></span> <span class="rem">/// The type parameter of the <see cref="Task{T}"/> returned by <paramref name="taskFunc"/>.</span> <span class="rem">/// </typeparam></span> <span class="rem">/// <param name="taskFunc">A function that returns a started task of type <typeparamref name="TResult"/>.</param></span> <span class="rem">/// <param name="cancellationToken"></span> <span class="rem">/// A cancellation token used to cancel the retry operation, but not operations that are already in flight</span> <span class="rem">/// or that already completed successfully.</span> <span class="rem">/// </param></span> <span class="rem">/// <returns></span> <span class="rem">/// A task that will run to completion if the original task completes successfully (either the</span> <span class="rem">/// first time or after retrying transient failures). If the task fails with a non-transient error or</span> <span class="rem">/// the retry limit is reached, the returned task will become faulted and the exception must be observed.</span> <span class="rem">/// </returns></span> [SuppressMessage(<span class="str">"Microsoft.Design"</span>, <span class="str">"CA1006:DoNotNestGenericTypesInMemberSignatures"</span>)] Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> taskFunc, CancellationToken cancellationToken); <span class="preproc">#endif</span> } } |
La idea básica que hay detrás de esta interfaz es poder envolver una acción en un determinado ámbito capaz de decidir si las excepciones que se produzcan son o no candidatas para reintentarse, por ejemplo porque entendemos que es un fallo de Sql Database y no relacionado con el mapeo, consulta etc…, cada cierto tiempo. Por supuesto, decidir si una excepción de una operación implica que la operación debe repetirse es algo configurable y no fijo, cuya implementación esta marcada por una nueva interfaz, que vemos a continuación.
IRetriableExceptionDetector
Mediante este contrato tendremos la posibilidad de especificar que excepciones son candidatas a “resolver” por medio de reintentos, el siguiente código representa este contrato
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<span class="rem">// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.</span> <span class="kwrd">namespace</span> System.Data.Entity.Infrastructure { <span class="kwrd">public</span> <span class="kwrd">interface</span> IRetriableExceptionDetector { <span class="rem">/// <summary></span> <span class="rem">/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="ex">The exception object to be verified.</param></span> <span class="rem">/// <returns><c>true</c> if the specified exception is considered as transient, otherwise <c>false</c>.</returns></span> <span class="kwrd">bool</span> ShouldRetryOn(Exception ex); } } |
IRetryDelayStrategy
Este es el último elemento que necesitamos, y probablemente el más sencillo puesto que tiene como única finalidad marcar el tiempo entre reintentos de operaciones fallidas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<span class="rem">// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.</span> <span class="kwrd">namespace</span> System.Data.Entity.Infrastructure { <span class="kwrd">public</span> <span class="kwrd">interface</span> IRetryDelayStrategy { <span class="rem">/// <summary></span> <span class="rem">/// Determines whether the action should be retried and the delay before the next attempt.</span> <span class="rem">/// </summary></span> <span class="rem">/// <param name="lastException">The exception thrown during the last execution attempt.</param></span> <span class="rem">/// <returns></span> <span class="rem">/// Returns the delay indicating how long to wait for before the next execution attempt if the action should be retried;</span> <span class="rem">/// <c>null</c> otherwise</span> <span class="rem">/// </returns></span> TimeSpan? GetNextDelay(Exception lastException); } } |
Bien, ahora que ya conocemos las piezas, ya podemos entender que tenemos para Sql Database, y esto, no es más que implementaciones concretas para estos contratos que podemos “inyectar” dentro de EF .
La implementación para el trabajo de Azure, Sql Database, se basa principalmente en la clase SqlAzureExecutionStrategy, que tendremos por defecto en el proveedor de Sql Server por defecto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<span class="rem">// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.</span> <span class="kwrd">namespace</span> System.Data.Entity.SqlServer { <span class="kwrd">using</span> System.Data.Entity.Infrastructure; <span class="rem">/// <summary></span> <span class="rem">/// An <see cref="ExecutionStrategy"/> that uses the <see cref="ExponentialRetryDelayStrategy"/> and</span> <span class="rem">/// <see cref="SqlAzureRetriableExceptionDetector"/>.</span> <span class="rem">/// </summary></span> [DbProviderName(<span class="str">"System.Data.SqlClient"</span>)] <span class="kwrd">public</span> <span class="kwrd">class</span> SqlAzureExecutionStrategy : ExecutionStrategy { <span class="kwrd">public</span> SqlAzureExecutionStrategy() : <span class="kwrd">base</span>(<span class="kwrd">new</span> ExponentialRetryDelayStrategy(), <span class="kwrd">new</span> SqlAzureRetriableExceptionDetector()) { } } } |
Como observará, en realidad esto no tiene nada más que el marcador de tiempos, ExponentialRetryDelayStrategy, que cuyo nombre indica hace un incremento exponencial basado en la siguiente fórmula:min (minDelay + coefficient * random(1, maxRandomFactor) * (exponentialBase ^ retryCount – 1), maxDelay), y un detector de excepciones personalizado que permitirá reintentar las siguientes excepciones.
- SqlException con diferentes numberos de error, revisar el código siguiente para ver los casos concretos.
- Un TimeoutException.
Como se establece la estrategia que queremos usar? por ejemplo esta para Sql Database?, pues bien, tan simple como hacemos otras inyecciones, por medio de nuestro DbConfiguration y el método AddExecutionStrategy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<span class="rem">/// <summary></span> <span class="rem">/// Call this method from the constructor of a class derived from <see cref="DbConfiguration" /> to add an</span> <span class="rem">/// <see cref="IExecutionStrategy"/> for use with the associated provider.</span> <span class="rem">/// </summary></span> <span class="rem">/// <remarks></span> <span class="rem">/// The <typeparamref name="T"/> type should have a <see cref="DbProviderNameAttribute"/> applied to it.</span> <span class="rem">/// This method is provided as a convenient and discoverable way to add configuration to the Entity Framework.</span> <span class="rem">/// Internally it works in the same way as using AddDependencyResolver to add an appropriate resolver for</span> <span class="rem">/// <see cref="IExecutionStrategy" />. This means that, if desired, the same functionality can be achieved using</span> <span class="rem">/// a custom resolver or a resolver backed by an Inversion-of-Control container.</span> <span class="rem">/// </remarks></span> <span class="rem">/// <typeparam name="T"> The type that implements <see cref="IExecutionStrategy"/>. </typeparam></span> <span class="rem">/// <param name="getExecutionStrategy"> A function that returns a new instance of an execution strategy. </param></span> <span class="kwrd">protected</span> <span class="kwrd">internal</span> <span class="kwrd">void</span> AddExecutionStrategy<T>(Func<T> getExecutionStrategy) <span class="kwrd">where</span> T : IExecutionStrategy { Check.NotNull(getExecutionStrategy, <span class="str">"getExecutionStrategy"</span>); _internalConfiguration.CheckNotLocked(<span class="str">"AddExecutionStrategy"</span>); <span class="kwrd">foreach</span> (var providerInvariantNameAttribute <span class="kwrd">in</span> DbProviderNameAttribute.GetFromType(<span class="kwrd">typeof</span>(T))) { _internalConfiguration.AddDependencyResolver( <span class="kwrd">new</span> ExecutionStrategyResolver<T>(providerInvariantNameAttribute.Name, <span class="rem">/*serverName:*/</span> <span class="kwrd">null</span>, getExecutionStrategy)); } } |
Si nos fijamos un poco veremos como lo primero que hace este método es recuperar el proveedor para el que aplica esta estrategia, buscando un atributo, DbProviderName. Una vez localizado este atributo se establece el dependency resolver con la estrategia indicada. A partir de ese momento, ya tendremos disponible nuestra estrategia de reintentos..
Bueno, esto ha sido todo, espero que os sigan gustando las novedades de EF 6 y que sigáis viendo todo lo nuevo que poco a poco vamos teniendo…
Unai