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

com.turbospaces.gcp.pubsub.config.PubsubInitializer Maven / Gradle / Ivy

There is a newer version: 2.0.33
Show newest version
package com.turbospaces.gcp.pubsub.config;

import java.util.List;
import java.util.Objects;

import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;

import com.google.api.gax.rpc.ApiException;
import com.google.cloud.pubsub.v1.SubscriptionAdminClient;
import com.google.cloud.spring.pubsub.PubSubAdmin;
import com.google.cloud.spring.pubsub.core.subscriber.PubSubSubscriberTemplate;
import com.google.protobuf.Duration;
import com.google.protobuf.FieldMask;
import com.google.pubsub.v1.DeadLetterPolicy;
import com.google.pubsub.v1.ExpirationPolicy;
import com.google.pubsub.v1.RetryPolicy;
import com.google.pubsub.v1.Subscription;
import com.google.pubsub.v1.Topic;
import com.google.pubsub.v1.UpdateSubscriptionRequest;
import com.turbospaces.api.jpa.CompositeStackTracer;
import com.turbospaces.cfg.ApplicationProperties;
import com.turbospaces.dispatch.TransactionalRequestHandler;
import com.turbospaces.gcp.pubsub.PubsubTopic;
import com.turbospaces.gcp.pubsub.consumer.PubsubContextWorkerFactory;
import com.turbospaces.gcp.pubsub.consumer.PubsubReplyTopicConsumer;
import com.turbospaces.gcp.pubsub.consumer.PubsubRequestTopicConsumer;
import com.turbospaces.rpc.DefaultRequestReplyMapper;
import com.turbospaces.rpc.QueuePostTemplate;

