Contrapresión: cuando el core va más rápido
Un canal no espera a que el frontend haya pintado. Qué pasa cuando el productor supera al consumidor, los tres remedios, y por qué el más simple —mandar menos— casi siempre gana.
Qué pasa sin contrapresión
Los tres síntomas
La interfaz se congela durante la operación. Y desconcierta porque el trabajo pesado está en Rust, que es justo donde debía estar. La causa no es el trabajo: son los avisos sobre el trabajo.
El progreso llega tarde y a saltos. La barra se queda parada y después salta al noventa por ciento. Es la cola vaciándose.
El progreso sigue llegando después de terminar. El síntoma que confirma el diagnóstico: el core acabó hace dos segundos y la barra sigue subiendo, porque quedaban mensajes en cola. Si ves esto, no hay duda.
Los tres remedios
El ritmo, escrito
El primer remedio en código, con las dos condiciones:
use std::time::{Duration, Instant};const CADA_N: usize = 100;const CADA_MS: u64 = 50; // ~20 mensajes por segundolet mut ultimo = Instant::now();for (i, item) in items.enumerate() { procesar(item).await?; let toca_por_cantidad = (i + 1) % CADA_N == 0; let toca_por_tiempo = ultimo.elapsed() >= Duration::from_millis(CADA_MS); if toca_por_cantidad || toca_por_tiempo { progreso.send(Progreso::Avance { hechos: i + 1, total })?; ultimo = Instant::now(); }}// y SIEMPRE el último, aunque no toque por ninguna de las dosprogreso.send(Progreso::Fin { total, fallidos })?;Las dos condiciones hacen falta y por motivos opuestos. La de cantidad protege del caso rápido: diez mil elementos instantáneos no producen diez mil mensajes. La de tiempo protege del caso lento: si cada elemento tarda dos segundos, esperar a cien elementos deja la barra quieta tres minutos.
Y el mensaje final va siempre, fuera de las dos condiciones. Si no, un flujo que termine en el elemento 1.050 deja la barra en 1.000 para siempre.