Tenemos un método de terceros, Foo
, que a veces se bloquea por razones desconocidas.
Estamos ejecutando un servidor tcp de subproceso único y llamamos a este método cada 30 segundos para verificar que el sistema externo esté disponible.
Para mitigar el problema con el punto muerto en el código de terceros, colocamos la llamada de ping en una Task.Run
para que el servidor no se bloquee.
Me gusta
async Task<bool> WrappedFoo() { var timeout = 10000; var task = Task.Run(() => ThirdPartyCode.Foo()); var delay = Task.Delay(timeout); if (delay == await Task.WhenAny(delay, task )) { return false; } else { return await task ; } }
Pero esto (en nuestra opinión) tiene el potencial de privar a la aplicación de hilos gratuitos. Dado que si una llamada a ThirdPartyCode.Foo
interbloquea, el subproceso nunca se recuperará de este interbloqueo y, si esto sucede con la frecuencia suficiente, es posible que nos quedemos sin recursos.
¿Existe un enfoque general sobre cómo se debe manejar el código de terceros que bloquea los puntos muertos?
Un CancellationToken
no funcionará porque la API de terceros no proporciona ninguna opción de cancelación.
Actualización: el método en cuestión es de SAPNCO.dll proporcionado por SAP para establecer y probar conexiones rfc a un sistema sap, por lo tanto, el método no es un simple ping de red. Cambié el nombre del método en la pregunta para evitar más malentendidos.
¿Existe un enfoque general sobre cómo se debe manejar el código de terceros que bloquea los puntos muertos?
Sí, pero no es fácil ni sencillo.
El problema con el código que se comporta mal es que no solo puede filtrar recursos (p. ej., subprocesos), sino que también puede conservar indefinidamente recursos importantes (p. ej., algún "control" o "bloqueo" interno).
La única forma de reclamar a la fuerza hilos y otros recursos es finalizar el proceso. El sistema operativo está acostumbrado a limpiar procesos que se comportan mal y es muy bueno en eso. Entonces, la solución aquí es iniciar un proceso secundario para realizar la llamada a la API. Su aplicación principal puede comunicarse con su proceso secundario mediante stdin/stdout redirigidos, y si el proceso secundario alguna vez se agota, la aplicación principal puede finalizarlo y reiniciarlo.
Desafortunadamente, esta es la única forma confiable de cancelar un código que no se puede cancelar.
Tu código no cancela la operación bloqueada. Use un CancellationTokenSource y pase un token de cancelación a Task.Run
en su lugar:
var cts=new CancellationTokenSource(timeout); try { await Task.Run(() => ThirdPartyCode.Ping(),cts.Token); return true; } catch(TaskCancelledException) { return false; }
Es muy posible que el bloqueo se deba a problemas de red o de DNS, no a un punto muerto real.
Eso todavía desperdicia un hilo esperando que se complete una operación de red. Puede usar el propio Ping.SendPingAsync de .NET para hacer ping de forma asincrónica y especificar un tiempo de espera:
var ping=new Ping(); var reply=await ping.SendPingAsync(ip,timeout); return reply.Status==IPStatus.Success;
La clase PingReply contiene información mucho más detallada que un simple éxito/fracaso. La propiedad Estado por sí sola diferencia entre problemas de enrutamiento, destinos inalcanzables, tiempos de espera, etc.
Cancelar una tarea es una operación colaborativa en la que pasa un CancellationToken
al método deseado y externamente usa CancellationTokenSource.Cancel
:
public void Caller() { try { CancellationTokenSource cts=new CancellationTokenSource(); Task longRunning= Task.Run(()=>CancellableThirdParty(cts.Token),cts.Token); Thread.Sleep(3000); //or condition /signal cts.Cancel(); }catch(OperationCancelledException ex) { //treat somehow } } public void CancellableThirdParty(CancellationToken token) { while(true) { // token.ThrowIfCancellationRequested() -- if you don't treat the cancellation here if(token.IsCancellationRequested) { // code to treat the cancellation signal //throw new OperationCancelledException($"[Reason]"); } } }
Como puede ver en el código anterior, para cancelar una tarea en curso, el método que se ejecuta dentro de ella debe estar estructurado en torno al indicador CancellationToken.IsCancellationRequested
o simplemente al método CancellationToken.ThrowIfCancellationRequested
, de modo que la persona que llama simplemente emita CancellationTokenSource.Cancel
.
Desafortunadamente, si el código de terceros no está diseñado en torno a CancellationToken
(no acepta un parámetro CancellationToken
), entonces no hay mucho que pueda hacer.