La condición de carrera de datos de red desde el infierno
Estaba escribiendo un cliente / servidor de red (Windows XP / C #) para trabajar con una aplicación similar en una estación de trabajo realmente antigua (Encore 32/77) escrita por otro desarrollador.
Lo que la aplicación hizo esencialmente fue compartir / manipular ciertos datos en el host para controlar el proceso del host que ejecuta el sistema con nuestra elegante interfaz de usuario con pantalla táctil de monitor múltiple basada en PC.
Lo hizo con una estructura de 3 capas. El proceso de comunicaciones leyó / escribió datos a / desde el host, realizó todas las conversiones de formato necesarias (endianness, formato de punto flotante, etc.) y escribió / leyó los valores a / desde una base de datos. La base de datos actuó como un intermediario de datos entre las comunicaciones y las IU de la pantalla táctil. La aplicación de la interfaz de usuario de la pantalla táctil generó interfaces de pantalla táctil en función de la cantidad de monitores conectados a la PC (esto lo detectó automáticamente).
En el marco de tiempo dado, un paquete de valores entre el host y nuestra PC solo podía enviar 128 valores máximos a través del cable a la vez con una latencia máxima de ~ 110 ms por viaje de ida y vuelta (UDP se usó con una conexión directa x-over ethernet entre las computadoras). Por lo tanto, el número de variables permitidas en función del número variable de pantallas táctiles adjuntas estaba bajo estricto control. Además, el host (aunque tenía una arquitectura multiprocesador bastante compleja con un bus de memoria compartida utilizado para la computación en tiempo real) tenía aproximadamente 1/100 de la potencia de procesamiento de mi teléfono celular, por lo que tenía la tarea de hacer el menor procesamiento posible y su servidor / client tuvo que escribirse en ensamblado para asegurar esto (el host estaba ejecutando una simulación en tiempo real que no podía verse afectada por nuestro programa).
El problema fue. Algunos valores, cuando se cambian en la pantalla táctil, no tomarían solo el valor recién ingresado, sino que se alternarían aleatoriamente entre ese valor y el valor anterior. Eso y solo en unos pocos valores específicos en unas pocas páginas específicas con una cierta combinación de páginas alguna vez exhibió el síntoma. Casi perdimos el problema por completo hasta que comenzamos a ejecutarlo a través del proceso inicial de aceptación del cliente
Para precisar el problema, elegí uno de los valores oscilantes:
- Revisé la aplicación de pantalla táctil, estaba oscilando
- Revisé la base de datos, oscilando
- Revisé la aplicación de comunicaciones, oscilando
Luego rompí los cables y comencé a decodificar manualmente las capturas de paquetes. Resultado:
- No oscila, pero los paquetes no se veían bien, había demasiados datos.
Revisé cada detalle del código de comunicaciones cientos de veces sin encontrar ningún defecto / error.
Finalmente comencé a enviar correos electrónicos al otro desarrollador preguntando en detalle cómo funcionaba su parte para ver si había algo que me faltaba. Entonces lo encontré.
Aparentemente, cuando envió datos, no eliminó el conjunto de datos antes de la transmisión, por lo que, esencialmente, estaba sobrescribiendo el último búfer utilizado con los nuevos valores sobrescribiendo los antiguos, pero los valores antiguos no sobrescritos aún se transmitían.
Entonces, si un valor estaba en la posición 80 de la matriz de datos y la lista de valores solicitados cambiaba a menos de 80 pero ese mismo valor estaba contenido en la nueva lista, entonces ambos valores existirían en el búfer de datos para ese búfer específico en cualquier tiempo dado.
El valor que se leía de la base de datos dependía del intervalo de tiempo en que la UI solicitaba el valor.
La solución fue dolorosamente simple. Lea el número de elementos entrantes en el búfer de datos (en realidad estaba contenido como parte del protocolo del paquete) y no lea el búfer más allá de ese número de elementos.
Lecciones aprendidas:
No dé por sentado la potencia informática moderna. Hubo un tiempo en que las computadoras no eran compatibles con Ethernet y cuando el vaciado de una matriz podía considerarse costoso. Si realmente desea ver hasta dónde hemos llegado, imagine un sistema que prácticamente no tiene forma de asignación de memoria dinámica. Es decir, el proceso ejecutivo tuvo que preasignar toda la memoria para todos los programas en orden y ningún programa podría crecer más allá de ese límite. Es decir, asignar más memoria a un programa sin volver a compilar todo el sistema podría causar un bloqueo masivo. Me pregunto si la gente hablará sobre los días previos a la recolección de basura bajo la misma luz algún día.
Al hacer redes con protocolos personalizados (o manejar la representación de datos binarios en general), asegúrese de leer las especificaciones hasta que comprenda cada función de cada valor que se envía a través de la tubería. Quiero decir, léelo hasta que te duelan los ojos. Las personas manejan datos manipulando bits o bytes individuales, tienen formas muy inteligentes y eficientes de hacer las cosas. Perder el más mínimo detalle podría romper el sistema.
El tiempo total para arreglarlo fue de 2 a 3 días y la mayoría de ese tiempo lo pasé trabajando en otras cosas cuando me sentí frustrado con esto.
Nota al margen: la computadora host en cuestión no era compatible con Ethernet de forma predeterminada. La tarjeta para manejarla fue hecha a medida y adaptada y la pila de protocolos prácticamente no existía. El desarrollador con el que estaba trabajando era un gran programador, no solo implementó una versión simplificada de UDP y una pila de Ethernet falsa mínima (el procesador no era lo suficientemente potente como para manejar una pila de Ethernet completa) en el sistema para este proyecto. pero lo hizo en menos de una semana. También había sido uno de los líderes del equipo original del proyecto que había diseñado y programado el sistema operativo en primer lugar. Digamos que cualquier cosa que haya tenido que compartir sobre computadoras / programación / arquitectura, sin importar cuánto tiempo me haya quedado boquiabierto o cuánto conozca, escucharía cada palabra.