import api.v1.ApiFactory;
import io.micrometer.core.instrument.MeterRegistry;
import io.opentracing.Tracer;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class PubsubInitializer implements InitializingBean {
    private static final String DLQ_POSTFIX = "dlq";
    private static final int MAX_RETRY_DLQ_CREATION_ATTEMPTS = 3;

    private final ApplicationProperties props;
    private final MeterRegistry meterRegistry;
    private final Tracer tracer;
    private final List topics;
    private final PubSubSubscriberTemplate subscriberTemplate;
    private final QueuePostTemplate postTemplate;
    private final ApiFactory apiFactory;
    private final CompositeStackTracer stackTracer;
    private final DefaultRequestReplyMapper mapper;
    private final List> handlers;
    private final PubsubContextWorkerFactory workerFactory;
    private final PubSubAdmin admin;
    private final SubscriptionAdminClient subscriptionAdminClient;

    public PubsubInitializer(
            ApplicationProperties props,
            Tracer tracer,
            MeterRegistry meterRegistry,
            List topics,
            PubSubSubscriberTemplate subscriberTemplate,
            QueuePostTemplate postTemplate,
            ApiFactory apiFactory,
            CompositeStackTracer stackTracer,
            DefaultRequestReplyMapper mapper,
            List> handlers, PubsubContextWorkerFactory workerFactory,
            PubSubAdmin admin, SubscriptionAdminClient subscriptionAdmin) {
        this.props = Objects.requireNonNull(props);
        this.tracer = Objects.requireNonNull(tracer);
        this.meterRegistry = Objects.requireNonNull(meterRegistry);
        this.topics = Objects.requireNonNull(topics);
        this.subscriberTemplate = Objects.requireNonNull(subscriberTemplate);
        this.postTemplate = Objects.requireNonNull(postTemplate);
        this.apiFactory = Objects.requireNonNull(apiFactory);
        this.stackTracer = Objects.requireNonNull(stackTracer);
        this.mapper = Objects.requireNonNull(mapper);
        this.handlers = Objects.requireNonNull(handlers);
        this.workerFactory = Objects.requireNonNull(workerFactory);
        this.admin = Objects.requireNonNull(admin);
        this.subscriptionAdminClient = Objects.requireNonNull(subscriptionAdmin);
    }

    @Override
    public void afterPropertiesSet() {
        try {
            for (PubsubTopic topic : topics) {
                createTopicAndSubscriptionIfNeeded(topic);
                if (topic.withCustomConfiguration()) {
                    return;
                }
                if (topic.isResponseTopic()) {
                    registerReplyTopicConsumer(topic);
                } else {
                    registerRequestTopicConsumer(topic);
                }
            }
        } catch (Throwable err) {
            throw new BeanCreationException("unable to create pubsub auto configuration", err);
        }
    }

    private void registerReplyTopicConsumer(PubsubTopic pubsubTopic) throws Throwable {
        var replyConsumer = new PubsubReplyTopicConsumer(props, meterRegistry, pubsubTopic, subscriberTemplate, mapper, workerFactory, apiFactory);
        replyConsumer.subscribe();
    }

    private void registerRequestTopicConsumer(PubsubTopic pubsubTopic) throws Throwable {
        var requestConsumer = new PubsubRequestTopicConsumer(
                props,
                tracer,
                meterRegistry,
                pubsubTopic,
                subscriberTemplate,
                apiFactory,
                stackTracer,
                postTemplate,
                handlers,
                workerFactory);
        requestConsumer.subscribe();
    }

    private void createTopicAndSubscriptionIfNeeded(PubsubTopic topicDefinition) {
        String topicName = topicDefinition.name().toString();
        if (Objects.isNull(admin.getTopic(topicName))) {
            log.info("created topic: {}", admin.createTopic(topicName).getName());
        }
        if (topicDefinition.withCustomConfiguration()) {
            log.debug("Skip autoconfiguration for topic: {}", topicDefinition.name().toString());
            return;
        }
        String appId = props.CLOUD_APP_ID.get();

        boolean recreateSubscriptions = props.PUBSUB_RECREATE_SUBSCRIPTIONS.get();
        boolean subscriptionExists;
        String subscriptionName = topicDefinition.subscriptionName(props);
        Subscription subscription = admin.getSubscription(subscriptionName);
        subscriptionExists = Objects.nonNull(subscription);
        if (!subscriptionExists || recreateSubscriptions) {
            Topic dlq = getDeadLetterTopic(topicName, appId, admin);
            var subscriptionBuilder = Subscription.newBuilder()
                    .setName(subscriptionName)
                    .setTopic(topicName)
                    .setMessageRetentionDuration(
                            Duration.newBuilder().setSeconds(props.PUBSUB_SUBSCRIPTION_MESSAGE_RETENTION_DURATION.get().getSeconds()))
                    .setEnableExactlyOnceDelivery(props.PUBSUB_SUBSCRIPTION_EXACTLY_ONCE_DELIVERY_ENABLED.get())
                    .setRetainAckedMessages(props.PUBSUB_SUBSCRIPTION_RETAIN_ACKED_MESSAGES.get())
                    .setDeadLetterPolicy(DeadLetterPolicy.newBuilder()
                            .setDeadLetterTopic(dlq.getName())
                            .setMaxDeliveryAttempts(props.PUBSUB_DEADLETTER_DELIVERY_ATTEMPT_MAX.get())
                            .build())
                    .setRetryPolicy(RetryPolicy.newBuilder()
                            .setMinimumBackoff(Duration.newBuilder()
                                    .setSeconds(props.PUBSUB_SUBSCRIPTION_RETRY_BACKOFF_MIN.get().toSeconds())
                                    .build())
                            .setMaximumBackoff(Duration.newBuilder()
                                    .setSeconds(props.PUBSUB_SUBSCRIPTION_RETRY_BACKOFF_MAX.get().toSeconds())
                                    .build())
                            .build())
                    .setEnableMessageOrdering(props.PUBSUB_MESSAGE_ORDERING_ENABLED.get());
            if (topicDefinition.withCustomAckDeadline()) {
                subscriptionBuilder.setAckDeadlineSeconds((int) props.PUBSUB_ACK_DEADLINE.get().toSeconds());
            }
            if (recreateSubscriptions && subscriptionExists) {
                log.info("removing subscription: {}", subscriptionName);
                admin.deleteSubscription(subscriptionName);
            }
            log.info("created subscription: {}", admin.createSubscription(subscriptionBuilder));
        }
        if (subscriptionExists) {
            updateSubscriptionSettingsIfChanged(subscriptionAdminClient, subscription);
        }
    }

    protected Topic getDeadLetterTopic(String topicName, String appId, PubSubAdmin client) {
        int retryAttempts = 0;
        String deadLetterTopic = String.format("%s-%s", topicName, DLQ_POSTFIX);
        String deadLetterSubscription = String.format("%s-%s-%s", topicName, appId, DLQ_POSTFIX);
        Topic dlq = null;
        do {
            try {
                if (Objects.isNull(client.getTopic(deadLetterTopic))) {
                    dlq = client.createTopic(deadLetterTopic);
                } else {
                    dlq = client.getTopic(deadLetterTopic);
                }

                if (Objects.isNull(client.getSubscription(deadLetterSubscription))) {
                    client.createSubscription(Subscription.newBuilder()
                            .setName(deadLetterSubscription)
                            .setTopic(deadLetterTopic)
                            .setExpirationPolicy(ExpirationPolicy.newBuilder().build()));
                }
            } catch (RuntimeException e) {
                log.error("Exception when creating dead letter topic/subscription", e);
                retryAttempts++;
            }
        } while (dlq == null && retryAttempts < MAX_RETRY_DLQ_CREATION_ATTEMPTS);

        if (dlq == null) {
            // Handle the case where the topic creation still failed after retries
            throw new RuntimeException("Failed to create or get the dead letter topic/subscription" +
                    " after multiple attempts.");
        }

        return dlq;
    }

    private void updateSubscriptionSettingsIfChanged(SubscriptionAdminClient adminClient, Subscription subscription) {
        var subscriptionBuilder = Subscription.newBuilder(subscription);
        var fieldMask = FieldMask.newBuilder();
        boolean configurationChanged = false;

        var exactlyOnceDelivery = props.PUBSUB_SUBSCRIPTION_EXACTLY_ONCE_DELIVERY_ENABLED.get();
        if (subscription.getEnableExactlyOnceDelivery() != exactlyOnceDelivery) {
            subscriptionBuilder.setEnableExactlyOnceDelivery(exactlyOnceDelivery);
            fieldMask.addPaths("enable_exactly_once_delivery");
            configurationChanged = true;
            log.debug("{}: enable_exactly_once_delivery {} => {}",
                    subscription.getName(),
                    subscription.getEnableExactlyOnceDelivery(),
                    exactlyOnceDelivery);
        }

        boolean retainAckedMessages = props.PUBSUB_SUBSCRIPTION_RETAIN_ACKED_MESSAGES.get();
        if (subscription.getRetainAckedMessages() != retainAckedMessages) {
            subscriptionBuilder.setRetainAckedMessages(retainAckedMessages);
            fieldMask.addPaths("retain_acked_messages");
            configurationChanged = true;
            log.debug("{}: retain_acked_messages {} => {}",
                    subscription.getName(),
                    subscription.getRetainAckedMessages(),
                    retainAckedMessages);
        }

        long messageRetentionSeconds = props.PUBSUB_SUBSCRIPTION_MESSAGE_RETENTION_DURATION.get().getSeconds();
        if (subscription.getMessageRetentionDuration().getSeconds() != messageRetentionSeconds) {
            Duration newMessageRetention = Duration.newBuilder().setSeconds(messageRetentionSeconds).build();
            subscriptionBuilder.setMessageRetentionDuration(newMessageRetention);
            fieldMask.addPaths("message_retention_duration");
            configurationChanged = true;
            log.debug("{}: message_retention_duration {} => {}",
                    subscription.getName(),
                    subscription.getMessageRetentionDuration().getSeconds(),
                    messageRetentionSeconds);
        }

        var ackDeadlineSeconds = (int) props.PUBSUB_ACK_DEADLINE.get().toSeconds();
        if (subscription.getAckDeadlineSeconds() != ackDeadlineSeconds) {
            subscriptionBuilder.setAckDeadlineSeconds(ackDeadlineSeconds);
            fieldMask.addPaths("ack_deadline_seconds");
            configurationChanged = true;
            log.debug("{}: ack_deadline_seconds {} => {}",
                    subscription.getName(),
                    subscription.getAckDeadlineSeconds(),
                    ackDeadlineSeconds);
        }

        if (configurationChanged) {
            var request = UpdateSubscriptionRequest.newBuilder().setSubscription(subscriptionBuilder).setUpdateMask(fieldMask);

            try {
                Subscription updatedSubscription = adminClient.updateSubscription(request.build());
                log.info("updated subscription: {}", updatedSubscription);
            } catch (ApiException e) {
                log.error("Failed to apply subscription updates", e);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy