Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Deadline algorithm #215

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

[WIP] Deadline algorithm #215

wants to merge 2 commits into from

Conversation

phausler
Copy link
Member

This is a really common need; to create a deadline for given work and throw if that deadline passes.

@phausler phausler added the v1.1 Post-1.0 work label Oct 14, 2022
@phausler phausler marked this pull request as draft October 14, 2022 18:45
Copy link
Member

@FranzBusch FranzBusch left a comment

Choose a reason for hiding this comment

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

I haven’t even seen this yet. I am a bit reluctant on this since it is called timeout however it doesn’t really do that since task cancellation is cooperative. The only real way to implement deadlines right now is to use unstructured concurrency.
Though I still agree this has value just unsure if we should call it timeout.

@ktoso
Copy link
Member

ktoso commented Oct 17, 2022

That's a bit of a misleading API... what people ask for and reinvent constantly currently is the unstructured task based one; this likely isn't what people expect by looking at the name.

We could tackle this by using a more descriptive name here, like withStructuredDeadline or something else to suggest that this is a structured concurrency thing, and thus WILL await anyway, but it'll cancel at least.

Perhaps we offer both... it is way easier to experiment in this package rather than in stdlib after all;


@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public func withTimeout<C: Clock, T: Sendable>(
in duration: C.Duration,
Copy link
Member

@ktoso ktoso Oct 17, 2022

Choose a reason for hiding this comment

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

I don't think such should be called "timeout", rather this still forms a deadline and should be called with...Deadline(in: .seconds...) because it is defining a deadline in some seconds, it is not a timeout "in". Specifically this is (in timeout: C.Duration where the now + timeout == deadline) wording wise.

Copy link
Member Author

Choose a reason for hiding this comment

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

isn't a deadline in 5 seconds just a timeout starting at now in 5 seconds?

Copy link
Member

Choose a reason for hiding this comment

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

I agree here with @ktoso that we should define this with the term deadline. The difference between a deadline and a timeout is quite important. A deadline can be reused and it will always be the same point in time w.r.t. the clock used. A timeout would need to recalculated against your current time. Furthermore, a deadline is less error prone since suspended tasks/threads won't influence the actual point in time calculation.

//===----------------------------------------------------------------------===//

@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
public struct TimeoutError<C: Clock>: Error {
Copy link
Member

Choose a reason for hiding this comment

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

For consistency this likely should be a DeadlineExceededError, we don't speak in terms of timeouts; timeout is the convenience API

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea that might be a better name.

@phausler
Copy link
Member Author

Per the structured versus unstructured case: it only means that the deadline passing and the work child task being cancelled only becomes an issue if that work child task is unresponsive for cancellation. The cases where this is an issue is where that child task is actually a function that does not participate in cooperative cancellation; e.g. it does not fully participate in swift concurrency. That all being said; I am not sure that a withDeadline that is structured would really be that great in that it will likely end up orphaning tasks when used.

@FranzBusch
Copy link
Member

Per the structured versus unstructured case: it only means that the deadline passing and the work child task being cancelled only becomes an issue if that work child task is unresponsive for cancellation. The cases where this is an issue is where that child task is actually a function that does not participate in cooperative cancellation; e.g. it does not fully participate in swift concurrency. That all being said; I am not sure that a withDeadline that is structured would really be that great in that it will likely end up orphaning tasks when used.

In general, I am with you here and I agree that we should offer this functionality; however, I think we really need to call out that this expects cooperative cancellation and might exceed the deadline. You never really know if some method that you call is cancelling at the first instance that cancellation is raised.

@Fab1n
Copy link

Fab1n commented Dec 21, 2022

This depends on iOS 16 and such, just because the dependency on the new clock feature. Would love to see a backport compatible version for iOS 13 and up.
What do you think?

@JJJensen
Copy link

JJJensen commented Dec 2, 2023

Would it make sense to have a variant that does not throw when the deadline is reached, but instead cancels the operation and then returns whatever "best effort" value the operation returns?

Like, e.g.:

    try await withThrowingTaskGroup(of: T.self) { group in
      group.addTask(operation: operation)
      group.addTask {
        try? await Task.sleep(until: deadline, clock: clock)
        throw SomePrivateError()
      }
      do {
        defer { group.cancelAll() }
        return try await group.next()!
      } catch is SomePrivateError { // The timing child ended first (the outer task may have been cancelled)
        return try await group.next()! // Allow operation() to return a best effort result after cancellation
      }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
v1.1 Post-1.0 work
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants