-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Handle paginated registry metadata responses #8219
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
Handle paginated registry metadata responses #8219
Conversation
In [4.1 List Package Releases](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#41-list-package-releases) it is stated that a server may respond with a `Link` header that contains a pointer to a subsequent page of results. SPM is not checking for this link in the `Link` header and so if a registry returns paginated results only the first page of versions is searched when resolving. Respect the `next` link in the `Link` header by loading the next page of results and building up a list of versions, continuing until there is no `next` link present in the `Link` header of the last result. Issue: swiftlang#8215
5266f86
to
022da0b
Compare
@swift-ci test |
@swift-ci test windows |
observabilityScope: ObservabilityScope, | ||
callbackQueue: DispatchQueue, | ||
completion: @escaping (Result<PackageMetadata, Error>) -> Void | ||
) { | ||
let completion = self.makeAsync(completion, on: callbackQueue) | ||
_ = Task { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the unstructured task spawned, how does this handle cancellation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, cancellation is not handled, though was it handled previously? As far as I can tell the callbackQueue
typically provided is DispatchQueue.sharedConcurrent
and DispatchQueue doesn't have a nice mechanism for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps:
@available(*, noasync, message: "Use the async alternative")
public func getPackageMetadata(
package: PackageIdentity,
timeout: DispatchTimeInterval? = .none,
observabilityScope: ObservabilityScope,
callbackQueue: DispatchQueue,
completion: @escaping (Result<PackageMetadata, Error>) -> Void
) {
let completion = self.makeAsync(completion, on: callbackQueue)
_ = Task {
do {
let result = try await self.getPackageMetadata(
package: package,
timeout: timeout,
observabilityScope: observabilityScope,
callbackQueue: callbackQueue
)
guard !Task.isCancelled else {
completion(.failure(CancellationError()))
return
}
completion(.success(result))
} catch {
guard !Task.isCancelled else {
completion(.failure(CancellationError()))
return
}
completion(.failure(error))
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Task.isCancelled
will always be false
since only structured child tasks get cancellation propagated from parent tasks. Task
is an unstructured task with no parents, the only primitives for structured concurrency that propagate cancellation are async let
and withTaskGroup
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that is consistent with my understanding. Seeing as the sync version of the API didn't handle cancellation before I think its reasonable to keep that behaviour.
However you're correct that the async version should handle cancellation, and I've updated the PR accordingly.
@swift-ci test |
} | ||
|
||
observabilityScope.emit(debug: "registry for \(package): \(registry)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Where'd this observability message go?
@@ -88,12 +88,205 @@ final class RegistryClientTests: XCTestCase { | |||
let registryClient = makeRegistryClient(configuration: configuration, httpClient: httpClient) | |||
let metadata = try await registryClient.getPackageMetadata(package: identity) | |||
XCTAssertEqual(metadata.versions, ["1.1.1", "1.0.0"]) | |||
XCTAssertEqual(metadata.alternateLocations!, [ | |||
XCTAssertEqual(metadata.alternateLocations, [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: There's one more happy kitten and this is less loud.
|
||
let response: LegacyHTTPClient.Response | ||
do { | ||
response = try await self.httpClient.get( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: I wonder if there is an openapi spec for the package registries. It probably wouldn't handle pagination out of the box (an assumption). But, it might eliminate some boilerplate and make mocking with Swift datatypes instead of JSON payloads.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@czechboy0 would know if OpenAPI specs have a way to communicate pagination.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes I refreshed the OpenAPI doc a few months ago, and if used for codegen, it would remove a bunch of the manual networking code here: https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/registry.openapi.yaml
OpenAPI itself doesn't get as high level as providing conventions for pagination. But the generated code will give you the Link header, it's documented in the OpenAPI doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you adopt Swift OpenAPI Generator, I think it'd be reasonable to file an issue on us to provide the Link header parsing out of the box, so you'd get typesafe access to the individual link values. That way you could drop most of this manual parsing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this looks good to me. I've just added a few thoughts and questions.
In [4.1 List Package Releases](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#41-list-package-releases) it is stated that a server may respond with a `Link` header that contains a pointer to a subsequent page of results. SwiftPM is not checking for the `next` link in the `Link` header and so if a registry returns paginated results, only the first page of versions is searched when resolving. This would result in a "version not found" error when in reality the version is present in a subsequent page of results. Respect the `next` link in the `Link` header by loading the next page of results and building up a list of versions, continuing until there is no `next` link present in the `Link` header of the last result. Registry servers that serve paginated results now have all their results read. Issue: swiftlang#8215
In [4.1 List Package Releases](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md#41-list-package-releases) it is stated that a server may respond with a `Link` header that contains a pointer to a subsequent page of results. SwiftPM is not checking for the `next` link in the `Link` header and so if a registry returns paginated results, only the first page of versions is searched when resolving. This would result in a "version not found" error when in reality the version is present in a subsequent page of results. Respect the `next` link in the `Link` header by loading the next page of results and building up a list of versions, continuing until there is no `next` link present in the `Link` header of the last result. Registry servers that serve paginated results now have all their results read. Issue: swiftlang#8215
Motivation:
In 4.1 List Package Releases it is stated that a server may respond with a
Link
header that contains a pointer to a subsequent page of results. SwiftPM is not checking for thenext
link in theLink
header and so if a registry returns paginated results, only the first page of versions is searched when resolving. This would result in a "version not found" error when in reality the version is present in a subsequent page of results.Modifications:
Respect the
next
link in theLink
header by loading the next page of results and building up a list of versions, continuing until there is nonext
link present in theLink
header of the last result.Result:
Registry servers that serve paginated results now have all their results read.
Issue: #8215