quinta-feira, 16 de setembro de 2010

Mensagens de Erro do Azure Tables

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().