All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.axoniq.flowcontrol.consumer.grpc.client.ClientResponseObserverSubscription Maven / Gradle / Ivy

There is a newer version: 1.2
Show newest version
/*
 * Copyright (c) 2021. AxonIQ
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.axoniq.flowcontrol.consumer.grpc.client;

import io.grpc.stub.ClientCallStreamObserver;
import io.grpc.stub.ClientResponseObserver;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/**
 * A reactive streams {@link Subscription} that also plays a role as gRPC {@link ClientResponseObserver}. All signals
 * (messages, errors, completes) received from the gRPC communication channel are going to be forwarded to the
 * subscriber of this subscription. Of course, the backpressure is respected.
 *
 * @param  the type of message being transmitted via this observer
 * @author Sara Pellegrini
 * @author Milan Savic
 * @since 1.0
 */
class ClientResponseObserverSubscription implements ClientResponseObserver, Subscription {

    private static final Logger logger = LoggerFactory.getLogger(ClientResponseObserverSubscription.class);
    private final Subscriber subscriber;
    private final AtomicBoolean cancelled = new AtomicBoolean(false);
    private final AtomicBoolean subscribed = new AtomicBoolean(false);
    private final AtomicReference> outgoingStream = new AtomicReference<>();
    private final Queue queue = new ConcurrentLinkedQueue<>();
    private final AtomicLong requested = new AtomicLong(0);
    private final AtomicBoolean signalGate = new AtomicBoolean(false);

    /**
     * Constructs this {@link ClientResponseObserverSubscription} with provided {@code subscriber}.
     *
     * @param subscriber the subscriber to forward signals recevied via gRPC communication channel to
     */
    public ClientResponseObserverSubscription(Subscriber subscriber) {
        this.subscriber = subscriber;
    }

    @Override
    public void beforeStart(ClientCallStreamObserver requestStream) {
        logger.debug("invoking before start on call {}", System.identityHashCode(this));
        if (cancelled.get()) {
            logger.debug("Call {} has been cancelled before start.", System.identityHashCode(this));
            return;
        }
        try {
            this.outgoingStream.set(requestStream);
            logger.debug("Disabling the automatic gRPC request for permits for call {}.",
                         System.identityHashCode(this));
            requestStream.disableAutoRequestWithInitial(0);
            logger.debug("gRPC manual request mode enabled for call {}.", System.identityHashCode(this));
        } catch (Exception e) {
            logger.warn("error during before start: ", e);
            submit(new Error(e));
        }
    }

    @Override
    public void onNext(Message value) {
        logger.trace("Message received for call {}.", System.identityHashCode(this));
        submit(new Next(value));
    }

    @Override
    public void onError(Throwable t) {
        logger.debug("Call completed exceptionally. ", t);
        submit(new Error(t));
    }

    @Override
    public void onCompleted() {
        logger.debug("Call {} completed.", System.identityHashCode(this));
        submit(new Complete());
    }

    @Override
    public void request(long n) {
        if (n <= 0) {
            subscriber.onError(new IllegalArgumentException("negative subscription request"));
            return;
        }
        int request = n > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) n;
        outgoingStream.get().request(request);
        requested.updateAndGet(current -> current + Math.min(Long.MAX_VALUE - current, n));
        signal();
    }

    @Override
    public void cancel() {
        logger.debug("The call has been cancelled.");
        cancelled.set(true);
        ClientCallStreamObserver streamObserver = outgoingStream.get();
        if (streamObserver != null) {
            streamObserver.onCompleted();
        }
    }

    void afterSubscribe() {
        subscribed.set(true);
        signal();
    }

    private void submit(Signal signal) {
        queue.add(signal);
        signal();
    }

    private void signal() {
        if (canSignal() && signalGate.compareAndSet(false, true)) {
            drain();
        }
    }

    private void drain() {
        while (canSignal()) {
            Signal signal = queue.poll();
            if (signal != null) {
                signal.run();
                if (signal.consumeRequest()) {
                    requested.decrementAndGet();
                }
            }
        }
        signalGate.set(false);
        signal();
    }

    private boolean canSignal() {
        return subscribed.get() &&
                !cancelled.get() &&
                !queue.isEmpty() &&
                (!queue.peek().consumeRequest() || requested.get() > 0);
    }

    private interface Signal {

        boolean consumeRequest();

        void run();
    }

    private class Next implements Signal {

        private final Message message;

        private Next(Message message) {
            this.message = message;
        }

        @Override
        public boolean consumeRequest() {
            return true;
        }

        @Override
        public void run() {
            subscriber.onNext(message);
        }
    }

    private class Complete implements Signal {

        @Override
        public boolean consumeRequest() {
            return false;
        }

        @Override
        public void run() {
            subscriber.onComplete();
        }
    }

    private class Error implements Signal {

        private final Throwable exception;

        private Error(Throwable exception) {
            this.exception = exception;
        }

        @Override
        public boolean consumeRequest() {
            return false;
        }

        @Override
        public void run() {
            subscriber.onError(exception);
        }
    }
}