taskSeq stops after the first await (Task.Yield/Task.Delay between yields) when the consuming project is built in Debug; Release works (TaskSeq 1.1.1, .NET SDK 10.0.400)

Aperta
#473 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
45/100
Tipo di issue
Bug
Chiarezza
Abbastanza chiara
Stato di attività
Attiva
Stack tecnologico
fsharp
Ambito
backend

Direzione di ricerca

Inizia con le espressioni taskSeq in Program.fs e confronta il comportamento generato con i flag del compilatore Debug e Release elencati nel report. Segui manualEnumeration attraverso GetAsyncEnumerator e MoveNextAsync, quindi aggiungi un test di regressione che copra gli awaits tra gli yields e la struttura del ciclo for. Il lavoro è completato quando l'enumerazione Debug restituisce ogni elemento previsto senza eccezioni o race condition.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug

Summary

With FSharp.Control.TaskSeq 1.1.1, a taskSeq that awaits between yields (do! Task.Yield() or do! Task.Delay n) stops after the first await when the consuming project is compiled in the Debug configuration. The same code enumerates every item in Release. TaskSeq.toListAsync, a manual GetAsyncEnumerator / MoveNextAsync loop and an ASP.NET Core response writer consuming the IAsyncEnumerable are all affected. A taskSeq without awaits between yields is fine.

Impact: dotnet test builds Debug by default, so tests that stream through a taskSeq silently observe only the first item. That is how I ran into it (an xunit test of a chunked HTTP writer that takes an IAsyncEnumerable).

Repro

Repro.fsproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="FSharp.Control.TaskSeq" Version="1.1.1" />
  </ItemGroup>
</Project>

Program.fs

open System.Threading.Tasks
open FSharp.Control

let awaitBetweenYields () =
    taskSeq {
        yield 1
        do! Task.Yield()
        yield 2
        do! Task.Delay 5
        yield 3
    }

let delayOnly () =
    taskSeq {
        yield 1
        do! Task.Delay 5
        yield 2
    }

let forLoopWithDelay () =
    taskSeq {
        for i in 1..3 do
            do! Task.Delay 1
            yield i
    }

let noAwaits () =
    taskSeq {
        yield 1
        yield 2
        yield 3
    }

let manualEnumeration (source: taskSeq<int>) =
    task {
        let e = source.GetAsyncEnumerator()
        let results = ResizeArray()
        while! e.MoveNextAsync() do
            results.Add e.Current
        return List.ofSeq results
    }

printfn "awaitBetweenYields toListAsync : %A" (awaitBetweenYields() |> TaskSeq.toListAsync).Result
printfn "awaitBetweenYields manual loop : %A" (manualEnumeration(awaitBetweenYields())).Result
printfn "delayOnly          toListAsync : %A" (delayOnly() |> TaskSeq.toListAsync).Result
printfn "forLoopWithDelay   toListAsync : %A" (forLoopWithDelay() |> TaskSeq.toListAsync).Result
printfn "noAwaits           toListAsync : %A" (noAwaits() |> TaskSeq.toListAsync).Result

Output:

$ dotnet run -c Debug
awaitBetweenYields toListAsync : [1]
awaitBetweenYields manual loop : [1]
delayOnly          toListAsync : [1]
forLoopWithDelay   toListAsync : []
noAwaits           toListAsync : [1; 2; 3]

$ dotnet run -c Release
awaitBetweenYields toListAsync : [1; 2; 3]
awaitBetweenYields manual loop : [1; 2; 3]
delayOnly          toListAsync : [1; 2]
forLoopWithDelay   toListAsync : [1; 2; 3]
noAwaits           toListAsync : [1; 2; 3]

No exception is thrown; MoveNextAsync simply returns false after the first await.

Environment

  • Windows 11 (10.0.26200), x64
  • .NET SDK 10.0.400, net10.0, FSharp.Core 10.1.400 (assembly 10.1.0.0, file version 10.104.26.38015)
  • Also reproduces with .NET SDK 11.0.100-preview.6, net11.0, FSharp.Core 11.0.0.0 (file version 11.1.126.36018)
  • FSharp.Control.TaskSeq 1.1.1

What I could narrow down

  • The awaitBetweenYields result is deterministic (3 of 3 runs each: Debug [1], Release [1; 2; 3]). The for shape flips between [] and [1] across Debug runs, so a race looks likely.
  • It is not the runtime or the dependencies: bin/Debug and bin/Release contain byte-identical FSharp.Core.dll and FSharp.Control.TaskSeq.dll, the deps.json files are identical and the runtimeconfig.json files differ only by MetadataUpdater.IsSupported. Copying the Release-compiled Repro.dll into the Debug output folder works; copying the Debug-compiled Repro.dll into the Release output folder fails. So it depends on how the consuming assembly (the one containing the taskSeq { } block) is compiled.
  • The full fsc argument diff between the two configurations is only: Debug --define:DEBUG --optimize- --tailcalls- -g versus Release --define:RELEASE --optimize+.
  • Building Release with the Debug flag set (dotnet run -c Release -p:Optimize=false -p:Tailcalls=false -p:DebugSymbols=true -p:DebugType=portable -p:DefineConstants=DEBUG) reproduces the bug. Building Debug with the Release flag set (dotnet run -c Debug -p:Optimize=true -p:Tailcalls=true -p:DebugSymbols=false -p:DebugType=none -p:DefineConstants=RELEASE) makes it work.
  • Flipping a single flag was not enough for me: Debug with -p:Optimize=true -p:Tailcalls=true (still -g) fails, Release with -p:Optimize=false -p:Tailcalls=false (no -g) works, and Debug with -p:DebugSymbols=false -p:DebugType=none (still --optimize-) fails.
  • The failing binaries carry DebuggableAttribute(IsJITOptimizerDisabled = true); the working ones do not.

Happy to provide anything else that helps.

Lingua principale
F#
Stelle
111
Fork
13
Merge medio
7h 59m
PR unite (30g)
3

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di fsprojects/FSharp.Control.TaskSeq

Tutte le issue di fsprojects/FSharp.Control.TaskSeq

Issue simili

Altre issue su Backend & API Design

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.