Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions infra/schema/testng-unit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
<class name="com.evolveum.midpoint.schema.processor.TestResourceSchema"/>
<class name="com.evolveum.midpoint.schema.processor.SchemaProcessorTest"/>
<class name="com.evolveum.midpoint.schema.util.XsdTypeConverterTest"/>
<class name="com.evolveum.midpoint.schema.util.TestTaskInformation"/>
<class name="com.evolveum.midpoint.schema.TestSchemaRegistry"/>
<class name="com.evolveum.midpoint.schema.TestJaxbParsing"/>
<class name="com.evolveum.midpoint.schema.TestObjectConstruction"/>
Expand Down
1 change: 1 addition & 0 deletions release-notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading