Repository navigation
Conversation
PUT /products with If-Match verified the version with a FOR UPDATE on the products row, and took the stock advisory lock only afterwards, to write. Sales (DeductStock) and transfers never touch the products row, they only take the advisory lock, so a sale could commit between the check and the lock: the version already validated did not include it, and the absolute quantities of the request erased it without a trace. That is the exact race the product version exists to catch. When the request carries stocks, the advisory lock is now taken before the version check and covers it and the write; a concurrent sale waits or the PUT answers 409. Updates without stocks still take no stock lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Revisado. No lo mergearía para esta entrega. El análisis es correcto: Lo dejo afuera por una razón de riesgo, no de calidad: es un cambio en el orden de los locks del camino de escritura de productos, el lugar donde un error se paga con un deadlock o con stock mal escrito, y son 111 líneas sobre el código más delicado del sistema. Para ganar eso hay que poder probarlo con concurrencia de verdad, y a esta altura del calendario no es donde pondría el riesgo. Segunda línea: si queda tiempo después de QA, lo retomaría antes que cualquier otro de los que dejé afuera. |
Ticket de Jira
https://proyectofinalfrlp.atlassian.net/browse/TESIS-999019
Descripción
Products::UpdateProductverificaba elIf-Match(TESIS-101) conlock!—FOR UPDATEsobre la fila deproducts— y tomaba el advisory lock de stock recién después, para escribir. Pero las ventas (DeductStock, que usan el alta manual, la ingesta por webhook y la edición de órdenes) y las transferencias no tocan la fila del producto: sólo toman el advisory lock. Quedaba una ventana:Es exactamente la carrera que la versión del producto existe para detectar. La ventana es chica, pero real (si la venta todavía tiene el lock, el PUT ya recibía 409, que es lo correcto).
El cambio: cuando el request trae stocks, el advisory lock se toma antes de verificar la versión y cubre el chequeo, el
update!y la escritura. Una venta concurrente espera, o el PUT responde 409. Sin stocks no se toma: editar el nombre no compite por stock con nadie, y conwait: falseenvolver ese caso devolvería 409 espurios (se mantiene la decisión que documentaba el comentario anterior).UpdateProduct#callenvuelve versión, update y escritura enwithin_stock_lock, que toma el lock sólo si hay stocks.write_stocks!ya no toma el lock: corre dentro de él.spec/poros/products/update_product_spec.rb(no había spec de este PORO): verifica desde otra sesión de Postgres que el lock está tomado mientras se calcula la versión —el advisory lock es reentrante dentro de la misma conexión, así que preguntarlo desde la del test daría «libre» por la razón equivocada—; que sin stocks no se toma; que escribe; y que una versión vieja no escribe nada.write_stocks!.Evidencia visual
N/A
Cómo probar
La carrera no se reproduce a mano con facilidad; el spec la fija:
bundle exec rspec spec/poros/products/update_product_spec.rb→ 4 ejemplos en verde.app/poros/products/update_product.rbdemaster, falla «holds the stock lock while it checks the version»: el lock está libre en ese momento.Verificación:
bundle exec rspec(1571 ejemplos, 0 fallas, cobertura 99.89% de línea),rubocopybrakemanlimpios.Impacto y consideraciones
¿Introduce breaking changes?
No. Un PUT con stocks que choca con una venta en curso responde 409 (ya podía pasar), en lugar de pisarla.
¿Requiere nuevas variables de entorno?
No
¿Afecta la arquitectura o genera un nuevo patrón?
No. Ajusta el alcance del lock dentro del criterio de ADR-009.
🤖 Generated with Claude Code