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

io.kestra.plugin.mqtt.RealtimeTrigger Maven / Gradle / Ivy

There is a newer version: 0.20.0
Show newest version
package io.kestra.plugin.mqtt;

import io.kestra.core.models.annotations.Example;
import io.kestra.core.models.annotations.Plugin;
import io.kestra.core.models.conditions.ConditionContext;
import io.kestra.core.models.executions.Execution;
import io.kestra.core.models.triggers.*;
import io.kestra.core.runners.RunContext;
import io.kestra.plugin.mqtt.services.Message;
import io.kestra.plugin.mqtt.services.MqttFactory;
import io.kestra.plugin.mqtt.services.MqttInterface;
import io.kestra.plugin.mqtt.services.SerdeType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

@SuperBuilder
@ToString
@EqualsAndHashCode
@Getter
@NoArgsConstructor
@Schema(
    title = "Consume a message in real-time from MQTT topics and create one execution per message.",
    description = "If you would like to consume multiple messages processed within a given time frame and process them in batch, you can use the [io.kestra.plugin.mqtt.Trigger](https://kestra.io/plugins/plugin-mqtt/triggers/io.kestra.plugin.mqtt.trigger) instead."
)
@Plugin(
    examples = {
        @Example(
            title = "Consume a message from MQTT topics in real-time.",
            full = true,
            code = """
                id: mqtt
                namespace: company.team

                tasks:
                  - id: log
                    type: io.kestra.plugin.core.log.Log
                    message: "{{ trigger.payload }}"

                triggers:
                  - id: realtime_trigger
                    type: io.kestra.plugin.mqtt.RealtimeTrigger
                    server: tcp://localhost:1883
                    clientId: kestraProducer
                    topic:
                    - kestra/sensors/cpu
                    - kestra/sensors/mem
                    serdeType: JSON"""
        )
    }
)
public class RealtimeTrigger extends AbstractTrigger implements RealtimeTriggerInterface, TriggerOutput, SubscribeInterface, MqttPropertiesInterface {

    @Builder.Default
    @NotNull
    private AbstractMqttConnection.Version version = AbstractMqttConnection.Version.V5;

    private String server;

    private String clientId;

    private Duration connectionTimeout;

    private Boolean httpsHostnameVerificationEnabled;

    private String authMethod;

    private String username;

    private String password;

    private Object topic;

    private String crt;

    @Builder.Default
    private SerdeType serdeType = SerdeType.JSON;

    @Builder.Default
    private Integer qos = 1;

    @Builder.Default
    @Getter(AccessLevel.NONE)
    private final AtomicBoolean isActive = new AtomicBoolean(true);

    @Builder.Default
    @Getter(AccessLevel.NONE)
    private final CountDownLatch waitForTermination = new CountDownLatch(1);

    @Override
    public Publisher evaluate(ConditionContext conditionContext, TriggerContext context) throws Exception {
        Subscribe task = Subscribe.builder()
            .id(this.id)
            .type(Subscribe.class.getName())
            .version(this.version)
            .server(this.server)
            .clientId(this.clientId)
            .connectionTimeout(this.connectionTimeout)
            .httpsHostnameVerificationEnabled(this.httpsHostnameVerificationEnabled)
            .authMethod(this.authMethod)
            .username(this.username)
            .password(this.password)
            .version(this.version)
            .topic(this.topic)
            .serdeType(this.serdeType)
            .qos(this.qos)
            .build();


        return Flux
            .from(publisher(task, conditionContext.getRunContext()))
            .map(record -> TriggerService.generateRealtimeExecution(this, conditionContext, context, new Output(record)));
    }

    public Publisher publisher(final Subscribe task, final RunContext runContext) throws Exception {
        final MqttInterface connection = MqttFactory.create(runContext, task);

        return Flux.create(emitter -> {
            try {

                final AtomicReference error = new AtomicReference<>();

                // The MQTT client is automatically shutdown if an exception is thrown in the client
                // e.g., while processing a message
                connection.onDisconnected(throwable -> {
                    error.set(throwable);
                    isActive.set(false); // proactively stop consuming
                });

                emitter.onDispose(() -> {
                    try {
                        connection.unsubscribe(runContext, task);
                        connection.close();
                    } catch (Exception e) {
                        runContext.logger().debug("Error while closing connection: " + e.getMessage());
                    } finally {
                        this.waitForTermination.countDown();
                    }
                });

                connection.subscribe(runContext, task, emitter::next);

                busyWait();

                // dispose
                if (error.get() != null) {
                    emitter.error(error.get());
                } else {
                    emitter.complete();
                }
            } catch (Exception e) {
                isActive.set(false);
                emitter.error(e);
            }
        });
    }

    private void busyWait() {
        while (isActive.get()) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                isActive.set(false); // proactively stop consuming
            }
        }
    }

    /**
     * {@inheritDoc}
     **/
    @Override
    public void kill() {
        stop(true);
    }

    /**
     * {@inheritDoc}
     **/
    @Override
    public void stop() {
        stop(false); // must be non-blocking
    }

    private void stop(boolean wait) {
        if (!isActive.compareAndSet(true, false)) {
            return;
        }
        if (wait) {
            try {
                this.waitForTermination.await();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
    }

    @Getter
    @AllArgsConstructor
    public class Output implements io.kestra.core.models.tasks.Output {

        private Integer id;

        private String topic;

        private Integer qos;

        private List properties;

        private Object payload;

        private Boolean retain;

        public Output(Message message) {
            this.id = message.getId();
            this.topic = message.getTopic();
            this.qos = message.getQos();
            this.properties = message.getProperties();
            this.payload = message.getPayload();
            this.retain = message.getRetain();
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy