From 9a41c3d0483ae78bdd0434ca306b8270dad23f2d Mon Sep 17 00:00:00 2001
From: Michael Calmer <mc@suse.de>
Date: Fri, 22 Mar 2024 14:47:20 +0100
Subject: [PATCH] unschedule the action chain together with the action

---
 .../rhn/domain/action/ActionChainFactory.java | 32 ++++++++++++++++
 .../rhn/domain/action/ActionFactory.java      |  3 +-
 .../redhat/rhn/taskomatic/TaskoFactory.java   | 10 ++---
 .../rhn/taskomatic/TaskoXmlRpcHandler.java    | 24 +++++++++---
 .../redhat/rhn/taskomatic/TaskomaticApi.java  | 38 +++++++++++++------
 ...c.Manager-4.3-fix-unschedule-action-chains |  2 +
 6 files changed, 86 insertions(+), 23 deletions(-)
 create mode 100644 java/spacewalk-java.changes.mc.Manager-4.3-fix-unschedule-action-chains

diff --git a/java/code/src/com/redhat/rhn/domain/action/ActionChainFactory.java b/java/code/src/com/redhat/rhn/domain/action/ActionChainFactory.java
index a12cbe715fb..c0f8b89462a 100644
--- a/java/code/src/com/redhat/rhn/domain/action/ActionChainFactory.java
+++ b/java/code/src/com/redhat/rhn/domain/action/ActionChainFactory.java
@@ -329,6 +329,22 @@ public static List<ActionChain> getActionChainsByServer(Server server) {
         );
     }
 
+    /**
+     * Get the action chain scheduled for given action
+     * @param action the action
+     * @return action chains or empty when the action is not part of an action chain
+     */
+    public static Optional<ActionChain> getActionChainsByAction(Action action) {
+        return getSession().createQuery(
+                "SELECT DISTINCT ac " +
+                        "FROM ActionChain ac " +
+                        "JOIN ActionChainEntry ace" +
+                        "  ON ace.actionChain = ac " +
+                        "WHERE ace.action = :action", ActionChain.class)
+                .setParameter("action", action)
+                .uniqueResultOptional();
+    }
+
 
     /**
      * Gets the next sort order value.
@@ -450,4 +466,20 @@ public static boolean isActionChainTargettingMinions(final ActionChain actionCha
             new HashMap<String, Object>() { { put("actionchain_id", actionChain.getId()); } }
         ) > 0);
     }
+
+    /**
+     * Return active actions of an action chain. Always the first action of every server
+     * @param actionChainIn the action chain
+     * @return returns a list of actions which are still active
+     */
+    public static List<Action> getActiveActionsForChain(ActionChain actionChainIn) {
+        return getSession().createQuery(
+                "SELECT sa.parentAction " +
+                        "FROM ActionChainEntry entry " +
+                        "JOIN ServerAction sa ON sa.server = entry.server AND sa.parentAction = entry.action " +
+                        "WHERE entry.actionChain = :actionChain " +
+                        "AND entry.sortOrder = 0", Action.class)
+                .setParameter("actionChain", actionChainIn)
+                .list();
+    }
 }
