Skip to content

[GSoC] Add failure-investigation accessors to the core client (stacked on #3348) - #3349

Open
Aryan95614 wants to merge 4 commits into
Netflix:masterfrom
Aryan95614:agent-client-mentor-showcase
Open

Aryan95614 wants to merge 4 commits into
Netflix:masterfrom
Aryan95614:agent-client-mentor-showcase

Conversation

@Aryan95614

@Aryan95614 Aryan95614 commented Aug 23, 2026 •

Copy link
Copy Markdown
Contributor

GSoC 2026 project: metadata service request improvements (mentor: @saikonen). Upstream twin of the fork review PR (Aryan95614#2), opened per mentor request for a static PR number.

Rebased onto master after #3348 merged (2026-09-15); the diff is only this PR's own commits.

Summary

Folds bounded failure investigation into the regular Metaflow client as three
accessors on the existing objects, instead of a separate metaflow/agent
module. Built on the core pagination/filtering from #3348.

Context / Motivation

Review on the earlier revision steered the run-listing and pagination pieces
into the core client (that's #3348). Following the same direction, the failure
investigation layer now lives on the client objects rather than a parallel
metaflow.agent package, so there's a single, discoverable API.

Changes Made

  • Flow.failed_runs(*tags, since=None, max_runs=None) -> Iterator[Run]: failed
    runs newest-first, via runs(*tags, _filters={"status:eq": "failed"}); tags
    are forwarded exactly as for runs, and since adds an inclusive
    ts_epoch:ge bound.
  • Run.failed_task -> Optional[Task]: the latest task that finished without
    success (newest step first), or None. A task counts as failed when it raised
    (exception is set) or a decorator such as @catch handled the failure;
    tasks that are still running or never started are skipped.
  • Task.failure_summary -> Optional[FailureSummary]: normalized
    {exception_type, message, stacktrace, attempt}, or None when the task has
    no exception. Read errors propagate rather than being swallowed.
  • FailureSummary is exported from metaflow.client and metaflow, next to
    Flow, Run and Task.
  • Removed the metaflow/agent module (failure_finder, pagination,
    run_lister, TaskRef/FailureInfo, injectable resolver/fetcher).

Behavior note: failed_runs relies on server-side filtering, so it raises
against the local metadata provider or a service without pagination/filtering
support: the same gate as runs(_filters=...).

Testing

  • test/unit/test_client_failure_investigation.py: 19 tests covering
    failed_runs filter/tags/bounds forwarding (including one through the real
    Flow.runs down to the provider call), failed_task iteration order and the
    crashed / decorator-handled / still-running cases, failure_summary
    construction + error propagation, _normalize_exception across
    mapping/object/string inputs, and the package exports.
  • Combined with the merged [GSoC] Add paginated, filtered run iteration to the core client #3348 run-listing tests (test_client_run_listing.py): 63 passed.
  • Checked against a local run with one raising step and one @catch step:
    failed_task returns the raising task and failure_summary carries its type and message.
  • black (repo-pinned 25.12.0) clean.

Trade-offs / Design Decisions

  • failed_task cannot use finished alone: Task.finished reads _task_ok,
    which the task runner sets to False when the step raises, so
    finished and not successful would match only decorator-handled failures
    and never the task that crashed. It checks exception is not None or finished
    after ruling out successful tasks.
  • Dropped the injectable task_resolver / artifact_fetcher and the TaskRef
    indirection: unnecessary once this is core; real Task objects are used and
    the methods are testable with mocks.
  • Kept FailureSummary as a small frozen dataclass in core.py rather than
    returning a bare dict, so the normalized fields are typed and discoverable.

@saikonen
saikonen requested review from saikonen and talsperre August 23, 2026 23:06
@codecov

codecov Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (master@8b500da). Learn more about missing BASE report.

Additional details and impacted files
@@            Coverage Diff            @@
##             master    #3349   +/-   ##
=========================================
  Coverage          ?   28.94%           
=========================================
  Files             ?      382           
  Lines             ?    52948           
  Branches          ?     9362           
=========================================
  Hits              ?    15327           
  Misses            ?    36469           
  Partials          ?     1152           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Shriprasad-P Shriprasad-P left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review

PR: [GSoC] Add failure-investigation accessors to the core client (stacked on #3348)

Touched: metaflow/client/core.py, metaflow/metadata_provider/metadata.py, metaflow/metaflow_config.py, metaflow/plugins/metadata_providers/service.py, test/unit/test_client_failure_investigation.py

  • Scoped to the stated goal; please confirm CI is green.
  • Size: +1249/-46.

Drive-by review after reading the diff.

Replaces the separate metaflow/agent module with core-client accessors, per
the review that record iteration and normal client operations belong in core:

- Flow.failed_runs(*, since=None, max_runs=None): iterate failed runs newest
  first via runs(filters={"status:eq": "failed"}); raises on local/older
  services like any other server-filtered listing.
- Run.failed_task: the first unsuccessful task in the run (newest step first).
- Task.failure_summary -> FailureSummary: normalized exception type/message/
  stacktrace/attempt, or None when the task has no exception. Read errors
  propagate rather than being swallowed into diagnostic strings.

Drops the injectable resolver/fetcher and the TaskRef indirection (uses real
Task objects). Adds test/unit/test_client_failure_investigation.py.
@Aryan95614
Aryan95614 force-pushed the agent-client-mentor-showcase branch from 7ea2f8c to 8cb149e Compare September 15, 2026 14:28
@saikonen
saikonen marked this pull request as ready for review September 15, 2026 23:05
@greptile-apps

greptile-apps Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[Medium risk] Adds failure investigation accessors to the client API.

The PR is not yet safe to merge because failed_task excludes decorator-handled failures and still misses terminal tasks killed before completion metadata is persisted.

Findings

  1. P1 Handled Failures Are Excluded ▶
  2. P1 Hard Failures Are Skipped ▶

Summary

The PR adds failure-investigation accessors to the core client and exports the new FailureSummary type.

  • Adds server-filtered Flow.failed_runs.
  • Adds Run.failed_task based on per-attempt metadata.
  • Adds normalized exception details through Task.failure_summary.
  • Adds focused unit coverage and public package exports.

Reviews (3) · Last reviewed commit: "Address review: decide failed_task from ..."

Comment thread metaflow/client/core.py Outdated
Comment thread metaflow/client/core.py
Comment thread metaflow/client/core.py Outdated
"""
for step in self:
for task in step:
if not task.successful:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this expression will be true for tasks that are running, pending, or have not launched because it simply pulls from the task["_success"].data

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

alternative could be

if task.finished and not task.successful:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on the problem, but finished on its own does not work here. Task.finished reads _task_ok, and task.py sets that to False when the step raises, so finished and not successful would match only failures a decorator handled (for example @catch) and never the task that actually crashed. I checked on a local run with one raising step and one @catch step: the raising task reads finished=False, successful=False with exception set; the @catch task reads finished=True, successful=False with exception None.

The latest push rules out successful tasks first, then returns the task if task.exception is not None or task.finished. That covers the crashed task and the handled one, and skips tasks that have not written their artifacts yet (running, pending, never launched), which read as unsuccessful, unfinished and without an exception. Tests cover all four cases.

Comment thread metaflow/client/core.py Outdated
Comment thread metaflow/client/core.py
…t FailureSummary

- failed_runs passed filters= but runs() only takes _filters=, so every call
  raised TypeError; the plain Mock used for runs in the tests hid it. Tests
  now autospec Flow so runs enforces its real signature, and one test goes
  through the real runs() down to the provider call.
- failed_runs takes tags positionally, like runs, and forwards them.
- failed_task returned any task whose _success is not True, which is also the
  case for tasks still running or never started. It now requires the task to
  have finished without success: an exception recorded, or finished and not
  successful (a decorator-handled failure). finished on its own does not work
  because _task_ok is False for a task that raised.
- FailureSummary is re-exported from metaflow.client and metaflow, next to
  Flow, Run and Task.
Comment thread metaflow/client/core.py Outdated
# has recorded its exception; a task whose failure a decorator
# handled (e.g. @catch) is finished but not successful. A task
# that has not written its artifacts yet is neither.
if task.exception is not None or task.finished:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Hard Failures Are Skipped

If a task is terminated before its final artifacts are persisted—for example by OOM, SIGKILL, or preemption—it has no _exception or _task_ok artifact. This condition treats that genuinely failed task as unfinished and skips it, so failed_task can return None or an unrelated earlier failure.

@saikonen saikonen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Apologies that the review turnaround has stretched for this. Hopefully you have a moment still to address the review comments. Otherwise let me know and I'll push some changes on top to get this past the finish line.

One generic note as well is that some of the user-facing docstrings have unnecessary implementation details. These could be streamlined a bit

Comment thread metaflow/client/core.py Outdated
exception_type=normalized["type"],
message=normalized["message"],
stacktrace=normalized["stacktrace"],
attempt=self.current_attempt,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

self.current_attempt points to the latest started attempt, so there can be a real mismatch here for example with using @retry

Comment thread metaflow/client/core.py
return False

@property
def failed_task(self) -> Optional[Task]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the returned failed task might not be the correct one. This can return a @catch handled task instead of the fatal one depending on timings

Comment thread metaflow/client/core.py Outdated
"""
for step in self:
for task in step:
if task.successful:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

task.successful along with task.exception and task.finished checks result in a lot of calls to the metadata- and datastore. Can we implement this in a more cost-effective manner?

…rt the exception's own attempt

- failed_task reads one metadata_dict per task and returns a task only when its
  latest started attempt is done and recorded attempt_ok=False, so @catch-handled
  failures and tasks still running a retry are skipped; no per-task artifact loads.
- failure_summary takes the attempt from the _exception artifact it reads, not the
  latest started attempt, so it stays correct under @Retry.
- Trim the user-facing docstrings.
@Aryan95614

Copy link
Copy Markdown
Contributor Author

Thanks @saikonen, and no worries on the timing. Pushed fixes for all three in 1596f08:

  • failed_task now decides from each task's status metadata (one metadata_dict read per task): a task is returned only when its latest started attempt is done and recorded attempt_ok=False. That skips @catch-handled failures and tasks still running a retry, and drops the per-task successful/exception/finished artifact loads; the exception artifact is only read for the task that is returned.
  • failure_summary takes the attempt from the _exception artifact it reads, so under @retry it reports the attempt that actually raised rather than the latest started one.
  • Trimmed the user-facing docstrings.

Tests cover the retry-in-progress, @catch, fatal and single-artifact-read cases. The failing test job is the minio image pull from quay.io (unauthorized), not this change.

Comment thread metaflow/client/core.py
Comment on lines +2422 to +2426
if (
metadata.get("attempt_ok") == "False"
and metadata.get("attempt-done") is not None
and metadata.get("attempt") == metadata.get("attempt-done")
):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Handled Failures Are Excluded

When a decorator such as @catch handles a task failure, it persists attempt_ok as "True". This condition only selects tasks where attempt_ok == "False", so failed_task skips the handled failure and can return None even though the accessor's stated contract says these tasks count as failed. The revised test also explicitly expects this failure to be skipped.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants