proud-parselmouth commented on code in PR #3044:
URL: https://github.com/apache/helix/pull/3044#discussion_r2203989240


##########
helix-core/src/main/java/org/apache/helix/HelixAdmin.java:
##########
@@ -422,6 +422,16 @@ void autoEnableMaintenanceMode(String clusterName, boolean 
enabled, String reaso
   void manuallyEnableMaintenanceMode(String clusterName, boolean enabled, 
String reason,
       Map<String, String> customFields);
 
+  /**
+   * Enable maintenance mode via automation systems (like HelixACM). To be 
called by automation services.
+   * @param clusterName
+   * @param enabled
+   * @param reason
+   * @param customFields user-specified KV mappings to be stored in the ZNode
+   */
+  void automationEnableMaintenanceMode(String clusterName, boolean enabled, 
String reason,

Review Comment:
   There are already 3 methods which similar name, `enableMM`, `autoEnableMM`, 
`manuallyEnableMM` and now `automationEnableMaintenanceMode`. 
   I have multiple queries here
   1. Is there a reason to not why we are not overloading the new method with 
the existing name `autoEnableMM`
   2. IMO, there should only be one method `enableMM` with different triggering 
entities. Should we create an issue in apache helix as todo for this?



##########
helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixAdmin.java:
##########
@@ -1201,23 +1219,73 @@ private void processMaintenanceMode(String clusterName, 
final boolean enabled,
         triggeringEntity == MaintenanceSignal.TriggeringEntity.CONTROLLER ? 
"automatically"
             : "manually", enabled ? "enters" : "exits", reason == null ? 
"NULL" : reason);
     final long currentTime = System.currentTimeMillis();
+
+    MaintenanceSignal maintenanceSignal = 
accessor.getProperty(keyBuilder.maintenance());
     if (!enabled) {
-      // Exit maintenance mode
-      accessor.removeProperty(keyBuilder.maintenance());
+      // Exit maintenance mode for this specific triggering entity
+
+      if (maintenanceSignal != null) {
+        // If a specific actor is exiting maintenance mode
+        boolean removed = 
maintenanceSignal.removeMaintenanceReason(triggeringEntity);
+
+        if (removed) {
+          // If there are still reasons for maintenance mode, update the ZNode
+          if (maintenanceSignal.getRecord().getListField("reasons") != null
+              && 
!maintenanceSignal.getRecord().getListField("reasons").isEmpty()) {
+            if (!accessor.setProperty(keyBuilder.maintenance(), 
maintenanceSignal)) {
+              throw new HelixException("Failed to update maintenance signal!");
+            }
+          } else {
+            // If this was the last reason, remove the maintenance ZNode 
entirely
+            accessor.removeProperty(keyBuilder.maintenance());
+          }
+        } else {
+          // Case where triggering entity doesn't have an entry
+          // Note: CONTROLLER/AUTOMATION is strict no-op, USER can do 
administrative override
+          if (triggeringEntity == MaintenanceSignal.TriggeringEntity.USER) {
+            // USER has special privilege to force exit maintenance mode as 
administrative override
+            logger.info("USER administrative override: forcefully exiting 
maintenance mode for cluster {}", clusterName);
+            accessor.removeProperty(keyBuilder.maintenance());
+          } else {
+            // CONTROLLER/AUTOMATION: strict no-op if their entry not found
+            logger.info("Entity {} doesn't have a maintenance reason entry, 
exit request ignored", triggeringEntity);
+          }
+        }
+      } else {

Review Comment:
   This else shouldn't be needed, do an early check after `if(!enabled)` and 
exit from the method if maintenance signal is null



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    LOG.debug("Before addition: Reasons list contains {} entries", 
reasons.size());
+
+    boolean found = false;
+    for (Map<String, String> entry : reasons) {
+      if 
(triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        entry.put(PauseSignalProperty.REASON.name(), reason);
+        entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+        found = true;
+        LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
+        break;
+      }
+    }
+
+    if (!found) {
+      Map<String, String> newEntry = new HashMap<>();
+      newEntry.put(PauseSignalProperty.REASON.name(), reason);
+      newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+      newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), 
triggerEntityStr);
+      reasons.add(newEntry);
+      LOG.debug("Added new entry for entity: {}", triggeringEntity);
+    }
+
+    updateReasonsListField(reasons);
+    LOG.debug("After addition: Reasons list contains {} entries", 
reasons.size());
+  }
+
+  /**
+   * Helper method to update the ZNRecord with the current reasons list.
+   * Each reason is stored as a single JSON string in the list.
+   *
+   * @param reasons The list of reason maps to store
+   */
+  private void updateReasonsListField(List<Map<String, String>> reasons) {
+    List<String> reasonsList = new ArrayList<>();
+
+    for (Map<String, String> entry : reasons) {
+      String jsonString = convertMapToJsonString(entry);
+      if (!jsonString.isEmpty()) {
+        reasonsList.add(jsonString);
+      }
+    }
+
+    _record.setListField(REASONS_LIST_FIELD, reasonsList);
+  }
+
+  /**
+   * Convert a map to a JSON-style string
+   */
+  private String convertMapToJsonString(Map<String, String> map) {
+    try {
+      return new ObjectMapper().writeValueAsString(map);
+    } catch (IOException e) {
+      LOG.warn("Failed to convert map to JSON string: {}", e.getMessage());
+      return "";
+    }
+  }
+
+  /**
+   * Get all maintenance reasons currently active.
+   *
+   * @return List of maintenance reasons as maps
+   */
+  public List<Map<String, String>> getMaintenanceReasons() {
+    List<Map<String, String>> reasons = new ArrayList<>();
+    List<String> reasonsList = _record.getListField(REASONS_LIST_FIELD);
+
+    if (reasonsList != null && !reasonsList.isEmpty()) {
+      for (String entryStr : reasonsList) {
+        Map<String, String> entry = parseJsonStyleEntry(entryStr);
+        if (!entry.isEmpty()) {
+          reasons.add(entry);
+        }
+      }
+    }
+
+    return reasons;
+  }
+
+  /**
+   * Parse an entry string in JSON format into a map
+   */
+  private Map<String, String> parseJsonStyleEntry(String entryStr) {
+    Map<String, String> map = new HashMap<>();
+    try {
+        return new ObjectMapper().readValue(entryStr,
+            TypeFactory.defaultInstance().constructMapType(HashMap.class, 
String.class, String.class));
+      } catch (IOException e) {
+        LOG.warn("Failed to parse JSON entry: {}, error: {}", entryStr, 
e.getMessage());
+      }
+    return map;
+  }
+
+  /**
+   * Remove a maintenance reason by triggering entity.
+   *
+   * @param triggeringEntity The entity whose reason should be removed
+   * @return true if a reason was removed, false otherwise
+   */
+  public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
+    LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+
+    boolean entityExists = false;
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (triggeringEntity.name().equals(entryEntity)) {
+        entityExists = true;
+        break;
+      }
+    }
+
+    if (!entityExists) {
+      LOG.info("Entity {} doesn't have a maintenance reason entry, ignoring 
exit request", triggeringEntity);
+      return false;
+    }
+
+    int originalSize = reasons.size();
+    LOG.debug("Before removal: Reasons list contains {} entries", 
reasons.size());
+
+    List<Map<String, String>> updatedReasons = new ArrayList<>();
+    String targetEntity = triggeringEntity.name();
+
+    // Only keep reasons that don't match the triggering entity
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (!targetEntity.equals(entryEntity)) {
+        updatedReasons.add(entry);
+      } else {
+        LOG.debug("Removing entry with reason: {} for entity: {}",
+            entry.get(PauseSignalProperty.REASON.name()), entryEntity);
+      }
+    }
+
+    boolean removed = updatedReasons.size() < originalSize;
+    LOG.debug("After removal: Reasons list contains {} entries", 
updatedReasons.size());
+
+    if (removed) {
+      updateReasonsListField(updatedReasons);
+
+      // Update the simpleFields with the most recent reason (for backward 
compatibility)
+      if (!updatedReasons.isEmpty()) {
+        // Sort by timestamp in descending order to get the most recent
+        updatedReasons.sort((r1, r2) -> {
+          long t1 = 
Long.parseLong(r1.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          long t2 = 
Long.parseLong(r2.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          return Long.compare(t2, t1);
+        });
+
+        Map<String, String> mostRecent = updatedReasons.get(0);
+        String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
+        long newTimestamp = 
Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+        TriggeringEntity newEntity = TriggeringEntity.valueOf(
+            mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
+
+        LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: 
{}",
+            newReason, newEntity, newTimestamp);
+
+        setReason(newReason);
+        setTimestamp(newTimestamp);
+        setTriggeringEntity(newEntity);
+      }
+    } else {
+      LOG.info("No matching maintenance reason found for entity: {}", 
triggeringEntity);
+    }
+
+    return removed;
+  }
+
+  /**
+   * Check if there are any active maintenance reasons.
+   *
+   * @return true if there are any reasons for maintenance, false otherwise
+   */
+  public boolean hasMaintenanceReasons() {

Review Comment:
   Do we need this method?



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    LOG.debug("Before addition: Reasons list contains {} entries", 
reasons.size());
+
+    boolean found = false;
+    for (Map<String, String> entry : reasons) {
+      if 
(triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        entry.put(PauseSignalProperty.REASON.name(), reason);
+        entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+        found = true;
+        LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
+        break;
+      }
+    }
+
+    if (!found) {
+      Map<String, String> newEntry = new HashMap<>();
+      newEntry.put(PauseSignalProperty.REASON.name(), reason);
+      newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+      newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), 
triggerEntityStr);
+      reasons.add(newEntry);
+      LOG.debug("Added new entry for entity: {}", triggeringEntity);
+    }
+
+    updateReasonsListField(reasons);
+    LOG.debug("After addition: Reasons list contains {} entries", 
reasons.size());
+  }
+
+  /**
+   * Helper method to update the ZNRecord with the current reasons list.
+   * Each reason is stored as a single JSON string in the list.
+   *
+   * @param reasons The list of reason maps to store
+   */
+  private void updateReasonsListField(List<Map<String, String>> reasons) {
+    List<String> reasonsList = new ArrayList<>();
+
+    for (Map<String, String> entry : reasons) {
+      String jsonString = convertMapToJsonString(entry);
+      if (!jsonString.isEmpty()) {
+        reasonsList.add(jsonString);
+      }
+    }
+
+    _record.setListField(REASONS_LIST_FIELD, reasonsList);
+  }
+
+  /**
+   * Convert a map to a JSON-style string
+   */
+  private String convertMapToJsonString(Map<String, String> map) {
+    try {
+      return new ObjectMapper().writeValueAsString(map);
+    } catch (IOException e) {
+      LOG.warn("Failed to convert map to JSON string: {}", e.getMessage());
+      return "";
+    }
+  }
+
+  /**
+   * Get all maintenance reasons currently active.
+   *
+   * @return List of maintenance reasons as maps
+   */
+  public List<Map<String, String>> getMaintenanceReasons() {
+    List<Map<String, String>> reasons = new ArrayList<>();
+    List<String> reasonsList = _record.getListField(REASONS_LIST_FIELD);
+
+    if (reasonsList != null && !reasonsList.isEmpty()) {
+      for (String entryStr : reasonsList) {
+        Map<String, String> entry = parseJsonStyleEntry(entryStr);
+        if (!entry.isEmpty()) {
+          reasons.add(entry);
+        }
+      }
+    }
+
+    return reasons;
+  }
+
+  /**
+   * Parse an entry string in JSON format into a map
+   */
+  private Map<String, String> parseJsonStyleEntry(String entryStr) {
+    Map<String, String> map = new HashMap<>();
+    try {
+        return new ObjectMapper().readValue(entryStr,
+            TypeFactory.defaultInstance().constructMapType(HashMap.class, 
String.class, String.class));
+      } catch (IOException e) {
+        LOG.warn("Failed to parse JSON entry: {}, error: {}", entryStr, 
e.getMessage());
+      }
+    return map;
+  }
+
+  /**
+   * Remove a maintenance reason by triggering entity.
+   *
+   * @param triggeringEntity The entity whose reason should be removed
+   * @return true if a reason was removed, false otherwise
+   */
+  public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
+    LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+
+    boolean entityExists = false;
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (triggeringEntity.name().equals(entryEntity)) {
+        entityExists = true;
+        break;
+      }
+    }
+
+    if (!entityExists) {
+      LOG.info("Entity {} doesn't have a maintenance reason entry, ignoring 
exit request", triggeringEntity);
+      return false;
+    }
+
+    int originalSize = reasons.size();
+    LOG.debug("Before removal: Reasons list contains {} entries", 
reasons.size());
+
+    List<Map<String, String>> updatedReasons = new ArrayList<>();
+    String targetEntity = triggeringEntity.name();
+
+    // Only keep reasons that don't match the triggering entity
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (!targetEntity.equals(entryEntity)) {
+        updatedReasons.add(entry);
+      } else {
+        LOG.debug("Removing entry with reason: {} for entity: {}",
+            entry.get(PauseSignalProperty.REASON.name()), entryEntity);
+      }
+    }
+
+    boolean removed = updatedReasons.size() < originalSize;
+    LOG.debug("After removal: Reasons list contains {} entries", 
updatedReasons.size());
+
+    if (removed) {
+      updateReasonsListField(updatedReasons);
+
+      // Update the simpleFields with the most recent reason (for backward 
compatibility)
+      if (!updatedReasons.isEmpty()) {
+        // Sort by timestamp in descending order to get the most recent
+        updatedReasons.sort((r1, r2) -> {
+          long t1 = 
Long.parseLong(r1.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          long t2 = 
Long.parseLong(r2.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          return Long.compare(t2, t1);
+        });
+
+        Map<String, String> mostRecent = updatedReasons.get(0);
+        String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
+        long newTimestamp = 
Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+        TriggeringEntity newEntity = TriggeringEntity.valueOf(
+            mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
+
+        LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: 
{}",
+            newReason, newEntity, newTimestamp);
+
+        setReason(newReason);
+        setTimestamp(newTimestamp);
+        setTriggeringEntity(newEntity);
+      }
+    } else {
+      LOG.info("No matching maintenance reason found for entity: {}", 
triggeringEntity);
+    }
+
+    return removed;
+  }
+
+  /**
+   * Check if there are any active maintenance reasons.
+   *
+   * @return true if there are any reasons for maintenance, false otherwise
+   */
+  public boolean hasMaintenanceReasons() {
+    return !getMaintenanceReasons().isEmpty();
+  }
+
+  /**
+   * Checks if there is a maintenance reason from a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to check
+   * @return true if there is a maintenance reason from this entity
+   */
+  public boolean hasMaintenanceReason(TriggeringEntity triggeringEntity) {
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    for (Map<String, String> entry : reasons) {
+      if 
(triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Gets the maintenance reason details for a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to get reason details for
+   * @return Map containing reason details, or null if not found
+   */
+  public Map<String, String> getMaintenanceReasonDetails(TriggeringEntity 
triggeringEntity) {
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    for (Map<String, String> entry : reasons) {
+      if 
(triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        return entry;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Gets the number of active maintenance reasons.
+   *
+   * @return The count of active maintenance reasons
+   */
+  public int getMaintenanceReasonsCount() {
+    return getMaintenanceReasons().size();
+  }
+
+  /**
+   * Gets the maintenance reason from a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to get reason for
+   * @return The reason string, or null if not found
+   */
+  public String getMaintenanceReason(TriggeringEntity triggeringEntity) {
+    Map<String, String> details = 
getMaintenanceReasonDetails(triggeringEntity);
+    return details != null ? details.get(PauseSignalProperty.REASON.name()) : 
null;
+  }
+
+  /**
+   * Reconcile legacy data from simpleFields into listFields.reasons if it's 
missing.
+   * This preserves maintenance data written by old USER clients that only set 
simpleFields.
+   *
+   * NOTE: Only reconciles USER data, as:
+   * - CONTROLLER is part of core Helix system and should use proper APIs
+   * - AUTOMATION is new and has no legacy clients
+   * - Only USER entities represent external legacy clients that may wipe data
+   */
+  public void reconcileLegacyData() {
+    // Check if simpleFields exist but corresponding listFields entry is 
missing
+    String simpleReason = getReason();
+    TriggeringEntity simpleEntity = getTriggeringEntity();
+    long simpleTimestamp = getTimestamp();
+
+    // Only reconcile USER data from legacy clients
+    // CONTROLLER and AUTOMATION should not have legacy data loss scenarios
+    if (simpleReason != null && !simpleReason.isEmpty() && simpleEntity == 
TriggeringEntity.USER

Review Comment:
   This might be more readable, if we return early like this
   ```
   reasons = getMaintaneanceReasons()
   if (simpleReason == null || simpleReason.isEmpty() || filterReasons(reasons, 
TriggeringEntity.USER).size() > 0){
      return
   }
   ... rest of the logic.
   ```



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    LOG.debug("Before addition: Reasons list contains {} entries", 
reasons.size());
+
+    boolean found = false;
+    for (Map<String, String> entry : reasons) {
+      if 
(triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        entry.put(PauseSignalProperty.REASON.name(), reason);
+        entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+        found = true;
+        LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
+        break;
+      }
+    }
+
+    if (!found) {
+      Map<String, String> newEntry = new HashMap<>();
+      newEntry.put(PauseSignalProperty.REASON.name(), reason);
+      newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+      newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), 
triggerEntityStr);
+      reasons.add(newEntry);
+      LOG.debug("Added new entry for entity: {}", triggeringEntity);
+    }
+
+    updateReasonsListField(reasons);
+    LOG.debug("After addition: Reasons list contains {} entries", 
reasons.size());
+  }
+
+  /**
+   * Helper method to update the ZNRecord with the current reasons list.
+   * Each reason is stored as a single JSON string in the list.
+   *
+   * @param reasons The list of reason maps to store
+   */
+  private void updateReasonsListField(List<Map<String, String>> reasons) {
+    List<String> reasonsList = new ArrayList<>();
+
+    for (Map<String, String> entry : reasons) {
+      String jsonString = convertMapToJsonString(entry);
+      if (!jsonString.isEmpty()) {
+        reasonsList.add(jsonString);
+      }
+    }
+
+    _record.setListField(REASONS_LIST_FIELD, reasonsList);
+  }
+
+  /**
+   * Convert a map to a JSON-style string
+   */
+  private String convertMapToJsonString(Map<String, String> map) {
+    try {
+      return new ObjectMapper().writeValueAsString(map);
+    } catch (IOException e) {
+      LOG.warn("Failed to convert map to JSON string: {}", e.getMessage());
+      return "";
+    }
+  }
+
+  /**
+   * Get all maintenance reasons currently active.
+   *
+   * @return List of maintenance reasons as maps
+   */
+  public List<Map<String, String>> getMaintenanceReasons() {
+    List<Map<String, String>> reasons = new ArrayList<>();
+    List<String> reasonsList = _record.getListField(REASONS_LIST_FIELD);
+
+    if (reasonsList != null && !reasonsList.isEmpty()) {
+      for (String entryStr : reasonsList) {
+        Map<String, String> entry = parseJsonStyleEntry(entryStr);
+        if (!entry.isEmpty()) {
+          reasons.add(entry);
+        }
+      }
+    }
+
+    return reasons;
+  }
+
+  /**
+   * Parse an entry string in JSON format into a map
+   */
+  private Map<String, String> parseJsonStyleEntry(String entryStr) {
+    Map<String, String> map = new HashMap<>();
+    try {
+        return new ObjectMapper().readValue(entryStr,
+            TypeFactory.defaultInstance().constructMapType(HashMap.class, 
String.class, String.class));
+      } catch (IOException e) {
+        LOG.warn("Failed to parse JSON entry: {}, error: {}", entryStr, 
e.getMessage());
+      }
+    return map;
+  }
+
+  /**
+   * Remove a maintenance reason by triggering entity.
+   *
+   * @param triggeringEntity The entity whose reason should be removed
+   * @return true if a reason was removed, false otherwise
+   */
+  public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
+    LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);

Review Comment:
   This method needs to be refactored.
   1.  Get maintenance `reasons = getMaintenanceReasons()`
   2. Get filtered reasons `filteredReasons = filterReasons(reasons, null, 
triggeringEntity)`. Write a method that would take includeEntities list and 
excludeEntitiesList
   3. Return `false` early, if ` !filteredReasons.size().equals(reasons.size())`
   4. In the list fields `reasons` we are always adding the reasons at the end, 
hence the above array `filteredReasons` should aways be sorted
   5. Always Set/Reset the simpleFields if filteredReasons.size() != 0
   6. Return true.



##########
helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixAdmin.java:
##########
@@ -948,8 +948,18 @@ public void enableMaintenanceMode(String clusterName, 
boolean enabled) {
   public boolean isInMaintenanceMode(String clusterName) {
     HelixDataAccessor accessor = new ZKHelixDataAccessor(clusterName, 
_baseDataAccessor);
     PropertyKey.Builder keyBuilder = accessor.keyBuilder();
-    return accessor.getBaseDataAccessor()
-        .exists(keyBuilder.maintenance().getPath(), AccessOption.PERSISTENT);
+
+    MaintenanceSignal signal = accessor.getProperty(keyBuilder.maintenance());
+
+    if (signal == null) {
+      return false;
+    }
+
+    // The cluster is in maintenance mode if the maintenance signal ZNode 
exists
+    // This includes cases where old clients have wiped listField data but 
simpleFields remain
+    // cluster should remain in maintenance mode as long as ZNode exists
+    return signal.hasMaintenanceReasons() ||

Review Comment:
   Should we remove the check for the empty string, as this may break backward 
compatibility?



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    LOG.debug("Before addition: Reasons list contains {} entries", 
reasons.size());
+
+    boolean found = false;
+    for (Map<String, String> entry : reasons) {
+      if 
(triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        entry.put(PauseSignalProperty.REASON.name(), reason);
+        entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+        found = true;
+        LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
+        break;
+      }
+    }
+
+    if (!found) {
+      Map<String, String> newEntry = new HashMap<>();
+      newEntry.put(PauseSignalProperty.REASON.name(), reason);
+      newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+      newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), 
triggerEntityStr);
+      reasons.add(newEntry);
+      LOG.debug("Added new entry for entity: {}", triggeringEntity);
+    }
+
+    updateReasonsListField(reasons);
+    LOG.debug("After addition: Reasons list contains {} entries", 
reasons.size());
+  }
+
+  /**
+   * Helper method to update the ZNRecord with the current reasons list.
+   * Each reason is stored as a single JSON string in the list.
+   *
+   * @param reasons The list of reason maps to store
+   */
+  private void updateReasonsListField(List<Map<String, String>> reasons) {
+    List<String> reasonsList = new ArrayList<>();
+
+    for (Map<String, String> entry : reasons) {
+      String jsonString = convertMapToJsonString(entry);
+      if (!jsonString.isEmpty()) {
+        reasonsList.add(jsonString);
+      }
+    }
+
+    _record.setListField(REASONS_LIST_FIELD, reasonsList);
+  }
+
+  /**
+   * Convert a map to a JSON-style string
+   */
+  private String convertMapToJsonString(Map<String, String> map) {
+    try {
+      return new ObjectMapper().writeValueAsString(map);
+    } catch (IOException e) {
+      LOG.warn("Failed to convert map to JSON string: {}", e.getMessage());
+      return "";
+    }
+  }
+
+  /**
+   * Get all maintenance reasons currently active.
+   *
+   * @return List of maintenance reasons as maps
+   */
+  public List<Map<String, String>> getMaintenanceReasons() {
+    List<Map<String, String>> reasons = new ArrayList<>();
+    List<String> reasonsList = _record.getListField(REASONS_LIST_FIELD);
+
+    if (reasonsList != null && !reasonsList.isEmpty()) {
+      for (String entryStr : reasonsList) {
+        Map<String, String> entry = parseJsonStyleEntry(entryStr);
+        if (!entry.isEmpty()) {
+          reasons.add(entry);
+        }
+      }
+    }
+
+    return reasons;
+  }
+
+  /**
+   * Parse an entry string in JSON format into a map
+   */
+  private Map<String, String> parseJsonStyleEntry(String entryStr) {
+    Map<String, String> map = new HashMap<>();
+    try {
+        return new ObjectMapper().readValue(entryStr,
+            TypeFactory.defaultInstance().constructMapType(HashMap.class, 
String.class, String.class));
+      } catch (IOException e) {
+        LOG.warn("Failed to parse JSON entry: {}, error: {}", entryStr, 
e.getMessage());
+      }
+    return map;
+  }
+
+  /**
+   * Remove a maintenance reason by triggering entity.
+   *
+   * @param triggeringEntity The entity whose reason should be removed
+   * @return true if a reason was removed, false otherwise
+   */
+  public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
+    LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+
+    boolean entityExists = false;
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (triggeringEntity.name().equals(entryEntity)) {
+        entityExists = true;
+        break;
+      }
+    }
+
+    if (!entityExists) {
+      LOG.info("Entity {} doesn't have a maintenance reason entry, ignoring 
exit request", triggeringEntity);
+      return false;
+    }
+
+    int originalSize = reasons.size();
+    LOG.debug("Before removal: Reasons list contains {} entries", 
reasons.size());
+
+    List<Map<String, String>> updatedReasons = new ArrayList<>();
+    String targetEntity = triggeringEntity.name();
+
+    // Only keep reasons that don't match the triggering entity
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (!targetEntity.equals(entryEntity)) {
+        updatedReasons.add(entry);
+      } else {
+        LOG.debug("Removing entry with reason: {} for entity: {}",
+            entry.get(PauseSignalProperty.REASON.name()), entryEntity);
+      }
+    }
+
+    boolean removed = updatedReasons.size() < originalSize;
+    LOG.debug("After removal: Reasons list contains {} entries", 
updatedReasons.size());
+
+    if (removed) {
+      updateReasonsListField(updatedReasons);
+
+      // Update the simpleFields with the most recent reason (for backward 
compatibility)
+      if (!updatedReasons.isEmpty()) {
+        // Sort by timestamp in descending order to get the most recent
+        updatedReasons.sort((r1, r2) -> {
+          long t1 = 
Long.parseLong(r1.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          long t2 = 
Long.parseLong(r2.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          return Long.compare(t2, t1);
+        });
+
+        Map<String, String> mostRecent = updatedReasons.get(0);
+        String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
+        long newTimestamp = 
Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+        TriggeringEntity newEntity = TriggeringEntity.valueOf(
+            mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
+
+        LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: 
{}",
+            newReason, newEntity, newTimestamp);
+
+        setReason(newReason);
+        setTimestamp(newTimestamp);
+        setTriggeringEntity(newEntity);
+      }
+    } else {
+      LOG.info("No matching maintenance reason found for entity: {}", 
triggeringEntity);
+    }
+
+    return removed;
+  }
+
+  /**
+   * Check if there are any active maintenance reasons.
+   *
+   * @return true if there are any reasons for maintenance, false otherwise
+   */
+  public boolean hasMaintenanceReasons() {
+    return !getMaintenanceReasons().isEmpty();
+  }
+
+  /**
+   * Checks if there is a maintenance reason from a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to check
+   * @return true if there is a maintenance reason from this entity
+   */
+  public boolean hasMaintenanceReason(TriggeringEntity triggeringEntity) {
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    for (Map<String, String> entry : reasons) {
+      if 
(triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Gets the maintenance reason details for a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to get reason details for
+   * @return Map containing reason details, or null if not found
+   */
+  public Map<String, String> getMaintenanceReasonDetails(TriggeringEntity 
triggeringEntity) {

Review Comment:
   Seems similar to `filterReasons`, why is it public?



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();

Review Comment:
   Here you filterOut the reasons which for the given `triggeringEntity` and 
then always add the reason as a new reason at the end of the reasons list



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    LOG.debug("Before addition: Reasons list contains {} entries", 
reasons.size());
+
+    boolean found = false;
+    for (Map<String, String> entry : reasons) {
+      if 
(triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        entry.put(PauseSignalProperty.REASON.name(), reason);
+        entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+        found = true;
+        LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
+        break;
+      }
+    }
+
+    if (!found) {
+      Map<String, String> newEntry = new HashMap<>();
+      newEntry.put(PauseSignalProperty.REASON.name(), reason);
+      newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+      newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), 
triggerEntityStr);
+      reasons.add(newEntry);
+      LOG.debug("Added new entry for entity: {}", triggeringEntity);
+    }
+
+    updateReasonsListField(reasons);
+    LOG.debug("After addition: Reasons list contains {} entries", 
reasons.size());
+  }
+
+  /**
+   * Helper method to update the ZNRecord with the current reasons list.
+   * Each reason is stored as a single JSON string in the list.
+   *
+   * @param reasons The list of reason maps to store
+   */
+  private void updateReasonsListField(List<Map<String, String>> reasons) {
+    List<String> reasonsList = new ArrayList<>();
+
+    for (Map<String, String> entry : reasons) {
+      String jsonString = convertMapToJsonString(entry);
+      if (!jsonString.isEmpty()) {
+        reasonsList.add(jsonString);
+      }
+    }
+
+    _record.setListField(REASONS_LIST_FIELD, reasonsList);
+  }
+
+  /**
+   * Convert a map to a JSON-style string
+   */
+  private String convertMapToJsonString(Map<String, String> map) {
+    try {
+      return new ObjectMapper().writeValueAsString(map);
+    } catch (IOException e) {
+      LOG.warn("Failed to convert map to JSON string: {}", e.getMessage());
+      return "";
+    }
+  }
+
+  /**
+   * Get all maintenance reasons currently active.
+   *
+   * @return List of maintenance reasons as maps
+   */
+  public List<Map<String, String>> getMaintenanceReasons() {
+    List<Map<String, String>> reasons = new ArrayList<>();
+    List<String> reasonsList = _record.getListField(REASONS_LIST_FIELD);
+
+    if (reasonsList != null && !reasonsList.isEmpty()) {
+      for (String entryStr : reasonsList) {
+        Map<String, String> entry = parseJsonStyleEntry(entryStr);
+        if (!entry.isEmpty()) {
+          reasons.add(entry);
+        }
+      }
+    }
+
+    return reasons;
+  }
+
+  /**
+   * Parse an entry string in JSON format into a map
+   */
+  private Map<String, String> parseJsonStyleEntry(String entryStr) {
+    Map<String, String> map = new HashMap<>();
+    try {
+        return new ObjectMapper().readValue(entryStr,
+            TypeFactory.defaultInstance().constructMapType(HashMap.class, 
String.class, String.class));
+      } catch (IOException e) {
+        LOG.warn("Failed to parse JSON entry: {}, error: {}", entryStr, 
e.getMessage());
+      }
+    return map;
+  }
+
+  /**
+   * Remove a maintenance reason by triggering entity.
+   *
+   * @param triggeringEntity The entity whose reason should be removed
+   * @return true if a reason was removed, false otherwise
+   */
+  public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
+    LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+
+    boolean entityExists = false;
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (triggeringEntity.name().equals(entryEntity)) {
+        entityExists = true;
+        break;
+      }
+    }
+
+    if (!entityExists) {
+      LOG.info("Entity {} doesn't have a maintenance reason entry, ignoring 
exit request", triggeringEntity);
+      return false;
+    }
+
+    int originalSize = reasons.size();
+    LOG.debug("Before removal: Reasons list contains {} entries", 
reasons.size());
+
+    List<Map<String, String>> updatedReasons = new ArrayList<>();
+    String targetEntity = triggeringEntity.name();
+
+    // Only keep reasons that don't match the triggering entity
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (!targetEntity.equals(entryEntity)) {
+        updatedReasons.add(entry);
+      } else {
+        LOG.debug("Removing entry with reason: {} for entity: {}",
+            entry.get(PauseSignalProperty.REASON.name()), entryEntity);
+      }
+    }
+
+    boolean removed = updatedReasons.size() < originalSize;
+    LOG.debug("After removal: Reasons list contains {} entries", 
updatedReasons.size());
+
+    if (removed) {
+      updateReasonsListField(updatedReasons);
+
+      // Update the simpleFields with the most recent reason (for backward 
compatibility)
+      if (!updatedReasons.isEmpty()) {
+        // Sort by timestamp in descending order to get the most recent
+        updatedReasons.sort((r1, r2) -> {
+          long t1 = 
Long.parseLong(r1.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          long t2 = 
Long.parseLong(r2.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          return Long.compare(t2, t1);
+        });
+
+        Map<String, String> mostRecent = updatedReasons.get(0);
+        String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
+        long newTimestamp = 
Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+        TriggeringEntity newEntity = TriggeringEntity.valueOf(
+            mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
+
+        LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: 
{}",
+            newReason, newEntity, newTimestamp);
+
+        setReason(newReason);
+        setTimestamp(newTimestamp);
+        setTriggeringEntity(newEntity);
+      }
+    } else {
+      LOG.info("No matching maintenance reason found for entity: {}", 
triggeringEntity);
+    }
+
+    return removed;
+  }
+
+  /**
+   * Check if there are any active maintenance reasons.
+   *
+   * @return true if there are any reasons for maintenance, false otherwise
+   */
+  public boolean hasMaintenanceReasons() {
+    return !getMaintenanceReasons().isEmpty();
+  }
+
+  /**
+   * Checks if there is a maintenance reason from a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to check
+   * @return true if there is a maintenance reason from this entity
+   */
+  public boolean hasMaintenanceReason(TriggeringEntity triggeringEntity) {
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    for (Map<String, String> entry : reasons) {
+      if 
(triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Gets the maintenance reason details for a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to get reason details for
+   * @return Map containing reason details, or null if not found
+   */
+  public Map<String, String> getMaintenanceReasonDetails(TriggeringEntity 
triggeringEntity) {
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    for (Map<String, String> entry : reasons) {
+      if 
(triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        return entry;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Gets the number of active maintenance reasons.
+   *
+   * @return The count of active maintenance reasons
+   */
+  public int getMaintenanceReasonsCount() {
+    return getMaintenanceReasons().size();
+  }
+
+  /**
+   * Gets the maintenance reason from a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to get reason for
+   * @return The reason string, or null if not found
+   */
+  public String getMaintenanceReason(TriggeringEntity triggeringEntity) {

Review Comment:
   Where is this called?
   can we instead do at the caller
   ```
   reasons = filterReasons(getMainteanancerReasons(), 
List.of(triggeringEntity), null)
   String mr = reasons.size() != 0 ? reasons.get(0).getOrDefault(REASON, null) 
: null
   ```



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    LOG.debug("Before addition: Reasons list contains {} entries", 
reasons.size());
+
+    boolean found = false;
+    for (Map<String, String> entry : reasons) {
+      if 
(triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        entry.put(PauseSignalProperty.REASON.name(), reason);
+        entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+        found = true;
+        LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
+        break;
+      }
+    }
+
+    if (!found) {
+      Map<String, String> newEntry = new HashMap<>();
+      newEntry.put(PauseSignalProperty.REASON.name(), reason);
+      newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+      newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), 
triggerEntityStr);
+      reasons.add(newEntry);
+      LOG.debug("Added new entry for entity: {}", triggeringEntity);
+    }
+
+    updateReasonsListField(reasons);
+    LOG.debug("After addition: Reasons list contains {} entries", 
reasons.size());
+  }
+
+  /**
+   * Helper method to update the ZNRecord with the current reasons list.
+   * Each reason is stored as a single JSON string in the list.
+   *
+   * @param reasons The list of reason maps to store
+   */
+  private void updateReasonsListField(List<Map<String, String>> reasons) {
+    List<String> reasonsList = new ArrayList<>();
+
+    for (Map<String, String> entry : reasons) {
+      String jsonString = convertMapToJsonString(entry);
+      if (!jsonString.isEmpty()) {
+        reasonsList.add(jsonString);
+      }
+    }
+
+    _record.setListField(REASONS_LIST_FIELD, reasonsList);
+  }
+
+  /**
+   * Convert a map to a JSON-style string
+   */
+  private String convertMapToJsonString(Map<String, String> map) {
+    try {
+      return new ObjectMapper().writeValueAsString(map);
+    } catch (IOException e) {
+      LOG.warn("Failed to convert map to JSON string: {}", e.getMessage());
+      return "";
+    }
+  }
+
+  /**
+   * Get all maintenance reasons currently active.
+   *
+   * @return List of maintenance reasons as maps
+   */
+  public List<Map<String, String>> getMaintenanceReasons() {
+    List<Map<String, String>> reasons = new ArrayList<>();
+    List<String> reasonsList = _record.getListField(REASONS_LIST_FIELD);
+
+    if (reasonsList != null && !reasonsList.isEmpty()) {
+      for (String entryStr : reasonsList) {
+        Map<String, String> entry = parseJsonStyleEntry(entryStr);
+        if (!entry.isEmpty()) {
+          reasons.add(entry);
+        }
+      }
+    }
+
+    return reasons;
+  }
+
+  /**
+   * Parse an entry string in JSON format into a map
+   */
+  private Map<String, String> parseJsonStyleEntry(String entryStr) {
+    Map<String, String> map = new HashMap<>();
+    try {
+        return new ObjectMapper().readValue(entryStr,
+            TypeFactory.defaultInstance().constructMapType(HashMap.class, 
String.class, String.class));
+      } catch (IOException e) {
+        LOG.warn("Failed to parse JSON entry: {}, error: {}", entryStr, 
e.getMessage());
+      }
+    return map;
+  }
+
+  /**
+   * Remove a maintenance reason by triggering entity.
+   *
+   * @param triggeringEntity The entity whose reason should be removed
+   * @return true if a reason was removed, false otherwise
+   */
+  public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
+    LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+
+    boolean entityExists = false;
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (triggeringEntity.name().equals(entryEntity)) {
+        entityExists = true;
+        break;
+      }
+    }
+
+    if (!entityExists) {
+      LOG.info("Entity {} doesn't have a maintenance reason entry, ignoring 
exit request", triggeringEntity);
+      return false;
+    }
+
+    int originalSize = reasons.size();
+    LOG.debug("Before removal: Reasons list contains {} entries", 
reasons.size());
+
+    List<Map<String, String>> updatedReasons = new ArrayList<>();
+    String targetEntity = triggeringEntity.name();
+
+    // Only keep reasons that don't match the triggering entity
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (!targetEntity.equals(entryEntity)) {
+        updatedReasons.add(entry);
+      } else {
+        LOG.debug("Removing entry with reason: {} for entity: {}",
+            entry.get(PauseSignalProperty.REASON.name()), entryEntity);
+      }
+    }
+
+    boolean removed = updatedReasons.size() < originalSize;
+    LOG.debug("After removal: Reasons list contains {} entries", 
updatedReasons.size());
+
+    if (removed) {
+      updateReasonsListField(updatedReasons);
+
+      // Update the simpleFields with the most recent reason (for backward 
compatibility)
+      if (!updatedReasons.isEmpty()) {
+        // Sort by timestamp in descending order to get the most recent
+        updatedReasons.sort((r1, r2) -> {
+          long t1 = 
Long.parseLong(r1.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          long t2 = 
Long.parseLong(r2.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          return Long.compare(t2, t1);
+        });
+
+        Map<String, String> mostRecent = updatedReasons.get(0);
+        String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
+        long newTimestamp = 
Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+        TriggeringEntity newEntity = TriggeringEntity.valueOf(
+            mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
+
+        LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: 
{}",
+            newReason, newEntity, newTimestamp);
+
+        setReason(newReason);
+        setTimestamp(newTimestamp);
+        setTriggeringEntity(newEntity);
+      }
+    } else {
+      LOG.info("No matching maintenance reason found for entity: {}", 
triggeringEntity);
+    }
+
+    return removed;
+  }
+
+  /**
+   * Check if there are any active maintenance reasons.
+   *
+   * @return true if there are any reasons for maintenance, false otherwise
+   */
+  public boolean hasMaintenanceReasons() {
+    return !getMaintenanceReasons().isEmpty();
+  }
+
+  /**
+   * Checks if there is a maintenance reason from a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to check
+   * @return true if there is a maintenance reason from this entity
+   */
+  public boolean hasMaintenanceReason(TriggeringEntity triggeringEntity) {
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    for (Map<String, String> entry : reasons) {
+      if 
(triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Gets the maintenance reason details for a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to get reason details for
+   * @return Map containing reason details, or null if not found
+   */
+  public Map<String, String> getMaintenanceReasonDetails(TriggeringEntity 
triggeringEntity) {
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    for (Map<String, String> entry : reasons) {
+      if 
(triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        return entry;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Gets the number of active maintenance reasons.
+   *
+   * @return The count of active maintenance reasons
+   */
+  public int getMaintenanceReasonsCount() {

Review Comment:
   I don't think we need this.



##########
helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java:
##########
@@ -112,4 +129,284 @@ public void setTimestamp(long timestamp) {
   public long getTimestamp() {
     return _record.getLongField(MaintenanceSignalProperty.TIMESTAMP.name(), 
-1);
   }
+
+  /**
+   * Add a new maintenance reason (or update an existing one if the triggering 
entity already has a reason).
+   *
+   * @param reason The reason for maintenance
+   * @param timestamp The timestamp when maintenance was triggered
+   * @param triggeringEntity The entity that triggered maintenance
+   */
+  public void addMaintenanceReason(String reason, long timestamp, 
TriggeringEntity triggeringEntity) {
+    LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: 
{}",
+        triggeringEntity, reason, timestamp);
+
+    // The triggering entity is our unique key - Overwrite any existing entry 
with this entity
+    String triggerEntityStr = triggeringEntity.name();
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+    LOG.debug("Before addition: Reasons list contains {} entries", 
reasons.size());
+
+    boolean found = false;
+    for (Map<String, String> entry : reasons) {
+      if 
(triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name())))
 {
+        entry.put(PauseSignalProperty.REASON.name(), reason);
+        entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+        found = true;
+        LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
+        break;
+      }
+    }
+
+    if (!found) {
+      Map<String, String> newEntry = new HashMap<>();
+      newEntry.put(PauseSignalProperty.REASON.name(), reason);
+      newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), 
Long.toString(timestamp));
+      newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), 
triggerEntityStr);
+      reasons.add(newEntry);
+      LOG.debug("Added new entry for entity: {}", triggeringEntity);
+    }
+
+    updateReasonsListField(reasons);
+    LOG.debug("After addition: Reasons list contains {} entries", 
reasons.size());
+  }
+
+  /**
+   * Helper method to update the ZNRecord with the current reasons list.
+   * Each reason is stored as a single JSON string in the list.
+   *
+   * @param reasons The list of reason maps to store
+   */
+  private void updateReasonsListField(List<Map<String, String>> reasons) {
+    List<String> reasonsList = new ArrayList<>();
+
+    for (Map<String, String> entry : reasons) {
+      String jsonString = convertMapToJsonString(entry);
+      if (!jsonString.isEmpty()) {
+        reasonsList.add(jsonString);
+      }
+    }
+
+    _record.setListField(REASONS_LIST_FIELD, reasonsList);
+  }
+
+  /**
+   * Convert a map to a JSON-style string
+   */
+  private String convertMapToJsonString(Map<String, String> map) {
+    try {
+      return new ObjectMapper().writeValueAsString(map);
+    } catch (IOException e) {
+      LOG.warn("Failed to convert map to JSON string: {}", e.getMessage());
+      return "";
+    }
+  }
+
+  /**
+   * Get all maintenance reasons currently active.
+   *
+   * @return List of maintenance reasons as maps
+   */
+  public List<Map<String, String>> getMaintenanceReasons() {
+    List<Map<String, String>> reasons = new ArrayList<>();
+    List<String> reasonsList = _record.getListField(REASONS_LIST_FIELD);
+
+    if (reasonsList != null && !reasonsList.isEmpty()) {
+      for (String entryStr : reasonsList) {
+        Map<String, String> entry = parseJsonStyleEntry(entryStr);
+        if (!entry.isEmpty()) {
+          reasons.add(entry);
+        }
+      }
+    }
+
+    return reasons;
+  }
+
+  /**
+   * Parse an entry string in JSON format into a map
+   */
+  private Map<String, String> parseJsonStyleEntry(String entryStr) {
+    Map<String, String> map = new HashMap<>();
+    try {
+        return new ObjectMapper().readValue(entryStr,
+            TypeFactory.defaultInstance().constructMapType(HashMap.class, 
String.class, String.class));
+      } catch (IOException e) {
+        LOG.warn("Failed to parse JSON entry: {}, error: {}", entryStr, 
e.getMessage());
+      }
+    return map;
+  }
+
+  /**
+   * Remove a maintenance reason by triggering entity.
+   *
+   * @param triggeringEntity The entity whose reason should be removed
+   * @return true if a reason was removed, false otherwise
+   */
+  public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
+    LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);
+
+    List<Map<String, String>> reasons = getMaintenanceReasons();
+
+    boolean entityExists = false;
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (triggeringEntity.name().equals(entryEntity)) {
+        entityExists = true;
+        break;
+      }
+    }
+
+    if (!entityExists) {
+      LOG.info("Entity {} doesn't have a maintenance reason entry, ignoring 
exit request", triggeringEntity);
+      return false;
+    }
+
+    int originalSize = reasons.size();
+    LOG.debug("Before removal: Reasons list contains {} entries", 
reasons.size());
+
+    List<Map<String, String>> updatedReasons = new ArrayList<>();
+    String targetEntity = triggeringEntity.name();
+
+    // Only keep reasons that don't match the triggering entity
+    for (Map<String, String> entry : reasons) {
+      String entryEntity = 
entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
+      if (!targetEntity.equals(entryEntity)) {
+        updatedReasons.add(entry);
+      } else {
+        LOG.debug("Removing entry with reason: {} for entity: {}",
+            entry.get(PauseSignalProperty.REASON.name()), entryEntity);
+      }
+    }
+
+    boolean removed = updatedReasons.size() < originalSize;
+    LOG.debug("After removal: Reasons list contains {} entries", 
updatedReasons.size());
+
+    if (removed) {
+      updateReasonsListField(updatedReasons);
+
+      // Update the simpleFields with the most recent reason (for backward 
compatibility)
+      if (!updatedReasons.isEmpty()) {
+        // Sort by timestamp in descending order to get the most recent
+        updatedReasons.sort((r1, r2) -> {
+          long t1 = 
Long.parseLong(r1.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          long t2 = 
Long.parseLong(r2.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+          return Long.compare(t2, t1);
+        });
+
+        Map<String, String> mostRecent = updatedReasons.get(0);
+        String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
+        long newTimestamp = 
Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
+        TriggeringEntity newEntity = TriggeringEntity.valueOf(
+            mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
+
+        LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: 
{}",
+            newReason, newEntity, newTimestamp);
+
+        setReason(newReason);
+        setTimestamp(newTimestamp);
+        setTriggeringEntity(newEntity);
+      }
+    } else {
+      LOG.info("No matching maintenance reason found for entity: {}", 
triggeringEntity);
+    }
+
+    return removed;
+  }
+
+  /**
+   * Check if there are any active maintenance reasons.
+   *
+   * @return true if there are any reasons for maintenance, false otherwise
+   */
+  public boolean hasMaintenanceReasons() {
+    return !getMaintenanceReasons().isEmpty();
+  }
+
+  /**
+   * Checks if there is a maintenance reason from a specific triggering entity.
+   *
+   * @param triggeringEntity The entity to check
+   * @return true if there is a maintenance reason from this entity
+   */
+  public boolean hasMaintenanceReason(TriggeringEntity triggeringEntity) {

Review Comment:
   Can we use `filterReasons` instead of this method, the caller can add a 
check on size, or this method can add a check on size.
   Again I don't see an explicit need of this method



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to