Lesson learned:

When writing a wrapper future or a wrapper stream, always remember the waker contract. When a future registers a waker and that waker fires, the runtime polls the outermost future, which eventually polls the wrapper. If your future swallows the poll without forwarding it, you’ll run into the same class of problems

https://www.e6data.com/blog/deadlocking-tokio-mutex-without-holding-lock

Deadlocking a Tokio Mutex without Holding a Lock | e6data

Learn why a Tokio mutex in async Rust can appear deadlocked even when unlocked, how a waker-contract violation traps permits, and how to fix it safely.