2

Is there a way to ensure that all transformation steps for a single Mono that created from a future are executed on the thread that subscribes and blocks?

For instance, the following code

public static void main(String[] args) {
  var future = new CompletableFuture<String>();
  var res = Mono.fromFuture(future).map(val -> {
    System.out.println("Thread: " + Thread.currentThread().getName());
    return val + "1";
  });

  new Thread(() -> {
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
    }
    future.complete("completed");
  }, "completer").start();

  res.block();
}

prints Thread: completer because the future is completed from the "completer" thread. I'm trying to figure out if there is a way to make it always print Thread: main.

SerCe
  • 5,826
  • 2
  • 32
  • 53

2 Answers2

3

No. When the main thread is blocked via .block(), that thread is specifically waiting for the onNext, onComplete, or onError signals of the stream (after all the upstream operators have executed). It does not somehow regain control before the upstream operators are invoked in order to execute the operators.

The closest thing you can do would be to ensure that:

  1. the subscription is executed on a specific Scheduler (via .subscribeOn) AND
  2. the future's completion value is published on the same Scheduler (via .publishOn).

For example:

Scheduler scheduler = Schedulers.parallel();
var res = Mono.fromFuture(future)
        .doFirst(() -> {   // Note: doFirst added in 3.2.10.RELEASE
            // prints a thread in the parallel Scheduler (specified by subscribeOn below)
            System.out.println("Subscribe Thread: " + Thread.currentThread().getName());
        })
        // specifies the Scheduler on which the the completion value
        // from above is published for downstream operators
        .publishOn(scheduler)
        .map(val -> {
            // prints a thread in the parallel Scheduler (specified by publishOn above)
            System.out.println("Operator Thread: " + Thread.currentThread().getName()); 
            return val + "1";
        })
        // specifies the Scheduler on which  upstream operators are subscribed
        .subscribeOn(scheduler);

However, note the following:

  • The subscribe occurs on a thread in the Scheduler, not the blocked main thread.
  • This approach just ensures that the same Scheduler is used, not the same Thread within the Scheduler. You could theoretically force the same Thread by using a single-threaded scheduler (e.g. Schedulers.newParallel("single-threaded", 1))
  • The .publishOn does not enforce that all operators operate on that Scheduler. It just affects downstream operators until the next .publishOn, or until the next async operator (such as .flatMap) which might utilize a different Scheduler.
Phil Clay
  • 4,028
  • 17
  • 22
0

As a very non-optimised proof of concept, this can be achieved in the following way:

Let's create an executor which is able to execute tasks "on demand" in a controlled way.

private static class SelfEventLoopExecutor implements Executor {
  private final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

  @Override
  public void execute(Runnable command) {
    boolean added = queue.add(command);
    assert added;
  }

  public void drainQueue() {
    Runnable r;
    while ((r = queue.poll()) != null) {
      r.run();
    }
  }
}

Next, create a subscriber which is able to use the executor to execute the tasks while waiting for the result instead of completely blocking the thread.

public static class LazyBlockingSubscriber<T> implements Subscriber<T> {
  private final SelfEventLoopExecutor selfExec;
  private volatile boolean completed = false;
  private volatile T value;
  private volatile Throwable ex;

  public LazyBlockingSubscriber(SelfEventLoopExecutor selfExec) {
    this.selfExec = selfExec;
  }

  @Override
  public void onSubscribe(Subscription s) {
    s.request(1);
  }

  @Override
  public void onNext(T t) {
    value = t;
    completed = true;
  }

  @Override
  public void onError(Throwable t) {
    ex = t;
    completed = true;
  }

  @Override
  public void onComplete() {
    completed = true;
  }

  public T block() throws Throwable {
    while (!completed) {
      selfExec.drainQueue();
    }
    if (ex != null) {
      throw ex;
    }
    return value;
  }
}

Now, we can modify the code in the following way

public static void main(String[] args) throws Throwable {
  var future = new CompletableFuture<String>();

  var selfExec = new SelfEventLoopExecutor(); // our new executor

  var res = Mono.fromFuture(future)
      .publishOn(Schedulers.fromExecutor(selfExec))  // schedule on the new executor
      .map(val -> {
        System.out.println("Thread: " + Thread.currentThread().getName());
        return val + "1";
      });

  new Thread(() -> {
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
    }
    future.complete("completed");
  }, "completer").start();

  var subs = new LazyBlockingSubscriber<String>(selfExec); // lazy subscribe
  res.subscribeWith(subs);
  subs.block(); // spin wait
}

As a result, the code prints Thread: main.

SerCe
  • 5,826
  • 2
  • 32
  • 53