AsyncThrowingStream unexpectedly hanging

You can show this with the following program. The program creates two pipes (pipe1 & pipe2). It never writes into pipe1 but it does write to pipe2. Then it triggers a child task which reads from pipe1 and the main task read from pipe2.

The expected behaviour is: The read from pipe2 succeeds immediately and our program returns.

What happens in reality is that FileHandle.bytes (due to the aforementioned implementation problem) starts a single blocking read from pipe1 (which will never return of course, because there's nothing to read) and the would-be-successful read from pipe2 never even gets started -> deadlock.

import Foundation


func go() async throws {
    let pipe1 = Pipe()
    let pipe2 = Pipe()

    var it2 = pipe2.fileHandleForReading.bytes.makeAsyncIterator()

    async let _ = {
        // child task that reads from pipe1 (which we never write into)
        // so it'll just sit there
        var it1 = pipe1.fileHandleForReading.bytes.makeAsyncIterator()
        let chunk1 = try await it1.next() // will never complete
        print("weird, unexpectedly read from pipe1", chunk1.debugDescription)
        return chunk1
    }()

    try await Task.sleep(nanoseconds: 500_000_000) // some time for it1 to start

    pipe2.fileHandleForWriting.write(Data("hello\n".utf8))

    print("written into pipe2, waiting for bytes to be readable (this should be instant...")
    fflush(stdout)

    let chunk2 = try await it2.next() // should immediately complete
    print("read from pipe2 (expected to work instantly)", chunk2.debugDescription)

    // returning from this function Should cancel chunk1's child task
}

try await go()

actual output:

$ swiftc -o AsyncBytesRead test.swift && ./AsyncBytesRead
written into pipe2, waiting for bytes to be readable (this should be instant...
[... hang forever ...]

blocking read:

We can check what's going on if we sample the program by running sample AsyncBytesRead which contains

    8628 Thread_1558316
      8628 completeTask(swift::AsyncContext*, swift::SwiftError*)  (in libswift_Concurrency.dylib) + 1  [0x271f767bd]
        8628 partial apply for implicit closure #1 in go()  (in AsyncBytesRead) + 1  [0x1023a9d7d]
          8628 implicit closure #1 in go()  (in AsyncBytesRead) + 1  [0x1023a9bc5]
            8628 closure #1 in implicit closure #1 in go()  (in AsyncBytesRead) + 1  [0x1023aa0fd]
              8628 NSFileHandle.AsyncBytes.Iterator.next()  (in Foundation) + 1  [0x18b4db1ad]
                8628 _AsyncBytesBuffer.reloadBufferAndNext()  (in Foundation) + 1  [0x18b4d9805]
                  8628 partial apply for closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:)  (in Foundation) + 1  [0x18b4da665]
                    8628 closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:)  (in Foundation) + 76  [0x18b4da090]
                      8628 read  (in libsystem_kernel.dylib) + 8  [0x18969a7dc]   <<<--- BLOCKING READ

and swift inspect dump-concurrency AsyncBytesRead can show that we indeed have the expected two tasks (the main task and the async let child task) and Foundation.IOActor.

$ sudo swift inspect dump-concurrency AsyncBytesRead
TASKS
        
      Task 0x1 - flags=future|enqueued enqueuePriority=0x15 maxPriority=0x0 address=0x12c804ff0
        async backtrace: partial apply for closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:)
                         _AsyncBytesBuffer.reloadBufferAndNext()
                         NSFileHandle.AsyncBytes.Iterator.next()
                         go()
                         async_MainTQ0_
                         thunk for @escaping @convention(thin) @async () -> ()
                         partial apply for thunk for @escaping @convention(thin) @async () -> ()
                         completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*)
        resume function: closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:) in Foundation
        task allocator: 3048 bytes in 6 chunks
        * 1 child task
             
        `--Task 0x2 - flags=childTask|future|asyncLetTask|running enqueuePriority=0x15 maxPriority=0x0 address=0x12d009a90
             current task on thread 0x17c72c
             parent: 0x12c804ff0
             waiting on thread: port=0x2003 id=0x17c721
             resume function: closure #1 in NSFileHandle.AsyncBytes.Iterator.init(file:) in Foundation
             task allocator: 1272 bytes in 4 chunks

ACTORS
  0x600002f9c0e0 Foundation.IOActor state=idle flags=0 maxPriority=0x0
    no jobs queued

THREADS
  Thread 0x17c72c - current task: 0x2
5 Likes