O Azure Table Storage, ou Azure Tables, ou ainda ATS, é um mecanismo de acesso a dados via REST, baseado em HTTP. Chique, hein? Poizé, basicamente isso quer dizer que você acessa dados no ATS mandando comandos em pacotes HTTP request para o servidor, e ele responde enfiando os dados em um pacote HTTP response e mandando de volta pra você.
Como HTTP é o idioma de comunicação, quando acontece um erro, faz sentido que este erro seja retornado como um código de erro HTTP. E aí as coisas podem ficar um pouco “estranhas” de interpretar. Por exemplo, se você, navegando na Internet, acessa uma URL que não existe, o servidor retorna um erro 404 – Page Not Found. Em REST, se você tenta acessar uma linha que não existe, é retornado o erro 404 – Resource Not Found. Faz sentido, mas da primeira vez que acontece, a gente fica parado olhando pra tela e imaginando o que diabos aquele erro 404 significa, porque “eu tô acessando uma tabela, e não uma pagina na Internet, uai”.
Já coloquei 2 posts sobre essas peculiaridades das mensagens de erro do Azure, que apareceram quando começamos a estudá-lo aqui na empresa. Mas agora que estamos fazendo um projeto cuja camada de dados é o Azure Tables, e estamos codificando pesado em cima dele, estão aparecendo mais destas “peculiaridades”. Então vou lista-las aqui, na esperança que poupe vários “uai’s” pro pessoal mexendo com o Azure.
1. As exceções DataServiceRequestException e DataServiceClientException
Quando acontece um erro de acesso a dados no ATS, geralmente a exceção retornada é do tipo DataServiceRequestException. E a mensagem de erro é “An error occurred while processing this request”. Esta exceção mais genérica encapsula uma exceção mais específica, do tipo DataServiceClientException. Esta exceção contém, na sua propriedade Message, um documento XML que descreve o erro ocorrido. Algo neste estilo:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<error xmlns="http://schemas.microsoft.com/ado/2007/08/dataservices/metadata">
<code>EntityAlreadyExists</code>
<message xml:lang="pt-BR">The specified entity already exists.</message>
</error>O interessante deste XML é o elemento code, que “quase” especifica o quê realmente aconteceu. “Quase” porque este código deve ser interpretado de acordo com a ação que a aplicação estava executando no momento do erro. Por exemplo, o código InvalidInput pode significar que durante a gravação de uma entidade, foi detectado que ela tem uma propriedade de um tipo de dados que não é suportado pelo Azure Tables; OU pode significar que foi feita uma consulta a uma tabela que acabou de ser criada, e sendo assim, ela não tem sua estrutura de colunas definida. Mas o importante é verificar o quê veio neste elemento code do XML contendo a mensagem de erro, e interpretar eta informação.
2. Códigos de erro e possíveis causas
A seguir está a listagem dos códigos de erro com os quais já esbarramos, e o que a aplicação estava tentando fazer quando o erro foi gerado.
Código de Erro
(valor do elemento code)Possíveis Causas ConditionNotMet - Erro de acesso concorrente. A aplicação leu uma entidade; ao tentar alterar ou excluir esta entidade, ela já havia sido alterada por um comando de gravação de dados em outro contexto. EntityAlreadyExists - Erro de chave primária. A aplicação tentou inserir uma entidade em uma tabela, e já existe na tabela outra entidade com os mesmos valores de Partition Key e Row Key da entidade sendo inserida. InternalError - Uso de operadores Linq não suportados pelo Azure Table Storage. (O caso com o qual esbarramos foi recuperar todas as entidades nas quais uma propriedade string começava por um determinado valor. Nada de like, nem operadores de desigualdade (> e <), nem de string.StartsWith() – que foi quem gerou o erro). InvalidInput - Gravação de entidade que tem propriedade cujo tipo não é suportado pelo ATS.
- Gravação de entidade que foi anexada ao contexto (TableServiceContext.AttachTo()), mas que está com a propriedade ETag nula.
- Consultas Linq em uma tabela que ainda não sofreu nenhuma inserção de entidades.
- Consultas Linq que usam tipos anônimos. Tivemos que fazer uma consulta que trazia as entidades desejadas do Azure, e em cima destas entidades, fazer outra consulta usando os tipos anônimos. Aí tudo funcionou ok.
- Gravação em batch (SaveChanges(SaveOptions.Batch)) na qual havia 2 ou mais entidades com as mesmas Partition e Row Key.ResourceNotFound - Consulta Linq que usa condição de igualdade nas propriedades PartitionKey e RowKey da tabela sendo consultada (where entity.PartitionKey == “…” && entity.RowKey == “…”) e que não retornam linhas. (Consultas que usam condição em outras propriedades retornam uma lista vazia)
- Erro de acesso concorrente. A aplicação leu uma entidade; ao tentar alterar ou excluir esta entidade, ela já havia sido excluída por um comando de gravação de dados em outro contexto.
- Consulta Linq em tabela que não existe no ATS.TableAlreadyExists Encontrado durante chamada a CloudTableClient.CreateTablesFromModel(). Aparentemente chamadas concorrentes a este método podem gerar esta exceção. Colocamos uma chamada a CreateTablesFromModel() no construtor estático da nossa classe de contexto de acesso a dados. Quando aumentamos o número de instâncias do role no Azure para 2, uma das instâncias subia ok, e a outra gerava esse erro. TableBeingDeleted Idem a TableAlreadyExists acima. OutOfRangeInput Gerado ao gravar uma entidade que tinha uma propriedade DateTime não inicializada. O valor mínimo de uma propriedade DateTime no Azure é 01/jan/1601 00:00:00, mas um valor DateTime não inicializado no C# contém a data 01/jan/0001. Não deu pau no DevStorage porque ele converte valores menores que 01/jan/1753 pra 01/jan/1753, mas o ATS não faz isso.
3. Como extrair o código de erro da exceção DataServiceClientException
Primeiro criamos um enum cujos nomes de elemento são exatamente iguais aos códigos de erro retornados pelo ATS.
public enum CausaErrosDataServiceException
{
EntityAlreadyExists,
ConditionNotMet,
InvalidInput,
ResourceNotFound,
Desconhecido
}
Depois criamos uma funcão que:
- Recebe uma exceção qualquer;
- Procura por uma exceção interna do tipo DataServiceClientException;
- Uma vez que ache esta exceção, extrai o valor do elemento /error/code presente no XML da mensagem de erro, o transforma num dos valores do enum acima, e retorna este valor.
/// <summary>
/// Traduz uma exceção acontecida no Azure para um dos valores do enum CausaErrosDataServiceException.
/// </summary>
/// <param name="erro">Exceção ocorrida no acesso ao Azure.</param>
/// <returns></returns>
public static CausaErrosDataServiceException CausaErro(Exception erro)
{
// Procura na pilha de exceções um erro da classe DataServiceClientException
while (!(erro is System.Data.Services.Client.DataServiceClientException) && (erro != null))
erro = erro.InnerException;
if (erro == null) return CausaErrosDataServiceException.Desconhecido;
// Neste momento já foi encontrado um erro do tipo DataServiceClientException, então a propriedade Message
// contém um XML válido descrevendo o erro
CausaErrosDataServiceException causa;
try
{
XmlDocument xmlErro = new XmlDocument();
xmlErro.LoadXml(erro.Message);
// O XML descrevendo um erro usa o namespace http://schemas.microsoft.com/ado/2007/08/dataservices/metadata, então
// é necessário criar um XmlNamespaceManager para poder referenciar os nós do documento no XPath
XmlNamespaceManager namespaceManager = new XmlNamespaceManager(xmlErro.NameTable);
namespaceManager.AddNamespace("n", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
// Faz consulta XPath para recuperar o elemento "<code>", que contém uma constante determinando a causa do erro
string s = xmlErro.SelectSingleNode("/n:error/n:code", namespaceManager).InnerText;
// Traduz esta constante para o enum CausaErrosDataServiceException
causa = (CausaErrosDataServiceException)Enum.Parse(typeof(CausaErrosDataServiceException), s);
}
catch
{
causa = CausaErrosDataServiceException.Desconhecido;
}
return causa;
}
À medida em que você for esbarrando com mais códigos de erro do Azure, você pode acrescenta-los no enum com os códigos de erro, para que eles sejam reconhecidos pela função CausaErro().