Saltar a contenido

ADR-0004 · La edición masiva escribe con los eventos de modelo suprimidos y propaga una sola vez por lote

  • Estado: aceptado
  • Decisores: hquintero
  • Fecha de la decisión: 2026-08-24

Contexto y problema

La edición masiva de productos (RFC 007) escribe sobre products y product_items en lotes de decenas o cientos de registros. El sistema ya tiene propagación automática por evento: ProductItemObserver::updated() invoca ProductEvaluationStrategy, que llama a ProductSyncDispatcherService y encola un job por cada canal de salida habilitado (WooCommerce, TiendaNaranja, Contimarket, Medusa, Algolia, Typesense, PC Manager).

Con ese mecanismo intacto, un lote de 300 variantes encolaría ~2.100 jobs de sincronización de golpe, y un lote que tocara dos campos, el doble. El proyecto ya arrastra problemas de saturación en las colas de salida (incidente 2026-06, ADR-0002).

Al mismo tiempo, la propagación no puede simplemente omitirse: hay campos que hoy no sincronizan solos — products no tiene observer registrado y ProductPartnerCategoryObserver sólo atiende UMarket — así que editar la marca, la descripción o la categoría de salida deja el catálogo desactualizado en los canales (RFC 007 §4.2).

Opciones consideradas

  1. Escribir con Model::withoutEvents() y propagar explícitamente una vez por lote (elegida).
  2. Dejar los eventos activos y confiar en que las colas absorban el pico.
  3. Dejar los eventos activos pero agregar coalescing en el despachador, para que descarte sincronizaciones repetidas de la misma variante en una ventana corta.

Decisión

El motor de edición masiva aplica los cambios dentro de Model::withoutEvents(), y al terminar el lote encola PropagateBulkEditJob con las variantes afectadas, en chunks y con retardo configurable, sobre una cola propia (redis_bulk_edit).

Dos razones frente a la opción 2: el pico no es sólo de volumen sino de duplicación —el mismo producto se sincronizaría una vez por cada campo tocado— y la cola compartida haría que un lote administrativo demore la sincronización de precios y stock, que es lo que sostiene la venta.

Frente a la opción 3: el coalescing en el despachador cambia el comportamiento de todos los flujos del sistema, incluidos los que hoy funcionan bien, para resolver un problema de un flujo nuevo. Es un cambio de mayor alcance y riesgo que el que justifica esta necesidad.

Al propagar explícitamente aparece además una decisión secundaria: qué contexto de sincronización usar. Se usan los contextos que los canales ya entienden (product_basic y product_status), no uno nuevo. SyncToWooCommerceJob resuelve el tipo con un match que lanza InvalidArgumentException ante un valor desconocido: un contexto inventado como product_category haría fallar todos los jobs de Woo del lote. product_basic ya arrastra la categoría — buildProductPayload() incluye categories — así que alcanza.

Cuando el campo editado pertenece al producto padre se encolan todas sus variantes, no sólo las seleccionadas: el dato modificado aplica a la familia entera.

Consecuencias

Positivas

  • Un lote de 300 productos genera 3 jobs de propagación en vez de ~2.100 cadenas de sincronización simultáneas.
  • La sincronización del lote no compite con las colas de negocio: cola, conexión y supervisor de Horizon propios.
  • Cierra el hueco de propagación de products y de la categoría de salida CL, que antes no sincronizaban por ningún camino.
  • La propagación es observable en el canal de log BULK_EDIT, con la referencia del lote en el contexto de cada línea.

Negativas / deuda asumida

  • La supresión de eventos es global durante la escritura. withoutEvents() desactiva el dispatcher de Eloquent para todos los modelos, no sólo para el que se está editando. Si en el futuro un observer hace algo imprescindible dentro de una escritura (no sólo sincronizar), el motor tendrá que saberlo.
  • Se duplica parcialmente la lógica de propagación. El motor decide qué variantes sincronizar y con qué contexto, replicando el criterio que ProductEvaluationStrategy aplica por campo. Si esa estrategia cambia, hay dos lugares que revisar.
  • Los cambios no aparecen en el ActivityLog de Spatie, que se alimenta de los eventos suprimidos. La única traza es el canal de log BULK_EDIT, en archivo: quien audite un producto desde la interfaz no verá ahí lo que hizo un lote. Es una consecuencia asumida junto con la decisión de no persistir el lote (RFC 007 §5.5, D7); reusar activity_log se evaluó y se descartó porque la tabla tiene 6,8M filas y su batch_uuid no está indexado.
  • Si el proceso muere entre la escritura y el encolado, el lote queda aplicado y sin propagar. Al no persistirse el lote, detectarlo requiere leer el log: hay líneas de cambio sin su línea de propagación. Se resuelve forzando una sincronización de esos productos.