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

io.smallrye.reactive.messaging.amqp.AmqpCreditBasedSender Maven / Gradle / Ivy

The newest version!
package io.smallrye.reactive.messaging.amqp;

import static io.smallrye.reactive.messaging.amqp.i18n.AMQPExceptions.ex;
import static io.smallrye.reactive.messaging.amqp.i18n.AMQPLogging.log;
import static java.time.Duration.ofSeconds;

import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Flow.Processor;
import java.util.concurrent.Flow.Subscriber;
import java.util.concurrent.Flow.Subscription;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.enterprise.inject.Instance;

import org.eclipse.microprofile.reactive.messaging.Message;

import io.opentelemetry.api.OpenTelemetry;
import io.smallrye.common.annotation.CheckReturnValue;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.helpers.Subscriptions;
import io.smallrye.mutiny.tuples.Tuple3;
import io.smallrye.reactive.messaging.amqp.ce.AmqpCloudEventHelper;
import io.smallrye.reactive.messaging.amqp.tracing.AmqpOpenTelemetryInstrumenter;
import io.smallrye.reactive.messaging.ce.OutgoingCloudEventMetadata;
import io.vertx.amqp.impl.AmqpMessageImpl;
import io.vertx.mutiny.amqp.AmqpSender;

public class AmqpCreditBasedSender implements Processor, Message>, Subscription {

    private final ConnectionHolder holder;
    private final Uni retrieveSender;
    private final AmqpConnectorOutgoingConfiguration configuration;
    private final AmqpConnector connector;

    private final AtomicReference upstream = new AtomicReference<>();
    private final AtomicReference>> downstream = new AtomicReference<>();
    private final AtomicBoolean once = new AtomicBoolean();
    private final boolean durable;
    private final long ttl;
    private final String configuredAddress;
    private final boolean tracingEnabled;
    private final boolean mandatoryCloudEventAttributeSet;
    private final boolean writeCloudEvents;
    private final boolean writeAsBinaryCloudEvent;
    private final int retryAttempts;
    private final int retryInterval;

    private final AmqpOpenTelemetryInstrumenter amqpInstrumenter;

    private volatile boolean isAnonymous;

    /**
     * A flag tracking if we are retrieving the credits for the sender.
     * It avoids flooding the broker with credit requests.
     */
    private volatile boolean creditRetrievalInProgress = false;

    private final long maxInflights;

    public AmqpCreditBasedSender(AmqpConnector connector, ConnectionHolder holder,
            AmqpConnectorOutgoingConfiguration configuration, Uni retrieveSender,
            Instance openTelemetryInstance) {
        this.connector = connector;
        this.holder = holder;
        this.retrieveSender = retrieveSender;
        this.configuration = configuration;
        this.durable = configuration.getDurable();
        this.ttl = configuration.getTtl();
        this.configuredAddress = configuration.getAddress().orElseGet(configuration::getChannel);
        this.tracingEnabled = configuration.getTracingEnabled();
        this.mandatoryCloudEventAttributeSet = configuration.getCloudEventsType().isPresent()
                && configuration.getCloudEventsSource().isPresent();
        this.writeCloudEvents = configuration.getCloudEvents();
        this.writeAsBinaryCloudEvent = configuration.getCloudEventsMode().equalsIgnoreCase("binary");

        this.retryAttempts = configuration.getReconnectAttempts();
        this.retryInterval = configuration.getReconnectInterval();
        this.maxInflights = configuration.getMaxInflightMessages();

        if (tracingEnabled) {
            amqpInstrumenter = AmqpOpenTelemetryInstrumenter.createForSender(openTelemetryInstance);
        } else {
            amqpInstrumenter = null;
        }
    }

    @Override
    public void subscribe(Subscriber> subscriber) {
        if (!downstream.compareAndSet(null, subscriber)) {
            Subscriptions.fail(subscriber, ex.illegalStateOnlyOneSubscriberAllowed());
        } else {
            if (upstream.get() != null) {
                subscriber.onSubscribe(this);
            }
        }
    }

    private Uni getSenderAndCredits() {
        return retrieveSender
                .onItem().call(sender -> {
                    isAnonymous = configuration.getUseAnonymousSender()
                            .orElseGet(() -> ConnectionHolder.supportAnonymousRelay(sender.connection()));
                    CompletableFuture future = new CompletableFuture<>();
                    holder.getContext().runOnContext(() -> {
                        setCreditsAndRequest(sender);
                        future.complete(null);
                    });
                    return Uni.createFrom().completionStage(future);
                });
    }

    @CheckReturnValue
    public Uni isConnected() {
        return isConnected(true);
    }

    public int getHealthTimeout() {
        return configuration.getHealthTimeout();
    }

