[Concurrency] Structured concurrency

I'm glad the proposal addresses your questions @Paulo_Faria. :slight_smile:

While we're here I'd like to point out something I'm very excited about and we've been working towards for a while in the server side swift ecosystem.

Extra: Distributed Deadlines

I don't want to side track the discussion too much from the concurrency proposal itself, so I've folded away the details of the distributed deadlines, feel free to read if interested.

It makes use of the instrumentation ecosystem that we worked on with @slashmo in his Swift Tracing Google Summer of Code this year.

:bulb: This is not part of the proposal but will be a fantastic use-case for the deadline and task semantics proposed.

Click here to expand details about Distributed Deadlines, e.g. across HTTP calls etc.

The deadline model proposed for the language here fits in perfectly with distributed computing and best effort deadline based cancellations of distributed requests or calls as well it turns out (!). (The moment you work with actors you realize everything is just a small distributed system :wink:).

With swift-tracing (which we prototyped as summer of code with @slashmo: GitHub - slashmo/gsoc-swift-tracing: WIP collection of Swift libraries enabling Tracing on Swift (Server) systems.) we intentionally designed it to not just be about Tracer types, but also any Instrument.

Now, libraries like Async HTTPClient are going to be instrumented using swift-tracing instruments. These instruments perform the task of:

  • "inject metadata from current baggage context / task context into outgoing request"
  • "extract any known metadata into baggage context / task context from incoming request"

So, we can trivially implement an instrument that carries deadlines:

public struct SwiftDeadlinePropagationInstrument: Instrument {
    public func inject<Carrier, Inject>(
        _ baggage: Baggage, into carrier: inout Carrier, using injector: Inject
    ) async where Inject: Injector, Carrier == Inject.Carrier {
        let deadline = await Task.currentDeadline()
        let deadlineRepr = "\(deadline)"  // TODO: properly serialize rather than just string repr
        injector(deadlineRepr, forKey: "X-Swift-Deadline", into: carrier)
    } // HTTP request gained `X-Swift-Deadline: ...` header

    public func extract<Carrier, Extract>(
        _ carrier: Carrier, into baggage: inout Baggage, using extractor: Extract
    ) async where Extract: Extractor, Carrier == Extract.Carrier {
        if let deadlineRepr = extractor(key: "X-Swift-Deadline", from: carrier),
           let deadline: Deadline = try? parse(deadlineRepr) {
            baggage.deadline = deadline // TODO assign only if earlier than existing deadline
        }
    } // then HTTPClient does Task.withDeadline(baggage.deadline) { ... user code ... }
}

But that's just the internals.

What this means is this:

// client code
let response = await try Task.withDeadline(in: .seconds(2)) { 
  await try http.get("http://example.com/hello")
}

and a server receiving such request:

// some fictional HTTP server:
handle("/hello") { // automatically has deadline set (!)

  // if this were to exceed the deadline it could throw!
  let work = await try makeSomeWork() 

  return response(work)
}

So we're able to best-effort* cancel unnecessary remote work without any extra work! As long as libraries we use are instrumented using Swift Tracing and users opt-into the deadline propagation instrument -- deadline propagation works even across nodes.

This pattern is well known and proven in the Go ecosystem: gRPC and Deadlines | gRPC and we'll be able to adopt it without code-noise relying on the Task acting as our context propagation.

  • (yeah clock drift is a thing, so instruments can add some wiggle room time to the deadlines)
5 Likes