From 84157faee895ecf2f5618437e7a0c322feee4e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vanesa=20Smo=C4=BEakov=C3=A1?= Date: Mon, 18 May 2026 19:16:07 +0200 Subject: [PATCH] MID-11186 Fix worker progress reporting --- .../task/ActivityBasedTaskInformation.java | 98 ++++++++++++++- .../task/ActivityProgressInformation.java | 2 +- .../util/task/ItemsProgressInformation.java | 12 ++ .../schema/util/TestTaskInformation.java | 114 ++++++++++++++++++ infra/schema/testng-unit.xml | 1 + release-notes.adoc | 1 + 6 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 infra/schema/src/test/java/com/evolveum/midpoint/schema/util/TestTaskInformation.java diff --git a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java index 0abf34ebebf..55ff8e9a9fb 100644 --- a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java +++ b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityBasedTaskInformation.java @@ -79,13 +79,11 @@ private ActivityBasedTaskInformation( if (activityStateOverview != null) { ActivityWorkersInformation workers = ActivityWorkersInformation.fromActivityStateOverview(activityStateOverview); - ActivityProgressInformation progress = ActivityProgressInformation.fromRootTask(rootTask, FULL_STATE_PREFERRED) - .find(activityPath); return new ActivityBasedTaskInformation( task, workers, computeStatus(activityStateOverview, task.getResultStatus(), workers), - progress != null ? progress : ActivityProgressInformation.unknown(activityPath), + selectSubtaskProgress(task, rootTask, activityPath, activityStateOverview), getLocalRootActivityState(task)); } else { return new ActivityBasedTaskInformation( @@ -97,6 +95,90 @@ private ActivityBasedTaskInformation( } } + /** + * Selects progress information suitable for displaying a subtask row. + * + * Worker subtasks are special: using the root activity progress would show the + * coordinator/aggregate progress for every worker. For workers, use the per-worker + * item progress from the activity overview. Non-worker subtasks keep the original + * root-task progress lookup. + */ + private static @NotNull ActivityProgressInformation selectSubtaskProgress( + @NotNull TaskType task, + @NotNull TaskType rootTask, + @NotNull ActivityPath activityPath, + @NotNull ActivityStateOverviewType activityStateOverview) { + + if (isWorkerTask(task)) { + ActivityProgressInformation workerItemsProgress = + createWorkerProgressFromOverview(task, activityPath, activityStateOverview); + return workerItemsProgress != null ? + workerItemsProgress : ActivityProgressInformation.unknown(activityPath); + } + + ActivityProgressInformation progress = ActivityProgressInformation.fromRootTask(rootTask, FULL_STATE_PREFERRED) + .find(activityPath); + return progress != null ? progress : ActivityProgressInformation.unknown(activityPath); + } + + private static boolean isWorkerTask(@NotNull TaskType task) { + ActivityBucketingStateType bucketing = getLocalRootActivityState(task).getBucketing(); + return bucketing != null + && bucketing.getBucketsProcessingRole() == BucketsProcessingRoleType.WORKER; + } + + /** + * Creates progress information for a worker row from the worker entry in the activity overview. + * + * The overview entry contains worker-local item counters. Converting them to + * {@link ActivityProgressInformation} lets the GUI display processed object counts + * for the worker instead of the coordinator's aggregate progress. + */ + private static @Nullable ActivityProgressInformation createWorkerProgressFromOverview( + @NotNull TaskType task, + @NotNull ActivityPath activityPath, + @NotNull ActivityStateOverviewType activityStateOverview) { + ItemsProgressInformation itemsProgress = findWorkerItemsProgress(task, activityStateOverview); + if (itemsProgress == null) { + return null; + } + + ActivityStateType localState = getLocalRootActivityState(task); + return new ActivityProgressInformation( + localState.getIdentifier(), + activityPath, + localState.getDisplayOrder(), + ActivityProgressInformation.RealizationState.fromFullState(localState.getRealizationState()), + null, + itemsProgress, + null); + } + + /** + * Finds item progress for the given worker task in the activity overview. + * + * The worker entry is matched by task OID. Its progress contains worker-local + * item/object counters, such as successfully processed, failed, skipped, and + * optionally expected total. + * + * @return item progress for the worker, or {@code null} if no matching progress is available + */ + private static @Nullable ItemsProgressInformation findWorkerItemsProgress( + @NotNull TaskType task, @NotNull ActivityStateOverviewType activityStateOverview) { + String oid = task.getOid(); + if (oid == null) { + return null; + } + + return activityStateOverview.getTask().stream() + .filter(taskOverview -> taskOverview.getTaskRef() != null) + .filter(taskOverview -> Objects.equals(taskOverview.getTaskRef().getOid(), oid)) + .map(ItemsProgressInformation::fromTaskOverview) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + @Override public String getProgressDescription(boolean longForm) { return progressInformation.toHumanReadableString(longForm); @@ -109,6 +191,16 @@ public double getProgress() { return 1.0; } + // Use direct item progress for leaf activities. If there is no expected total, + // return -1 so the GUI shows only the textual progress label. + if (progressInformation.getChildren().isEmpty()) { + ItemsProgressInformation itemsProgress = progressInformation.getItemsProgress(); + if (itemsProgress != null) { + float percentage = itemsProgress.getPercentage(); + return Float.isNaN(percentage) ? -1 : percentage; + } + } + // We need to list only leaf activities. Then compare them to the completed ones. // Current drawback is that activities and their sub activities are created when they // start, not during the task creation. This means that progress is not accurate for diff --git a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java index 2083fc51b88..66ccb0f30e5 100644 --- a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java +++ b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ActivityProgressInformation.java @@ -105,7 +105,7 @@ public class ActivityProgressInformation implements DebugDumpable, Serializable /** Identifier is estimated from the path. Use only if it needs not be precise. */ static @NotNull ActivityProgressInformation unknown(ActivityPath activityPath) { return unknown( - activityPath.isEmpty() ? activityPath.last() : null, + activityPath.isEmpty() ? null : activityPath.last(), activityPath); } diff --git a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java index fd821edc1eb..d77a93b05d7 100644 --- a/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java +++ b/infra/schema/src/main/java/com/evolveum/midpoint/schema/util/task/ItemsProgressInformation.java @@ -55,6 +55,18 @@ private ItemsProgressInformation(int progress, int errors, Integer expectedProgr return accumulator.toProgressInformation(); } + /** + * Creates item progress information from a worker entry in the parent activity state overview. + * + * The entry contains worker-local item counters. Using these counters lets the GUI display + * processed object counts for worker rows instead of the parent/coordinator aggregate progress. + */ + static @Nullable ItemsProgressInformation fromTaskOverview(@NotNull ActivityTaskStateOverviewType overview) { + Accumulator accumulator = new Accumulator(); + accumulator.add(overview.getProgress()); + return accumulator.toProgressInformation(); + } + static ItemsProgressInformation fromFullState(@NotNull ActivityStateType state, @NotNull ActivityPath activityPath, @NotNull TaskType task, @NotNull TaskResolver resolver) { if (BucketingUtil.isCoordinator(state)) { diff --git a/infra/schema/src/test/java/com/evolveum/midpoint/schema/util/TestTaskInformation.java b/infra/schema/src/test/java/com/evolveum/midpoint/schema/util/TestTaskInformation.java new file mode 100644 index 00000000000..54e3e954ec9 --- /dev/null +++ b/infra/schema/src/test/java/com/evolveum/midpoint/schema/util/TestTaskInformation.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2026 Evolveum and contributors + * + * Licensed under the EUPL-1.2 or later. + */ + +package com.evolveum.midpoint.schema.util; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.testng.annotations.Test; + +import com.evolveum.midpoint.schema.AbstractSchemaTest; +import com.evolveum.midpoint.schema.util.task.ActivityBasedTaskInformation; +import com.evolveum.midpoint.schema.util.task.ActivityProgressInformation; +import com.evolveum.midpoint.schema.util.task.TaskInformation; +import com.evolveum.midpoint.xml.ns._public.common.common_3.*; + +public class TestTaskInformation extends AbstractSchemaTest { + + private static final String ROOT_OID = "00000000-0000-0000-0000-000000000001"; + private static final String WORKER_1_OID = "00000000-0000-0000-0000-000000000101"; + private static final String WORKER_2_OID = "00000000-0000-0000-0000-000000000102"; + + /** + * Verifies that worker task progress is taken from the matching worker item overview. + * + * The root activity overview contains aggregate bucket progress, but worker rows should + * display worker-local processed-object counts instead of the coordinator progress. + */ + @Test + public void testWorkerProgressUsesWorkerItemOverview() { + TaskType root = createRootTaskWithWorkerOverview(); + TaskType worker = createWorkerTask(WORKER_1_OID); + + TaskInformation information = TaskInformation.createForTask(worker, root); + + assertThat(information).isInstanceOf(ActivityBasedTaskInformation.class); + assertThat(information.getProgressDescriptionShort()) + .as("worker progress label") + .isEqualTo("5"); + assertThat(information.getProgress()) + .as("worker progress bar value") + .isEqualTo(-1); + + ActivityProgressInformation progressInformation = + ((ActivityBasedTaskInformation) information).getProgressInformation(); + assertThat(progressInformation.getBucketsProgress()) + .as("worker bucket progress") + .isNull(); + assertThat(progressInformation.getItemsProgress()) + .as("worker item progress") + .isNotNull(); + assertThat(progressInformation.getItemsProgress().getProgress()) + .as("worker processed items") + .isEqualTo(5); + assertThat(progressInformation.getItemsProgress().getErrors()) + .as("worker failed items") + .isEqualTo(1); + + TaskInformation information2 = TaskInformation.createForTask(createWorkerTask(WORKER_2_OID), root); + assertThat(information2.getProgressDescriptionShort()) + .as("second worker progress label") + .isEqualTo("42"); + } + + private static TaskType createRootTaskWithWorkerOverview() { + ActivityStateOverviewType overview = new ActivityStateOverviewType() + .bucketProgress( + new BucketProgressOverviewType() + .completeBuckets(140) + .totalBuckets(257)) + .task(workerOverview(WORKER_1_OID, 2, 1, 2)) + .task(workerOverview(WORKER_2_OID, 42, 0, 0)); + + return new TaskType() + .oid(ROOT_OID) + .activityState( + new TaskActivityStateType() + .tree( + new ActivityTreeStateType() + .activity(overview))); + } + + private static ActivityTaskStateOverviewType workerOverview( + String oid, int successfullyProcessed, int failed, int skipped) { + return new ActivityTaskStateOverviewType() + .taskRef(oid, TaskType.COMPLEX_TYPE) + .bucketsProcessingRole(BucketsProcessingRoleType.WORKER) + .progress( + new ItemsProgressOverviewType() + .successfullyProcessed(successfullyProcessed) + .failed(failed) + .skipped(skipped)); + } + + private static TaskType createWorkerTask(String oid) { + ActivityBucketingStateType bucketing = new ActivityBucketingStateType() + .bucketsProcessingRole(BucketsProcessingRoleType.WORKER); + + ActivityStateType activity = new ActivityStateType() + .realizationState(ActivityRealizationStateType.IN_PROGRESS_LOCAL) + .bucketing(bucketing); + + TaskActivityStateType activityState = new TaskActivityStateType() + .localRoot(new ActivityPathType()) + .activity(activity); + + return new TaskType() + .oid(oid) + .parent(ROOT_OID) + .activityState(activityState); + } +} diff --git a/infra/schema/testng-unit.xml b/infra/schema/testng-unit.xml index 1d65d3f0f94..502d11a0eca 100644 --- a/infra/schema/testng-unit.xml +++ b/infra/schema/testng-unit.xml @@ -123,6 +123,7 @@ + diff --git a/release-notes.adoc b/release-notes.adoc index 78482ca3a0f..99f5c3636bc 100644 --- a/release-notes.adoc +++ b/release-notes.adoc @@ -104,6 +104,7 @@ Overall, midPoint 4.10 opens up the world of identity management and governance * Fixed translation of archetype display labels in assignment picker and summary panel. See bug:MID-11177[] * Fix 10k limit in certification queries using iterative search. See bug:MID-11043[] * Fixed generic "Fatal error" message in object tables to show available list/search error details. See bug:MID-10911[] +* Fixed worker progress reporting for distributed bucketed tasks. See bug:MID-11186[] === Releases Of Other Components