¿Cómo funciona el backpressure en streams reactivos (especificación Reactive Streams, Project Reactor/RxJava)?
Cuando un productor rápido se cruza con un consumidor lento, algo tiene que ceder — el backpressure es el protocolo que le permite al consumidor decir "todavía no" en vez de ahogarse.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo funciona el backpressure en streams reactivos (especificación Reactive Streams, Project Reactor/RxJava)?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Por qué los sistemas push necesitan un freno
En una llamada síncrona clásica, el consumidor marca el ritmo: llama a una función, bloquea, y sólo pide más trabajo cuando está listo. Los pipelines reactivos rompen esa garantía por diseño: un Publisher empuja datos de forma asíncrona, y puede producir elementos más rápido de lo que un Subscriber puede consumirlos. Un socket que transmite cotizaciones de mercado, un consumidor de Kafka vaciando una partición caliente, o un cursor de base de datos que corre más rápido que una agregación lenta río abajo son la misma forma de problema: la oferta supera a la demanda, y ese desbalance tiene que aterrizar en algún lado. Sin un mecanismo de control se convierte en un buffer sin límite que termina en un OutOfMemoryError, en datos descartados en silencio, o en un consumidor cada vez más atrasado hasta que todo el pipeline colapsa.
La especificación Reactive Streams —adoptada casi textual en java.util.concurrent.Flow desde Java 9, e implementada por Project Reactor, RxJava y Akka Streams— resuelve esto con un protocolo híbrido pull-sobre-push construido con cuatro interfaces: Publisher<T>, Subscriber<T>, Subscription y Processor<T,R> (un componente que es ambas cosas). Su única regla rectora es engañosamente simple: un Publisher nunca debe entregarle a un Subscriber más elementos de los que ese Subscriber pidió. La demanda, no la oferta, es la que maneja el flujo.
#El protocolo, paso a paso
- El Subscriber llama a
publisher.subscribe(subscriber). - El Publisher llama a
subscriber.onSubscribe(subscription), entregando unSubscription— el único canal que tiene el Subscriber para señalar demanda o cancelar. - El Subscriber llama a
subscription.request(n)para decir "puedo manejar n elementos más ahora mismo". - El Publisher llama a
onNext(item)como máximo n veces — nunca más que la demanda pendiente sin satisfacer. - El ciclo se repite hasta que el Subscriber llama a
cancel(), o el Publisher termina el stream cononComplete()oonError(throwable).
#Cuando la demanda no se puede honrar
Un consumidor lento o un productor con picos de tráfico eventualmente van a crear un momento en el que llegan elementos nuevos sin demanda pendiente que los absorba. Las librerías reactivas exponen un puñado de estrategias con nombre justo para ese momento:
| Estrategia | Comportamiento ante el desborde | Mejor uso |
|---|---|---|
| BUFFER | Encola el excedente (acotado o sin límite) hasta que la demanda se ponga al día | Carga con picos, pero drenable a la larga |
| DROP | Descarta los elementos más nuevos que exceden la demanda | Perder algo ocasionalmente es aceptable, el orden de lo que sobrevive sí importa |
| LATEST | Conserva sólo el elemento no consumido más reciente | Valores en vivo que reemplazan a los anteriores — una cotización, una lectura de sensor |
| ERROR | Termina el stream con onError | El desborde indica un bug o una violación dura de SLA, no operación normal |
#Código: control manual de demanda con Project Reactor
La mayoría del código con Reactor nunca toca request(n) directamente — llamar a .subscribe(consumer) pide implícitamente Long.MAX_VALUE, o sea "sin límite", confiando en que los operadores río arriba igual respeten a un consumidor lento vía buffers internos y operadores como limitRate. Pero cuando necesitás frenar vos mismo a un consumidor lento específico, BaseSubscriber te da el protocolo crudo:
Flux<Long> ticks = Flux.interval(Duration.ofMillis(1)).onBackpressureBuffer(1000,dropped -> log.warn("Dropped tick: {}", dropped),BufferOverflowStrategy.DROP_OLDEST);ticks.publishOn(Schedulers.parallel()).subscribe(new BaseSubscriber<Long>() {@Overrideprotected void hookOnSubscribe(Subscription subscription) {request(10); // demanda inicial: "mandame 10, nada más"}@Overrideprotected void hookOnNext(Long tick) {processTick(tick); // trabajo lento, ej. una escritura a la baserequest(1); // pedí exactamente uno más cuando estés listo}@Overrideprotected void hookOnError(Throwable error) {log.error("El stream de ticks falló", error);}});
Cada llamada a request(1) sucede recién después de que processTick termina, así que estructuralmente el Publisher no puede adelantarse al consumidor más de un tick. RxJava traza la misma línea entre Observable (sin soporte de backpressure, pensado para secuencias cortas o guiadas por UI) y Flowable (con backpressure completo): Flowable.create(emitter -> { ... }, BackpressureStrategy.BUFFER) elige de entrada uno de los mismos cuatro resultados — BUFFER, DROP, LATEST, ERROR.
🧠 Check your understanding
0/3 · 0/3 answered1. En el protocolo Reactive Streams, ¿qué se necesita antes de que un Publisher pueda llamar a onNext sobre un Subscriber?
2. ¿Qué estrategia de backpressure encaja con una cotización de precio en vivo, donde sólo importa el valor más fresco y los viejos no sirven?
3. ¿Por qué un stream de movimiento del mouse nunca puede honrar backpressure de verdad?