diff --git a/java/code/src/com/redhat/rhn/domain/action/ActionFactory.java b/java/code/src/com/redhat/rhn/domain/action/ActionFactory.java
index ca14b0204fc..c52edd76b23 100644
--- a/java/code/src/com/redhat/rhn/domain/action/ActionFactory.java
+++ b/java/code/src/com/redhat/rhn/domain/action/ActionFactory.java
@@ -857,7 +857,8 @@ public static List<ServerAction> listServerActionsForServer(Server serverIn,
      * @return List of ServerAction objects
      */
     @SuppressWarnings("unchecked")
-    public static List<ServerAction> listServerActionsForServer(Server serverIn, List<ActionStatus> statusList, Date createdDate) {
+    public static List<ServerAction> listServerActionsForServer(Server serverIn, List<ActionStatus> statusList,
+                                                                Date createdDate) {
         Map<String, Object> params = new HashMap<>();
         params.put("server", serverIn);
         params.put("statusList", statusList);
diff --git a/java/code/src/com/redhat/rhn/taskomatic/TaskoFactory.java b/java/code/src/com/redhat/rhn/taskomatic/TaskoFactory.java
index 85d0ffb8021..c26d05db083 100644
--- a/java/code/src/com/redhat/rhn/taskomatic/TaskoFactory.java
+++ b/java/code/src/com/redhat/rhn/taskomatic/TaskoFactory.java
@@ -338,15 +338,15 @@ public static TaskoSchedule lookupScheduleById(Long scheduleId) {
     }
 
     /**
-     * lookup schedule by label
+     * list schedule by label
+     *
      * @param jobLabel schedule label
-     * @return schedule
+     * @return list of schedule
      */
-    public static TaskoSchedule lookupScheduleByLabel(String jobLabel) {
+    public static List<TaskoSchedule> listScheduleByLabel(String jobLabel) {
         Map<String, Object> params = new HashMap<>();
         params.put("job_label", jobLabel);
-        return (TaskoSchedule) singleton.lookupObjectByNamedQuery(
-                                       "TaskoSchedule.lookupByLabel", params);
+        return singleton.listObjectsByNamedQuery("TaskoSchedule.lookupByLabel", params);
     }
 
     /**
diff --git a/java/code/src/com/redhat/rhn/taskomatic/TaskoXmlRpcHandler.java b/java/code/src/com/redhat/rhn/taskomatic/TaskoXmlRpcHandler.java
index 362d72d478b..e43b63b544b 100644
--- a/java/code/src/com/redhat/rhn/taskomatic/TaskoXmlRpcHandler.java
+++ b/java/code/src/com/redhat/rhn/taskomatic/TaskoXmlRpcHandler.java
@@ -87,11 +87,12 @@ public TaskoSchedule lookupScheduleById(Integer scheduleId) {
 
     /**
      * lookup schedule by label
+     *
      * @param jobLabel schedule label
      * @return schedule
      */
-    public TaskoSchedule lookupScheduleByLabel(String jobLabel) {
-        return TaskoFactory.lookupScheduleByLabel(jobLabel);
+    public List<TaskoSchedule> listScheduleByLabel(String jobLabel) {
+        return TaskoFactory.listScheduleByLabel(jobLabel);
     }
 
     /**
@@ -231,7 +232,7 @@ public Integer unscheduleBunch(Integer orgId, String jobLabel) {
         // quartz unschedules job after trigger end time
         // so better handle quartz and schedules separately
         if ((scheduleList.isEmpty()) && (trigger == null)) {
-            log.error("Unscheduling of bunch {}failed: no such job label", jobLabel);
+            log.error("Unscheduling of bunch {} failed: no such job label", jobLabel);
             return 0;
         }
         for (TaskoSchedule schedule : scheduleList) {
@@ -317,8 +318,21 @@ public Date scheduleSingleBunchRun(Integer orgId, String bunchName, Map params,
     public Date scheduleSingleBunchRun(Integer orgId, String bunchName, String jobLabel, Map params, Date start)
         throws NoSuchBunchTaskException, InvalidParamException, SchedulerException {
         TaskoBunch bunch = doBasicCheck(orgId, bunchName, jobLabel);
-        // create schedule
-        TaskoSchedule schedule = new TaskoSchedule(orgId, bunch, jobLabel, params, start, null, null);
+        List<TaskoSchedule> taskoSchedules = TaskoFactory.listScheduleByLabel(jobLabel);
+
+        TaskoSchedule schedule;
+        if (taskoSchedules.isEmpty()) {
+            // create schedule
+            schedule = new TaskoSchedule(orgId, bunch, jobLabel, params, start, null, null);
+        }
+        else {
+            // update existing schedule
+            schedule = taskoSchedules.get(0);
+            schedule.setBunch(bunch);
+            schedule.setDataMap(params);
+            schedule.setActiveFrom(start);
+            schedule.setActiveTill(start);
+        }
         TaskoFactory.save(schedule);
         TaskoFactory.commitTransaction();
         log.info("Schedule created for {}. Creating quartz Job...", jobLabel);
diff --git a/java/code/src/com/redhat/rhn/taskomatic/TaskomaticApi.java b/java/code/src/com/redhat/rhn/taskomatic/TaskomaticApi.java
index 79540be8dea..42942463964 100644
--- a/java/code/src/com/redhat/rhn/taskomatic/TaskomaticApi.java
+++ b/java/code/src/com/redhat/rhn/taskomatic/TaskomaticApi.java
@@ -52,9 +52,9 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 import redstone.xmlrpc.XmlRpcClient;
 import redstone.xmlrpc.XmlRpcException;
@@ -736,20 +736,34 @@ public void scheduleSubscribeChannels(User user, SubscribeChannelsAction action)
     public void deleteScheduledActions(Map<Action, Set<Server>> actionMap)
         throws TaskomaticApiException {
 
-        Stream<Action> actionsToBeUnscheduled = actionMap.entrySet().stream()
-            // select Actions that have no minions besides those in the specified set
-            // (those that have any other minion should NOT be unscheduled!)
-            .filter(e -> e.getKey().getServerActions().stream()
-                    .map(ServerAction::getServer)
-                    .filter(MinionServerUtils::isMinionServer)
-                    .allMatch(s -> e.getValue().contains(s))
-            )
-            .map(Map.Entry::getKey);
-
-        List<String> jobLabels = actionsToBeUnscheduled
+        List<Action> actionsToBeUnscheduled = actionMap.entrySet().stream()
+                // select Actions that have no minions besides those in the specified set
+                // (those that have any other minion should NOT be unscheduled!)
+                .filter(e -> e.getKey().getServerActions().stream()
+                        .map(ServerAction::getServer)
+                        .filter(MinionServerUtils::isMinionServer)
+                        .allMatch(s -> e.getValue().contains(s))
+                )
+                .map(Map.Entry::getKey)
+                .collect(Collectors.toList());
+
+        Set<ActionChain> affectedActionChains = actionsToBeUnscheduled.stream()
+                .map(a -> ActionChainFactory.getActionChainsByAction(a).orElse(null))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        List<String> jobLabels = actionsToBeUnscheduled.stream()
                 .map(a -> MINION_ACTION_JOB_PREFIX + a.getId())
+                .filter(job -> !TaskoFactory.listScheduleByLabel(job).isEmpty())
                 .collect(Collectors.toList());
 
+        affectedActionChains.forEach(ac -> {
+            List<Action> activeActionsForChain = ActionChainFactory.getActiveActionsForChain(ac);
+            if (activeActionsForChain.removeAll(actionsToBeUnscheduled) && activeActionsForChain.isEmpty()) {
+                jobLabels.add(MINION_ACTIONCHAIN_JOB_PREFIX + ac.getId());
+            }
+        });
+
         if (!jobLabels.isEmpty()) {
             LOG.debug("Unscheduling jobs: {}", jobLabels);
             invoke("tasko.unscheduleSatBunches", jobLabels);
diff --git a/java/spacewalk-java.changes.mc.Manager-4.3-fix-unschedule-action-chains b/java/spacewalk-java.changes.mc.Manager-4.3-fix-unschedule-action-chains
new file mode 100644
index 00000000000..a0935171f47
--- /dev/null
+++ b/java/spacewalk-java.changes.mc.Manager-4.3-fix-unschedule-action-chains
@@ -0,0 +1,2 @@
+- when an action is unscheduled which belong to an action chain
+  unschedule the action chain as well (bsc#1221784)
