Skip to content

Add a withDeadline structured-concurrency helper - #432

Draft
eseay wants to merge 2 commits into
mainfrom
eddie/with-deadline-helper
Draft

Add a withDeadline structured-concurrency helper#432
eseay wants to merge 2 commits into
mainfrom
eddie/with-deadline-helper

Conversation

@eseay

@eseay eseay commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds withDeadline, a small structured-concurrency helper that races an operation against a deadline and cancels the loser. It's unused for now — a pure addition, laying groundwork for replacing TimeoutTimer's callback-based state machine in a later change.

  • withDeadline(_:operation:) in Deadline.swift races operation against Task.sleep in a TaskGroup, returning nil if the deadline wins.
  • Nothing else is modified; TimeoutTimer and its call sites are untouched.

Follow-up

TimeoutTimer.swift is a four-state machine that exists to reconcile a timer against callback-based ordering — sticky cancellation, a deinit-disarm, and a .canceled && timedOut → .deadlineExceeded correlation that's currently duplicated in two places in ProtocolClient.swift. None of that reconciliation is needed once callers can just await withDeadline(...): losing the race is directly observable, so there's nothing left to correlate.

The plan is for ProtocolClient's unary and streaming request paths to adopt withDeadline directly in a follow-up change. Once nothing references it, TimeoutTimer.swift and its dedicated unit tests get deleted outright. The existing end-to-end timeout test that exercises real behavior through ProtocolClient stays and gets adjusted in place, since it's pinning behavior rather than the state machine.

Keeping this PR scoped to just the helper — with no consumers yet — keeps it small and independently reviewable.

Test plan

  • swift build && swift test — 92/92 passing, suite completes in ~3s
  • DeadlineTests covers: operation wins, deadline wins, no deadline, negative-timeout clamping, operation cancellation on a lost race, and a regression check that a won race returns promptly rather than blocking for the full deadline
  • SwiftLint clean

@eseay
eseay force-pushed the eddie/with-deadline-helper branch from d2c7114 to 376a47d Compare July 31, 2026 15:57
@eseay
eseay requested a review from rebello95 July 31, 2026 16:06

@rebello95 rebello95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be great to see a (stacked?) PR demonstrating how this will be used before merging dead code

///
/// - returns: The result of `operation`, or `nil` if the deadline elapsed first.
func withDeadline<T: Sendable>(
_ timeout: TimeInterval?,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be non-optional, since this function provides no additive value in the nil case

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea we can leave this open until the other stuff is done. I like that idea.

@eseay
eseay marked this pull request as draft August 8, 2026 16:36
Adds the primitive that will replace TimeoutTimer's four-state machine
as the async migration proceeds. withDeadline races an operation
against a deadline in a task group and returns nil if the deadline
wins, canceling the operation.

TimeoutTimer exists to reconcile a timer against callback ordering,
hence its sticky-cancel invariant and deinit-disarm. Under structured
concurrency the race is the mechanism, and losing it is directly
observable, so the correlation currently duplicated at
ProtocolClient.swift:123 and :344 becomes unnecessary.

Pure addition: nothing calls this yet and no existing file is
modified. TimeoutTimer and its tests are untouched and will be
removed once ProtocolClient is inverted.

Covered by 6 tests, including a regression test asserting a won race
returns promptly -- without group.cancelAll() the helper would block
for the full deadline, which is otherwise invisible.

Signed-off-by: Eddie Seay <eddie.seay@cfacorp.com>
@eseay
eseay force-pushed the eddie/with-deadline-helper branch from 376a47d to 5abc5d9 Compare August 8, 2026 18:11
Addresses review feedback: the helper provided no additive value in the
nil case, where it just forwarded straight to operation().

Now that the first caller exists, the nil branch is the caller's
concern -- ProtocolClient's config.timeout is optional, so it unwraps
and picks between a deadlined and an undeadlined call rather than
pushing that choice down here. Drops the guard-let branch and the
test covering it.

Signed-off-by: Eddie Seay <eddie.seay@cfacorp.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants