jueves, 17 de octubre de 2019

Retryer funcional

Hasta ahora estaba utilizando PostSharp para mi infraestructura de reintentos y muy contento oye. Utilizar AOP para estas cosas de crosscutting concerns está muy guapo y solo con eso ya está a milenios luz de la mayoría de los proyectos laborales que te puedes encontrar.

Pero tenía un problema y es que no puedes modificar en tiempo de ejecución nada. No puedes decidir si una función se reintenta o no y no puedes cambiar los parámetros del reintento. Todo está decidido en tiempo de diseño y no hay manera de cambiarlo en tiempo de ejecución. Tampoco se puede tener la misma función decorada con 2 retryers diferentes que sean usadas por distintas partes del sistema.

Dándole vueltas he pensado que se podría utilizar un patrón decorador para configurar las funciones en tiempo de ejecución; dándole más vueltas he pensado que con programación funcional puedes hacer un decorador mucho más guapo y flexible que con OOP y dándole más vueltas todavía he pensado que se puede refactorizar el código para no repetir ni una sola línea de código gracias a la programación funcional.

El problema es que me surgen dudas sobre hasta que punto esta implementación puede ocasionar problemas o si tengo que tener en cuenta el uso de ConfigureAwait (lo uso?, no lo uso?, parametrizo su uso?, si alguien que usa este código y aplica ConfigureAwait rompe algo?).

Resumiendo; aquí os dejo la implementación. Si le pegáis un vistazo os agradecería que me comentaseis todos los matices o problemas que se os ocurran para ir mejorando esto y poder darle el visto bueno para producción algún día.

UPDATE: Según todas mis pruebas; es completamente seguro utilizar en la función llamada ConfigureAwait(false) en caso de no necesitar ningún contexto que se pierda.

Resulta que según .NET maneja los contextos de sincronización, la función llamada (el Retryer en este caso) puede continuar su ejecución en otro hilo sin el contexto original y la función llamadora podría seguir con su contexto omitiendo ConfigureAwait. También pasaría lo mismo en el código interno de la función que queremos reintentar. Si ésta necesita un contexto que se pierde puede omitir ConfigureAwait para recapturar el contexto después del await aunque el Retryer use ConfigureAwait(false). En este último caso no se ganaría mucho en cuestión de rendimiento pero, como es algo que no podemos evitar, independiza la implementación del Retryer de la necesidad de contexto de su llamador y de lo que llama.

UPDATE2: El token de cancelación pasado al retryer puede estar o no asociado a la tarea interna que el retryer ejecuta. Si están asociados; la cancelación del la tarea interna cancela el bucle de reintentos y la espera. Si no están asociados, se podría cancelar la tarea interna e indicar al retryer que siga intentándolo. También se podría cancelar el bucle del retryer y su espera sin cancelar la tarea interna (que podría cancelarse por su propio timeout, por ejemplo).

Esto nos proporciona una flexibilidad bastante buena y nos permite abarcar la mayoría de las situaciones que debe cumplir el sistema.

No hay comentarios:

Publicar un comentario