    private Uni isConnected(boolean attemptConnection) {
        return holder.isConnected()
                .chain(ok -> {
                    if (!ok && attemptConnection) {
                        // Retry connection, this normally happen during the "send" call
                        return holder.getOrEstablishConnection()
                                .chain(x -> isConnected(false));
                    }
                    return Uni.createFrom().item(ok);
                });
    }

    @Override
    public void onSubscribe(Subscription subscription) {
        if (this.upstream.compareAndSet(null, subscription)) {
            Subscriber> subscriber = downstream.get();
            if (subscriber != null) {
                subscriber.onSubscribe(this);
            }
        } else {
            Subscriber> subscriber = downstream.get();
            if (subscriber != null) {
                subscriber.onSubscribe(Subscriptions.CANCELLED);
            }
        }
    }

    /**
     * Must be called on the context having created the AMQP connection
     *
     * @param sender the sender
     * @return the remaining credits
     */
    private long setCreditsAndRequest(AmqpSender sender) {
        long credits = sender.remainingCredits();
        Subscription subscription = upstream.get();
        if (credits != 0L && subscription != Subscriptions.CANCELLED) {
            // Request upfront the sender remaining credits or the max inflights
            long request = maxInflights > 0 ? Math.min(credits, maxInflights) : credits;
            log.retrievedCreditsForChannel(configuration.getChannel(), credits);
            subscription.request(request);
            return credits;
        }
        if (credits == 0L && subscription != Subscriptions.CANCELLED) {
            onNoMoreCredit(sender);
        }
        return 0L;
    }

    void requestUpstream() {
        Subscription subscription = upstream.get();
        if (subscription != null && subscription != Subscriptions.CANCELLED) {
            subscription.request(1);
        }
    }

    @Override
    public void onNext(Message message) {
        if (isCancelled()) {
            return;
        }

        Subscriber> subscriber = this.downstream.get();

        try {
            send(message, durable, ttl, configuredAddress, isAnonymous)
                    .subscribe().with(tuple -> {
                        if (tuple != null) { // No serialization issue
                            subscriber.onNext(tuple.getItem1());
                            long remainingCredits = tuple.getItem3();
                            if (remainingCredits == 0) { // no more credit, request more
                                onNoMoreCredit(tuple.getItem2());
                            } else { // keep the request one more message
                                requestUpstream();
                            }
                        } else {
                            requestUpstream();
                        }
                    }, subscriber::onError);
        } catch (Exception e) {
            // Message can be sent - nacking and skipping.
            message.nack(e);
            log.serializationFailure(configuration.getChannel(), e);
            requestUpstream();
        }
    }

    private void onNoMoreCredit(AmqpSender sender) {
        if (!creditRetrievalInProgress) {
            creditRetrievalInProgress = true;
            log.noMoreCreditsForChannel(configuration.getChannel());
            holder.getContext().runOnContext(() -> {
                if (isCancelled()) {
                    return;
                }
                long c = setCreditsAndRequest(sender);
                if (c == 0L) { // still no credits, schedule a periodic retry
                    holder.getVertx().setPeriodic(configuration.getCreditRetrievalPeriod(), id -> {
                        if (setCreditsAndRequest(sender) != 0L || isCancelled()) {
                            // Got our new credits or the application has been terminated,
                            // we cancel the periodic task.
                            holder.getVertx().cancelTimer(id);
                            creditRetrievalInProgress = false;
                        } else {
                            log.stillNoMoreCreditsForChannel(configuration.getChannel());
                        }
                    });
                } else {
                    creditRetrievalInProgress = false;
                }
            });
        }
    }

    private boolean isCancelled() {
        Subscription subscription = upstream.get();
        return subscription == Subscriptions.CANCELLED || subscription == null;
    }

    @Override
    public void onError(Throwable throwable) {
        Subscription sub = upstream.getAndSet(Subscriptions.CANCELLED);
        Subscriber> subscriber = this.downstream.get();
        if (sub != null && sub != Subscriptions.CANCELLED && subscriber != null) {
            subscriber.onError(throwable);
        }
    }

    @Override
    public void onComplete() {
        Subscription sub = upstream.getAndSet(Subscriptions.CANCELLED);
        Subscriber> subscriber = this.downstream.get();
        if (sub != null && sub != Subscriptions.CANCELLED && subscriber != null) {
            subscriber.onComplete();
        }
    }

    @Override
    public void request(long l) {
        // Delay the retrieval of the sender and the request until we get a request.
        if (!once.getAndSet(true)) {
            getSenderAndCredits()
                    .subscribe().with(s -> {
                    }, f -> downstream.get().onError(f));
        }
    }

