Re: [PATCH] io_uring: clear IO_RING_F_DRAIN_ACTIVE on drain completion flush
From: Wei Gao
Date: Mon Oct 19 2026 - 22:51:05 EST
On Mon, Oct 19, 2026 at 02:50:16PM -0600, Jens Axboe wrote:
> On 10/18/26 1:01 AM, Wei Gao wrote:
> > Fix:
> > Make io_queue_deferred() return a bool indicating whether any drain
> > request was active or deferred. In __io_submit_flush_completions(), track
> > whether any completing request had REQ_F_IO_DRAIN. Clear
> > IO_RING_F_DRAIN_ACTIVE only when a drained request has completed
> > (has_drain), no drain was active or deferred (!drain_dispatched), and
> > ctx->defer_list is empty. This prevents premature clearance if an
> > unrelated completion flushes while a drain request is still executing
> > in io-wq.
>
> Didn't quite feel right to me, below is with some LLM assistance
> to try and get to the bottom of it.
>
> Thanks for digging into this. I don't think it's just a race with a
> slow io-wq free, though. DRAIN_ACTIVE is still set when the next request gets initialized, so it's forced async and goes through the defer list
> every time, even if the drain request is freed in time.een read.
>
> Basing the clear off any REQ_F_IO_DRAIN request in the flush is not
> enough:
>
> - A drain request marked REQ_F_REISSUE counts as has_drain, but
> io_free_batch_list() punts it back to io-wq in that same flush. With
> an empty defer list, we clear DRAIN_ACTIVE while the drain is still
> pending.
>
> - A second IOSQE_IO_DRAIN request that fails init after io_init_drain()
> has run gets completed via io_req_defer_failed() right away. If that
> happens while an earlier drain is still running in io-wq, we end the
> drain, and the next request is issued before the earlier drain
> completes.
>
> It also only works if the drain request completes through the flush.
> Without DEFER_TASKRUN, io-wq posts the CQE directly from
> io_req_complete_post(), so the application can see the CQE and submit
> before anything clears the flag.
>
> A drain request dispatched from the defer list always runs alone, so
> we can just remember which one it is and end the drain when its CQE is
> flushed, and have io-wq complete drain requests via task_work so that
> happens under ->uring_lock. Something like the below. Can you give it
> a spin with your reproducer?
Thanks a lot for the thorough explanation and pointing out the edge cases.
Your proposed fix works as expected. I ran 10,000 iterations of test with
your patch applied, and all tests passed without hitting the timeout.
Feel free to add:
Tested-by: Wei Gao wegao@xxxxxxxx
>
>
> --
> Jens Axboe