-1

I have a resource (r) with two non-blocking operations (this is from external library that I cannot change):

Mono<String> op1(String someInput);
Mono<String> op2(String otherInput);

op1() and op2() can be called over different threads but cannot execute concurrently. When op1 is called depends upon external factors, same is true about op2. Two operations are unrelated(op1 may be called 100 times and op2 1 times). How to ensure mutually exclusive processing of op1 and op2. If op1 /op2 were blocking, java 'synchronized' would have solved it. External library methods fail if the op1 and op2 processing happens simulataneously.

How to enforce such synchronization without using EmitProcessor(which is deprecated) so that op1 and op2 can be called from different Scheduler threads? or is there a built-in standard solution in WebFlux api to address such scenarios?

(There is a solution using EventProcessor but want to avoid it as EventProcessor is deprecated Nonblocking ReentrantLock with Reactor)

hmble
  • 140
  • 2
  • 11
  • 1
    how do you decide about the "and vice-versa" part? can't you just decide to always subscribe to op1 first then op2? in that case, `op1.then(op2)` could do the trick. – Simon Baslé May 03 '21 at 09:10
  • @SimonBaslé op1 and op2 are callbacks and their execution depends upon consumers, i.e. consumer1 controls when op1 should be called and consumer2 controls op2. So they are not being called side-by-side and hence cannot use then, map, delayUntil etc. I am looking for a solution on the lines of https://stackoverflow.com/questions/52998809/nonblocking-reentrantlock-with-reactor but not using EventProcessor, not sure if something already exists? thank you (added more info in question to clear this) – hmble May 03 '21 at 13:38
  • Are you talking about [mutual exclusion (mutex)](https://en.wikipedia.org/wiki/Mutual_exclusion) over Mono subscription ? I am not sure reactor has anything for that. However, you could try to force usage of a single-threaded scheduler to avoid concurrent execution. It has the benefit to simplify the locking logic. Note that I'm neither sure about performance impact nor scheduling propagation behavior downstream. You should look at `subscribeOn` and `publishOn` methods. Note: that is exactly what happens for UI techs. They usually use a "UI thread" on which all display update must happen. – amanin May 04 '21 at 06:28
  • you are contradicting yourself in your explanation. First you say that op1 is controlling when op2 is beeing called. Then you say that op1 and op2 are controlled by consumers. So which is it going to be? is op2 controlled by op1 or by a consumer2? – Toerktumlare May 04 '21 at 23:06
  • Frequency of op1 and op2 call depends upon api callers, op1 and op2 are not dependent on each other but they both cannot be processing at same time. Sorry to confuse but by sequential call I didn’t means first call op1 and then op2(by sequential processing I meant non-concurrent processing). I will also update the question to clarify this. – hmble May 05 '21 at 03:34
  • @SimonBaslé is the question clear now? – hmble May 07 '21 at 15:06
  • Thanks @SimonBaslé , below solution solves the issue I was facing, let me know if you see any issue with it. – hmble May 16 '21 at 20:14

2 Answers2

1

There is currently nothing built in for this in Reactor.

The solution in that other answer can probably be updated to Sinks.Many new API, it appears the associated project has indeed been updated: https://github.com/alex-pumpkin/reactor-lock

You could also use https://github.com/reactor/reactor-pool but that would be a bit overkill.

Simon Baslé
  • 27,105
  • 5
  • 69
  • 70
0

Below solution ensures mutually exclusive processing of op1 and op2:

public class Locker {
    private final AtomicBoolean locked = new AtomicBoolean(true);
    private final Flux<Boolean> notifier;
    private final Sinks.Many<Boolean> notifierSink;

    public Locker() {
        this.notifierSink = Sinks.many().multicast().onBackpressureBuffer(1, false);
        this.notifier = notifierSink.asFlux();
        this.notifierSink.emitNext(true, Sinks.EmitFailureHandler.FAIL_FAST);
    }

    public <T> Flux<T> lockThenProcess(Duration lockTimeout, Flux<T> job) {
        return notifier.filter(v -> obtainLock())
                .next()
                .transform(locked -> lockTimeout == null ? locked : locked.timeout(lockTimeout))
                .doOnSubscribe(s -> log.debug("obtaining lock"))
                .doOnError(th -> log.error("can't obtain lock: " + th.getMessage(), th))
                .flatMapMany(v -> job)
                .doFinally(s -> {
                    if (releaseLock()) {
                        log.debug("released lock");
                        notifierSink.emitNext(true, Sinks.EmitFailureHandler.FAIL_FAST);
                    }
                });
    }

    private synchronized boolean obtainLock() {
        return locked.getAndSet(false);
    }

    private synchronized boolean releaseLock() {
        locked.set(true);
        return locked.get();
    }
}

Then, call op1 and op2 (on any threads) as below:

op1Trigger.concatMap(v -> locker.lockThenProcess(Duration.ofMinutes(1), r.op1(input).flux()))

and

op2Trigger.concatMap(v -> locker.lockThenProcess(Duration.ofMinutes(1), r.op2(input).flux()))
hmble
  • 140
  • 2
  • 11