Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Async body currency type #46

Closed
wants to merge 2 commits into from

Conversation

czechboy0
Copy link
Contributor

Motivation

An early prototype of #9.

Modifications

TODO

Result

TODO

Test Plan

TODO

import struct Foundation.Data // only for convenience initializers

/// The type representing a request or response body.
public final class Body: @unchecked Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this a class? Also can we get rid of the @unchecked Sendable here and rather make ourselves a LockedValueBox abstraction that is conditionally Sendable when its contents are. This way we are making sure we really are Sendable

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is this a class?

Because the body is inherently a reference type that cannot be freely copied.

Also can we get rid of the @unchecked Sendable here and rather make ourselves a LockedValueBox abstraction that is conditionally Sendable when its contents are. This way we are making sure we really are Sendable.

Sure, that's an option that we can take as well, fortunately that won't affect the public API, so we can do that at any time.

public typealias DataType = ArraySlice<UInt8>

/// How many times the provided sequence can be iterated.
public enum IterationBehavior: Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

This probably should be a struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because API evolution? If so, I'd happily mark this as @frozen or give some other hint that we won't be expanding the enum.

///
/// Supports retries and redirects, as a new iterator is created each
/// time.
case multiple
Copy link
Member

Choose a reason for hiding this comment

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

Are we ever expecting an AsyncSequence based body to be multiple here? Right now that just doesn't exist in the ecosystem.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, for example any body created from a sync collection can be iterated multiple times. An async sequence for reading a file could also be legal to iterate multiple times. Basically, we want to provide the tools for those who want to support e.g. retryable file uploads.

public let iterationBehavior: IterationBehavior

/// The total length of the body, if known.
public enum Length: Sendable {
Copy link
Member

Choose a reason for hiding this comment

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

Same here. Probably a struct

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because API evolution? If so, I'd happily mark this as @frozen or give some other hint that we won't be expanding the enum.

}
}

extension Body: Equatable {
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure if we really need Equatable and Hashable on the body

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do, as this type will be owned by generated Equatable/Hashable types.

Comment on lines 370 to 390
public convenience init(
stream: AsyncThrowingStream<some StringProtocol, any Error>,
length: Body.Length
) {
self.init(
sequence: .init(stream.map(\.asBodyChunk)),
length: length,
iterationBehavior: .single
)
}

public convenience init(
stream: AsyncStream<some StringProtocol>,
length: Body.Length
) {
self.init(
sequence: .init(stream.map(\.asBodyChunk)),
length: length,
iterationBehavior: .single
)
}
Copy link
Member

Choose a reason for hiding this comment

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

Same here as above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same answer - iterationBehavior: .single is why this initializer is helpful.

Comment on lines 451 to 461
/// Accumulates the full body in-memory into a single buffer
/// up to `maxBytes`, converts it to Foundation.Data, and returns it.
/// - Parameters:
/// - maxBytes: The maximum number of bytes this method is allowed
/// to accumulate in memory before it throws an error.
/// - Throws: `TooManyBytesError` if the the body contains more
/// than `maxBytes`.
public func collectAsData(upTo maxBytes: Int) async throws -> Data {
let bytes: DataType = try await collect(upTo: maxBytes)
return Data(bytes)
}
Copy link
Member

Choose a reason for hiding this comment

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

IMO this doesn't carry its weight for me since it is a one-liner for the user.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was meant to mirror collectAsString in a way, and tries to help with systems where Data is the very common data type (iOS apps). Plus, it'll be called from generated code, from where we really try to only make simple calls, as opposed to embedding too much logic.

Comment on lines 479 to 481
self.produceNext = {
try await iterator.next()
}
Copy link
Member

Choose a reason for hiding this comment

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

Why are we not storing the iterator here but a closure?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The intent here is type-erasure, is there a better way than to go through a closure?

Copy link
Member

Choose a reason for hiding this comment

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

Even if AsyncIteratorProtocol had primary associated type, we would prefer a closure as it will get specialised but an existential not, as of today.

typealias AsyncIterator = Body.Iterator
typealias Element = DataType

private let produceIterator: () -> AsyncIterator
Copy link
Member

Choose a reason for hiding this comment

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

Same here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}

/// A wrapper for a sync sequence.
private struct WrappedSyncSequence<S: Sequence>: AsyncSequence
Copy link
Member

Choose a reason for hiding this comment

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

FWIW we have this in async-algorithms.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

@dnadoba dnadoba left a comment

Choose a reason for hiding this comment

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

@inlinable needs to be added to everything that is generic or it will be very slow

Comment on lines +218 to +242
public func mapChunks(
_ transform: @escaping @Sendable (Element) async -> Element
) -> HTTPBody {
let validatedTransform: @Sendable (Element) async -> Element
switch length {
case .known:
validatedTransform = { element in
let transformedElement = await transform(element)
guard transformedElement.count == element.count else {
fatalError(
"OpenAPIRuntime.HTTPBody.mapChunks transform closure attempted to change the length of a chunk in a body which has a total length specified, this is not allowed."
)
}
return transformedElement
}
case .unknown:
validatedTransform = transform
}
return HTTPBody(
sequence: map(validatedTransform),
length: length,
iterationBehavior: iterationBehavior
)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

What's the use case for this method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

All these use cases (streaming encryption/unencryption/zipping/unzipping) will not work as they will all change the chunk length. These are also stateful operations and the @Sendable requirement will make this needlessly hard. In addition they will usually buffer data and will need an explicit flush at the end which is not possible with this API. These use cases should just be modelled as AsyncSequence operators/transformers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I can be convinced, I'll remove the API, it's not pulling its weight.

// MARK: - String-based bodies

extension StringProtocol {
fileprivate var asBodyChunk: HTTPBody.DataType {
Copy link
Member

Choose a reason for hiding this comment

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

@inlinable or otherwise quite slow as this is a generic over StringProtocol

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is an internal symbol, I thought the compiler doesn't need inlinable annotations for internal symbols, only public ones? (It should be inlining these even without the annotation I thought.)

Copy link
Member

Choose a reason for hiding this comment

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

You call this in a public generic function (HTTPBody.init(data: some StringProtocol, ...)) which forwards the generic type to this function call. Therefore it needs to be @inlinable or it can't be specialised.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would @_usableFromInline be enough here?

Copy link
Member

Choose a reason for hiding this comment

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

No, it will allow it to be called form @inlinable but not allow for specialisation. This means you also need to make it internal which sucks but that's how it is.
You can also read more about it in the proposal but please fell free to continue asking me directly :)

@czechboy0
Copy link
Contributor Author

@inlinable needs to be added to everything that is generic or it will be very slow

Everything that's generic and public, right?

@dnadoba
Copy link
Member

dnadoba commented Sep 5, 2023

And internal functions if they are called from public generic functions and forward the generic type to internal functions.

@czechboy0
Copy link
Contributor Author

@dnadoba @FranzBusch Let's move the conversation to the PR that has this plus the new currency types, as that's where I put more usage examples: https://github.com/apple/swift-openapi-runtime/pull/47/files#diff-e089a09ba06f85ed1377a6d7ec2a0c36a9f91a3d1b276134aa3156ec6484cede

@czechboy0 czechboy0 closed this Sep 5, 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.

3 participants