Immaginatevi una sera a casa, avete un problema con un servizio o un contratto e decidete di chiamare il call center. Un call center e' pensato e progettato per essere scalabile (ho detto scalabile, non cortese :-)), tutte le chiamate vengono accettate, accodate ed evase il prima possibile. Bene, chiamate e dopo un secondo inizia la musichetta che vi mette in attesa (coda). Se va male qualche cosa, cade la linea e riprovate. Se va bene iniziate a parlare con l'operatore. Se va bene finite la richiesta, avete la risposta e chiudete. se va male cade la linea con l'operatore e ripartite da capo.
Il mondo del cloud computing non e' poi cosi' tanto differente. Quando dovete salvare un record non fate null'altro che fare una HTTP POST (nel caso il servizio sia RESTish) con il record in formato XML e il provider tradurra' questo payload XML in un record (ad esempio trasformandolo in uno statement SQL che eseguira' su SQL Server).
Se stessimo parlando di effettuare la stessa operazione sul nostro database locale, la latenza sarebbe molto ridotta, in quanto legata al throughtput della rete locale (solitamente molto alto) e da quanto il nostro database engine e' occupato.
Con uno storage nel cloud il numero di variabili aumenta incredibilmente, ed oltre a quelle appena menzionate, possiamo ricordare le condizioni di rete fra noi ed il cloud provider, il server web che accetta la richiesta, la componente che transcodifica, e cosi' via.
Quindi, se con il nostro SQL Server locale il numero di timeout era un'eccezione, con lo storage nel cloud il timeout diventa la regola. Come mitigare questo problema?
Una delle soluzioni e' proprio quello di fare quello che avete fatto con il call center, ci riprovate. In altre parole, prima di rilanciare l'eccezzione al chiamante, ci riprovate qualche volta. Ma quale schema adottare?
Come tutte le cose e' bene iniziare con qualche cosa di semplice, ed eventualmente nel futuro si puo' raffinare. Iniziamo quindi con una classe base (il contratto) che definisca la nostra policy di retry (il codice e' ispirato dall'esempio StorageClient fornito con Windows Azure SDK):
public abstract class RetryPolicy
{
public abstract void Retry(Action action);
}
Ovviamente non puo' mancare la policy che non faccia nulla:
public class NoRetryPolicy : RetryPolicy
{
public override void Retry(Action action)
{
try
{
action();
}
catch (RetryException e)
{
throw e.InnerException;
}
}
}
Dato che questa policy risulta abbastanza inutile, possiamo implementarne una che riprovi per un certo numero di volte ad un determinato intervallo:
public class NRetryPolicy : RetryPolicy
{
private int numberOfRetries;
private TimeSpan intervalBetweenRetries;
public NRetryPolicy(int numberOfRetries, TimeSpan intervalBetweenRetries)
{
this.numberOfRetries = numberOfRetries;
this.intervalBetweenRetries = intervalBetweenRetries;
}
public override void Retry(Action action)
{
int retryCount = numberOfRetries;
do
{
try
{
action();
break;
}
catch (RetryException e)
{
if (retryCount == 0)
{
throw e.InnerException;
}
if (intervalBetweenRetries > TimeSpan.Zero)
{
Thread.Sleep(intervalBetweenRetries);
}
}
}
while (retryCount-- > 0);
}
}
Ora, immaginiamo di voler riprovare a salvare il record 3 volte a distanza di 1 secondo, dovremo scrivere qualche cosa del tipo:
NRetryPolicy retryPolicy = new NRetryPolicy(3, TimeSpan.FromSeconds(1));
retryPolicy.Retry(() =>
{
// Salvo il record
if (<e' un timeout ?>)
throw new RetryException("Throwing an exception from RetryTest");
});
In questo modo, il nostro codice riprovera' al massimo tre volte prima di fallire definitivamente. Ovviamente lo schema e' molto semplice e potrebbe essere ottimizzato ulteriormente, ma questo lo lascio ad un prossimo appuntamento. Per ora, abbiamo prodotto una tecnica semplice per mitigare un timeout ricorrente rendendolo meno ricorrente.