microsoft / microsoft/agent-framework-durable-extension

Executor instances with the same name in different workflows silently collide

Open
#74 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

workflows
Dominant language
Python
Stars
16
Forks
10
Avg merge
3d 9h
Merged PRs (30d)
9

Description

Summary

Executors are registered and dispatched through a single global, name-keyed namespace with no workflow scoping. When two workflows contain different executor instances that happen to share a name, the second registration is silently dropped and that workflow's executor runs the first workflow's code.

Sharing one executor instance across multiple workflows is supported, works today, and must keep working. This issue is not about that case.

Same failure class as #50, but it needs no duplicate workflow names, so #66 does not address it. Spotted by @ahmedmuhsin while reviewing #66.

Repro

  1. Build two workflows, each with its own executor instance under the same name:

    var a = new WorkflowBuilder(new FunctionExecutor<string>("Step", HandlerA)).WithName("WorkflowA").Build();
    var b = new WorkflowBuilder(new FunctionExecutor<string>("Step", HandlerB)).WithName("WorkflowB").Build();
    
  2. Register both:

    options.AddWorkflow(a);
    options.AddWorkflow(b);
    
  3. Run WorkflowB.

Expected: either WorkflowB's Step runs HandlerB, or the conflict is reported at registration.

Actual: WorkflowB's Step runs HandlerA. No error at registration or at runtime.

workflows=2 executors=1
'Step' boundToA=True  boundToB=False
Contrast: the shared-instance case works
FunctionExecutor<string> shared = new("Step", Handler);
var a = new WorkflowBuilder(shared).WithName("WorkflowA").Build();
var b = new WorkflowBuilder(shared).WithName("WorkflowB").Build();

Also gives Executors.Count == 1, but here that is correct: binding.RawValue is reference-equal across both workflows, so collapsing to one registration preserves behavior. Core MAF places no ownership restriction on reusing an executor instance across workflows, unlike using a Workflow as a subworkflow of multiple parents.

RawValue reference equality is therefore what separates legitimate reuse from a real collision.

Root cause

Two places key on the bare executor name and keep the first writer:

  1. ExecutorRegistry.RegisterTryAdd drops later registrations of the same name without error:

    this._executors.TryAdd(executorName, new ExecutorRegistration(executorId, binding));
    
  2. ServiceCollectionExtensions.BuildWorkflowRegistrationRecursive — one HashSet<string> registeredActivities is threaded through every workflow, and activity names are derived as dafx-{executorName}, so dafx-Step is registered once globally against the first workflow's binding.

Dispatch is name-only, with no workflow context available to disambiguate:

string executorName = WorkflowNamingHelper.ToWorkflowName(activityFunctionName);
if (!durableOptions.Workflows.Executors.TryGetExecutor(executorName, out ExecutorRegistration? registration))

Impact

Silent and result-affecting rather than a startup failure — both workflows appear registered and runnable, and the problem surfaces only as wrong results at runtime. Names like Step, Start, Process, or Validate are natural choices and likely to collide across independently authored workflows.

Suggested fix

In ExecutorRegistry.Register, when executorName is already registered:

  • if ReferenceEquals(existing.Binding.RawValue, binding.RawValue), treat as reuse and no-op;
  • otherwise throw, naming the executor and both workflows.

This mirrors what #66 did for duplicate workflow names, keeps the shared-instance scenario working, and leaves derived activity names untouched so in-flight orchestration histories stay valid.

Scoping keys by workflow (e.g. {workflowName}/{executorName}) was considered and rejected: it changes every derived activity name, breaking in-flight orchestrations, and registers a shared executor once per referencing workflow for no benefit.

Caveat to settle first: an executor supplied per workflow via a factory produces distinct instances that are logically the same executor, and a reference check would reject it. Same limitation #66 has for calling Build() twice. If that pattern needs support, this needs an identity concept beyond reference equality.

Acceptance criteria

  • Reusing a single executor instance across multiple workflows continues to work, covered by a test.
  • Two workflows containing different executor instances that share a name no longer silently cross-wire; the conflict is reported at registration time, naming the executor and the workflows involved.
  • Derived activity names are unchanged, or any change is called out for backward compatibility with in-flight orchestrations.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ExecutorRegistry.Register and ServiceCollectionExtensions.BuildWorkflowRegistrationRecursive, then trace the name-only dispatch shown in the issue. Compare registrations by RawValue reference equality, preserving shared-instance reuse while reporting distinct-instance conflicts with both workflow names. Add coverage for both cases and verify derived activity names remain unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend, distributed-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.