Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -507,6 +507,9 @@ protected void handleJobTimeout(JobContext jobCtx, WorkflowContext workflowCtx,
_clusterStatusMonitor.updateJobCounters(jobCfg, TaskState.TIMED_OUT);
_rebalanceScheduler.removeScheduledRebalance(jobResource);
TaskUtil.cleanupJobIdealStateExtView(_manager.getHelixDataAccessor(), jobResource);
// Aggregate the terminal state of every task into a job-level summary (computed after INIT
// tasks are marked TASK_ABORTED above so the counts reflect the final states).
jobCtx.updateTaskStatusSummary();
// New pipeline trigger for workflow status update
// TODO: Enhance the pipeline and remove this because this operation is expansive
RebalanceUtil.scheduleOnDemandPipeline(_manager.getClusterName(),0L,false);
Expand All @@ -529,6 +532,10 @@ protected void failJob(String jobName, WorkflowContext workflowContext, JobConte
// New pipeline trigger for workflow status update
// TODO: Enhance the pipeline and remove this because this operation is expansive
RebalanceUtil.scheduleOnDemandPipeline(_manager.getClusterName(),0L,false);

// Aggregate the terminal state of every task into a job-level summary (computed after INIT
// tasks are marked TASK_ABORTED above so the counts reflect the final states).
jobContext.updateTaskStatusSummary();
}

// Compute real assignment from theoretical calculation with applied throttling
Expand Down Expand Up @@ -935,6 +942,10 @@ protected void markJobComplete(final String jobName, final JobContext jobContext
reportControllerInducedDelay(dataProvider, _clusterStatusMonitor, workflowConfig, jobConfig,
currentTime);
}

// Aggregate the terminal state of every task into a job-level summary so that partial failures
// remain visible even though the job itself is COMPLETED (e.g. FailureThreshold set high).
jobContext.updateTaskStatusSummary();
}

protected void markJobFailed(String jobName, JobContext jobContext, WorkflowConfig workflowConfig,
Expand Down
78 changes: 78 additions & 0 deletions helix-core/src/main/java/org/apache/helix/task/JobContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,30 @@
*/

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.helix.HelixProperty;
import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Provides a typed interface to the context information stored by {@link TaskRebalancer} in the
* Helix property store.
*/
public class JobContext extends HelixProperty {
private static final Logger LOG = LoggerFactory.getLogger(JobContext.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private enum ContextProperties {
START_TIME, // Time at which this JobContext was created
STATE,
Expand All @@ -48,6 +56,9 @@ private enum ContextProperties {
INFO,
NAME,
EXECUTION_START_TIME, // Time at which the first task of this job got scheduled
TASK_STATUS_SUMMARY, // Aggregated per-task terminal status summary (JSON), set when the job
// reaches a terminal state. Surfaces partial failures even when the job
// itself is COMPLETED (e.g. FailureThreshold set high so all tasks run).
}

// Note: This field needs to be set if any of the job context fields have been changed.
Expand Down Expand Up @@ -337,6 +348,73 @@ public long getExecutionStartTime() {
return Long.parseLong(tStr);
}

/**
* Task partition states that represent a terminal failure (the task was given up and will not be
* retried, or it errored/timed out). Used to compute the aggregated task status summary.
*/
private static boolean isFailedState(TaskPartitionState state) {
return state == TaskPartitionState.TASK_ERROR || state == TaskPartitionState.TASK_ABORTED
|| state == TaskPartitionState.TIMED_OUT || state == TaskPartitionState.ERROR;
}

/**
* Aggregates the state of every scheduled task in this job into a compact, job-level summary and
* stores it as a simple field (JSON). This lets operators observe partial failures even when the
* job's own status flag is COMPLETED, which happens when FailureThreshold is set high so that all
* partition tasks are allowed to run to completion. Intended to be called by the controller when
* the job reaches a terminal state (completed / failed / timed out).
* The summary shape is:
* {@code {"total":N,"completed":X,"failed":Y,"other":Z,"byState":{...},"failedTasks":[...]}}.
*/
public void updateTaskStatusSummary() {
Set<Integer> partitions = getPartitionSet();
Map<String, Integer> byState = new TreeMap<>();
List<Integer> failedTasks = Lists.newArrayList();
int completed = 0;
int failed = 0;
for (int p : partitions) {
TaskPartitionState state = getPartitionState(p);
String key = (state == null) ? "UNSCHEDULED" : state.name();
byState.merge(key, 1, Integer::sum);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is byState ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good one, byState is just the raw per-state histogram behind the coarse counts. each TaskPartitionState name (plus UNSCHEDULED when a partition has no state yet) maps to how many tasks are in that state. added a javadoc para on updateTaskStatusSummary() spelling it out.

if (state == TaskPartitionState.COMPLETED) {
completed++;
} else if (isFailedState(state)) {
failed++;
failedTasks.add(p);
}
}
failedTasks.sort(null);

Map<String, Object> summary = new LinkedHashMap<>();
summary.put("total", partitions.size());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend using string constants instead of hard coding these keys in the map

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah makes sense. pulled all the summary keys (total, completed, failed, timedOut and the Tasks arrays) plus UNSCHEDULED into named SUMMARY_KEY_ and SUMMARY_STATE_UNSCHEDULED constants, and the map builds off those now instead of inline literals.

summary.put("completed", completed);
summary.put("failed", failed);
summary.put("other", partitions.size() - completed - failed);
summary.put("byState", byState);
summary.put("failedTasks", failedTasks);

try {
String json = OBJECT_MAPPER.writeValueAsString(summary);
if (!json.equals(getTaskStatusSummary())) {
_record.setSimpleField(ContextProperties.TASK_STATUS_SUMMARY.name(), json);
markJobContextAsModified();
}
} catch (JsonProcessingException e) {
// The summary is a best-effort convenience field; never let it disrupt the job status
// update path.
LOG.warn("Failed to serialize task status summary for job {}", getName(), e);
}
}

/**
* @return the aggregated per-task status summary as a JSON string, or null if it has not been
* computed yet (i.e. the job has not reached a terminal state). See
* {@link #updateTaskStatusSummary()} for the shape.
*/
public String getTaskStatusSummary() {
return _record.getSimpleField(ContextProperties.TASK_STATUS_SUMMARY.name());
}

/**
* Get MapField for the given partition.
* @param p
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package org.apache.helix.integration.task;

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.helix.HelixManager;
import org.apache.helix.HelixManagerFactory;
import org.apache.helix.InstanceType;
import org.apache.helix.integration.manager.ClusterControllerManager;
import org.apache.helix.integration.manager.MockParticipantManager;
import org.apache.helix.task.JobConfig;
import org.apache.helix.task.JobContext;
import org.apache.helix.task.TaskConfig;
import org.apache.helix.task.TaskConstants;
import org.apache.helix.task.TaskDriver;
import org.apache.helix.task.TaskFactory;
import org.apache.helix.task.TaskPartitionState;
import org.apache.helix.task.TaskResult;
import org.apache.helix.task.TaskState;
import org.apache.helix.task.TaskStateModelFactory;
import org.apache.helix.task.TaskUtil;
import org.apache.helix.task.Workflow;
import org.apache.helix.tools.ClusterSetup;

/**
* Standalone end-to-end driver for the task status summary against a REAL, already
* running ZooKeeper (default localhost:2191). It spins up an actual Helix cluster in-process
* (controller + participants running the Task state model), submits one job with a mix of
* completing and failing tasks and a high FailureThreshold, waits for the job to reach COMPLETED,
* and prints / verifies the aggregated task status summary stored in the JobContext.
*
* Run with the helix-core test classpath, e.g.:
* <pre>
* java -cp "$(cat /tmp/helix-cp.txt):helix-core/target/classes:helix-core/target/test-classes" \
* org.apache.helix.integration.task.JobTaskSummaryDriver localhost:2191
* </pre>
* Exits 0 on success, 1 on failure.
*/
public class JobTaskSummaryDriver {
private static final String CLUSTER_NAME = "TASK_STATUS_SUMMARY_CLUSTER";
private static final int NUM_NODES = 3;
private static final int START_PORT = 13900;
private static final int NUM_TASKS = 6;
private static final int FATAL_TASK = 1; // FATAL_FAILED -> TASK_ABORTED
private static final int EXCEPTION_TASK = 3; // throws -> retried to exhaustion -> TASK_ERROR
private static final int EXPECTED_FAILED = 2;
private static final int EXPECTED_COMPLETED = NUM_TASKS - EXPECTED_FAILED;

public static void main(String[] args) throws Exception {
String zkAddr = args.length > 0 ? args[0] : "localhost:2191";
System.out.println("=== Task status summary e2e driver against real ZK " + zkAddr + " ===");

ClusterSetup setupTool = new ClusterSetup(zkAddr);
MockParticipantManager[] participants = new MockParticipantManager[NUM_NODES];
ClusterControllerManager controller = null;
HelixManager manager = null;
boolean ok = false;

try {
// 1. (Re)create the cluster and register participants.
setupTool.addCluster(CLUSTER_NAME, true);
for (int i = 0; i < NUM_NODES; i++) {
setupTool.addInstanceToCluster(CLUSTER_NAME, instanceName(i));
}

// 2. Start participants running the Task state model backed by MockTask.
for (int i = 0; i < NUM_NODES; i++) {
participants[i] = new MockParticipantManager(zkAddr, CLUSTER_NAME, instanceName(i));
Map<String, TaskFactory> taskFactoryReg = new HashMap<>();
taskFactoryReg.put(MockTask.TASK_COMMAND, MockTask::new);
participants[i].getStateMachineEngine().registerStateModelFactory(
TaskConstants.STATE_MODEL_NAME,
new TaskStateModelFactory(participants[i], taskFactoryReg));
participants[i].syncStart();
}

// 3. Start the controller.
controller = new ClusterControllerManager(zkAddr, CLUSTER_NAME, "controller_0");
controller.syncStart();

// 4. Admin manager + TaskDriver.
manager = HelixManagerFactory.getZKHelixManager(CLUSTER_NAME, "Admin",
InstanceType.ADMINISTRATOR, zkAddr);
manager.connect();
TaskDriver driver = new TaskDriver(manager);

// 5. Build one job with NUM_TASKS tasks; some fail terminally. FailureThreshold is set high
// (a common operator workaround) so all tasks run and the job still ends COMPLETED.
String jobResource = "dataValidationJob";
JobConfig.Builder jobBuilder = new JobConfig.Builder().setCommand(MockTask.TASK_COMMAND)
.setTimeoutPerTask(10000).setMaxAttemptsPerTask(2).setFailureThreshold(Integer.MAX_VALUE);

List<TaskConfig> taskConfigs = new ArrayList<>();
for (int j = 0; j < NUM_TASKS; j++) {
TaskConfig.Builder cb = new TaskConfig.Builder().setTaskId("task_" + j);
if (j == FATAL_TASK) {
cb.addConfig(MockTask.TASK_RESULT_STATUS, TaskResult.Status.FATAL_FAILED.name());
} else if (j == EXCEPTION_TASK) {
cb.addConfig(MockTask.THROW_EXCEPTION, Boolean.TRUE.toString());
}
cb.setTargetPartition(String.valueOf(j));
taskConfigs.add(cb.build());
}
jobBuilder.addTaskConfigs(taskConfigs);

Workflow flow =
WorkflowGenerator.generateSingleJobWorkflowBuilder(jobResource, jobBuilder).build();
System.out.println("Submitting workflow '" + jobResource + "' with " + NUM_TASKS

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we know need these print statements in tests anymore?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch, not needed. that was a standalone main() driver, not a TestNG test, and it needed an external ZK plus it just duplicated TestJobTaskStatusSummary. dropped the whole file so the prints go with it. coverage stays via that integration test plus a new deterministic TestJobContextTaskStatusSummary unit test.

+ " tasks (" + EXPECTED_FAILED + " designed to fail) ...");
driver.start(flow);

// 6. Wait for terminal COMPLETED state (job flag hides the failures).
TaskState finalState = driver.pollForWorkflowState(jobResource, TaskState.COMPLETED);
System.out.println("Workflow reached state: " + finalState);

String namespacedJob = TaskUtil.getNamespacedJobName(jobResource);
JobContext ctx = driver.getJobContext(namespacedJob);

System.out.println("\nPer-partition states:");
for (int pId : ctx.getPartitionSet()) {
System.out.printf(" partition %d (%s) -> %s%n", pId, ctx.getTaskIdForPartition(pId),
ctx.getPartitionState(pId));
}

String summary = ctx.getTaskStatusSummary();
System.out.println("\n>>> JobContext TASK_STATUS_SUMMARY:\n " + summary);

ok = verify(finalState, ctx, summary);
System.out.println("\n=== RESULT: " + (ok ? "PASS" : "FAIL") + " ===");
} finally {
if (manager != null && manager.isConnected()) {
manager.disconnect();
}
if (controller != null && controller.isConnected()) {
controller.syncStop();
}
for (MockParticipantManager p : participants) {
if (p != null && p.isConnected()) {
p.syncStop();
}
}
try {
setupTool.deleteCluster(CLUSTER_NAME);
} catch (Exception e) {
System.out.println("Cleanup: could not delete cluster: " + e.getMessage());
}
}
System.exit(ok ? 0 : 1);
}

private static boolean verify(TaskState finalState, JobContext ctx, String summary) {
boolean ok = true;
if (finalState != TaskState.COMPLETED) {
System.out.println("ASSERT FAIL: workflow state expected COMPLETED but was " + finalState);
ok = false;
}
if (summary == null) {
System.out.println("ASSERT FAIL: task status summary is null");
return false;
}
int completed = 0;
int failed = 0;
for (int pId : ctx.getPartitionSet()) {
TaskPartitionState s = ctx.getPartitionState(pId);
if (s == TaskPartitionState.COMPLETED) {
completed++;
} else if (s == TaskPartitionState.TASK_ABORTED || s == TaskPartitionState.TASK_ERROR
|| s == TaskPartitionState.TIMED_OUT || s == TaskPartitionState.ERROR) {
failed++;
}
}
ok &= expect("ground-truth completed", completed, EXPECTED_COMPLETED);
ok &= expect("ground-truth failed", failed, EXPECTED_FAILED);
ok &= expect("summary contains completed count",
summary.contains("\"completed\":" + EXPECTED_COMPLETED) ? 1 : 0, 1);
ok &= expect("summary contains failed count",
summary.contains("\"failed\":" + EXPECTED_FAILED) ? 1 : 0, 1);
ok &= expect("summary contains total count",
summary.contains("\"total\":" + NUM_TASKS) ? 1 : 0, 1);
return ok;
}

private static boolean expect(String what, int actual, int expected) {
boolean pass = actual == expected;
if (!pass) {
System.out.printf("ASSERT FAIL: %s expected %d but was %d%n", what, expected, actual);
}
return pass;
}

private static String instanceName(int i) {
return "localhost_" + (START_PORT + i);
}
}
Loading
Loading