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

quarta-feira, 15 de setembro de 2010

Dev@SrNimbus: Modelar x Estimar

(Eu vou juntar os posts “Dev@SrNimbus” depois em algum lugar, mas estou encontrando algumas “verdades quase evidentes” que é o tipo de coisa que vale a pena registrar. A verdade quase evidente é aquela que todo mundo, após lê-la, diz “mas isso é evidente!”. É. É evidente, mas você só se toca pro fato depois que alguém mostrou ele pra você.)

A seção 8 apresenta uma forma de estimar, priorizar e planejar um projeto de desenvolvimento de software […]. Contudo, você deve manter em mente que […] estas questões são em grande parte irrelevantes até que o seu time tenha aprendido a entregar, de forma consistente, um produto de qualidade que traga valor ao cliente.

Visual Studio Tem System: Better Software Development for Agile Teams, Section 8

Não adianta esforços para melhorar o seu processo de criação e gerenciamento de estimativas se você ainda não tem um processo bem definido para o levantamento e implementação destas funcionalidades. O processo de levantamento e implementação é a base para o processo de estimativas e priorização. O primeiro fornece dados para o segundo. Se o primeiro não está produzindo dados sólidos, o segundo processo, por melhor que seja, não gerará resultados satisfatórios.

Em uma metodologia Ágil, que é a que escolhemos implementar aqui na Sr. Nimbus, o processo de levantamento e implementação de funcionalidades necessita a incorporação de várias práticas na cultura da empresa:

  • Uso de mecanismo de controle de versão;
  • Build e integração frenquentes;
  • TDD;
  • Modelagem da aplicação.

Hoje já fazemos o TDD razoavelmente bem, e fazemos o uso “comum” do TFSCC - Team Foundation Source Code Control, o mecanismo de controle de versão do TFS. Sempre li sobre a prática “build & integrate often” de Agile, mas só recentemente, ao ter que integrar o trabalho executado em 3 frentes distintas do projeto, vi o valor que esta prática pode trazer. No quesito “modelagem”, ainda fazemos isto de forma completamente artesanal, sem nenhum processo que embase esta prática.

Então, por mais doída que seja esta afirmação, ainda não estamos em um ponto para desenvolver um processo formal de estimativa de esforço. Claro que estimamos, mas de forma informal, e não raramente, estas estimativas contém uma variação grande. O que estamos buscando é uma forma de diminuir esta variação – e a formalização de um processo de estimativa ajudará nisto. Mas a principal lição aqui é que esforços para a formalização do processo de estimativas e priorização só produzirão resultados concretos após a incorporação das 4 práticas citadas acima no nosso processo de levantamento e implementação de funcionalidades. Esse vai ser nosso “roadmap” para a implementação de um processo de gerenciamento do ciclo de vida de um projeto de software.

sexta-feira, 3 de setembro de 2010

Criar link "Executar como Administrador"

O Visual Studio requer que ele seja executado como administrador para você poder rodar aplicações dentro do Development Fabric e/ou usando o Development Storage do Azure. Para evitar ter que clicar com botão direito e selecionar "Executar como Administrador" toda vez que você vai rodar o VS, modifique o link do Visual Studio para já executar automaticamente como Administrador. Nas propriedades do link, selecione o botão "Avançados", e marque a opção "Executar como administrador":



(De qualquer modo, ainda aparece aquela mensagezinha irritante "Deseja que o programa a seguir faça alterações nesse computador?" do UAC - User Access Control, ou Controle de Conta de Usuário. Você pode desabilitar o UAC no Painel de Controle > Sistema e Segurança > Alterar Configurações de Controle de Conta de Usuário, mas fique avisado de que seu computador fica menos protegido porque blábláblá blábláblá blábláblá.)