Saltar al contenido

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:

Rust
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.