Skip to content

Commit

Permalink
swift: refactor public functionality as precursor to L7 filters (#858)
Browse files Browse the repository at this point in the history
## Context

As part of the work to support platform (Swift/Kotlin) L7 filters, we are updating the public interfaces/functionality of the Envoy Mobile library to combine the concepts of a `StreamEmitter` and `ResponseHandler` into a "stream".

## Goals

- Unify the existing response handler and stream emitter types into a single "stream" type that will make it easier to communicate with an underlying filter manager for both outbound requests and inbound responses
- Provide guard rails that prevent the consumer from making mistakes, such as setting callback handlers after starting a stream and thus missing updates
- Provide a clear interface to the user for creating and starting streams
- Focus on the basics (bi-directional streaming support), which will allow for other wrappers to be written in the future (i.e., GRPC, unary, Rx, etc.)
- Update interfaces to utilize the new wrapper types introduced on master (`ResponseHeaders`, `RequestHeaders`, etc.)

## Types

- `StreamClient`: The publicly exposed interface of `EnvoyEngine`. Provides a function for starting a new stream using Envoy Mobile, and is built using `StreamClientBuilder`
- `StreamPrototype`: Type that allows users to set up a stream (providing callbacks, etc.) prior to starting it
- `Stream`: Produced by calling `start()` on a `StreamPrototype`, and allows the consumer to send data over the stream and eventually close it
- `GRPCClient` / `GRPCStreamPrototype` / `GRPCStream`: Types wrapping the above 3 types, converting the existing gRPC types to utilize the new functionality

To start a stream, consumers now do something like this:

```swift
let streamClient = try StreamClientBuilder().build()
...
```
```swift
let headers = RequestHeadersBuilder(method: .get, authority: "envoyproxy.io", path: "/test")
  .addUpstreamHttpProtocol(.http2)
  .build()

streamClient
  .newStream()
  .setOnResponseHeaders { ... }
  .setOnResponseData { ... }
  .start()
  .sendHeaders(headers, endStream: false)
  .sendData(data)
  .close()
```

## Tests

- Unit tests have been added / updated accordingly.
- Sample apps have also been updated (CI validates via "hello world").

## Notes

- A follow-up PR will migrate Android to mirror these
- Documentation will be updated separately

Signed-off-by: Michael Rebello <[email protected]>
Signed-off-by: JP Simard <[email protected]>
  • Loading branch information
rebello95 authored and jpsim committed Nov 29, 2022
1 parent 7dd6f21 commit 1b980b9
Show file tree
Hide file tree
Showing 49 changed files with 1,164 additions and 1,589 deletions.
24 changes: 10 additions & 14 deletions mobile/envoy-mobile.tulsiproj/Configs/all.tulsigen
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,16 @@
"//library/swift/src:ios_framework_archive",
"//library/swift/test:envoy_client_builder_tests",
"//library/swift/test:envoy_client_builder_tests_lib",
"//library/swift/test:envoy_client_tests",
"//library/swift/test:envoy_client_tests_lib",
"//library/swift/test:grpc_request_builder_tests",
"//library/swift/test:grpc_request_builder_tests_lib",
"//library/swift/test:grpc_response_handler_tests",
"//library/swift/test:grpc_response_handler_tests_lib",
"//library/swift/test:grpc_stream_emitter_tests",
"//library/swift/test:grpc_stream_emitter_tests_lib",
"//library/swift/test:request_builder_tests",
"//library/swift/test:request_builder_tests_lib",
"//library/swift/test:request_mapper_tests",
"//library/swift/test:request_mapper_tests_lib",
"//library/swift/test:response_handler_tests",
"//library/swift/test:response_handler_tests_lib",
"//library/swift/test:grpc_active_stream_tests",
"//library/swift/test:grpc_active_stream_tests_lib",
"//library/swift/test:grpc_request_headers_builder_tests",
"//library/swift/test:grpc_request_headers_builder_tests_lib",
"//library/swift/test:headers_builder_tests",
"//library/swift/test:headers_builder_tests_lib",
"//library/swift/test:request_headers_builder_tests",
"//library/swift/test:request_headers_builder_tests_lib",
"//library/swift/test:response_headers_tests",
"//library/swift/test:response_headers_tests_lib",
"//library/swift/test:retry_policy_mapper_tests",
"//library/swift/test:retry_policy_mapper_tests_lib",
"//library/swift/test:retry_policy_tests",
Expand Down
40 changes: 20 additions & 20 deletions mobile/examples/objective-c/hello_world/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#pragma mark - ViewController

@interface ViewController ()
@property (nonatomic, strong) EnvoyClient *envoy;
@property (nonatomic, strong) id<StreamClient> client;
@property (nonatomic, assign) int requestCount;
@property (nonatomic, strong) NSMutableArray<Result *> *results;
@property (nonatomic, weak) NSTimer *requestTimer;
Expand All @@ -36,8 +36,8 @@ - (instancetype)init {
- (void)startEnvoy {
NSLog(@"Starting Envoy...");
NSError *error;
EnvoyClientBuilder *builder = [[EnvoyClientBuilder alloc] init];
self.envoy = [builder buildAndReturnError:&error];
StreamClientBuilder *builder = [[StreamClientBuilder alloc] init];
self.client = [builder buildAndReturnError:&error];
if (error) {
NSLog(@"Starting Envoy failed: %@", error);
} else {
Expand Down Expand Up @@ -65,38 +65,38 @@ - (void)performRequest {
self.requestCount++;
NSLog(@"Starting request to '%@'", _REQUEST_PATH);

int requestID = self.requestCount;
// Note: this request will use an http/1.1 stream for the upstream request.
// The Swift example uses h2. This is done on purpose to test both paths in end-to-end tests
// in CI.
RequestBuilder *builder = [[RequestBuilder alloc] initWithMethod:RequestMethodGet
scheme:_REQUEST_SCHEME
authority:_REQUEST_AUTHORITY
path:_REQUEST_PATH];
Request *request = [builder build];
ResponseHandler *handler = [[ResponseHandler alloc] initWithQueue:dispatch_get_main_queue()];
int requestID = self.requestCount;
RequestHeadersBuilder *builder = [[RequestHeadersBuilder alloc] initWithMethod:RequestMethodGet
scheme:_REQUEST_SCHEME
authority:_REQUEST_AUTHORITY
path:_REQUEST_PATH];
[builder addUpstreamHttpProtocol:UpstreamHttpProtocolHttp1];
RequestHeaders *headers = [builder build];

__weak ViewController *weakSelf = self;
[handler onHeaders:^(NSDictionary<NSString *, NSArray<NSString *> *> *headers,
NSInteger statusCode, BOOL endStream) {
NSLog(@"Response status (%i): %ld\n%@", requestID, statusCode, headers);
NSString *body = [NSString stringWithFormat:@"Status: %ld", statusCode];
StreamPrototype *prototype = [self.client newStreamPrototype];
[prototype setOnResponseHeadersWithClosure:^(ResponseHeaders *headers, BOOL endStream) {
int statusCode = [[[headers valueForName:@":status"] firstObject] intValue];
NSLog(@"Response status (%i): %i\n%@", requestID, statusCode, headers);
NSString *body = [NSString stringWithFormat:@"Status: %i", statusCode];
[weakSelf addResponseBody:body
serverHeader:[headers[@"server"] firstObject]
serverHeader:[[headers valueForName:@"server"] firstObject]
identifier:requestID
error:nil];
}];

[handler onData:^(NSData *data, BOOL endStream) {
[prototype setOnResponseDataWithClosure:^(NSData *data, BOOL endStream) {
NSLog(@"Response data (%i): %ld bytes", requestID, data.length);
}];

[handler onError:^(EnvoyError *error) {
[prototype setOnErrorWithClosure:^(EnvoyError *error) {
// TODO: expose attemptCount. https://github.com/lyft/envoy-mobile/issues/823
NSLog(@"Error (%i): Request failed: %@", requestID, error.message);
}];

[self.envoy send:request body:nil trailers:nil handler:handler];
Stream *stream = [prototype startWithQueue:dispatch_get_main_queue()];
[stream sendHeaders:headers endStream:YES];
}

- (void)addResponseBody:(NSString *)body
Expand Down
40 changes: 21 additions & 19 deletions mobile/examples/swift/hello_world/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ final class ViewController: UITableViewController {
private var requestCount = 0
private var results = [Result<Response, RequestError>]()
private var timer: Timer?
private var envoy: EnvoyClient?
private var client: StreamClient?

override func viewDidLoad() {
super.viewDidLoad()
do {
NSLog("Starting Envoy...")
self.envoy = try EnvoyClientBuilder().build()
self.client = try StreamClientBuilder().build()
} catch let error {
NSLog("Starting Envoy failed: \(error)")
}
Expand All @@ -38,33 +38,36 @@ final class ViewController: UITableViewController {
}

private func performRequest() {
guard let envoy = self.envoy else {
guard let client = self.client else {
NSLog("Failed to start request - Envoy is not running")
return
}

self.requestCount += 1
NSLog("Starting request to '\(kRequestPath)'")

let requestID = self.requestCount
// Note: this request will use an h2 stream for the upstream request.
// The Objective-C example uses http/1.1. This is done on purpose to test both paths in
// end-to-end tests in CI.
let request = RequestBuilder(method: .get, scheme: kRequestScheme,
authority: kRequestAuthority,
path: kRequestPath)
.addUpstreamHttpProtocol(.http2)
.build()
let handler = ResponseHandler()
.onHeaders { [weak self] headers, statusCode, _ in
let requestID = self.requestCount
let headers = RequestHeadersBuilder(method: .get, scheme: kRequestScheme,
authority: kRequestAuthority, path: kRequestPath)
.addUpstreamHttpProtocol(.http2)
.build()

client
.newStreamPrototype()
.setOnResponseHeaders { [weak self] headers, _ in
let statusCode = headers.httpStatus ?? -1
let response = Response(id: requestID, body: "Status: \(statusCode)",
serverHeader: headers.value(forName: "server")?.first ?? "")
NSLog("Response status (\(requestID)): \(statusCode)\n\(headers)")
self?.add(result: .success(Response(id: requestID, body: "Status: \(statusCode)",
serverHeader: headers["server"]?.first ?? "")))
self?.add(result: .success(response))
}
.onData { data, _ in
.setOnResponseData { data, _ in
NSLog("Response data (\(requestID)): \(data.count) bytes")
}
.onError { [weak self] error in
.setOnError { [weak self] error in
let message: String
if let attemptCount = error.attemptCount {
message = "failed within Envoy library after \(attemptCount) attempts: \(error.message)"
Expand All @@ -73,11 +76,10 @@ final class ViewController: UITableViewController {
}

NSLog("Error (\(requestID)): \(message)")
self?.add(result: .failure(RequestError(id: requestID,
message: message)))
self?.add(result: .failure(RequestError(id: requestID, message: message)))
}

envoy.send(request, body: nil, trailers: nil, handler: handler)
.start()
.sendHeaders(headers, endStream: true)
}

private func add(result: Result<Response, RequestError>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,5 @@ interface ResponseFilter: Filter {
* This should be considered a terminal state, and invalidates any previous attempts to
* `stopIteration{...}`.
*/
fun onCanceled()
fun onCancel()
}
14 changes: 5 additions & 9 deletions mobile/library/swift/src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,27 @@ licenses(["notice"]) # Apache 2
swift_static_framework(
name = "ios_framework",
srcs = glob([
"Data+Extension.swift",
"EnvoyClient.swift",
"EnvoyClientBuilder.swift",
"EnvoyError.swift",
"EnvoyStreamEmitter.swift",
"HTTPClient.swift",
"Headers.swift",
"HeadersBuilder.swift",
"LogLevel.swift",
"Request.swift",
"RequestBuilder.swift",
"RequestHeaders.swift",
"RequestHeadersBuilder.swift",
"RequestMapper.swift",
"RequestMethod.swift",
"RequestTrailers.swift",
"RequestTrailersBuilder.swift",
"ResponseHandler.swift",
"ResponseHeaders.swift",
"ResponseHeadersBuilder.swift",
"ResponseTrailers.swift",
"ResponseTrailersBuilder.swift",
"RetryPolicy.swift",
"RetryPolicyMapper.swift",
"StreamEmitter.swift",
"Stream.swift",
"StreamCallbacks.swift",
"StreamClient.swift",
"StreamClientBuilder.swift",
"StreamPrototype.swift",
"UpstreamHttpProtocol.swift",
"filters/*.swift",
"grpc/*.swift",
Expand Down
50 changes: 14 additions & 36 deletions mobile/library/swift/src/EnvoyClient.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
@_implementationOnly import EnvoyEngine
import Foundation

/// Envoy's implementation of `HTTPClient`, buildable using `EnvoyClientBuilder`.
/// Envoy's implementation of `HTTPClient`, buildable using `StreamClientBuilder`.
@objcMembers
public final class EnvoyClient: NSObject {
final class EnvoyClient: NSObject {
private let engine: EnvoyEngine

private enum ConfigurationType {
Expand All @@ -12,15 +12,15 @@ public final class EnvoyClient: NSObject {
}

private init(configType: ConfigurationType, logLevel: LogLevel, engine: EnvoyEngine) {
self.engine = engine
super.init()

switch configType {
case .yaml(let configYAML):
self.engine.run(withConfigYAML: configYAML, logLevel: logLevel.stringValue)
case .typed(let config):
self.engine.run(withConfig: config, logLevel: logLevel.stringValue)
}
self.engine = engine
super.init()

switch configType {
case .yaml(let configYAML):
self.engine.run(withConfigYAML: configYAML, logLevel: logLevel.stringValue)
case .typed(let config):
self.engine.run(withConfig: config, logLevel: logLevel.stringValue)
}
}

/// Initialize a new Envoy instance using a typed configuration.
Expand All @@ -42,30 +42,8 @@ public final class EnvoyClient: NSObject {
}
}

extension EnvoyClient: HTTPClient {
public func start(_ request: Request, handler: ResponseHandler) -> StreamEmitter {
let httpStream = self.engine.startStream(with: handler.underlyingCallbacks)
httpStream.sendHeaders(request.outboundHeaders(), close: false)
return EnvoyStreamEmitter(stream: httpStream)
}

@discardableResult
public func send(_ request: Request, body: Data?,
trailers: [String: [String]]?, handler: ResponseHandler)
-> CancelableStream
{
let httpStream = self.engine.startStream(with: handler.underlyingCallbacks)
if let body = body, let trailers = trailers { // Close with trailers
httpStream.sendHeaders(request.outboundHeaders(), close: false)
httpStream.send(body, close: false)
httpStream.sendTrailers(trailers)
} else if let body = body { // Close with data
httpStream.sendHeaders(request.outboundHeaders(), close: false)
httpStream.send(body, close: true)
} else { // Close with headers-only
httpStream.sendHeaders(request.outboundHeaders(), close: true)
}

return EnvoyStreamEmitter(stream: httpStream)
extension EnvoyClient: StreamClient {
func newStreamPrototype() -> StreamPrototype {
return StreamPrototype(engine: self.engine)
}
}
31 changes: 0 additions & 31 deletions mobile/library/swift/src/EnvoyStreamEmitter.swift

This file was deleted.

30 changes: 0 additions & 30 deletions mobile/library/swift/src/HTTPClient.swift

This file was deleted.

16 changes: 16 additions & 0 deletions mobile/library/swift/src/Headers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,19 @@ public class Headers: NSObject {
self.headers = headers
}
}

// MARK: - Equatable

extension Headers {
public override func isEqual(_ object: Any?) -> Bool {
return (object as? Self)?.headers == self.headers
}
}

// MARK: - CustomStringConvertible

extension Headers {
public override var description: String {
return "\(type(of: self)) \(self.headers.description)"
}
}
Loading

0 comments on commit 1b980b9

Please sign in to comment.