Skip to content

Restore ordered delivery of repository and registry delegate callbacks - #10408

Merged
plemarquand merged 2 commits into
swiftlang:mainfrom
plemarquand:serial-delegate-calls
Aug 17, 2026
Merged

Restore ordered delivery of repository and registry delegate callbacks#10408
plemarquand merged 2 commits into
swiftlang:mainfrom
plemarquand:serial-delegate-calls

Conversation

@plemarquand

@plemarquand plemarquand commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Motivation:

RepositoryManager and RegistryDownloadsManager fired each delegate callback from its own unstructured Task:

    Task { await delegate?.willFetch(...) }
    ...
    Task { await delegate?.didFetch(...) }

Two independently created tasks start in whatever order the executor picks, so nothing stopped didFetch starting before willFetch. The actor proxy they awaited serializes the calls it receives, but it can't do anything about the order they arrive in.

Before #8721 these went through delegateQueue.async { ... } and every caller passed .sharedConcurrent. A concurrent queue dequeues FIFO, so delivery started in order while the callbacks still ran independently. Moving to Task dropped the ordering and didn't put anything back.

Modifications:

Add SerialEventQueue to Basics, basically an AsyncStream that a single task drains, holding the delegate it delivers to. emit yields into the stream and returns, so it doesn't block the caller, and the drain task calls the delegate one event at a time in the order they were emitted.

Both managers hold one directly now and emit through it. That replaces the two DelegateProxy types (which had turned into pass-throughs restating every protocol method).

Result:

Callbacks are delivered in the order they were emitted, so didFetch can no longer land before willFetch.

The tradeoff is that a slow delegate now delays the callbacks queued behind it, which the old .sharedConcurrent dispatch didn't do. Ordering callbacks means waiting on them, so I dont think theres a way around that short of a queue per package. These delegates format a string and write a line, so realistically it shouldn't matter.

Also stops allocating when theres no delegate. Task { await delegate?.foo() } allocated and enqueued a task even when delegate was nil, which the registry progress handler did once per chunk received.

Add `SerialEventQueue` to Basics, basically an AsyncStream that one task
drains in order, holding the delegate it delivers to. `emit` returns as soon
as the event is queued, so it doesn't block the caller, and delivery is
serialized so callbacks arrive in the order they were emitted.

Both managers hold one directly now and emit through it, which replaces the
two `DelegateProxy` types (pass-throughs restating every protocol method by
that point).

Callbacks are now delivered in emission order. The tradeoff is that a slow
delegate delays the callbacks queued behind it, which the old
`.sharedConcurrent` dispatch didn't do; thats the cost of ordering them, and
these delegates just format a string and write a line so it shouldn't matter
in practice.

Also stops allocating when theres no delegate. `Task { await delegate?.foo() }`
allocated and enqueued a task even when delegate was nil, which the registry
progress handler did once per chunk received. `delegate?.emit` bails out first.
@plemarquand
plemarquand force-pushed the serial-delegate-calls branch from 61873c3 to f635f8e Compare August 13, 2026 17:34
@plemarquand

Copy link
Copy Markdown
Contributor Author

@swift-ci test

@plemarquand

Copy link
Copy Markdown
Contributor Author

@swift-ci test windows platform

let (stream, continuation) = AsyncStream.makeStream(of: Event.self, bufferingPolicy: .unbounded)
self.events = continuation

Task {

@jakepetroules jakepetroules Aug 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This Task should be saved as an ivar and cancelled on deinit. Also, it'd be ideal if you could provide a function that allows the caller for wait for the task to finish.

@plemarquand
plemarquand force-pushed the serial-delegate-calls branch from f643680 to dc0e65b Compare August 14, 2026 20:41
@plemarquand

Copy link
Copy Markdown
Contributor Author

@swift-ci test

@plemarquand

Copy link
Copy Markdown
Contributor Author

@swift-ci test windows

1 similar comment
@plemarquand

Copy link
Copy Markdown
Contributor Author

@swift-ci test windows

@plemarquand

Copy link
Copy Markdown
Contributor Author

@swift-ci test windows platform

1 similar comment
@plemarquand

Copy link
Copy Markdown
Contributor Author

@swift-ci test windows platform

@plemarquand
plemarquand merged commit 9074024 into swiftlang:main Aug 17, 2026
181 of 185 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants