From 4389a3bf8b71672e61650d457e5b58dacf9e0ef7 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Fri, 29 May 2026 01:06:16 +0300 Subject: [PATCH] x.async: respect timeout deadline when job wins race --- vlib/x/async/context.v | 2 + vlib/x/async/context_internal_test.v | 26 ++++++++++++ vlib/x/async/timeout.v | 62 +++++++++++++++++++--------- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/vlib/x/async/context.v b/vlib/x/async/context.v index 472545691..0e4377e97 100644 --- a/vlib/x/async/context.v +++ b/vlib/x/async/context.v @@ -29,6 +29,7 @@ mut: cancel_src CancelSource has_deadline bool deadline_at time.Time + deadline_src CancelSource } fn new_cancel_context(parent context.Context) (&AsyncContext, context.CancelFn) { @@ -61,6 +62,7 @@ fn new_timeout_context(parent context.Context, timeout time.Duration) (&AsyncCon mutex: sync.new_mutex() has_deadline: true deadline_at: deadline_at + deadline_src: deadline_src } if !ctx.propagate_existing_parent_error() { spawn watch_parent_context(mut ctx) diff --git a/vlib/x/async/context_internal_test.v b/vlib/x/async/context_internal_test.v index 2fb626d7a..d16cb1ff6 100644 --- a/vlib/x/async/context_internal_test.v +++ b/vlib/x/async/context_internal_test.v @@ -35,6 +35,32 @@ fn test_async_context_timeout_closes_done_and_sets_deadline_error() { } } +fn test_timeout_result_detects_only_owned_deadline_miss() { + mut ctx, cancel := new_timeout_context(context.background(), 1 * time.second) + defer { + cancel() + } + assert !TimeoutResult{ + finished_at: ctx.deadline_at.add(-1 * time.nanosecond) + }.finished_after_owned_timeout(ctx) + assert TimeoutResult{ + finished_at: ctx.deadline_at.add(1 * time.nanosecond) + }.finished_after_owned_timeout(ctx) + + mut background := context.background() + parent, parent_cancel := context.with_timeout(mut background, 1 * time.second) + defer { + parent_cancel() + } + mut child, child_cancel := new_timeout_context(parent, 2 * time.second) + defer { + child_cancel() + } + assert !TimeoutResult{ + finished_at: child.deadline_at.add(1 * time.nanosecond) + }.finished_after_owned_timeout(child) +} + fn test_async_context_parent_cancel_propagates_to_child() { parent, parent_cancel := new_cancel_context(context.background()) mut child, child_cancel := new_cancel_context(context.Context(parent)) diff --git a/vlib/x/async/timeout.v b/vlib/x/async/timeout.v index 31ca8d11e..7cf709fb4 100644 --- a/vlib/x/async/timeout.v +++ b/vlib/x/async/timeout.v @@ -4,7 +4,8 @@ import context import time struct TimeoutResult { - err IError = none + err IError = none + finished_at time.Time } // with_timeout runs f with a background context and returns an error if timeout expires first. @@ -40,29 +41,49 @@ pub fn with_timeout_context(parent context.Context, timeout time.Duration, f Job done_ch := ctx.done() select { result := <-result_ch { - if result.err !is none { - ctx_err := ctx.err() - if ctx_err !is none && ctx_err.msg() == context_deadline_exceeded - && result.err.msg() == context_deadline_exceeded { - if async_ctx.was_canceled_by_timeout() { - return error(err_timeout) - } - } - return result.err - } + handle_timeout_result(mut ctx, async_ctx, result)! return } _ := <-done_ch { - err := ctx.err() - if err !is none { - if err.msg() == context_deadline_exceeded && async_ctx.was_canceled_by_timeout() { - return error(err_timeout) - } - return err + handle_timeout_done(mut ctx, async_ctx)! + return + } + } +} + +fn handle_timeout_result(mut ctx context.Context, async_ctx &AsyncContext, result TimeoutResult) ! { + ctx_err := ctx.err() + if result.finished_after_owned_timeout(async_ctx) { + if ctx_err !is none && !async_ctx.was_canceled_by_timeout() { + return ctx_err + } + return error(err_timeout) + } + if result.err !is none { + if ctx_err !is none && ctx_err.msg() == context_deadline_exceeded + && result.err.msg() == context_deadline_exceeded { + if async_ctx.was_canceled_by_timeout() { + return error(err_timeout) } + } + return result.err + } +} + +fn handle_timeout_done(mut ctx context.Context, async_ctx &AsyncContext) ! { + err := ctx.err() + if err !is none { + if err.msg() == context_deadline_exceeded && async_ctx.was_canceled_by_timeout() { return error(err_timeout) } + return err } + return error(err_timeout) +} + +fn (result TimeoutResult) finished_after_owned_timeout(async_ctx &AsyncContext) bool { + return async_ctx.deadline_src == .timeout && (async_ctx.deadline_at < result.finished_at + || async_ctx.deadline_at == result.finished_at) } fn run_timeout_job(ctx context.Context, f JobFn, result_ch chan TimeoutResult) { @@ -71,9 +92,12 @@ fn run_timeout_job(ctx context.Context, f JobFn, result_ch chan TimeoutResult) { mut job_ctx := ctx f(mut job_ctx) or { result_ch <- TimeoutResult{ - err: err + err: err + finished_at: time.now() } return } - result_ch <- TimeoutResult{} + result_ch <- TimeoutResult{ + finished_at: time.now() + } } -- 2.39.5