A condição de corrida de dados de rede do inferno
Eu estava escrevendo um cliente / servidor de rede (Windows XP / C #) para trabalhar com um aplicativo semelhante em uma estação de trabalho muito antiga (Encore 32/77) criada por outro desenvolvedor.
O que o aplicativo fez essencialmente foi compartilhar / manipular certos dados no host para controlar o processo do host que está executando o sistema com nossa sofisticada interface do usuário com tela sensível ao toque para vários monitores baseada em PC.
Isso foi feito com uma estrutura de três camadas. O processo de comunicação leu / gravou dados para / do host, realizou todas as conversões de formato necessárias (endianness, formato de ponto flutuante etc.) e gravou / leu os valores para / de um banco de dados. O banco de dados atuou como um intermediário de dados entre as comunicações e as UIs da tela sensível ao toque. O aplicativo da interface do usuário da tela de toque gerou interfaces de tela de toque com base em quantos monitores foram conectados ao PC (ele detectou isso automaticamente).
No período de tempo determinado, um pacote de valores entre o host e o nosso PC só podia enviar 128 valores no máximo por fio, com uma latência máxima de ~ 110ms por ida e volta (o UDP era usado com uma conexão Ethernet direta de x-over entre os computadores). Portanto, o número de variáveis permitidas com base no número variável de telas sensíveis ao toque anexadas estava sob controle estrito. Além disso, o host (apesar de ter uma arquitetura multiprocessador bastante complexa com barramento de memória compartilhada usado para computação em tempo real) tinha cerca de 1/100 da capacidade de processamento do meu telefone celular, por isso foi encarregado de fazer o menor processamento possível e seu servidor / client teve que ser escrito em assembly para garantir isso (o host estava executando uma simulação em tempo real que não podia ser afetada pelo nosso programa).
A questão era. Alguns valores, quando alterados na tela sensível ao toque, não pegam apenas o valor recém-inserido, mas alternam aleatoriamente entre esse valor e o valor anterior. Isso e apenas em alguns valores específicos em algumas páginas específicas com uma certa combinação de páginas já exibiu o sintoma. Quase perdemos o problema completamente até começarmos a executá-lo no processo inicial de aceitação do cliente
Para definir o problema, escolhi um dos valores oscilantes:
- Eu verifiquei o aplicativo Touchscreen, estava oscilando
- Eu verifiquei o banco de dados, oscilando
- Eu verifiquei o aplicativo de comunicação, oscilando
Então eu comecei o wireshark e comecei a decodificar manualmente as capturas de pacotes. Resultado:
- Não oscilando, mas os pacotes não pareciam corretos, havia muitos dados.
Percorri todos os detalhes do código de comunicação cem vezes, sem encontrar falhas / erros.
Finalmente, comecei a enviar e-mails para o outro desenvolvedor, perguntando em detalhes como o fim dele funcionava para ver se havia algo que estava faltando. Então eu encontrei.
Aparentemente, quando ele enviou dados, ele não liberou a matriz de dados antes da transmissão; portanto, basicamente, ele estava substituindo o último buffer usado com os novos valores substituindo os antigos, mas os valores antigos não substituídos ainda estão sendo transmitidos.
Portanto, se um valor estivesse na posição 80 da matriz de dados e a lista de valores solicitados mudasse para menos de 80, mas esse mesmo valor estivesse contido na nova lista, os dois valores existiriam no buffer de dados para esse buffer específico em qualquer Tempo dado.
O valor lido do banco de dados dependia do intervalo de tempo em que a interface do usuário estava solicitando o valor.
A correção foi dolorosamente simples. Leia o número de itens recebidos no buffer de dados (na verdade, ele estava contido como parte do protocolo de pacote) e não leia o buffer além desse número de itens.
Lições aprendidas:
Não tome como garantido o poder da computação moderna. Houve um tempo em que os computadores não eram compatíveis com Ethernet e a descarga de uma matriz podia ser considerada cara. Se você realmente deseja ver até onde chegamos, imagine um sistema que praticamente não tem forma de alocação dinâmica de memória. Ou seja, o processo executivo teve que pré-alocar toda a memória para todos os programas em ordem e nenhum programa poderia crescer além desse limite. IE, alocar mais memória para um programa sem recompilar todo o sistema pode causar uma falha maciça. Eu me pergunto se as pessoas vão falar sobre os dias pré-coleta de lixo à mesma luz algum dia.
Ao trabalhar em rede com protocolos personalizados (ou manipular a representação de dados binários em geral), certifique-se de ler as especificações até entender todas as funções de todos os valores enviados pelo canal. Quero dizer, leia até seus olhos doerem. As pessoas manipulam dados manipulando bits ou bytes individuais, têm maneiras muito inteligentes e eficientes de fazer as coisas. A falta dos mínimos detalhes pode danificar o sistema.
O tempo total para consertar foi de 2 a 3 dias, com a maior parte do tempo gasto trabalhando em outras coisas quando fiquei frustrado com isso.
Nota: o computador host em questão não suporta ethernet por padrão. O cartão para conduzi-lo foi feito sob medida e adaptado e a pilha de protocolos praticamente não existia. O desenvolvedor com quem eu estava trabalhando era um grande programador, ele não apenas implementou uma versão simplificada do UDP e uma pilha Ethernet falsa mínima (o processador não era poderoso o suficiente para lidar com uma pilha Ethernet completa) no sistema para este projeto mas ele fez isso em menos de uma semana. Ele também fora um dos líderes originais da equipe do projeto que havia projetado e programado o SO em primeiro lugar. Vamos apenas dizer, qualquer coisa que ele já tenha compartilhado sobre computadores / programação / arquitetura, não importa quanto tempo acabe ou quanto eu já seja novo, eu ouviria cada palavra.