io.kestra.plugin.mqtt.RealtimeTrigger Maven / Gradle / Ivy
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_realtime_trigger
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