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

Implement asynchronous interceptors #186

Merged
merged 16 commits into from
Oct 16, 2023
Merged

Implement asynchronous interceptors #186

merged 16 commits into from
Oct 16, 2023

Conversation

rebello95
Copy link
Collaborator

@rebello95 rebello95 commented Sep 29, 2023

Summary

This PR refactors the interceptor implementation offered by Connect-Swift to support asynchronous processing inside interceptors. This is useful when clients want to perform work prior to sending outbound requests or returning responses to callers, such as:

  • Refreshing an OAuth token before attaching it to the outbound request
  • Rejecting an outbound request (and preventing it from being sent) based on the result of some asynchronous work
  • Waiting to make decisions on requests/responses until more data has arrived (such as waiting to do something with a stream before request data has been passed to an interceptor)

Resolves #104.

Docs are being updated in connectrpc/connectrpc.com#68.

Considerations

When implementing these, the following considerations were made:

  • Allowing interceptors to fail/reject outbound requests (like Alamofire does)
    • This PR adds this functionality
  • Whether we should expose async/await interfaces or callbacks/closures
    • In practice, exposing async/await interfaces wouldn't dramatically change interceptor implementations (it'd effectively be return await finishProcessingRequest(request) versus proceed(request)), but it would place significant limitations on interceptors by disallowing them from doing any buffering on streams due to the fact that data cannot be passed to any interceptors while one is being awaited. For this reason, we opted for exposing closures which allows interceptors to proceed() the chain at any point, potentially after receiving additional invocations from the client with more information. Implementations can still wrap these calls with async/await if desired (example below, and in Interceptor.swift)
  • Whether we should enforce additional guardrails for streaming to ensure an interceptor continues interceptor iteration in the right order, such as headers -> data -> data -> data -> trailers

Example of wrapping an interceptor with async/await:

final class AsyncInterceptor: Interceptor, Sendable {
   func unaryFunction() -> UnaryFunction {
       return .init { request, proceed in
           Task {
               proceed(await self.handleRequest(request))
           }
       } responseFunction: { response, proceed in
           Task {
               proceed(await self.handleUnaryResponse(response))
           }
       } responseMetricsFunction: { metrics, proceed in
           Task {
               proceed(await self.handleUnaryResponseMetrics(metrics))
           }
       }
   }

   func streamFunction() -> StreamFunction {...}
}

Approach

  • Interceptors are expected to be instantiated once per request or stream.
  • Each interceptor has the opportunity to perform asynchronous work before passing a potentially altered value to the next interceptor in the chain. When the end of the chain is reached, the final value is passed to the networking client where it is sent to the server or to the calling client.
  • Interceptors may also fail outbound requests before they're sent, thus preventing subsequent interceptors from being invoked and returning a specified error back to the original caller.
  • Interceptors are closure-based and are passed both the current value and a closure which should be called to resume the interceptor chain. Propagation will not continue until this closure is called. Additional values may still be passed to a given interceptor even though it has not yet continued the chain.
  • Implementations should be thread-safe (hence the Sendable requirement on interceptor closures), as closures can be invoked from different threads during the span of a request or stream due to the asynchronous nature of other interceptors which may be present in the chain.
  • Interceptors can also be written using async/await by incorporating a Task (as shown above).

Notes

  • New tests cover manually invoking the interceptor chain as well as sending and receiving data through the chain with a real client
  • Some of the changes from Relax Sendable requirements for interceptors #182 have been reverted as part of this PR due to the fact that interceptor closures must be Sendable/thread-safe per the discussion above
  • I built this branch locally with swiftSettings: [.unsafeFlags(["-Xfrontend", "-strict-concurrency=complete"])] to ensure that it doesn't introduce new strict concurrency warnings
  • This PR also replaces some "filter" wording with "interceptor" for consistency (I've spent too much time in Envoy land in the past...)

@rebello95 rebello95 marked this pull request as ready for review October 3, 2023 12:10
@rebello95 rebello95 requested a review from eseay October 3, 2023 12:10
rebello95 added a commit to connectrpc/connectrpc.com that referenced this pull request Oct 10, 2023
Updates the documentation to reflect the changes in connectrpc/connect-swift#186.
Copy link
Contributor

@eseay eseay left a comment

Choose a reason for hiding this comment

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

A few little insignificant comments around code style. The majority of the changes are quite straight forward, despite the unhelpful Git diff. ProtocolClient and InterceptorChain changes are a bit more dense; however, the 100% test coverage is amazing.

@rebello95
Copy link
Collaborator Author

Thank you for the review @eseay!

@rebello95 rebello95 merged commit b33d0aa into main Oct 16, 2023
9 checks passed
@rebello95 rebello95 deleted the async-interceptors branch October 16, 2023 13:00
rebello95 added a commit to connectrpc/connectrpc.com that referenced this pull request Oct 18, 2023
Updates the documentation to reflect the changes in connectrpc/connect-swift#186.

The copy matches `Interceptor.swift`'s inline documentation from that PR with the addition of an auth example here.
@mycroftcanner
Copy link

Thanks for implementing this! Are you going to tag a new version soon?

@rebello95
Copy link
Collaborator Author

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.

Asynchronous Interceptors
3 participants