Re: [PATCH] io_uring: clear IO_RING_F_DRAIN_ACTIVE on drain completion flush
From: Jens Axboe
Date: Tue Oct 20 2026 - 10:42:12 EST
On 10/19/26 8:46 PM, Wei Gao wrote:
> 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
Great thanks, done. Thanks for the report and initial patch
and the testing!
--
Jens Axboe