Skip to content

Commit 4f829df

Browse files
Address review comments
1 parent 36830c7 commit 4f829df

File tree

3 files changed

+169
-191
lines changed

3 files changed

+169
-191
lines changed

helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixAdmin.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,12 +1224,23 @@ private void processMaintenanceMode(String clusterName, final boolean enabled,
12241224
if (!enabled) {
12251225
// Exit maintenance mode for this specific triggering entity
12261226

1227-
if (maintenanceSignal != null) {
1227+
// Early return if no maintenance signal exists
1228+
if (maintenanceSignal == null) {
1229+
if (triggeringEntity == MaintenanceSignal.TriggeringEntity.USER) {
1230+
logger.info("USER administrative override: no maintenance signal exists, nothing to remove");
1231+
} else {
1232+
// CONTROLLER/AUTOMATION: strict no-op
1233+
logger.info("Entity {} attempted to exit maintenance mode but no maintenance signal exists", triggeringEntity);
1234+
}
1235+
return;
1236+
}
1237+
12281238
// If a specific actor is exiting maintenance mode
12291239
boolean removed = maintenanceSignal.removeMaintenanceReason(triggeringEntity);
12301240

12311241
if (removed) {
12321242
// If there are still reasons for maintenance mode, update the ZNode
1243+
12331244
if (maintenanceSignal.getRecord().getListField("reasons") != null
12341245
&& !maintenanceSignal.getRecord().getListField("reasons").isEmpty()) {
12351246
if (!accessor.setProperty(keyBuilder.maintenance(), maintenanceSignal)) {
@@ -1249,15 +1260,6 @@ private void processMaintenanceMode(String clusterName, final boolean enabled,
12491260
} else {
12501261
// CONTROLLER/AUTOMATION: strict no-op if their entry not found
12511262
logger.info("Entity {} doesn't have a maintenance reason entry, exit request ignored", triggeringEntity);
1252-
}
1253-
}
1254-
} else {
1255-
// No maintenance signal exists
1256-
if (triggeringEntity == MaintenanceSignal.TriggeringEntity.USER) {
1257-
logger.info("USER administrative override: no maintenance signal exists, nothing to remove");
1258-
} else {
1259-
// CONTROLLER/AUTOMATION: strict no-op
1260-
logger.info("Entity {} attempted to exit maintenance mode but no maintenance signal exists", triggeringEntity);
12611263
}
12621264
}
12631265
} else {

helix-core/src/main/java/org/apache/helix/model/MaintenanceSignal.java

Lines changed: 90 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121

2222
import java.util.ArrayList;
23+
import java.util.Arrays;
2324
import java.util.HashMap;
2425
import java.util.List;
2526
import java.util.Map;
@@ -131,7 +132,7 @@ public long getTimestamp() {
131132
}
132133

133134
/**
134-
* Add a new maintenance reason (or update an existing one if the triggering entity already has a reason).
135+
* Add a new maintenance reason. If the triggering entity already has a reason, it will be replaced.
135136
*
136137
* @param reason The reason for maintenance
137138
* @param timestamp The timestamp when maintenance was triggered
@@ -141,34 +142,22 @@ public void addMaintenanceReason(String reason, long timestamp, TriggeringEntity
141142
LOG.info("Adding maintenance reason for entity: {}, reason: {}, timestamp: {}",
142143
triggeringEntity, reason, timestamp);
143144

144-
// The triggering entity is our unique key - Overwrite any existing entry with this entity
145-
String triggerEntityStr = triggeringEntity.name();
146-
147145
List<Map<String, String>> reasons = getMaintenanceReasons();
148146
LOG.debug("Before addition: Reasons list contains {} entries", reasons.size());
149147

150-
boolean found = false;
151-
for (Map<String, String> entry : reasons) {
152-
if (triggerEntityStr.equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name()))) {
153-
entry.put(PauseSignalProperty.REASON.name(), reason);
154-
entry.put(MaintenanceSignalProperty.TIMESTAMP.name(), Long.toString(timestamp));
155-
found = true;
156-
LOG.debug("Updated existing entry for entity: {}", triggeringEntity);
157-
break;
158-
}
159-
}
148+
// Filter out any existing reasons for this triggering entity
149+
List<Map<String, String>> filteredReasons = filterReasons(reasons, null,
150+
Arrays.asList(triggeringEntity));
160151

161-
if (!found) {
162-
Map<String, String> newEntry = new HashMap<>();
163-
newEntry.put(PauseSignalProperty.REASON.name(), reason);
164-
newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), Long.toString(timestamp));
165-
newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), triggerEntityStr);
166-
reasons.add(newEntry);
167-
LOG.debug("Added new entry for entity: {}", triggeringEntity);
168-
}
152+
// Always add the new reason at the end of the list
153+
Map<String, String> newEntry = new HashMap<>();
154+
newEntry.put(PauseSignalProperty.REASON.name(), reason);
155+
newEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), Long.toString(timestamp));
156+
newEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), triggeringEntity.name());
157+
filteredReasons.add(newEntry);
169158

170-
updateReasonsListField(reasons);
171-
LOG.debug("After addition: Reasons list contains {} entries", reasons.size());
159+
updateReasonsListField(filteredReasons);
160+
LOG.debug("After addition: Reasons list contains {} entries", filteredReasons.size());
172161
}
173162

174163
/**
@@ -247,73 +236,77 @@ public boolean removeMaintenanceReason(TriggeringEntity triggeringEntity) {
247236
LOG.info("Removing maintenance reason for entity: {}", triggeringEntity);
248237

249238
List<Map<String, String>> reasons = getMaintenanceReasons();
250-
251-
boolean entityExists = false;
252-
for (Map<String, String> entry : reasons) {
253-
String entryEntity = entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
254-
if (triggeringEntity.name().equals(entryEntity)) {
255-
entityExists = true;
256-
break;
257-
}
258-
}
259-
260-
if (!entityExists) {
239+
List<Map<String, String>> filteredReasons = filterReasons(reasons, null,
240+
Arrays.asList(triggeringEntity));
241+
242+
// Return false early if no entities were filtered out
243+
if (filteredReasons.size() == reasons.size()) {
261244
LOG.info("Entity {} doesn't have a maintenance reason entry, ignoring exit request", triggeringEntity);
262245
return false;
263246
}
264247

265-
int originalSize = reasons.size();
266-
LOG.debug("Before removal: Reasons list contains {} entries", reasons.size());
248+
// Update reasons list field with filtered reasons
249+
updateReasonsListField(filteredReasons);
267250

268-
List<Map<String, String>> updatedReasons = new ArrayList<>();
269-
String targetEntity = triggeringEntity.name();
251+
// Always set/reset the simpleFields if filteredReasons.size() != 0
252+
if (!filteredReasons.isEmpty()) {
253+
// Get the most recent reason (last element, since list is in chronological order)
254+
Map<String, String> mostRecent = filteredReasons.get(filteredReasons.size() - 1);
255+
String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
256+
long newTimestamp = Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
257+
TriggeringEntity newEntity = TriggeringEntity.valueOf(
258+
mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
270259

271-
// Only keep reasons that don't match the triggering entity
272-
for (Map<String, String> entry : reasons) {
273-
String entryEntity = entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
274-
if (!targetEntity.equals(entryEntity)) {
275-
updatedReasons.add(entry);
276-
} else {
277-
LOG.debug("Removing entry with reason: {} for entity: {}",
278-
entry.get(PauseSignalProperty.REASON.name()), entryEntity);
279-
}
260+
LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: {}",
261+
newReason, newEntity, newTimestamp);
262+
263+
setReason(newReason);
264+
setTimestamp(newTimestamp);
265+
setTriggeringEntity(newEntity);
280266
}
281267

282-
boolean removed = updatedReasons.size() < originalSize;
283-
LOG.debug("After removal: Reasons list contains {} entries", updatedReasons.size());
284-
285-
if (removed) {
286-
updateReasonsListField(updatedReasons);
287-
288-
// Update the simpleFields with the most recent reason (for backward compatibility)
289-
if (!updatedReasons.isEmpty()) {
290-
// Sort by timestamp in descending order to get the most recent
291-
updatedReasons.sort((r1, r2) -> {
292-
long t1 = Long.parseLong(r1.get(MaintenanceSignalProperty.TIMESTAMP.name()));
293-
long t2 = Long.parseLong(r2.get(MaintenanceSignalProperty.TIMESTAMP.name()));
294-
return Long.compare(t2, t1);
295-
});
296-
297-
Map<String, String> mostRecent = updatedReasons.get(0);
298-
String newReason = mostRecent.get(PauseSignalProperty.REASON.name());
299-
long newTimestamp = Long.parseLong(mostRecent.get(MaintenanceSignalProperty.TIMESTAMP.name()));
300-
TriggeringEntity newEntity = TriggeringEntity.valueOf(
301-
mostRecent.get(MaintenanceSignalProperty.TRIGGERED_BY.name()));
302-
303-
LOG.info("Updated to most recent reason: {}, entity: {}, timestamp: {}",
304-
newReason, newEntity, newTimestamp);
305-
306-
setReason(newReason);
307-
setTimestamp(newTimestamp);
308-
setTriggeringEntity(newEntity);
268+
return true;
269+
}
270+
271+
/**
272+
* Filter maintenance reasons based on include and exclude entity lists.
273+
*
274+
* @param reasons The original list of maintenance reasons
275+
* @param includeEntities List of entities to include (null means include all)
276+
* @param excludeEntities List of entities to exclude (null means exclude none)
277+
* @return Filtered list of maintenance reasons (maintains original order)
278+
*/
279+
private List<Map<String, String>> filterReasons(List<Map<String, String>> reasons,
280+
List<TriggeringEntity> includeEntities,
281+
List<TriggeringEntity> excludeEntities) {
282+
List<Map<String, String>> filtered = new ArrayList<>();
283+
284+
for (Map<String, String> reason : reasons) {
285+
String triggeredByStr = reason.get(MaintenanceSignalProperty.TRIGGERED_BY.name());
286+
if (triggeredByStr == null) {
287+
continue;
288+
}
289+
290+
TriggeringEntity entity;
291+
try {
292+
entity = TriggeringEntity.valueOf(triggeredByStr);
293+
} catch (IllegalArgumentException ex) {
294+
LOG.warn("Unknown triggering entity: {}, skipping", triggeredByStr);
295+
continue;
296+
}
297+
298+
if ((includeEntities != null && !includeEntities.contains(entity)) ||
299+
(excludeEntities != null && excludeEntities.contains(entity))) {
300+
continue;
309301
}
310-
} else {
311-
LOG.info("No matching maintenance reason found for entity: {}", triggeringEntity);
302+
303+
filtered.add(reason);
312304
}
313305

314-
return removed;
306+
return filtered;
315307
}
316308

309+
317310
/**
318311
* Check if there are any active maintenance reasons.
319312
*
@@ -331,49 +324,14 @@ public boolean hasMaintenanceReasons() {
331324
*/
332325
public boolean hasMaintenanceReason(TriggeringEntity triggeringEntity) {
333326
List<Map<String, String>> reasons = getMaintenanceReasons();
334-
for (Map<String, String> entry : reasons) {
335-
if (triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name()))) {
336-
return true;
337-
}
338-
}
339-
return false;
327+
List<Map<String, String>> filteredReasons = filterReasons(reasons,
328+
Arrays.asList(triggeringEntity), null);
329+
return !filteredReasons.isEmpty();
340330
}
341331

342-
/**
343-
* Gets the maintenance reason details for a specific triggering entity.
344-
*
345-
* @param triggeringEntity The entity to get reason details for
346-
* @return Map containing reason details, or null if not found
347-
*/
348-
public Map<String, String> getMaintenanceReasonDetails(TriggeringEntity triggeringEntity) {
349-
List<Map<String, String>> reasons = getMaintenanceReasons();
350-
for (Map<String, String> entry : reasons) {
351-
if (triggeringEntity.name().equals(entry.get(MaintenanceSignalProperty.TRIGGERED_BY.name()))) {
352-
return entry;
353-
}
354-
}
355-
return null;
356-
}
357332

358-
/**
359-
* Gets the number of active maintenance reasons.
360-
*
361-
* @return The count of active maintenance reasons
362-
*/
363-
public int getMaintenanceReasonsCount() {
364-
return getMaintenanceReasons().size();
365-
}
366333

367-
/**
368-
* Gets the maintenance reason from a specific triggering entity.
369-
*
370-
* @param triggeringEntity The entity to get reason for
371-
* @return The reason string, or null if not found
372-
*/
373-
public String getMaintenanceReason(TriggeringEntity triggeringEntity) {
374-
Map<String, String> details = getMaintenanceReasonDetails(triggeringEntity);
375-
return details != null ? details.get(PauseSignalProperty.REASON.name()) : null;
376-
}
334+
377335

378336
/**
379337
* Reconcile legacy data from simpleFields into listFields.reasons if it's missing.
@@ -390,23 +348,23 @@ public void reconcileLegacyData() {
390348
TriggeringEntity simpleEntity = getTriggeringEntity();
391349
long simpleTimestamp = getTimestamp();
392350

393-
// Only reconcile USER data from legacy clients
394-
// CONTROLLER and AUTOMATION should not have legacy data loss scenarios
395-
if (simpleReason != null && !simpleReason.isEmpty() && simpleEntity == TriggeringEntity.USER
396-
&& simpleTimestamp > 0 && !hasMaintenanceReason(simpleEntity)) {
351+
// Early return if no simple reason exists, not a USER entity, or USER already has a reason
352+
List<Map<String, String>> reasons = getMaintenanceReasons();
353+
if (simpleReason == null || simpleEntity != TriggeringEntity.USER ||
354+
!filterReasons(reasons, Arrays.asList(TriggeringEntity.USER), null).isEmpty()) {
355+
return;
356+
}
397357

398-
// Legacy USER data exists but not in listFields - preserve it
399-
Map<String, String> legacyEntry = new HashMap<>();
400-
legacyEntry.put(PauseSignalProperty.REASON.name(), simpleReason);
401-
legacyEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), String.valueOf(simpleTimestamp));
402-
legacyEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), simpleEntity.name());
358+
// Legacy USER data exists but not in listFields - preserve it
359+
Map<String, String> legacyEntry = new HashMap<>();
360+
legacyEntry.put(PauseSignalProperty.REASON.name(), simpleReason);
361+
legacyEntry.put(MaintenanceSignalProperty.TIMESTAMP.name(), String.valueOf(simpleTimestamp));
362+
legacyEntry.put(MaintenanceSignalProperty.TRIGGERED_BY.name(), simpleEntity.name());
403363

404-
List<Map<String, String>> reasons = getMaintenanceReasons();
405-
reasons.add(legacyEntry);
406-
updateReasonsListField(reasons);
364+
reasons.add(legacyEntry);
365+
updateReasonsListField(reasons);
407366

408-
LOG.info("Reconciled legacy USER maintenance data: reason={}, timestamp={}",
409-
simpleReason, simpleTimestamp);
410-
}
367+
LOG.info("Reconciled legacy USER maintenance data: reason={}, timestamp={}",
368+
simpleReason, simpleTimestamp);
411369
}
412370
}

0 commit comments

Comments
 (0)