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

com.turbospaces.gcp.pubsub.consumer.RequestReplyPubsubConsumer Maven / Gradle / Ivy

package com.turbospaces.gcp.pubsub.consumer;

import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;

import org.slf4j.MDC;
import org.springframework.integration.handler.MessageProcessor;
import org.springframework.messaging.Message;

import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.protobuf.Any;
import com.turbospaces.api.Topic;
import com.turbospaces.api.facade.ResponseWrapperFacade;
import com.turbospaces.cfg.ApplicationProperties;
import com.turbospaces.common.NonBlockingCallOnly;
import com.turbospaces.common.PlatformUtil;
import com.turbospaces.executor.ContextWorker;
import com.turbospaces.gcp.pubsub.PubsubRecord;
import com.turbospaces.gcp.pubsub.PubsubWorkUnit;
import com.turbospaces.http.HttpProto;
import com.turbospaces.mdc.MdcTags;
import com.turbospaces.mdc.MdcUtil;
import com.turbospaces.metrics.MetricTags;
import com.turbospaces.rpc.DefaultRequestReplyMapper;

import api.v1.ApiFactory;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.vavr.CheckedRunnable;
import jakarta.inject.Inject;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class RequestReplyPubsubConsumer implements MessageProcessor {
    private final ApplicationProperties props;
    private final MeterRegistry meterRegistry;
    private final Topic topic;
    private final DefaultRequestReplyMapper reqReplyMapper;
    private final PubsubContextWorkerFactory workerFactory;
    private final ApiFactory apiFactory;

    @Inject
    public RequestReplyPubsubConsumer(
            ApplicationProperties props,
            MeterRegistry meterRegistry,
            Topic topic,
            DefaultRequestReplyMapper reqReplyMapper,
            PubsubContextWorkerFactory workerFactory,
            ApiFactory apiFactory) {
        this.meterRegistry = Objects.requireNonNull(meterRegistry);
        this.props = Objects.requireNonNull(props);
        this.topic = Objects.requireNonNull(topic);
        this.reqReplyMapper = Objects.requireNonNull(reqReplyMapper);
        this.workerFactory = Objects.requireNonNull(workerFactory);
        this.apiFactory = Objects.requireNonNull(apiFactory);
    }
    @Override
    public Object processMessage(Message message) {
        PubsubWorkUnit record = new PubsubRecord(topic, message);
        var messageReceiveTime = System.currentTimeMillis();

        ContextWorker worker = workerFactory.worker(record).ifPresent(record); // ~ schedule in parallel
        FluentFuture.from(worker.submit(new CheckedRunnable() {
            @Override
            public void run() throws Throwable {
                var mapper = apiFactory.responseMapper();

                ResponseWrapperFacade respw = mapper.unpack(record);
                Any body = respw.body();
                api.v1.Headers headers = respw.headers();
                String operation = PlatformUtil.toLowerUnderscore(body.getTypeUrl());
                String status = respw.status().errorCode().toString().toLowerCase().intern();

                MdcUtil.setMdc(record, operation, headers);

                Thread currentThread = Thread.currentThread();
                String oldName = currentThread.getName();
                String newName = oldName + "|" + operation;
                currentThread.setName(newName);

                String createdAt = record.attributes().get(HttpProto.HEADER_X_TIMESTAMP);
                if (createdAt != null) {
                    var respCreationTimestamp = Long.parseLong(createdAt);
                    var respReceiveTimestamp = System.currentTimeMillis();
                    var networkTrip = respReceiveTimestamp - respCreationTimestamp;
                    if (networkTrip > props.PUBSUB_NETWORK_TRIP_ALERT_THRESHOLD.get().toMillis()) {
                        log.warn("High network trip: IN ::: (t-{}):(t-{}(m-{},s-{})) attributes:{}",
                                record.topic(),
                                body.getTypeUrl(),
                                headers.getMessageId(),
                                status,
                                record.attributes());
                    }
                }

                try {
                    NonBlockingCallOnly.run(new CheckedRunnable() {
                        @Override
                        public void run() throws Throwable {
                            UUID messageId = UUID.fromString(headers.getMessageId());
                            if (reqReplyMapper.contains(messageId)) {
                                log.debug("IN ::: (t-{}):(t-{}(m-{},s-{})) attributes:{}",
                                        record.topic(),
                                        body.getTypeUrl(),
                                        headers.getMessageId(),
                                        status,
                                        record.attributes());
                                reqReplyMapper.complete(messageId, respw);
                            }
                        }
                    });
                } finally {
                    record.ack().whenComplete(new BiConsumer() {
                        @Override
                        public void accept(Void result, Throwable ex) {
                            if (Objects.nonNull(ex)) {
                                try {
                                    MdcUtil.setMdc(record, operation, headers);
                                    log.error("pubsub ack failure ::: (t-{}):(t-{}(m-{},s-{}))",
                                            record.topic(),
                                            body.getTypeUrl(),
                                            headers.getMessageId(),
                                            status,
                                            ex);
                                } finally {
                                    MdcUtil.clearMdc(record);
                                }
                            } else {
                                try {
                                    MdcUtil.setMdc(record, operation, headers);
                                    meterRegistry.timer(
                                            "pubsub.reply.ack.delay",
                                            List.of(Tag.of("topic", MDC.get(MdcTags.MDC_TOPIC)),
                                                    Tag.of(MetricTags.OPERATION, MDC.get(MdcTags.MDC_OPERATION)) //
                                    )).record(System.currentTimeMillis() - messageReceiveTime, TimeUnit.MILLISECONDS);
                                    log.trace("pubsub ack success ::: (t-{}):(t-{}(m-{},s-{}))",
                                            record.topic(),
                                            body.getTypeUrl(),
                                            headers.getMessageId(),
                                            status);
                                } finally {
                                    MdcUtil.clearMdc(record);
                                }
                            }
                        }
                    });
                    currentThread.setName(oldName);
                    MdcUtil.clearMdc(record);
                }
            }
        })).addCallback(new FutureCallback() {
            @Override
            public void onSuccess(Object result) {

            }
            @Override
            public void onFailure(Throwable t) {
                log.error(t.getMessage(), t);
            }
        }, MoreExecutors.directExecutor());

        return new Object();
    }
}