Skip to content

Conversation

@phausler
Copy link
Member

This is just for illustration for discussion about the Buffer proposal

@phausler phausler requested a review from FranzBusch November 23, 2022 00:36
func enqueue<Buffer: AsyncBuffer>(_ item: Input, buffer: Buffer) async where Buffer.Input == Input, Buffer.Output == Output {

func push(_ item: Input) async {
var buffer = self.buffer
Copy link

@tcldr tcldr Nov 23, 2022

Choose a reason for hiding this comment

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

I think there's probably a race occurring here. After the buffer is copied, this method can suspend on await buffer.push(). Then, while suspended, pop() could get called, make its own copy of the buffer, then suspend on await buffer.pop(). Now you have two copies of the buffer – one of which will overwrite the other in their respective defers.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea that is pretty much spot on. Hence why we can't let it be a structure. So I am at a loss on how it can be done other than requiring both the state container and buffer definition to be actors.

Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of using Actors. Is there a way we can provide the locking mechanisms to the developer so they can handle concurrent access in their implementation ?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think that handling out the locking mechanism out is a wise move.

The more I think about it, the more that there seems to me of two options having a buffer algorithm.

  1. Only offer the knobs of controlling a limit on the buffer but let anything else be left to the developer to write from scratch
  2. express the exclusivity between push and pop via a requirement to actor.

Copy link

@tcldr tcldr Nov 23, 2022

Choose a reason for hiding this comment

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

Personally, I think it would be simpler to not wrap it in the enclosing actor, and manage it as a struct with some managed critical state. The simplest protocol would probably end up being something like:

@rethrows
public protocol AsyncBuffer: Sendable {
  associatedtype Input: Sendable
  associatedtype Output: Sendable
  func push(_ element: Input) async
  func pop() async throws -> Output?
  func finish(throwing: Error?)
}

Note how the push and pop are not mutating. This makes it a bit tougher to just conform any old struct. It will need to have reference semantics in that regard.

As I mentioned on the Swift forums, whether we make it an actor or not, implementors will still need fair warning in the docs that this is fairly easy to mess up and the compiler won't ensure that they've done it correctly. For 1.0 though, in the interests of time, I would probably go with your option 1, @phausler.

Copy link

Choose a reason for hiding this comment

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

It should be no issue to add the member function with the AsyncBuffer factory later.

Copy link
Contributor

@twittemb twittemb Nov 23, 2022

Choose a reason for hiding this comment

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

Stupid question: why do we still need an actor (AsyncBufferState) in this implementation. Shouldn't we rely on the fact that AsyncBuffer is Sendable and that it is up to the developer to guarantee the safety for accessing its internal storage when conforming to the protocol?

Copy link

@tcldr tcldr Nov 24, 2022

Choose a reason for hiding this comment

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

I agree, @twittemb . But more broadly, I wonder if the buffer algorithm needs this level of customisation at all. I can only think of four buffering strategies that fit the definition of a 'buffer':

  1. unbounded
  2. earliest n
  3. latest n
  4. throughput n (back-pressure compliant buffer)

I think the broader goal of an algorithm that supports the processing of a sequence in arbitrary ways – like the game example described in the Buffer proposal – is tremendously exciting, but would be better served by an algorithm with a name that reflects that versatility. It would feel a little strange to process game moves in a Buffer, but acceptable in a Processor or a Subject.

My preference would be to simplify Buffer to the single generic parameter AsyncBufferSequence<Base: AsyncSequence & Sendable> with a range of buffering policies that can easily be extended if we see any other common buffering strategies requested/emerge.

And in the future, post 1.0, a generalised Processor/Subject algorithm would be able to support developers who wish to hand-roll a buffering strategy themselves.

@phausler phausler closed this Feb 10, 2023
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.

4 participants