microsoft / microsoft/agent-framework-durable-extension
Executor instances with the same name in different workflows silently collide
Nobody has claimed this yet.
- 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
-
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(); -
Register both:
options.AddWorkflow(a); options.AddWorkflow(b); -
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:
-
ExecutorRegistry.Register—TryAdddrops later registrations of the same name without error:this._executors.TryAdd(executorName, new ExecutorRegistration(executorId, binding)); -
ServiceCollectionExtensions.BuildWorkflowRegistrationRecursive— oneHashSet<string> registeredActivitiesis threaded through every workflow, and activity names are derived asdafx-{executorName}, sodafx-Stepis 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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