Skip to content

Client parent_tasks/child_tasks resolve wrong tasks when a foreach index is a string prefix of another #3341

Description

@winklemad

Problem

LocalMetadataProvider.filter_tasks_by_metadata matches metadata values with re.match, which anchors at the start but not the end:

# metaflow/plugins/metadata_providers/local.py:264
regex = re.compile(pattern)
...
and regex.match(meta.get("value", ""))

Task.parent_tasks / child_tasks build the pattern from foreach-execution-path, so for a foreach with 11 or more items the pattern middle:1 also matches the values middle:10 and middle:11.

Reproduction

Local metadata, 12 item foreach, start -> middle -> tail -> join -> end:

class ForeachPrefixFlow(FlowSpec):
    @step
    def start(self):
        self.items = list(range(12))
        self.next(self.middle, foreach="items")
    # middle -> tail -> join -> end
python flow.py run --max-workers 4

Then through the Client API:

WRONG PARENTS  ForeachPrefixFlow/<run>/tail/14  fep=middle:1  -> ['middle:1', 'middle:10', 'middle:11']
1 of 12 tail tasks have the wrong parent count

Only the task whose path is a string prefix of another is affected. Every other index resolves to exactly one parent.

The spin CLI turns this into a hard failure:

$ python flow.py spin ForeachPrefixFlow/<run>/tail/14
[.../tail/0] Internal error:
[.../tail/0] Step tail is not a join step but it gets multiple inputs.
[.../tail/0] Task failed.

Control, same run, a non colliding task:

$ python flow.py spin ForeachPrefixFlow/<run>/tail/15     # fep=middle:0
Task finished successfully.

Possible fix

regex.fullmatch instead of regex.match on that line looks like it covers it. The two deliberate pattern shapes seem unaffected:

  • ".*" still matches everything, and it also short circuits earlier in the function
  • the foreach join pattern "<path>,.*" still matches deeper paths such as middle:1,inner:0

Applying that one change locally takes the run above from 1 of 12 tail tasks have the wrong parent count to 0 of 12, makes spin .../tail/14 finish successfully, and leaves the control task passing.

ServiceMetadataProvider forwards the pattern to /filtered_tasks?pattern=... rather than matching in process, so this is only about the local provider. Whether the same anchoring question applies on the service side is not something I can see from here.

Before I open a PR

plugins/metadata_providers/ is Core Runtime, and CONTRIBUTING asks for an acknowledged issue and an agreed approach first, so I would rather check than guess.

Is fullmatch the direction you want, or would you prefer the anchoring live in the pattern construction in client/core.py instead? Happy to write it either way. I have the reproduction flow and a unit test covering the prefix collision plus the .* and "<path>,.*" cases ready to go.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions