Skip to content

Commit 8252bde

Browse files
committed
Rework the conversions into a active clock accessor and add clock ids
1 parent a940305 commit 8252bde

2 files changed

Lines changed: 791 additions & 51 deletions

File tree

proposals/0526-deadline.md

Lines changed: 74 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ The fundamental entry point for working with deadlines is a single function: `wi
139139
/// - Returns: The result of the operation if it completes successfully before or after the deadline expires.
140140
///
141141
/// - Throws: The error thrown by the operation
142-
nonisolated(nonsending) public func withDeadline<Return: ~Copyable, Failure: Error, C: Clock>(
142+
nonisolated(nonsending) public func withDeadline<Return: ~Copyable, Failure: Error, C: Clock & Identifiable>(
143143
_ expiration: C.Instant,
144144
tolerance: C.Instant.Duration? = nil,
145145
clock: C = ContinuousClock(),
@@ -206,7 +206,7 @@ Constructing an instant every time is not per se the most terse; so a simple ext
206206
with the same compositional advantage as the primary entry point.
207207

208208
```swift
209-
nonisolated(nonsending) public func withDeadline<Return: ~Copyable, Failure: Error, C: Clock>(
209+
nonisolated(nonsending) public func withDeadline<Return: ~Copyable, Failure: Error, C: Clock & Identifiable>(
210210
in timeout: C.Instant.Duration,
211211
tolerance: C.Instant.Duration? = nil,
212212
clock: C = ContinuousClock(),
@@ -358,71 +358,92 @@ This error is propagated from whenever the task (or child task) is cancelled via
358358
The reason specified will then be available to the `CancellationError` and can be retrieved from the `reason`
359359
property on the cancellation error.
360360

361-
#### Current deadlines and Clock
361+
#### Accessing active deadlines
362362

363-
The deadline cannot be expressed as an existential since that would not be easily usable. So
364-
to access the current applied deadline a common accessor would need to be used.
365-
This most generally can be expressed as an earliest ContinuousClock instant representing
366-
the deadline. This will also resolve a long standing pain point for clocks for the conversion
367-
between reasonable time-bases.
368-
369-
To accomplish this a new required method will be added to the `Clock` protocol and
370-
a default implementation will then also be provided such that existing implementations
371-
wont break but will have a sensible default that can be customized to something more
372-
appropriate.
363+
External systems may need to interoperate with active deadlines. This means that the
364+
applied deadline needs to be retrievable, however this particularly becomes
365+
tricky since the clock is generic for the deadlines. To that end
366+
the accessor for the active deadline is generic for a clock type
373367

374368
```swift
375-
public protocol Clock<Duration>: Sendable {
376-
associatedtype Duration
377-
associatedtype Instant: InstantProtocol where Instant.Duration == Duration
378-
379-
var now: Instant { get }
380-
var minimumResolution: Instant.Duration { get }
381-
382-
func sleep(until deadline: Instant, tolerance: Instant.Duration?) async throws
369+
extension Task where Success == Never, Failure == Never {
370+
public static var hasActiveDeadline: Bool { get }
383371

384-
// New addition:
385-
func earliestContinuousInstant(from instant: Instant) -> ContinousClock.Instant
372+
public static func activeDeadline<C: Clock & Identifiable>(for clock: C) -> C.Instant?
386373
}
374+
```
387375

388-
extension Clock {
389-
public func earliestContinuousInstant(from instant: Instant) -> ContinuousClock.Instant { .now }
390-
}
376+
If any deadline is active then the static property `hasActiveDeadline` returns true.
377+
This only applies to the current task if the execution of that task is within a call
378+
to `withDeadline`. This allows for determining the return of the `deadline` static
379+
function to be used to know if a known clock has a value being applied as a current
380+
deadline. This does mean that the usage must be aware of the potential clocks being
381+
used. This is however a requirement since to use the deadline itself the clock must
382+
be known for any sort of usage to an external system.
391383

392-
extension ContinuousClock {
393-
public func earliestContinuousInstant(from instant: Instant) -> ContinuousClock.Instant { instant }
394-
}
395384

396-
extension SuspendingClock {
397-
public func earliestContinuousInstant(from instant: Instant) -> ContinuousClock.Instant {
398-
let delta = instant - .now
399-
return ContinousClock.now + delta
385+
```swift
386+
if Task.hasActiveDeadline {
387+
if let deadline = Task.activeDeadline(for: ContinuousClock()) {
388+
// use the deadline as a ContinuousClock.Instant
389+
}
390+
if let deadline = Task.activeDeadline(for: SuspendingClock()) {
391+
// use the deadline as a SuspendingClock.Instant
400392
}
401393
}
402394
```
403395

404-
This will mean that ContinousClock and SuspendingClock will gain new implementations
405-
of this which will then return the earliest instant represented in reference to the
406-
continuous clock.
407-
408-
This all then means that the deadline APIs can use that implementation to vend
409-
out a earliest continuous deadline, alleviating the issues around existential `InstantProtocol`
410-
types and providing a reasonable frame of reference for expiration.
396+
When the call to `activeDeadline(for:)` is made, the query looks up the most narrow
397+
application of any specified deadline with that clock, if the current nesting of
398+
`withDeadline` calls does not used the specified clock type then the next nesting
399+
up the call stack is used.
400+
401+
If the nesting of `withDeadline` is stacked with a `ContinuousClock` deadline of
402+
"in two seconds" and then a `SuspendingClock` of "in three seconds" and a new
403+
nesting is made of a `ContinuousClock` is made for "in 10 seconds" the last
404+
10 seconds is known to be less narrow than the outer 2 seconds continuous clock
405+
deadline. This means that within the scope of the "in 10 seconds" deadline the query
406+
for the `activeDeadline(for: ContinuousClock.self)` would return the deadline of within
407+
2 seconds and the `activeDeadline(for: SuspendingClock.self)` would return the deadline
408+
of within 3 seconds. Since clock instants cannot be compared without potentially
409+
arbitrarily lossy conversions it means that the query for the current applied deadline
410+
is only accurate to the specific clock type given.
411+
412+
#### Identification of Clocks for coalescing
413+
414+
The expected behavior when setting a deadline is that any active deadline, given a specific clock,
415+
will always apply with the most narrow deadline available. Specifically if a deadline is
416+
active for an expiration of `in 10 seconds` and a new deadline is applied for `in 5 seconds`
417+
relative both relative to the continuous clock, then the applied deadline within the new
418+
scope is the `in 5 seconds`. Likewise if the reverse was applied; where it is already at `in 5 seconds`
419+
and a new scope is applied to `in 10 seconds` both on the continuous clock, then the internal
420+
logic will effectively skip the `in 10 seconds` since that deadline is known to beyond the current
421+
active deadline. This must have some way of determining if a given clock passed in to
422+
the `withDeadline` functions is that same specific clock. To that end, the clocks are
423+
required to be identified. The two major clocks; `ContinuousClock` and `SuspendingClock`
424+
both will gain a new conformance to `Identifiable` and each of which will have a new ID
425+
type of `SystemClockID`. As a side effect this means that new APIs can be written as:
426+
`where C: Clock & Identifiable, C.ID == SystemClockID`. That particular refinement not only
427+
allows for the direct identification of the specific clock but also a constraint to the
428+
standard system clocks.
429+
430+
Note: since the system clock may grow additional identifiers it is left as non-exhaustive.
411431

412432
```swift
413-
extension Task where Success == Never, Failure == Never {
414-
public static var currentEarliestContinuousDeadline: ContinuousClock.Instant? { get }
433+
@nonexhaustive
434+
public enum SystemClockID: Hashable {
435+
case continuous
436+
case suspending
415437
}
416-
```
417438

418-
This accessor does not preclude any potential more rich type information for deadlines
419-
and clocks being added later, but provides a straightforward interface for interoperation
420-
with external systems. The Task deadline can only be reasonable vended to the current task
421-
since that value would be only transient upon a given deadline - therefore accessing it
422-
externally would be rather prone to race conditions. The per task lookup will traverse
423-
the nested deadlines and calculate the earliest of all of the applied deadlines that would
424-
fire per the ContinuousClock. By doing so this leaves the least inaccuracy of measurement
425-
(and control by custom clocks as well) to translate to existing external systems.
439+
extension ContinuousClock: Identifiable {
440+
public var id: SystemClockID { .continuous }
441+
}
442+
443+
extension SuspendingClock: Identifiable {
444+
public var id: SystemClockID { .suspending }
445+
}
446+
```
426447

427448
### Behavioral Details
428449

@@ -795,6 +816,8 @@ nesting. This approach was not taken because:
795816
explicit `withDeadline` API would remain useful even if such a mechanism were added.
796817

797818
## Changelog
819+
- 1.2 Revisde for feedback
820+
- Added accessors to add a way to access the active deadlines
798821
- 1.1 Returned for revision
799822
- The typed throws signature was altered to avoid an extra error type
800823
- Removed the restriction around the instant requiring the duration type to be `Swift.Duration`

0 commit comments

Comments
 (0)