Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions proposals/nnnn-delayed-enqueuing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# Delayed Enqueuing for Executors

* Proposal: [SE-NNNN](NNNN-scheduling-executor.md)
* Authors: [Alastair Houghton](https://github.com/al45tair)
* Review Manager: TBD
* Status: **Pitch**
* Implementation: On main branch
* Review: ([first
pitch](https://forums.swift.org/t/pitch-custom-main-and-global-executors/77247))
([second pitch](https://forums.swift.org/t/pitch-2-custom-main-and-global-executors/78437))
([third pitch](https://forums.swift.org/t/pitch-3-custom-main-and-global-executors/80638))

## Introduction

(This was previously pitched as part of the Custom Main and Global
Executors proposal, but has been split out into a separate proposal
here.)

The Swift Concurrency runtime provides some C entry points that allow
code to schedule jobs on the global executor after a delay or at a
point in time. These entry points are not ideal; in particular:

* `swift_task_enqueueGlobalWithDelay` does not support tolerances,
will only use the continuous clock, and will only schedule on the
global executor

* `swift_task_enqueueGlobalWithDeadline` expresses the timestamp as
two `long long` values, one for seconds and one for nanoseconds,
using a similar format for the tolerance (or "leeway"). It only
supports the built-in clocks, and will only schedule on the global
executor.

The `Executor` protocol already provides a means for Swift code to
enqueue a job, and it makes sense that it should also provide a way
for Swift code to enqueue with a delay or deadline.

## Motivation

As part of the work on custom executors, we will need to be able to
implement the aforementioned entry points such that calling them will
hand off to the user's custom executor implementation.

We would also like to be able to support arbitrary `Clock`
implementations, rather than being stuck with either just the
`ContinuousClock`, or a choice of `ContinuousClock` and
`SuspendingClock`.

## Proposed solution

Since there are extant custom `Executor` implementations, we can't
easily extend `Executor` itself --- if we did so, the extension would
have to be optional, and then there would need to be a way to discover
that we didn't have the new methods. It seems better to add a new
protocol, `SchedulingExecutor`, that we can use to determine whether
or not a given executor supports the new API surface:

```swift
protocol SchedulingExecutor: Executor {
...
/// Enqueue a job to run after a specified delay.
///
/// You need only implement one of the two enqueue functions here;
/// the default implementation for the other will then call the one
/// you have implemented.
///
/// Parameters:
///
/// - job: The job to schedule.
/// - after: A `Duration` specifying the time after which the job
/// is to run. The job will not be executed before this
/// time has elapsed.
/// - tolerance: The maximum additional delay permissible before the
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if the "impermissible" happens and the time has exceeded the tolerance given? Is the job then never run, or is it run as soon as possible? Is there some sort of error raised?

Copy link
Contributor Author

@al45tair al45tair Nov 25, 2025

Choose a reason for hiding this comment

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

The behaviour I'd expect from an executor implementation is that it will try to run jobs before the deadline (e.g. a simple strategy might be to run the jobs in deadline order), but never before after, that job scheduling will be best effort and that all jobs will execute. So maybe "permissible" is the wrong word.

I can imagine that there might be a use-case for a hard deadline, for instance if you have a real-time scheduler, but I think we would want to add more enqueue methods to support that kind of behaviour (for example, I think you'd want an estimate of the time the job could run for, and you'd want the enqueue method to return a Bool to tell you whether it had actually been able to schedule the job). That's outside of the scope of this work though, I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense--just want to make sure that this is sufficiently specified.

/// job is executed. `nil` means no limit.
/// - clock: The clock used for the delay.
func enqueue<C: Clock>(_ job: consuming ExecutorJob,
after delay: C.Duration,
tolerance: C.Duration?,
clock: C)

/// Enqueue a job to run at a specified time.
///
/// You need only implement one of the two enqueue functions here;
/// the default implementation for the other will then call the one
/// you have implemented.
///
/// Parameters:
///
/// - job: The job to schedule.
/// - at: The `Instant` at which the job should run. The job
/// will not be executed before this time.
/// - tolerance: The maximum additional delay permissible before the
/// job is executed. `nil` means no limit.
/// - clock: The clock used for the delay..
func enqueue<C: Clock>(_ job: consuming ExecutorJob,
at instant: C.Instant,
tolerance: C.Duration?,
clock: C)
...
}
```

The reason for having both an `after delay:` method and an `at
instant:` method is that converting from a delay to an instant is
potentially lossy, and some executors may have direct support for one
_or_ the other, but not necessarily both.

As an implementer, you will only need to implement _one_ of the two
APIs to get both of them working; there is a default implementation
that will do the necessary mathematics for you to implement the other
one.

Using a cast to check that an executor conforms to
`SchedulingExecutor` is potentially expensive, so we will additionally
provide a new method on `Executor`:

```swift
protocol Executor {
...
/// Return this executable as a SchedulingExecutor, or nil if that is
/// unsupported.
///
/// Executors can implement this method explicitly to avoid the use of
/// a potentially expensive runtime cast.
var asSchedulingExecutor: (any SchedulingExecutor)? { get }
...
}
```

along with a default implementation that does the expensive cast. The
idea here is that a concrete `Executor` implementation can provide its
own implementation of `asSchedulingExecutor`, e.g.

```swift
class MyExecutor: SchedulingExecutor {
...
var asSchedulingExecutor: (any SchedulingExecutor)? {
self
}
...
}
```

which the compiler can optimize since it can see statically at compile
time that `MyExecutor` implements `SchedulingExecutor` and doesn't
have to do an expensive runtime search through the protocol
conformance tables.

Finally, to support these `Clock`-based APIs, we will add to the `Clock`
protocol as follows:

```swift
protocol Clock {
...
/// Run the given job on an unspecified executor at some point
/// after the given instant.
///
/// Parameters:
///
/// - job: The job we wish to run
/// - at instant: The time at which we would like it to run.
/// - tolerance: The ideal maximum delay we are willing to tolerate.
///
func run(_ job: consuming ExecutorJob,
at instant: Instant, tolerance: Duration?)

/// Enqueue the given job on the specified executor at some point after the
/// given instant.
///
/// The default implementation uses the `run` method to trigger a job that
/// does `executor.enqueue(job)`. If a particular `Clock` knows that the
/// executor it has been asked to use is the same one that it will run jobs
/// on, it can short-circuit this behaviour and directly use `run` with
/// the original job.
///
/// Parameters:
///
/// - job: The job we wish to run
/// - on executor: The executor on which we would like it to run.
/// - at instant: The time at which we would like it to run.
/// - tolerance: The ideal maximum delay we are willing to tolerate.
///
func enqueue(_ job: consuming ExecutorJob,
on executor: some Executor,
at instant: Instant, tolerance: Duration?)
...
}
```

There is a default implementation of the `enqueue` method on `Clock`,
which calls the `run` method; if you attempt to use a `Clock` with an
executor that does not understand it, and that `Clock` does not
implement the `run` method, you will get a fatal error at runtime.

Executors that do not specifically recognise a particular clock may
choose instead to have their `enqueue(..., clock:)` methods call the
clock's `enqueue()` method; this will allow the clock to make an
appropriate decision as to how to proceed.

### Embedded Swift

We will not be able to support the new `Clock`-based `enqueue` APIs on
Embedded Swift at present because it does not allow protocols to
contain generic functions.

## Source compatibility

There should be no source compatibility concerns, as this proposal is
purely additive from a source code perspective---all new protocol
methods will have default implementations, so existing code should
just build and work.

## ABI compatibility

On Darwin we have a number of functions in the runtime that form part
of the ABI and we will need those to continue to function as expected.

## Implications on adoption

Software wishing to adopt these new features will need to target a
Concurrency runtime version that has support for them.

## Future directions

This is a prerequisite for the custom main and global executors work.

## Alternatives considered

### Adding conversion functions and traits for `Clock`s

An alternative approach to the `clock.run()` and `clock.enqueue()`
APIs was explored in an earlier revision of this proposal; the idea
was that `Clock` would provide API to convert its `Instant` and
`Duration` types to those provided by some other `Clock`, and then
each `Clock` would expose a `traits` property that specified features
of the clock that could be matched against the support a given
executor might have for time-based execution.

The benefit of this is that it allows any executor to use any `Clock`,
albeit on a best-effort basis. The downside is that clock conversions
will necessarily be lossy in nature, and also would only work on the
assumption that `Clock` types were actually measuring time in a
similar manner (i.e. one second in `Clock` A is equal to one second in
`Clock` B). It might also result in unusual behaviour in some cases,
e.g. where an executor did not pay attention to some clock trait that
ordinarily would affect behaviour.

We decided after some discussion that it was better instead for
executors to know which `Clock` types they directly support, and in
cases where they are handed an unknown `Clock`, have the `Clock`
itself take responsibility for appropriately scheduling a job.

### Adding special support for canonicalizing `Clock`s

There are situations where you might create a derived `Clock`, that is
implemented under the covers by reference to some other clock. One
way to support that might be to add a `canonicalClock` property that
you can fetch to obtain the underlying clock, then provide conversion
functions to convert `Instant` and `Duration` values as appropriate.

After implementing this, it became apparent that it wasn't really
necessary and complicated the API without providing any significant
additional capability. A derived `Clock` can simply implement the
`run` and/or `enqueue` methods instead.

## Acknowledgements

Thanks to Cory Benfield, Franz Busch, David Greenaway, Rokhini Prabhu,
Rauhul Varma, Johannes Weiss, Matt Wright and John McCall for their
input on this proposal.