I have a multicast sink with backpressure buffer with the default size. To this sink I emit events asynchronously every 25ms from a dedicated thread. There are 3 subscribers A, B and C where B pretends to do some work through thread sleep. A and C use subscribeOn
and B uses publishOn
on dedicated schedulers.
I have 5 questions about the behavior of the backpressure along with the schedulers and publishOn
vs subscribeOn
.
- why subscribeOn for A and C doesn't have any effect and elements are published on the emitting thread?
- A and C stop receiving events at 257 where B is already at ~15, shouldn't A and C stop at around ~256+15=271 ?
- after i=512 emissions start to fail on FAIL_OVERFLOW; is it because both buffers (B's publishOn 256 a sink's 256) are full? it is not true as B at this point already processed ~29 events
- A and C start receiving events back again after B=192, why 192? also, they receive the events on the B's scheduler, why?
- changing B's
publishOn
tosubscribeOn
pushes back the emitting thread respecting the slowest consumer (B).subscribeOn
has again no effect, why?
Here is the runnable code. Reactor version 3.4.14
.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Sinks;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.concurrent.Executors.newScheduledThreadPool;
import static reactor.core.scheduler.Schedulers.fromExecutorService;
public class SinkBackpressureBuffer {
private static final Logger LOGGER = LoggerFactory.getLogger("SinkLogger");
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws InterruptedException {
final Sinks.Many<Integer> sink = Sinks.many().multicast().onBackpressureBuffer();
new Thread(() -> {
sleep(1000);
for (int i = 1; i <= 550; i++) { // has to be bigger than 256 + 256
final Sinks.EmitResult emitResult = sink.tryEmitNext(i);
if (emitResult != Sinks.EmitResult.OK) {
LOGGER.error("Emit for i={}, res={}", i, emitResult);
} else {
LOGGER.info("Emit for i={}, res={}", i, emitResult);
}
sleep(25);
}
}).start();
sink.asFlux()
.subscribeOn(fromExecutorService(newScheduledThreadPool(5, new NamedThreadFactory("AAA")))) // has no effect
.subscribe(i -> {
LOGGER.info("A: {}", i);
});
sink.asFlux()
.publishOn(fromExecutorService(newScheduledThreadPool(5, new NamedThreadFactory("BBB"))))
.subscribe(i -> {
sleep(500);
LOGGER.info("B: {}", i);
});
sink.asFlux()
.subscribeOn(fromExecutorService(newScheduledThreadPool(5, new NamedThreadFactory("CCC")))) // has no effect
.subscribe(i -> {
LOGGER.info("C: {}", i);
});
LOGGER.info("done");
sleep(500000);
}
public static class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger sequence = new AtomicInteger(1);
private final String prefix;
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
int seq = sequence.getAndIncrement();
thread.setName(prefix + (seq > 1 ? "-" + seq : ""));
thread.setDaemon(true);
return thread;
}
}
}