generated from kestra-io/plugin-template
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement Realtime Trigger (#377)
- Loading branch information
1 parent
4bb05f1
commit aa48158
Showing
6 changed files
with
299 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
src/main/java/io/kestra/plugin/gcp/pubsub/RealtimeTrigger.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
package io.kestra.plugin.gcp.pubsub; | ||
|
||
import io.kestra.core.models.annotations.Example; | ||
import io.kestra.core.models.annotations.Plugin; | ||
import io.kestra.core.models.annotations.PluginProperty; | ||
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.gcp.pubsub.model.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 org.slf4j.Logger; | ||
import reactor.core.publisher.Flux; | ||
|
||
import java.time.Duration; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
|
||
@SuperBuilder | ||
@ToString | ||
@EqualsAndHashCode | ||
@Getter | ||
@NoArgsConstructor | ||
@Schema( | ||
title = "React and consume messages in a Pub/Sub topic." | ||
) | ||
@Plugin( | ||
examples = { | ||
@Example( | ||
code = { | ||
"topic: test-topic", | ||
"maxRecords: 10" | ||
} | ||
) | ||
} | ||
) | ||
public class RealtimeTrigger extends AbstractTrigger implements RealtimeTriggerInterface, TriggerOutput<Consume.Output>, PubSubConnectionInterface { | ||
|
||
private String projectId; | ||
|
||
private String serviceAccount; | ||
|
||
@Builder.Default | ||
private List<String> scopes = Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"); | ||
|
||
private String topic; | ||
|
||
@Schema( | ||
title = "The Pub/Sub subscription", | ||
description = "The Pub/Sub subscription. It will be created automatically if it didn't exist and 'autoCreateSubscription' is enabled." | ||
) | ||
@PluginProperty(dynamic = true) | ||
private String subscription; | ||
|
||
@Schema( | ||
title = "Whether the Pub/Sub subscription should be created if not exist" | ||
) | ||
@PluginProperty | ||
@Builder.Default | ||
private Boolean autoCreateSubscription = true; | ||
|
||
@Builder.Default | ||
private final Duration interval = Duration.ofSeconds(60); | ||
|
||
@PluginProperty | ||
@Schema(title = "Max number of records, when reached the task will end.") | ||
private Integer maxRecords; | ||
|
||
@PluginProperty | ||
@Schema(title = "Max duration in the Duration ISO format, after that the task will end.") | ||
private Duration maxDuration; | ||
|
||
@Builder.Default | ||
@PluginProperty | ||
@NotNull | ||
@Schema(title = "The serializer/deserializer to use.") | ||
private SerdeType serdeType = SerdeType.STRING; | ||
|
||
@Override | ||
public Publisher<Execution> evaluate(ConditionContext conditionContext, TriggerContext context) throws Exception { | ||
Consume task = Consume.builder() | ||
.topic(this.topic) | ||
.subscription(this.subscription) | ||
.autoCreateSubscription(this.autoCreateSubscription) | ||
.projectId(this.projectId) | ||
.serviceAccount(this.serviceAccount) | ||
.scopes(this.scopes) | ||
.maxRecords(this.maxRecords) | ||
.maxDuration(this.maxDuration) | ||
.serdeType(this.serdeType) | ||
.build(); | ||
|
||
return Flux.from(task.stream(conditionContext.getRunContext())) | ||
.map(message -> TriggerService.generateRealtimeExecution(this, context, message)) | ||
.next(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
src/test/java/io/kestra/plugin/gcp/pubsub/RealtimeTriggerTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package io.kestra.plugin.gcp.pubsub; | ||
|
||
import com.google.common.collect.ImmutableMap; | ||
import io.kestra.core.models.executions.Execution; | ||
import io.kestra.core.queues.QueueFactoryInterface; | ||
import io.kestra.core.queues.QueueInterface; | ||
import io.kestra.core.repositories.LocalFlowRepositoryLoader; | ||
import io.kestra.core.runners.FlowListeners; | ||
import io.kestra.core.runners.RunContextFactory; | ||
import io.kestra.core.runners.Worker; | ||
import io.kestra.core.schedulers.AbstractScheduler; | ||
import io.kestra.core.schedulers.DefaultScheduler; | ||
import io.kestra.core.schedulers.SchedulerTriggerStateInterface; | ||
import io.kestra.core.utils.IdUtils; | ||
import io.kestra.core.utils.TestsUtils; | ||
import io.kestra.plugin.gcp.pubsub.model.Message; | ||
import io.kestra.plugin.gcp.pubsub.model.SerdeType; | ||
import io.micronaut.context.ApplicationContext; | ||
import io.micronaut.context.annotation.Value; | ||
import io.micronaut.test.extensions.junit5.annotation.MicronautTest; | ||
import jakarta.inject.Inject; | ||
import jakarta.inject.Named; | ||
import org.junit.jupiter.api.Test; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.util.Base64; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.concurrent.CountDownLatch; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.concurrent.atomic.AtomicReference; | ||
|
||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.hamcrest.Matchers.is; | ||
|
||
@MicronautTest | ||
class RealtimeTriggerTest { | ||
|
||
@Inject | ||
private ApplicationContext applicationContext; | ||
|
||
@Inject | ||
private SchedulerTriggerStateInterface triggerState; | ||
|
||
@Inject | ||
private FlowListeners flowListenersService; | ||
|
||
@Inject | ||
@Named(QueueFactoryInterface.EXECUTION_NAMED) | ||
private QueueInterface<Execution> executionQueue; | ||
|
||
@Inject | ||
protected LocalFlowRepositoryLoader repositoryLoader; | ||
|
||
@Inject | ||
private RunContextFactory runContextFactory; | ||
|
||
@Value("${kestra.variables.globals.project}") | ||
private String project; | ||
|
||
@Test | ||
void flow() throws Exception { | ||
// mock flow listeners | ||
CountDownLatch queueCount = new CountDownLatch(1); | ||
|
||
// scheduler | ||
try ( | ||
Worker worker = applicationContext.createBean(Worker.class, IdUtils.create(), 8, null); | ||
AbstractScheduler scheduler = new DefaultScheduler( | ||
this.applicationContext, | ||
this.flowListenersService, | ||
this.triggerState | ||
); | ||
) { | ||
AtomicReference<Execution> last = new AtomicReference<>(); | ||
|
||
// wait for execution | ||
executionQueue.receive(RealtimeTriggerTest.class, execution -> { | ||
last.set(execution.getLeft()); | ||
|
||
queueCount.countDown(); | ||
assertThat(execution.getLeft().getFlowId(), is("realtime-listen")); | ||
}); | ||
|
||
|
||
worker.run(); | ||
scheduler.run(); | ||
|
||
repositoryLoader.load(Objects.requireNonNull(RealtimeTriggerTest.class.getClassLoader().getResource("flows/pubsub/realtime.yaml"))); | ||
|
||
// publish message to trigger the flow | ||
Publish task = Publish.builder() | ||
.id(Publish.class.getSimpleName()) | ||
.type(Publish.class.getName()) | ||
.topic("test-topic") | ||
.projectId(this.project) | ||
.from( | ||
List.of( | ||
Message.builder().data("Hello World").build() | ||
) | ||
) | ||
.build(); | ||
task.run(TestsUtils.mockRunContext(runContextFactory, task, ImmutableMap.of())); | ||
|
||
queueCount.await(1, TimeUnit.MINUTES); | ||
|
||
Map<String, Object> variables = last.get().getTrigger().getVariables(); | ||
assertThat(new String(Base64.getDecoder().decode((String) variables.get("data")), StandardCharsets.UTF_8), is("Hello World")); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
id: realtime-listen | ||
namespace: io.kestra.tests | ||
|
||
triggers: | ||
- id: watch | ||
type: io.kestra.plugin.gcp.pubsub.RealtimeTrigger | ||
projectId: "{{globals.project}}" | ||
topic: test-topic | ||
subscription: test-subscription | ||
|
||
tasks: | ||
- id: end | ||
type: io.kestra.core.tasks.debugs.Return | ||
format: "{{task.id}} > {{taskrun.startDate}}" |