From 665d757dce3d6093e3f7ca851463e75c3dfb72a1 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 7 Jun 2026 16:57:18 +0530 Subject: [PATCH] fix(ioscompanion): classify connection deadline expiry as caller budget --- internal/driver/ioscompanion/transport/runner.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/driver/ioscompanion/transport/runner.go b/internal/driver/ioscompanion/transport/runner.go index a62915e..af12f81 100644 --- a/internal/driver/ioscompanion/transport/runner.go +++ b/internal/driver/ioscompanion/transport/runner.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "net" + "os" "os/exec" "sync" "time" @@ -132,13 +133,18 @@ func (c *runnerCompanion) call(ctx context.Context, method string, params map[st // wrapTransport classifies a read/write failure. A caller-imposed cancel or // deadline is the caller's slowness budget, not a connection loss, so it does // not carry the unavailable sentinel: a child restart would not make the call -// faster. Either way the connection is desynced and reconnects on the next -// call. +// faster. The connection's own deadline is only ever set from the caller's +// context, so a deadline-exceeded network error is the same budget expiry +// even when it beats the context's done flag by a hair. Either way the +// connection is desynced and reconnects on the next call. func (c *runnerCompanion) wrapTransport(ctx context.Context, stage, method string, err error) error { c.dirty = true if ctxErr := ctx.Err(); ctxErr != nil { return fmt.Errorf("runner %s interrupted (%s): %w", method, stage, ctxErr) } + if errors.Is(err, os.ErrDeadlineExceeded) { + return fmt.Errorf("runner %s interrupted (%s): %w", method, stage, err) + } return fmt.Errorf("runner transport: %w: %s %s: %v", ErrCompanionUnavailable, stage, method, err) }