    @Override
    public void cancel() {
        Subscription sub = upstream.getAndSet(Subscriptions.CANCELLED);
        if (sub != null && sub != Subscriptions.CANCELLED) {
            sub.cancel();
        }
    }

    private Uni, AmqpSender, Long>> send(Message msg, boolean durable, long ttl, String configuredAddress,
            boolean isAnonymousSender) {
        final io.vertx.mutiny.amqp.AmqpMessage amqp = getMessage(msg, durable, ttl, configuredAddress, isAnonymousSender);
        if (amqp == null) {
            return Uni.createFrom().nullItem();
        }
        if (tracingEnabled) {
            amqpInstrumenter.traceOutgoing(msg, new AmqpMessage<>(amqp, null, null, false, true));
        }
        return retrieveSender.onItem().transformToUni(s -> s.sendWithAck(amqp)
                // We are on Vert.x context that created the client, we can access the remaining credits and update it.
                .replaceWith(() -> Tuple3., AmqpSender, Long> of(msg, s, s.remainingCredits())))
                .onFailure().retry().withBackOff(ofSeconds(1), ofSeconds(retryInterval)).atMost(retryAttempts)
                .onItemOrFailure().call((s, failure) -> {
                    if (failure != null) {
                        return Uni.createFrom().completionStage(msg.nack(failure));
                    } else {
                        return Uni.createFrom().completionStage(msg.ack());
                    }
                });
    }

    private io.vertx.mutiny.amqp.AmqpMessage getMessage(Message msg, boolean durable, long ttl, String configuredAddress,
            boolean isAnonymousSender) {
        io.vertx.mutiny.amqp.AmqpMessage amqp;
        OutgoingCloudEventMetadata ceMetadata = msg.getMetadata(OutgoingCloudEventMetadata.class)
                .orElse(null);

        if (msg instanceof AmqpMessage) {
            amqp = ((AmqpMessage) msg).getAmqpMessage();
        } else if (msg.getPayload() instanceof io.vertx.mutiny.amqp.AmqpMessage) {
            amqp = (io.vertx.mutiny.amqp.AmqpMessage) msg.getPayload();
        } else if (msg.getPayload() instanceof io.vertx.amqp.AmqpMessage) {
            amqp = new io.vertx.mutiny.amqp.AmqpMessage((io.vertx.amqp.AmqpMessage) msg.getPayload());
        } else if (msg.getPayload() instanceof org.apache.qpid.proton.message.Message) {
            org.apache.qpid.proton.message.Message message = (org.apache.qpid.proton.message.Message) msg.getPayload();
            AmqpMessageImpl vertxMessage = new AmqpMessageImpl(message);
            amqp = new io.vertx.mutiny.amqp.AmqpMessage(vertxMessage);
        } else {
            amqp = AmqpMessageConverter.convertToAmqpMessage(msg, durable, ttl);
        }

        if (writeCloudEvents && (ceMetadata != null || mandatoryCloudEventAttributeSet)) {
            // We encode the outbound record as Cloud Events if:
            // - cloud events are enabled -> writeCloudEvents
            // - the incoming message contains Cloud Event metadata (OutgoingCloudEventMetadata -> ceMetadata)
            // - or if the message does not contain this metadata, the type and source are configured on the channel
            if (writeAsBinaryCloudEvent) {
                amqp = AmqpCloudEventHelper.createBinaryCloudEventMessage(amqp, ceMetadata, this.configuration);
            } else {
                amqp = AmqpCloudEventHelper.createStructuredEventMessage(amqp, ceMetadata, this.configuration);
            }
        }

        String actualAddress = getActualAddress(msg, amqp, configuredAddress, isAnonymousSender);
        if (connector.getClients().isEmpty()) {
            log.messageNoSend(actualAddress);
            return null;
        }

        if (!actualAddress.equals(amqp.address())) {
            amqp.getDelegate().unwrap().setAddress(actualAddress);
        }

        log.sendingMessageToAddress(actualAddress);
        return amqp;
    }

    private String getActualAddress(Message message, io.vertx.mutiny.amqp.AmqpMessage amqp, String configuredAddress,
            boolean isAnonymousSender) {
        String address = amqp.address();
        if (address != null) {
            if (isAnonymousSender) {
                return address;
            } else {
                log.unableToUseAddress(address, configuredAddress);
                return configuredAddress;
            }
        }

        return message.getMetadata(OutgoingAmqpMetadata.class)
                .flatMap(o -> {
                    String addressFromMessage = o.getAddress();
                    if (addressFromMessage != null && !isAnonymousSender) {
                        log.unableToUseAddress(addressFromMessage, configuredAddress);
                        return Optional.empty();
                    }
                    return Optional.ofNullable(addressFromMessage);
                })
                .orElse(configuredAddress);
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy