[Pitch] Task Naming API

Hi Swift Evolution,

The problem we are trying to solve with this pitch is that Swift Concurrency currently has no mechanism for labeling a Swift Task.

We propose adding APIs to allow for the creation of Swift Tasks with names. These names would be readable by a developer's own code and could be used by runtime-analysis tools, such as a debugger, in order to aid developers.

The proposal PR is up here.

7 Likes

Are the older "nameless" versions of these APIs going to be deprecated (or made ABI-only entrypoints)?

I think there are corner cases where two non-deprecated APIs that differ only in having or not having an extra defaulted argument becomes ambiguous. Plus, it'd be API surface area that we wouldn't have if we were starting from the beginning.

If, on the other hand, there are important reasons why the "nameless" versions need to continue on as first-class API, then I think these should not have a default value for name (such that omitting it always means that the "nameless" version is invoked unambiguously).


"Current name" implies that there could be other, different past or future names, but as far as I can tell the task name cannot be modified, and certainly not through this get-only property?

2 Likes

For what it's worth we've already had these situations in the past and we kept both declarations so far. When we added taskExecutor we added the new one, with "new" availability.

  @available(SwiftStdlib 6.0, *)
  @discardableResult
  @_alwaysEmitIntoClient
  public static func detached(
    executorPreference taskExecutor: (any TaskExecutor)?,
    priority: TaskPriority? = nil,
    operation: sending @escaping () async -> Success
  ) -> Task<Success, Failure>

We didn't do anything special for them, nor did we remove the old overloads. I think we'd probably do the same here.

This example (if I'm not missing something) shows executorPreference without a default value. My specific feedback here is that an overload with additional argument that is defaulted may pose problems with ambiguity.

3 Likes

This is following existing naming convention on Task APIs:

var currentPriority: TaskPriority 
public func withUnsafeCurrentTask<T>(body: (UnsafeCurrentTask?) throws -> T) rethrows -> T

where "current" means "take it from the executing (current) context".

Although this convention was broken by basePriority but the introduction of that property had some process issues actually... I'd prefer to stick to the "current" meaning, but it's up for discussion of course.

It's also commonly used in Thing.current in task-local variables.

In "current task," the adjective modifies "task"; in "current name," the adjective modifies "name."

Unless I'm mistaken, the proposed API is an instance property on a specific task, whether current or not. If I have an instance of type Task, can it have a past or future name?

4 Likes

I see, yes that's a good point -- the proposal should be clear that we're adding an overload without a default value which hopefully would not conflict...

It's quite tricky as we keep adding more overloads of those APIs but we'll have to figure it out like that.

1 Like

I see the confusion.

The proposal needs to be more clear here; if we're to follow existing terminology it would be:

static var currentName: String? { get } 
var name: String? { get } 
2 Likes

In that case, I think it's fine :slight_smile:

The pitched design does (explicitly!) have a default value on all the proposed name arguments, though.

1 Like

Thanks for the review, let me cleanup some of that.

// edit: I proposed some of the fixups into @harjas 's PR :+1:

1 Like

That makes sense to me!

1 Like

I am very much in support of this proposal. While the motivation calls out its usefulness in debugging my use-case is around testing. With the introduction of TaskExecutors it is almost possible to write a deterministic executor that can be used for testing small isolated pieces of code that depend on a precise execution order of jobs. Below is a prototype of such an executor using task IDs for defining the order.

TestExecutor
import Dispatch

final class TestExecutor: TaskExecutor, @unchecked Sendable {
    private let queue = DispatchSerialQueue(label: "queue")

    var order = [UInt64]()
    var jobs = [UInt64: UnownedJob]()

    init(taskIDOrder: [UInt64]) {
        self.order = Array(taskIDOrder.reversed())
    }

    func enqueue(_ job: consuming ExecutorJob) {
        let unownedJob = UnownedJob(job)
        let taskID = _getJobTaskId(unownedJob)
        print("Enqueue job \(taskID)")
        queue.async {
            if self.order.last == taskID {
                print("Running job \(taskID)")
                self.order.removeLast()
                unownedJob.runSynchronously(on: self.asUnownedTaskExecutor())
                print("Done running job \(taskID)")

                while let nextJobId = self.order.last, let nextJob = self.jobs[nextJobId] {
                    self.order.removeLast()
                    self.jobs.removeValue(forKey: nextJobId)

                    print("Running job \(nextJobId)")
                    nextJob.runSynchronously(on: self.asUnownedTaskExecutor())
                    print("Done running job \(taskID)")
                }
            } else {
                print("Queuing job \(taskID)")
                self.jobs[taskID] = unownedJob
            }
        }
    }
}

@_silgen_name("swift_task_getJobTaskId")
internal func _getJobTaskId(_ job: UnownedJob) -> UInt64

Now the problem with that is that task IDs are monotonically increasing throughout the lifetime of a process making them really unfit to derive an order. However, the proposed task names here would be a great fit. The only thing that is missing in this proposal is to add APIs to Unowned/ExecutorJob to query for the task name of the job.

5 Likes