Importante: Não será apresentando sobre como criar projeto do tipo ConsoleApplication e classes no visual studio code, pois não é o objetivo.
Objetivo: preparar aplicações para lidar com falhas ao consumir recursos externos sobre uma rede de computadores ou códigos isolados e de forma transparente tentar novamente a execução da operação.
Uma ilustração:
(Imagem: https://docs.microsoft.com/en-us/azure/architecture/patterns/retry)
1 – A aplicação submeteu uma requisição e a resposta foi HTTP 500 (Erro interno no servidor)
2 – A aplicação aguardou um intervalo de tempo, realizou novamente a requisição com os mesmo dados anteriormente e a resposta foi HTTP 500.
3 – A aplicação aguardou um novo intervalo, realizou novamente a requisição com os mesmos dados anteriormente e a respota foi HTTP 200 (SUCESSO).
Qual o problema iremos resolver para aplicar um exemplo de implementação?
Uma aplicação precisa gerar pedido em outro sistema através de uma api disponibilizada na internet, devido ao alto volume de requisições em alguns momentos sobrecargas no servidor ocorrem, retornando “HTTP 503 – Service Unavailable”, como é uma api de terceiros, não existindo a possibilidade de tratar na origem, será aplicado o padrão Retry, então essa aplicação será configurada para em casos de problemas na geração de pedido, após passados 30s enviar novamente a requisição a api, com limite máximo de 3 tentativas e não obtendo êxito será registrado o log do erro para analise e reporte ao fornecedor.
Vamos ao código de demonstração do funcionamento na prática do Retry? Go Go Go 🙂
Foi criado uma entidade chamada “PurchaseOrder” que será usada para trafegar os dados de Pedidos de um sistema (client) para outro sistema(serviço).
public class PurchaseOrder
{
public string ReferenceNumber { get; set; }
public string CodeProduct { get; set; }
public int Quantity { get; set; }
}
Criado a interface generica “IBusinessOperation”:
public interface IBusinessOperation
{
T Execute();
}
Criado a classe “GenerateOrder” que implementa o método Execute da interface “IBusinessOperation”:
public class GenerateOrder: IBusinessOperation {
private PurchaseOrder _purchaseOrder;
private Queue _errors;
public GenerateOrder() {
_purchaseOrder = new PurchaseOrder();
_errors = new Queue ();
}
public GenerateOrder(PurchaseOrder purchaseOrder, List exception = null) {
_errors = new Queue ();
this._purchaseOrder = purchaseOrder;
if (exception != null) {
exception.ForEach(x = >_errors.Enqueue(x));
}
}
public PurchaseOrder Execute() {
if (this._errors.Any()) {
var error = this._errors.Peek();
this._errors.Dequeue();
throw error;
}
//Ocorreu com sucesso
this._purchaseOrder.ReferenceNumber = DateTime.Now.ToString("yyyyMMddhhmmssfff");
return this._purchaseOrder;
}
}
Criando a classe “Retry” que receberá os dados para requisição, tempo de espera para realizar nova requisição (em caso de erros) e total máximo de tentativas.
public class Retry :IBusinessOperation {
private ILog _log;
private IBusinessOperation _operation;
private int _maxAttempts;
private int _delay;
private int _attempts;
private List _test;
private List _errors;
public Retry(
ILog log, IBusinessOperation op, int maxAttempts, int delay, Type ignoreTest) {
this._attempts = 0;
this._test = new List ();
this._errors = new List ();
this._log = log;
this._operation = op;
this._maxAttempts = maxAttempts;
this._delay = delay;
this._test.Add(ignoreTest);
}
public T Execute() {
do {
try {
_log.Register($ "Tentativa Nro. {this._attempts + 1}");
return this._operation.Execute();
}
catch(Exception e) {
_log.Register($ "Tentativa Nro. {this._attempts + 1} com erro \"{e.Message}\" ");
this._errors.Add(e);
this._attempts++;
var exists = this._test.Contains(e.GetType());
if (this._attempts >= this._maxAttempts || !exists) {
_log.Register($ "Excedeu {this._maxAttempts} tentativas e não será reenviado.");
throw e;
}
try {
_log.Register($ "Aguardando {(this._delay / 1000)}s para a proxima tentativa");
Thread.Sleep(this._delay);
}
catch(ThreadInterruptedException f) {
//ignore
}
}
} while ( true );
}
}
Criado a classe “RetryExponentialBackoff” que receberá os dados para requisição, tempo de espera para realizar nova requisição (em caso de erros) e total máximo de tentativas. O tempo de espera será randomico.
public class RetryExponentialBackoff :IBusinessOperation {
private static Random RANDOM = new Random();
private ILog _log;
private IBusinessOperation _operation;
private int _maxAttempts;
private int _maxDelay;
private int _attempts;
private List _test;
private List _errors;
public RetryExponentialBackoff(
ILog log, IBusinessOperation op, int maxAttempts, int maxDelay, Type ignoreTest) {
this._test = new List ();
this._errors = new List ();
this._attempts = 0;
this._log = log;
this._operation = op;
this._maxAttempts = maxAttempts;
this._maxDelay = maxDelay;
this._test.Add(ignoreTest);
}
public T Execute() {
do {
try {
_log.Register($ "Tentativa Nro. {this._attempts + 1}");
return this._operation.Execute();
}
catch(Exception e) {
_log.Register($ "Tentativa Nro. {this._attempts + 1} com erro \"{e.Message}\" ");
this._errors.Add(e);
this._attempts++;
var exists = this._test.Contains(e.GetType());
if (this._attempts >= this._maxAttempts || !exists) {
_log.Register($ "Excedeu {this._maxAttempts} tentativas e não será reenviado.");
throw e;
}
try {
int testDelay = RANDOM.Next(50000);
int delay = testDelay < this._maxDelay ? testDelay: _maxDelay;
_log.Register($ "Aguardando {(delay / 1000)}s para a proxima tentativa");
Thread.Sleep(delay);
}
catch(Exception f) {
//ignore
}
}
} while ( true );
}
}
Criado a interface “ILog” para registrar os log’s de sistema.
public interface ILog {
void Register(string message);
}
Criado a classe “LogConsole” que implementa o método Register da interface “ILog”:
public class LogConsole: ILog {
public LogConsole() {}
public void Register(string message) {
Console.WriteLine("/***********************************************************/");
Console.WriteLine("");
Console.WriteLine($ "Data: {DateTime.Now.ToString("
dd / MM / yyyy HH: mm: ss ")}");
Console.WriteLine($ "Message Info: {message}");
Console.WriteLine("");
Console.WriteLine("/***********************************************************/");
}
}
Enfim a “program” que estará simulando alguns problemas aplicando o “Retry”:
public class Program {
private static IBusinessOperation operation;
private static ILog _log;
public static void Main(String[] args) {
_log = new LogConsole();
NoErrors();
ErrorNoRetry();
ErrorWithRetry();
ErrorWithRetryNetworking();
ErrorWithRetryExceeded();
}
private static void NoErrors() {
var purchaseOrder = new PurchaseOrder() {
CodeProduct = "COD10",
Quantity = 500
};
operation = new GenerateOrder(purchaseOrder);
operation.Execute();
_log.Register("Executado sem erros.");
}
private static void ErrorNoRetry() {
var purchaseOrder = new PurchaseOrder() {
CodeProduct = "COD01",
Quantity = 1000
};
operation = new GenerateOrder(purchaseOrder, new List {
new WebException("503 Service Unavailable – HTTP")
});
try {
operation.Execute();
}
catch(WebException e) {
_log.Register("Executado com erro e não tentou novamente por que não utiliza \"o padrão Retry\".");
}
}
private static void ErrorWithRetry() {
var purchaseOrder = new PurchaseOrder() {
CodeProduct = "COD02",
Quantity = 1000
};
Retry retry = new Retry (
_log, new GenerateOrder(purchaseOrder, new List {
new WebException("503 Service Unavailable – HTTP")
}), 3, //3 tentativas
5000, //2 s de espera entre as tentativas
typeof(WebException) //tipo de erro que deve passar pelas 3 tentativas
);
operation = retry;
var purchaseOrderReturn = operation.Execute();
_log.Register($ "Reference Number \"{purchaseOrderReturn.ReferenceNumber}\" da Ordem de Compra, gerado com sucesso");
}
private static void ErrorWithRetryExceeded() {
int tentativas = 3;
try {
var purchaseOrder = new PurchaseOrder() {
CodeProduct = "COD03",
Quantity = 1000
};
Retry retry = new Retry (
_log, new GenerateOrder(purchaseOrder, new List {
new WebException("503 Service Unavailable – HTTP"),
new WebException("503 Service Unavailable – HTTP"),
new WebException("503 Service Unavailable – HTTP"),
new WebException("503 Service Unavailable – HTTP"),
}), tentativas, //3 tentativas
30000, //30 s de espera entre as tentativas
typeof(WebException) //tipo de erro que deve passar pelas 3 tentativas
);
operation = retry;
var purchaseOrderReturn = operation.Execute();
_log.Register($ "Reference Number \"{purchaseOrderReturn.ReferenceNumber}\" da Ordem de Compra, gerado com sucesso");
}
catch(Exception ex) {
_log.Register($ "Erro \"{ex.Message}\" disparado após {tentativas}.");
}
}
private static void ErrorWithRetryNetworking() {
var purchaseOrder = new PurchaseOrder() {
CodeProduct = "COD04",
Quantity = 1000
};
RetryExponentialBackoff retry = new RetryExponentialBackoff (
_log, new GenerateOrder(purchaseOrder, new List {
new WebException("503 Service Unavailable – HTTP")
}), 6, //6 tentativas
5000, //5 s de espera entre tentativas
typeof(WebException) //tipo de erro que deve passar pelas 6 tentativas
);
operation = retry;
var purchaseOrderReturn = operation.Execute();
_log.Register($ "Reference Number \"{purchaseOrderReturn.ReferenceNumber}\" da Ordem de Compra, gerado com sucesso");
}
}
Executando a programa será exibido algumas tentativas com erro e no final sucesso, em outra requisição somente erros retornados até que a solução para com as tentativas e exibe via console.
Resultado da execução:
/***********************************************************/
Data: 20/02/2020 22:33:50
Message Info: Executado sem erros.
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:50
Message Info: Executado com erro e não tentou novamente por que não utiliza "o padrão Retry".
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:50
Message Info: Tentativa Nro. 1
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:50
Message Info: Tentativa Nro. 1 com erro "503 Service Unavailable – HTTP"
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:50
Message Info: Aguardando 5s para a proxima tentativa
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:55
Message Info: Tentativa Nro. 2
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:55
Message Info: Reference Number "20200220103355158" da Ordem de Compra, gerado com sucesso
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:55
Message Info: Tentativa Nro. 1
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:55
Message Info: Tentativa Nro. 1 com erro "503 Service Unavailable – HTTP"
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:33:55
Message Info: Aguardando 5s para a proxima tentativa
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:00
Message Info: Tentativa Nro. 2
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:00
Message Info: Reference Number "20200220103400166" da Ordem de Compra, gerado com sucesso
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:00
Message Info: Tentativa Nro. 1
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:00
Message Info: Tentativa Nro. 1 com erro "503 Service Unavailable – HTTP"
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:00
Message Info: Aguardando 30s para a proxima tentativa
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:30
Message Info: Tentativa Nro. 2
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:30
Message Info: Tentativa Nro. 2 com erro "503 Service Unavailable – HTTP"
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:34:30
Message Info: Aguardando 30s para a proxima tentativa
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:35:00
Message Info: Tentativa Nro. 3
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:35:00
Message Info: Tentativa Nro. 3 com erro "503 Service Unavailable – HTTP"
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:35:00
Message Info: Excedeu 3 tentativas e não será reenviado.
/***********************************************************/
/***********************************************************/
Data: 20/02/2020 22:35:00
Message Info: Erro "503 Service Unavailable – HTTP" disparado após 3.
/***********************************************************/
Espero que tenha ajudado.
Até a próxima e caso não publique mais nada esse ano, boas festas!!!
[]’s
Código-Fonte: https://github.com/joaopauloit/desginer-patterns