Repository navigation
Wrapping combinators (TaskSeq.map, taskSeq { for .. }) yield the final item twice over an external IAsyncEnumerable on a non-default TaskScheduler (Orleans grain) #452
Description
Summary
TaskSeq.map and taskSeq { for item in upstream do yield ... } over an externally produced IAsyncEnumerable<T> each yield the final item twice when the consumption runs on a non-default TaskScheduler — observed deterministically inside a Microsoft Orleans grain activation (Orleans' per-activation scheduler). Enumerating the same IAsyncEnumerable<T> directly (the exact await foreach desugaring: GetAsyncEnumerator / MoveNextAsync / Current / DisposeAsync) yields the correct count.
Environment
FSharp.Control.TaskSeq0.6.0- .NET 10 (
net10.0), F# - Reproduces identically on Microsoft Orleans 10.1.0 and 10.2.2 (the enumerable being wrapped is produced by Orleans'
IAsyncEnumerableGrainExtensionmachinery — batchedMoveNextpulls over grain calls)
The discriminator (what isolates it to the wrapping construct)
One grain method consumes the same 3-item upstream stream three ways and returns the three counts as an ordinary unary reply, so nothing about our own streaming-reply transport participates in the measurement:
let! direct = TaskSeq.toListAsync (upstream.watch (label, 3)) // plain enumeration of the source
let! viaMap =
upstream.watch (label2, 3)
|> TaskSeq.map (fun tick -> tick.note) // wrapped
|> TaskSeq.toListAsync
let! viaFor =
TaskSeq.toListAsync (taskSeq { // wrapped
for tick in upstream.watch (label3, 3) do yield tick.note
})Result, measured 2026-08-18, stable across runs and across both Orleans versions:
(List.length direct, List.length viaMap, List.length viaFor) = (3, 4, 4)
direct = 3 is correct; both wrapping forms report 4 items for a 3-item stream, the last item duplicated.
What tracing showed
Instrumenting all enumerator layers on the failing path localized it precisely: the taskSeq wrapper yielded one extra MoveNextAsync = true with a stale Current immediately after the inner enumerator had already answered false.
What we ruled out
- Our own enumerable implementation:
directenumeration is correct everywhere, including batched sources, empty sources, and a throwing producer. - The obvious structural suspects: four progressively more faithful offline models on the ordinary thread pool — an all-synchronous enumerator, a batched one, one whose terminating
MoveNextAsynccompletes asynchronously, and a full in-process re-implementation of the Orleans pull loop driving two stacked legs — none reproduced it. The trigger appears to require the customTaskScheduleran Orleans activation runs on.
Repro status (honest)
We could not reduce it to a standalone console repro — that is the main obstacle to a better report, and why this issue points at a live test instead. In our repository it reproduces deterministically:
- The discriminator test (asserts
direct = 3; deliberately does not pin the divergent value so an upstream fix cannot break our suite): https://github.com/Neftedollar/orleans-fsharp/blob/e31b2e3c5190acb1cbcaadd4078dccd8d67b6b95/tests/Orleans.FSharp.Integration/FunctionalPhaseFIntegrationTests.fs#L598-L627 - The in-grain probe that performs the three consumptions: https://github.com/Neftedollar/orleans-fsharp/blob/e31b2e3c5190acb1cbcaadd4078dccd8d67b6b95/tests/Orleans.FSharp.Integration/FunctionalPhaseFFixture.fs#L305-L324
To observe the raw counts: clone the branch, change the discriminator's assertion to print the tuple, and run dotnet test tests/Orleans.FSharp.Integration --filter "FullyQualifiedName~consuming an upstream stream inside a grain".
Happy to run any diagnostic build or instrumented package against this environment if that helps narrow it down.
Activity
Neftedollar commentedon Aug 18, 2026
Addendum — a second, independent manifestation of TaskSeq 0.6.0's missing dynamic path, hit in the same repository today:
.NET SDK 10.0.400's F# compiler sends
taskSeq { }bodies down the dynamic resumable-code path that SDK 10.0.201 compiled statically, and every enumeration then throws:Observed as a clean A/B: the identical code (a set of
taskSeqproducers withtry/finally, gates, andlet!-over-task bodies — e.g. https://github.com/Neftedollar/orleans-fsharp/blob/6d06fe8/tests/Orleans.FSharp.Integration/FunctionalPhaseFFixture.fs) is green on SDK 10.0.201 in both Debug and Release, and fails wholesale on 10.0.400 (GitHub'subuntu-latestrunners picked .400 up around 2026-08-12, which is what first surfaced it — CI floated10.0.xwhile local machines were on .201). Our mitigation is pinning the SDK to 10.0.201 (Neftedollar/orleans-fsharp#28), which is obviously temporary.Two asks, either of which unblocks unpinning:
TaskSeqBuilder.Run— theNotImplementedExceptionbranch — so compiler-driven static/dynamic flips stop being fatal;Happy to run candidate builds against the failing matrix; the repro is deterministic per SDK version.
majocha commentedon Aug 24, 2026
This looks like a compiler regression. Could you repost this issue to dotnet/fsharp?
dsyme commentedon Aug 24, 2026
/repo-assist Look into the bug in the dynamic implementation and see if you can fix it, else analyse it, add test cases even if failing (mark them skipped if so).
You may need to update SDK etc.
github-actions commentedon Aug 24, 2026
✓ Repo Assist completed successfully, see workflow run.
test: add regression tests investigating #452 dynamic-path duplicate-…
github-actions commentedon Aug 24, 2026
Merge pull request #458 from fsprojects/repo-assist/investigate-issue…
Neftedollar commentedon Aug 25, 2026
Thanks — I reran the real Orleans reproduction against the current released package instead of 0.6.0.
Matrix:
Both Orleans versions produce the same tuple:
So there are now two separate conclusions:
NotImplementedExceptionis gone with TaskSeq 1.1.1. We can move the repository from SDK 10.0.201 to 10.0.400, so I do not think a newdotnet/fsharpissue is warranted for the current released package.TaskSeq.map. ThetaskSeq { for x in source do yield x }manifestation is fixed. The standalone scheduler approximations added in [repo-assist] Investigate #452: add regression tests for dynamic-path scheduler bug #458 are useful guards, but they do not cover the real Orleans scheduling/pull path which still returns 4 here.The integration probe is in
FunctionalPhaseFIntegrationTests.fs, with the grain-side enumeration inFunctionalPhaseFFixture.fs. I have also run the exact probe in both supported Orleans matrix legs.I can extract those two actors into a small standalone Orleans repro project if that would make the remaining
TaskSeq.mapissue easier to debug.Neftedollar commentedon Aug 25, 2026
Important correction to my previous comment: an SDK A/B shows that the apparent
taskSeq { for ... }fix is specific to TaskSeq 1.1.1's dynamic path, and that path has two more serious Orleans regressions.Same package and Orleans code, changing only the SDK/compiler:
TaskSeq.maptaskSeq { for ... }asTaskSeqdeliveryThe two dynamic-path failures reproduce individually, not only in the full suite. With SDK 10.0.201 and the same TaskSeq 1.1.1 package, both pass together in 5 seconds.
So:
NotImplementedException, but the dynamic implementation is not yet safe for this Orleans workload.TaskSeq.mapandtaskSeq { for ... }.This changes my earlier conclusion: the SDK/compiler flip is the trigger, but the resulting failures are semantic differences in TaskSeq's dynamic implementation rather than only the old missing-implementation exception. I can extract the Orleans repro if useful.
Neftedollar commentedon Aug 25, 2026
Standalone reproduction is now public:
https://github.com/Neftedollar/taskseq-orleans-repro
It has no Orleans.FSharp package/project reference. A tiny C# boundary exists only so Orleans' official Roslyn generator can emit proxies/metadata; every TaskSeq producer and consumer is an F# delegate executed inside the real Orleans grain turn.
The public CI runs eight cells: SDK 10.0.201/10.0.400 × Orleans 10.1.0/10.2.2 × Debug/Release:
https://github.com/Neftedollar/taskseq-orleans-repro/actions/runs/32856726674
TaskSeq.maptaskSeq { for ... }finallyThe repository's normal tests characterize that table and stay green. Running with
-p:AssertCorrect=trueinstead asserts healthy(3,3,3)plus disposal reachingfinally, producing the corresponding failing tests and exact actual values.One correction to my previous A/B comment: the SDK 10.0.400 disposal regression is specifically the Debug/dynamic path in this minimized repro. Release takes the static path and behaves like 10.0.201. Also, the channel-backed
asTaskSeqdelivery symptom from the larger suite did not survive minimization, so this repository deliberately does not claim it.github-actions commentedon Aug 28, 2026
majocha commentedon Sep 6, 2026
Smoke tests now fail in debug configuration in VS test explorer. So I tried dotnet test with sdk 10.0.400, same failure. Wrong items produced, sequences differ.
github-actions commentedon Sep 18, 2026
🤖 This is an automated response from Repo Assist.
Follow-up on
@majocha's report (2026-09-06) that the smoke tests fail in Debug: I've confirmed this and root-caused it to be the same upstream compiler regression already tracked in #473, not a new symptom.Reproduction: with .NET SDK
10.0.400andglobal.jsontemporarily rolled forward to pick it up, the two existing regression tests inTaskSeq.Issue452.Tests.fs(TaskSeq.map/taskSeq { for .. }over an externalIAsyncEnumerableon a customTaskScheduler) both fail in Debug, returning[]instead of[10; 20; 30]— the sequence is truncated to nothing rather than duplicating the final item. The same tests pass in Release on the identical SDK. Repo-pinned SDK10.0.111passes in both configurations.This matches the dotnet/fsharp#20466 Debug-mode state-machine lowering bug (fix in progress at dotnet/fsharp#20469) documented in #473 and in this repo's new README "Known issues" section: any
taskSeq { }with awaits between yields, compiled with SDK10.0.400+ in Debug, can silently drop items. The Orleans "duplicated final item" symptom reported originally in this issue may be a distinct, real bug in this library's handling of externally-produced enumerators on non-default schedulers — but the newest failure mode reported here (wrong/missing items in Debug on10.0.400) is the external compiler bug, not something fixable in this repo's source.No source change is needed for the Debug/SDK-10.0.400 symptom — it will resolve once the upstream fix ships. The original Orleans duplication question (last confirmed reproducing only inside real Orleans grain scheduling, not in the standalone regression tests) remains open and still needs a maintainer or Orleans-environment reproduction to make further progress, as noted in the previous investigation.
Recommend keeping this issue open, but noting in the description/labels that the Debug-mode symptom is external (tracked via #473 / dotnet/fsharp#20466) while the original Orleans duplication remains unconfirmed outside Orleans itself.
Add this agentic workflow to your repo
To install this agentic workflow, run