Repository: kestra-io/kestra
Issue: FlowListener should not filter FlowWithException

### Issue description

Currently, the FlowListener filter any FlowWithException which means consumers have no idea of these flows.

The issue is that a flow can become a FlowWithException after an update, which means the flow definition will be wrond and potentially leads to exception or strange behaviour on the executor.

We should update the FlowListener to send FlowWithException and update all consumes to handle them properly.

Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/core/src/main/java/io/kestra/core/runners/FlowListeners.java b/core/src/main/java/io/kestra/core/runners/FlowListeners.java
index 039a12271d..3f07e51ca5 100644
--- a/core/src/main/java/io/kestra/core/runners/FlowListeners.java
+++ b/core/src/main/java/io/kestra/core/runners/FlowListeners.java
@@ -2,7 +2,6 @@
 
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import io.kestra.core.models.flows.FlowWithException;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
 import io.kestra.core.models.flows.Flow;
@@ -17,7 +16,6 @@
 import java.util.Optional;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
-import java.util.stream.Collectors;
 
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
@@ -26,9 +24,6 @@
 @Singleton
 @Slf4j
 public class FlowListeners implements FlowListenersInterface {
-    private static final ObjectMapper MAPPER = JacksonMapper.ofJson();
-    private static final TypeReference<List<Flow>> TYPE_REFERENCE = new TypeReference<>(){};
-
     private Boolean isStarted = false;
     private final QueueInterface<Flow> flowQueue;
     private final List<Flow> flows;
@@ -42,10 +37,7 @@ public FlowListeners(
         @Named(QueueFactoryInterface.FLOW_NAMED) QueueInterface<Flow> flowQueue
     ) {
         this.flowQueue = flowQueue;
-        this.flows = flowRepository.findAllForAllTenants()
-            .stream()
-            .filter(flow -> !(flow instanceof FlowWithException))
-            .collect(Collectors.toList());
+        this.flows = flowRepository.findAllForAllTenants();
     }
 
     @Override
@@ -150,6 +142,6 @@ public void listen(BiConsumer<Flow, Flow> consumer) {
     @Override
     public List<Flow> flows() {
         // we forced a deep clone to avoid concurrency where instance are changed during iteration (especially scheduler).
-        return MAPPER.readValue(MAPPER.writeValueAsString(this.flows), TYPE_REFERENCE);
+        return new ArrayList<>(this.flows);
     }
 }
diff --git a/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java b/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java
index 2a7dc859f3..184e08f3eb 100644
--- a/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java
+++ b/core/src/main/java/io/kestra/core/schedulers/AbstractScheduler.java
@@ -6,6 +6,7 @@
 import io.kestra.core.models.conditions.ConditionContext;
 import io.kestra.core.models.executions.Execution;
 import io.kestra.core.models.flows.Flow;
+import io.kestra.core.models.flows.FlowWithException;
 import io.kestra.core.models.triggers.AbstractTrigger;
 import io.kestra.core.models.triggers.PollingTriggerInterface;
 import io.kestra.core.models.triggers.Trigger;
@@ -66,7 +67,7 @@ public abstract class AbstractScheduler implements Scheduler {
     @Getter
     private volatile List<FlowWithTrigger> schedulable = new ArrayList<>();
     @Getter
-    private volatile Map<String, FlowWithPollingTriggerNextDate> schedulableNextDate = new HashMap<>();
+    private volatile Map<String, FlowWithPollingTriggerNextDate> schedulableNextDate = new ConcurrentHashMap<>();
 
     @SuppressWarnings("unchecked")
     @Inject
@@ -183,8 +184,8 @@ private synchronized void computeSchedulable(List<Flow> flows) {
 
         this.schedulable = flows
             .stream()
-            .filter(flow -> flow.getTriggers() != null && flow.getTriggers().size() > 0)
-            .filter(flow -> !flow.isDisabled())
+            .filter(flow -> flow.getTriggers() != null && !flow.getTriggers().isEmpty())
+            .filter(flow -> !flow.isDisabled() && !(flow instanceof FlowWithException))
             .flatMap(flow -> flow.getTriggers()
                 .stream()
                 .filter(abstractTrigger -> !abstractTrigger.isDisabled() && abstractTrigger instanceof PollingTriggerInterface)
diff --git a/core/src/main/java/io/kestra/core/services/AbstractFlowTriggerService.java b/core/src/main/java/io/kestra/core/services/AbstractFlowTriggerService.java
index acbc6e1076..0590d6852f 100644
--- a/core/src/main/java/io/kestra/core/services/AbstractFlowTriggerService.java
+++ b/core/src/main/java/io/kestra/core/services/AbstractFlowTriggerService.java
@@ -3,6 +3,7 @@
 import io.kestra.core.models.conditions.types.MultipleCondition;
 import io.kestra.core.models.executions.Execution;
 import io.kestra.core.models.flows.Flow;
+import io.kestra.core.models.flows.FlowWithException;
 import io.kestra.core.models.triggers.AbstractTrigger;
 import io.kestra.core.models.triggers.multipleflows.MultipleConditionStorageInterface;
 import io.kestra.core.models.triggers.multipleflows.MultipleConditionWindow;
@@ -47,7 +48,7 @@ public List<Execution> computeExecutionsFromFlowTriggers(Execution execution, Li
             // prevent recursive flow triggers
             .filter(flow -> flowService.removeUnwanted(flow, execution))
             // ensure flow & triggers are enabled
-            .filter(flow -> !flow.isDisabled())
+            .filter(flow -> !flow.isDisabled() && !(flow instanceof FlowWithException))
             .filter(flow -> flow.getTriggers() != null && !flow.getTriggers().isEmpty())
             // validate flow triggers conditions excluding multiple conditions
             .flatMap(flow -> flowTriggers(flow).map(trigger -> new FlowWithFlowTrigger(flow, trigger)))


Test patch: