diff --git a/mobile/examples/objective-c/hello_world/ViewController.m b/mobile/examples/objective-c/hello_world/ViewController.m index 1aa00768c368..3a989e42cf50 100644 --- a/mobile/examples/objective-c/hello_world/ViewController.m +++ b/mobile/examples/objective-c/hello_world/ViewController.m @@ -103,7 +103,7 @@ - (void)performRequest { [weakSelf addResponseMessage:message headerMessage:headerMessage error:nil]; }]; - [prototype setOnErrorWithClosure:^(EnvoyError *error, StreamIntel *ignored) { + [prototype setOnErrorWithClosure:^(EnvoyError *error, FinalStreamIntel *ignored) { // TODO: expose attemptCount. https://github.com/envoyproxy/envoy-mobile/issues/823 NSString *message = [NSString stringWithFormat:@"failed within Envoy library %@", error.message]; diff --git a/mobile/examples/swift/hello_world/AsyncDemoFilter.swift b/mobile/examples/swift/hello_world/AsyncDemoFilter.swift index b48939a37f71..c25e10ef4352 100644 --- a/mobile/examples/swift/hello_world/AsyncDemoFilter.swift +++ b/mobile/examples/swift/hello_world/AsyncDemoFilter.swift @@ -59,7 +59,9 @@ final class AsyncDemoFilter: AsyncResponseFilter { return .resumeIteration(headers: builder.build(), data: data, trailers: trailers) } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) {} + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} - func onCancel(streamIntel: StreamIntel) {} + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} } diff --git a/mobile/examples/swift/hello_world/BufferDemoFilter.swift b/mobile/examples/swift/hello_world/BufferDemoFilter.swift index f2a916c4fd1e..1def3fb67749 100644 --- a/mobile/examples/swift/hello_world/BufferDemoFilter.swift +++ b/mobile/examples/swift/hello_world/BufferDemoFilter.swift @@ -40,7 +40,9 @@ final class BufferDemoFilter: ResponseFilter { return .resumeIteration(headers: builder.build(), data: self.body, trailers: trailers) } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) {} + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} - func onCancel(streamIntel: StreamIntel) {} + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} } diff --git a/mobile/examples/swift/hello_world/DemoFilter.swift b/mobile/examples/swift/hello_world/DemoFilter.swift index c1002e8ba35e..27e79c3e6b8b 100644 --- a/mobile/examples/swift/hello_world/DemoFilter.swift +++ b/mobile/examples/swift/hello_world/DemoFilter.swift @@ -25,7 +25,9 @@ struct DemoFilter: ResponseFilter { return .continue(trailers: trailers) } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) {} + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} - func onCancel(streamIntel: StreamIntel) {} + func onCancel(streamIntel: FinalStreamIntel) {} + + func onComplete(streamIntel: FinalStreamIntel) {} } diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/StreamPrototype.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/StreamPrototype.kt index 863e3e009ce3..cfca4cdb93b6 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/StreamPrototype.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/StreamPrototype.kt @@ -61,6 +61,7 @@ open class StreamPrototype(private val engine: EnvoyEngine) { /** * Specify a callback for when response headers are received by the stream. + * If `endStream` is `true`, the stream is complete, pending an onComplete callback. * * @param closure Closure which will receive the headers and flag indicating if the stream * is headers-only. @@ -75,7 +76,7 @@ open class StreamPrototype(private val engine: EnvoyEngine) { /** * Specify a callback for when a data frame is received by the stream. - * If `endStream` is `true`, the stream is complete. + * If `endStream` is `true`, the stream is complete, pending an onComplete callback. * * @param closure Closure which will receive the data and flag indicating whether this * is the last data frame. @@ -90,7 +91,7 @@ open class StreamPrototype(private val engine: EnvoyEngine) { /** * Specify a callback for when trailers are received by the stream. - * If the closure is called, the stream is complete. + * If the closure is called, the stream is complete, pending an onComplete callback. * * @param closure Closure which will receive the trailers. * @return This stream, for chaining syntax. diff --git a/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/ResponseFilter.kt b/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/ResponseFilter.kt index c8dbcf7b6e63..41991cb44ac4 100644 --- a/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/ResponseFilter.kt +++ b/mobile/library/kotlin/io/envoyproxy/envoymobile/filters/ResponseFilter.kt @@ -50,6 +50,7 @@ interface ResponseFilter : Filter { /** * Called at most once when an error within Envoy occurs. * + * Only one of onError, onCancel, or onComplete will be called per stream. * This should be considered a terminal state, and invalidates any previous attempts to * `stopIteration{...}`. * @@ -62,6 +63,7 @@ interface ResponseFilter : Filter { /** * Called at most once when the client cancels the stream. * + * Only one of onError, onCancel, or onComplete will be called per stream. * This should be considered a terminal state, and invalidates any previous attempts to * `stopIteration{...}`. * @@ -71,8 +73,9 @@ interface ResponseFilter : Filter { fun onCancel(streamIntel: StreamIntel, finalStreamIntel: FinalStreamIntel) /** - * Called at most once when the stream is complete. + * Called at most once when the stream completes gracefully. * + * Only one of onError, onCancel, or onComplete will be called per stream. * This should be considered a terminal state, and invalidates any previous attempts to * `stopIteration{...}`. * diff --git a/mobile/library/objective-c/EnvoyEngine.h b/mobile/library/objective-c/EnvoyEngine.h index 187fa11cbf81..25f45aeca9e4 100644 --- a/mobile/library/objective-c/EnvoyEngine.h +++ b/mobile/library/objective-c/EnvoyEngine.h @@ -17,6 +17,9 @@ typedef NSDictionary EnvoyEvent; /// Contains internal HTTP stream metrics, context, and other details. typedef envoy_stream_intel EnvoyStreamIntel; +// Contains one time HTTP stream metrics, context, and other details. +typedef envoy_final_stream_intel EnvoyFinalStreamIntel; + #pragma mark - EnvoyHTTPCallbacks /// Interface that can handle callbacks from an HTTP stream. @@ -27,14 +30,17 @@ typedef envoy_stream_intel EnvoyStreamIntel; */ @property (nonatomic, assign) dispatch_queue_t dispatchQueue; +// Formatting for block properties is inconsistent and not configurable. +// clang-format off + /** * Called when all headers get received on the async HTTP stream. * @param headers the headers received. * @param endStream whether the response is headers-only. * @param streamIntel internal HTTP stream metrics, context, and other details. */ -@property (nonatomic, copy) void (^onHeaders) - (EnvoyHeaders *headers, BOOL endStream, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) void (^onHeaders)( + EnvoyHeaders *headers, BOOL endStream, EnvoyStreamIntel streamIntel); /** * Called when a data frame gets received on the async HTTP stream. @@ -43,26 +49,26 @@ typedef envoy_stream_intel EnvoyStreamIntel; * @param endStream whether the data is the last data frame. * @param streamIntel internal HTTP stream metrics, context, and other details. */ -@property (nonatomic, copy) void (^onData) - (NSData *data, BOOL endStream, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) void (^onData)( + NSData *data, BOOL endStream, EnvoyStreamIntel streamIntel); -// clang-format off /** * Called when all trailers get received on the async HTTP stream. * Note that end stream is implied when on_trailers is called. * @param trailers the trailers received. * @param streamIntel internal HTTP stream metrics, context, and other details. */ -@property (nonatomic, copy) void (^onTrailers) - (EnvoyHeaders *trailers, EnvoyStreamIntel streamIntel); -// clang-format on +@property (nonatomic, copy) void (^onTrailers)( + EnvoyHeaders *trailers, EnvoyStreamIntel streamIntel); /** * Called when the async HTTP stream has an error. * @param streamIntel internal HTTP stream metrics, context, and other details. + * @param finalStreamIntel one time HTTP stream metrics, context, and other details. */ -@property (nonatomic, copy) void (^onError) - (uint64_t errorCode, NSString *message, int32_t attemptCount, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) void (^onError)( + uint64_t errorCode, NSString *message, int32_t attemptCount, EnvoyStreamIntel streamIntel, + EnvoyFinalStreamIntel finalStreamIntel); /** * Called when the async HTTP stream is canceled. @@ -70,9 +76,22 @@ typedef envoy_stream_intel EnvoyStreamIntel; * response is already complete. It will fire no more than once, and no other callbacks for the * stream will be issued afterwards. * @param streamIntel internal HTTP stream metrics, context, and other details. + * @param finalStreamIntel one time HTTP stream metrics, context, and other details. + */ +@property (nonatomic, copy) void (^onCancel)( + EnvoyStreamIntel streamIntel, EnvoyFinalStreamIntel finalStreamIntel); + +/** + * Final call made when an HTTP stream is closed gracefully. + * Note this may already be inferred from a prior callback with endStream=TRUE, and this only needs + * to be handled if information from finalStreamIntel is desired. + * @param streamIntel internal HTTP stream metrics, context, and other details. + * @param finalStreamIntel one time HTTP stream metrics, context, and other details. */ -@property (nonatomic, copy) void (^onCancel)(EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) void (^onComplete)( + EnvoyStreamIntel streamIntel, EnvoyFinalStreamIntel finalStreamIntel); +// clang-format on @end #pragma mark - EnvoyHTTPFilter @@ -114,79 +133,86 @@ extern const int kEnvoyFilterResumeStatusResumeIteration; @interface EnvoyHTTPFilter : NSObject +// Formatting for block properties is inconsistent and not configurable. +// clang-format off + /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - EnvoyHeaders *, forward headers -@property (nonatomic, copy) NSArray * (^onRequestHeaders) - (EnvoyHeaders *headers, BOOL endStream, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) NSArray * (^onRequestHeaders)( + EnvoyHeaders *headers, BOOL endStream, EnvoyStreamIntel streamIntel); /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - NSData *, forward data /// 2 - EnvoyHeaders *, optional pending headers -@property (nonatomic, copy) NSArray * (^onRequestData) - (NSData *data, BOOL endStream, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) NSArray * (^onRequestData)( + NSData *data, BOOL endStream, EnvoyStreamIntel streamIntel); /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - EnvoyHeaders *, forward trailers /// 2 - EnvoyHeaders *, optional pending headers /// 3 - NSData *, optional pending data -@property (nonatomic, copy) NSArray * (^onRequestTrailers) - (EnvoyHeaders *trailers, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) NSArray * (^onRequestTrailers)( + EnvoyHeaders *trailers, EnvoyStreamIntel streamIntel); /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - EnvoyHeaders *, forward headers -@property (nonatomic, copy) NSArray * (^onResponseHeaders) - (EnvoyHeaders *headers, BOOL endStream, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) NSArray * (^onResponseHeaders)( + EnvoyHeaders *headers, BOOL endStream, EnvoyStreamIntel streamIntel); /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - NSData *, forward data /// 2 - EnvoyHeaders *, optional pending headers -@property (nonatomic, copy) NSArray * (^onResponseData) - (NSData *data, BOOL endStream, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) NSArray * (^onResponseData)( + NSData *data, BOOL endStream, EnvoyStreamIntel streamIntel); /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - EnvoyHeaders *, forward trailers /// 2 - EnvoyHeaders *, optional pending headers /// 3 - NSData *, optional pending data -@property (nonatomic, copy) NSArray * (^onResponseTrailers) - (EnvoyHeaders *trailers, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy)NSArray * (^onResponseTrailers)( + EnvoyHeaders *trailers, EnvoyStreamIntel streamIntel); -@property (nonatomic, copy) void (^onCancel)(EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) void (^onCancel)( + EnvoyStreamIntel streamIntel, EnvoyFinalStreamIntel finalStreamIntel); -@property (nonatomic, copy) void (^onError) - (uint64_t errorCode, NSString *message, int32_t attemptCount, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) void (^onError)( + uint64_t errorCode, NSString *message, int32_t attemptCount, EnvoyStreamIntel streamIntel, + EnvoyFinalStreamIntel finalStreamIntel); -@property (nonatomic, copy) void (^setRequestFilterCallbacks) - (id callbacks); +@property (nonatomic, copy) void (^onComplete)( + EnvoyStreamIntel streamIntel, EnvoyFinalStreamIntel finalStreamIntel); + +@property (nonatomic, copy) void (^setRequestFilterCallbacks)( + id callbacks); -// clang-format off /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - EnvoyHeaders *, optional pending headers /// 2 - NSData *, optional pending data /// 3 - EnvoyHeaders *, optional pending trailers -@property (nonatomic, copy) NSArray * (^onResumeRequest) - (EnvoyHeaders *_Nullable headers, NSData *_Nullable data, EnvoyHeaders *_Nullable trailers, - BOOL endStream, EnvoyStreamIntel streamIntel); +@property (nonatomic, copy) NSArray * (^onResumeRequest)( + EnvoyHeaders *_Nullable headers, NSData *_Nullable data, EnvoyHeaders *_Nullable trailers, + BOOL endStream, EnvoyStreamIntel streamIntel); -@property (nonatomic, copy) void (^setResponseFilterCallbacks) - (id callbacks); +@property (nonatomic, copy) void (^setResponseFilterCallbacks)( + id callbacks); /// Returns tuple of: /// 0 - NSNumber *,filter status /// 1 - EnvoyHeaders *, optional pending headers /// 2 - NSData *, optional pending data /// 3 - EnvoyHeaders *, optional pending trailers -@property (nonatomic, copy) NSArray * (^onResumeResponse) - (EnvoyHeaders *_Nullable headers, NSData *_Nullable data, EnvoyHeaders *_Nullable trailers, - BOOL endStream, EnvoyStreamIntel streamIntel); -// clang-format on +@property (nonatomic, copy) NSArray * (^onResumeResponse)( + EnvoyHeaders *_Nullable headers, NSData *_Nullable data, EnvoyHeaders *_Nullable trailers, + BOOL endStream, EnvoyStreamIntel streamIntel); +// clang-format on @end #pragma mark - EnvoyHTTPFilterFactory diff --git a/mobile/library/objective-c/EnvoyEngineImpl.m b/mobile/library/objective-c/EnvoyEngineImpl.m index 5744c72aa097..7bd444d81372 100644 --- a/mobile/library/objective-c/EnvoyEngineImpl.m +++ b/mobile/library/objective-c/EnvoyEngineImpl.m @@ -332,6 +332,20 @@ static void ios_http_filter_set_response_callbacks(envoy_http_filter_callbacks c } } +static void ios_http_filter_on_complete(envoy_stream_intel stream_intel, + envoy_final_stream_intel final_stream_intel, + const void *context) { + // This code block runs inside the Envoy event loop. Therefore, an explicit autoreleasepool block + // is necessary to act as a breaker for any Objective-C allocation that happens. + @autoreleasepool { + EnvoyHTTPFilter *filter = (__bridge EnvoyHTTPFilter *)context; + if (filter.onComplete == nil) { + return; + } + filter.onComplete(stream_intel, final_stream_intel); + } +} + static void ios_http_filter_on_cancel(envoy_stream_intel stream_intel, envoy_final_stream_intel final_stream_intel, const void *context) { @@ -342,7 +356,7 @@ static void ios_http_filter_on_cancel(envoy_stream_intel stream_intel, if (filter.onCancel == nil) { return; } - filter.onCancel(stream_intel); + filter.onCancel(stream_intel, final_stream_intel); } } @@ -363,7 +377,8 @@ static void ios_http_filter_on_error(envoy_error error, envoy_stream_intel strea encoding:NSUTF8StringEncoding]; release_envoy_error(error); - filter.onError(error.error_code, errorMessage, error.attempt_count, stream_intel); + filter.onError(error.error_code, errorMessage, error.attempt_count, stream_intel, + final_stream_intel); } } @@ -457,6 +472,8 @@ - (int)registerFilterFactory:(EnvoyHTTPFilterFactory *)filterFactory { api->on_resume_request = ios_http_filter_on_resume_request; api->set_response_callbacks = ios_http_filter_set_response_callbacks; api->on_resume_response = ios_http_filter_on_resume_response; + // TODO(goaway) HTTP filter on_complete not currently implemented. + // api->on_complete = ios_http_filter_on_complete; api->on_cancel = ios_http_filter_on_cancel; api->on_error = ios_http_filter_on_error; api->release_filter = ios_http_filter_release; diff --git a/mobile/library/objective-c/EnvoyHTTPStreamImpl.m b/mobile/library/objective-c/EnvoyHTTPStreamImpl.m index 9c737d07c79c..6d17abfecb65 100644 --- a/mobile/library/objective-c/EnvoyHTTPStreamImpl.m +++ b/mobile/library/objective-c/EnvoyHTTPStreamImpl.m @@ -68,7 +68,10 @@ EnvoyHTTPCallbacks *callbacks = c->callbacks; EnvoyHTTPStreamImpl *stream = c->stream; dispatch_async(callbacks.dispatchQueue, ^{ - // TODO: If the callback queue is not serial, clean up is not currently thread-safe. + if (callbacks.onComplete) { + callbacks.onComplete(stream_intel, final_stream_intel); + } + assert(stream); [stream cleanUp]; }); @@ -90,7 +93,7 @@ EnvoyHTTPStreamImpl *stream = c->stream; dispatch_async(callbacks.dispatchQueue, ^{ if (callbacks.onCancel) { - callbacks.onCancel(stream_intel); + callbacks.onCancel(stream_intel, final_stream_intel); } // TODO: If the callback queue is not serial, clean up is not currently thread-safe. @@ -111,7 +114,8 @@ length:error.message.length encoding:NSUTF8StringEncoding]; release_envoy_error(error); - callbacks.onError(error.error_code, errorMessage, error.attempt_count, stream_intel); + callbacks.onError(error.error_code, errorMessage, error.attempt_count, stream_intel, + final_stream_intel); } // TODO: If the callback queue is not serial, clean up is not currently thread-safe. diff --git a/mobile/library/swift/BUILD b/mobile/library/swift/BUILD index 436822ff2781..a3dba953dc9c 100644 --- a/mobile/library/swift/BUILD +++ b/mobile/library/swift/BUILD @@ -12,6 +12,7 @@ swift_library( "EngineBuilder.swift", "EngineImpl.swift", "EnvoyError.swift", + "FinalStreamIntel.swift", "Headers.swift", "HeadersBuilder.swift", "LogLevel.swift", diff --git a/mobile/library/swift/FinalStreamIntel.swift b/mobile/library/swift/FinalStreamIntel.swift new file mode 100644 index 000000000000..a662c5bfe662 --- /dev/null +++ b/mobile/library/swift/FinalStreamIntel.swift @@ -0,0 +1,97 @@ +@_implementationOnly import EnvoyEngine +import Foundation + +/// Exposes one time HTTP stream metrics, context, and other details. +@objcMembers +public final class FinalStreamIntel: StreamIntel { + /// The time the request started, in ms since the epoch. + public let requestStartMs: UInt64 + /// The time the DNS resolution for this request started, in ms since the epoch. + public let dnsStartMs: UInt64 + /// The time the DNS resolution for this request completed, in ms since the epoch. + public let dnsEndMs: UInt64 + /// The time the upstream connection started, in ms since the epoch. (1) + public let connectStartMs: UInt64 + /// The time the upstream connection completed, in ms since the epoch. (1) + public let connectEndMs: UInt64 + /// The time the SSL handshake started, in ms since the epoch. (1) + public let sslStartMs: UInt64 + /// The time the SSL handshake completed, in ms since the epoch. (1) + public let sslEndMs: UInt64 + /// The time the first byte of the request was sent upstream, in ms since the epoch. + public let sendingStartMs: UInt64 + /// The time the last byte of the request was sent upstream, in ms since the epoch. + public let sendingEndMs: UInt64 + /// The time the first byte of the response was received, in ms since the epoch. + public let responseStartMs: UInt64 + /// The time the last byte of the request was received, in ms since the epoch. + public let requestEndMs: UInt64 + /// True if the upstream socket had been used previously. + public let socketReused: Bool + /// The number of bytes sent upstream. + public let sentByteCount: UInt64 + /// The number of bytes received from upstream. + public let receivedByteCount: UInt64 + + // NOTE(1): These fields may not be set if socket_reused is false. + + public init( + streamId: Int64, + connectionId: Int64, + attemptCount: UInt64, + requestStartMs: UInt64, + dnsStartMs: UInt64, + dnsEndMs: UInt64, + connectStartMs: UInt64, + connectEndMs: UInt64, + sslStartMs: UInt64, + sslEndMs: UInt64, + sendingStartMs: UInt64, + sendingEndMs: UInt64, + responseStartMs: UInt64, + requestEndMs: UInt64, + socketReused: Bool, + sentByteCount: UInt64, + receivedByteCount: UInt64 + ) { + self.requestStartMs = requestStartMs + self.dnsStartMs = dnsStartMs + self.dnsEndMs = dnsEndMs + self.connectStartMs = connectStartMs + self.connectEndMs = connectEndMs + self.sslStartMs = sslStartMs + self.sslEndMs = sslEndMs + self.sendingStartMs = sendingStartMs + self.sendingEndMs = sendingEndMs + self.responseStartMs = responseStartMs + self.requestEndMs = requestEndMs + self.socketReused = socketReused + self.sentByteCount = sentByteCount + self.receivedByteCount = receivedByteCount + super.init(streamId: streamId, connectionId: connectionId, attemptCount: attemptCount) + } +} + +extension FinalStreamIntel { + internal convenience init(_ cIntel: EnvoyStreamIntel, _ cFinalIntel: EnvoyFinalStreamIntel) { + self.init( + streamId: cIntel.stream_id, + connectionId: cIntel.connection_id, + attemptCount: cIntel.attempt_count, + requestStartMs: cFinalIntel.request_start_ms, + dnsStartMs: cFinalIntel.dns_start_ms, + dnsEndMs: cFinalIntel.dns_end_ms, + connectStartMs: cFinalIntel.connect_start_ms, + connectEndMs: cFinalIntel.connect_end_ms, + sslStartMs: cFinalIntel.ssl_start_ms, + sslEndMs: cFinalIntel.ssl_end_ms, + sendingStartMs: cFinalIntel.sending_start_ms, + sendingEndMs: cFinalIntel.sending_end_ms, + responseStartMs: cFinalIntel.response_start_ms, + requestEndMs: cFinalIntel.request_end_ms, + socketReused: cFinalIntel.socket_reused != 0, + sentByteCount: cFinalIntel.sent_byte_count, + receivedByteCount: cFinalIntel.received_byte_count + ) + } +} diff --git a/mobile/library/swift/StreamCallbacks.swift b/mobile/library/swift/StreamCallbacks.swift index 177689bb0660..3285220a405e 100644 --- a/mobile/library/swift/StreamCallbacks.swift +++ b/mobile/library/swift/StreamCallbacks.swift @@ -12,8 +12,9 @@ final class StreamCallbacks { )? var onData: ((_ body: Data, _ endStream: Bool, _ streamIntel: StreamIntel) -> Void)? var onTrailers: ((_ trailers: ResponseTrailers, _ streamIntel: StreamIntel) -> Void)? - var onCancel: ((_ streamIntel: StreamIntel) -> Void)? - var onError: ((_ error: EnvoyError, _ streamIntel: StreamIntel) -> Void)? + var onComplete: ((_ streamintel: FinalStreamIntel) -> Void)? + var onCancel: ((_ streamintel: FinalStreamIntel) -> Void)? + var onError: ((_ error: EnvoyError, _ streamIntel: FinalStreamIntel) -> Void)? } extension EnvoyHTTPCallbacks { @@ -27,13 +28,14 @@ extension EnvoyHTTPCallbacks { self.onHeaders = { callbacks.onHeaders?(ResponseHeaders(headers: $0), $1, StreamIntel($2)) } self.onData = { callbacks.onData?($0, $1, StreamIntel($2)) } self.onTrailers = { callbacks.onTrailers?(ResponseTrailers(headers: $0), StreamIntel($1)) } - self.onCancel = { callbacks.onCancel?(StreamIntel($0)) } - self.onError = { errorCode, message, attemptCount, streamIntel in + self.onComplete = { callbacks.onCancel?(FinalStreamIntel($0, $1)) } + self.onCancel = { callbacks.onCancel?(FinalStreamIntel($0, $1)) } + self.onError = { errorCode, message, attemptCount, streamIntel, finalStreamIntel in // The initializer below will return nil if `attemptCount` is negative. // This is the desired behavior because the bridge layer uses -1 to signify absence. let error = EnvoyError(errorCode: errorCode, message: message, attemptCount: UInt32(exactly: attemptCount), cause: nil) - callbacks.onError?(error, StreamIntel(streamIntel)) + callbacks.onError?(error, FinalStreamIntel(streamIntel, finalStreamIntel)) } } } diff --git a/mobile/library/swift/StreamIntel.swift b/mobile/library/swift/StreamIntel.swift index 65ae0390a7c1..8acd4e8e3c0f 100644 --- a/mobile/library/swift/StreamIntel.swift +++ b/mobile/library/swift/StreamIntel.swift @@ -3,7 +3,7 @@ import Foundation /// Exposes internal HTTP stream metrics, context, and other details. @objcMembers -public final class StreamIntel: NSObject, Error { +public class StreamIntel: NSObject, Error { // An internal identifier for the stream. -1 if not set. public let streamId: Int64 // An internal identifier for the connection carrying the stream. -1 if not set. diff --git a/mobile/library/swift/StreamPrototype.swift b/mobile/library/swift/StreamPrototype.swift index 48947bdf8c91..ac2fe975aac7 100644 --- a/mobile/library/swift/StreamPrototype.swift +++ b/mobile/library/swift/StreamPrototype.swift @@ -59,6 +59,7 @@ public class StreamPrototype: NSObject { } /// Specify a callback for when response headers are received by the stream. + /// If `endStream` is `true`, the stream is complete, pending an onComplete callback. /// /// - parameter closure: Closure which will receive the headers /// and flag indicating if the stream is headers-only. @@ -74,7 +75,7 @@ public class StreamPrototype: NSObject { } /// Specify a callback for when a data frame is received by the stream. - /// If `endStream` is `true`, the stream is complete. + /// If `endStream` is `true`, the stream is complete, pending an onComplete callback. /// /// - parameter closure: Closure which will receive the data /// and flag indicating whether this is the last data frame. @@ -89,7 +90,7 @@ public class StreamPrototype: NSObject { } /// Specify a callback for when trailers are received by the stream. - /// If the closure is called, the stream is complete. + /// If the closure is called, the stream is complete, pending an onComplete callback. /// /// - parameter closure: Closure which will receive the trailers. /// @@ -110,7 +111,7 @@ public class StreamPrototype: NSObject { /// - returns: This stream, for chaining syntax. @discardableResult public func setOnError( - closure: @escaping (_ error: EnvoyError, _ streamIntel: StreamIntel) -> Void + closure: @escaping (_ error: EnvoyError, _ streamIntel: FinalStreamIntel) -> Void ) -> StreamPrototype { self.callbacks.onError = closure return self @@ -124,7 +125,21 @@ public class StreamPrototype: NSObject { /// - returns: This stream, for chaining syntax. @discardableResult public func setOnCancel( - closure: @escaping (_ streamIntel: StreamIntel) -> Void + closure: @escaping (_ streamIntel: FinalStreamIntel) -> Void + ) -> StreamPrototype { + self.callbacks.onCancel = closure + return self + } + + /// Specify a callback for when the stream completes gracefully. + /// If the closure is called, the stream is complete. + /// + /// - parameter closure: Closure which will be called when the stream is canceled. + /// + /// - returns: This stream, for chaining syntax. + @discardableResult + public func setOnComplete( + closure: @escaping (_ streamIntel: FinalStreamIntel) -> Void ) -> StreamPrototype { self.callbacks.onCancel = closure return self diff --git a/mobile/library/swift/filters/Filter.swift b/mobile/library/swift/filters/Filter.swift index 72a3f4b7159c..4272708cc64c 100644 --- a/mobile/library/swift/filters/Filter.swift +++ b/mobile/library/swift/filters/Filter.swift @@ -111,14 +111,18 @@ extension EnvoyHTTPFilter { } } - self.onError = { errorCode, message, attemptCount, streamIntel in + self.onError = { errorCode, message, attemptCount, streamIntel, finalStreamIntel in let error = EnvoyError(errorCode: errorCode, message: message, attemptCount: UInt32(exactly: attemptCount), cause: nil) - responseFilter.onError(error, streamIntel: StreamIntel(streamIntel)) + responseFilter.onError(error, streamIntel: FinalStreamIntel(streamIntel, finalStreamIntel)) } - self.onCancel = { streamIntel in - responseFilter.onCancel(streamIntel: StreamIntel(streamIntel)) + self.onCancel = { streamIntel, finalStreamIntel in + responseFilter.onCancel(streamIntel: FinalStreamIntel(streamIntel, finalStreamIntel)) + } + + self.onComplete = { streamIntel, finalStreamIntel in + responseFilter.onComplete(streamIntel: FinalStreamIntel(streamIntel, finalStreamIntel)) } } diff --git a/mobile/library/swift/filters/ResponseFilter.swift b/mobile/library/swift/filters/ResponseFilter.swift index 08d7a5561da7..72d79961eb82 100644 --- a/mobile/library/swift/filters/ResponseFilter.swift +++ b/mobile/library/swift/filters/ResponseFilter.swift @@ -39,18 +39,29 @@ public protocol ResponseFilter: Filter { /// Called at most once when an error within Envoy occurs. /// + /// Only one of `onError`, `onCancel`, or `onComplete` will be called per stream. /// This should be considered a terminal state, and invalidates any previous attempts to /// `stopIteration{...}`. /// /// - parameter error: The error that occurred within Envoy. - /// - parameter streamIntel: Internal HTTP stream metrics, context, and other details. - func onError(_ error: EnvoyError, streamIntel: StreamIntel) + /// - parameter streamIntel: Final internal HTTP stream metrics, context, and other details. + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) /// Called at most once when the client cancels the stream. /// + /// Only one of `onError`, `onCancel`, or `onComplete` will be called per stream. /// This should be considered a terminal state, and invalidates any previous attempts to /// `stopIteration{...}`. /// - /// - parameter streamIntel: Internal HTTP stream metrics, context, and other details. - func onCancel(streamIntel: StreamIntel) + /// - parameter streamIntel: Final internal HTTP stream metrics, context, and other details. + func onCancel(streamIntel: FinalStreamIntel) + + /// Called at most once when the stream completes gracefully. + /// + /// Only one of `onError`, `onCancel`, or `onComplete` will be called per stream. + /// This should be considered a terminal state, and invalidates any previous attempts to + /// `stopIteration{...}`. + /// + /// - parameter streamIntel: Final internal HTTP stream metrics, context, and other details. + func onComplete(streamIntel: FinalStreamIntel) } diff --git a/mobile/library/swift/grpc/GRPCStreamPrototype.swift b/mobile/library/swift/grpc/GRPCStreamPrototype.swift index c69c1234a1d7..adb7ce8a56b1 100644 --- a/mobile/library/swift/grpc/GRPCStreamPrototype.swift +++ b/mobile/library/swift/grpc/GRPCStreamPrototype.swift @@ -87,7 +87,7 @@ public final class GRPCStreamPrototype: NSObject { /// - returns: This handler, which may be used for chaining syntax. @discardableResult public func setOnError( - _ closure: @escaping (_ error: EnvoyError, _ streamIntel: StreamIntel) -> Void + _ closure: @escaping (_ error: EnvoyError, _ streamIntel: FinalStreamIntel) -> Void ) -> GRPCStreamPrototype { self.underlyingStream.setOnError(closure: closure) return self @@ -101,11 +101,25 @@ public final class GRPCStreamPrototype: NSObject { /// - returns: This stream, for chaining syntax. @discardableResult public func setOnCancel( - closure: @escaping (_ streamInte: StreamIntel) -> Void + closure: @escaping (_ streamIntel: FinalStreamIntel) -> Void ) -> GRPCStreamPrototype { self.underlyingStream.setOnCancel(closure: closure) return self } + + /// Specify a callback for when the stream completes gracefully. + /// If the closure is called, the stream is complete. + /// + /// - parameter closure: Closure which will be called when the stream is closed. + /// + /// - returns: This stream, for chaining syntax. + @discardableResult + public func setOnComplete( + closure: @escaping (_ streamIntel: FinalStreamIntel) -> Void + ) -> GRPCStreamPrototype { + self.underlyingStream.setOnComplete(closure: closure) + return self + } } private enum GRPCMessageProcessor { diff --git a/mobile/library/swift/mocks/MockStream.swift b/mobile/library/swift/mocks/MockStream.swift index 598959ec0547..8aa331f03c7e 100644 --- a/mobile/library/swift/mocks/MockStream.swift +++ b/mobile/library/swift/mocks/MockStream.swift @@ -72,7 +72,7 @@ public final class MockStream: Stream { /// Simulate the stream receiving a cancellation signal from Envoy. public func receiveCancel() { - self.mockStream.callbacks.onCancel(EnvoyStreamIntel()) + self.mockStream.callbacks.onCancel(EnvoyStreamIntel(), EnvoyFinalStreamIntel()) } /// Simulate Envoy returning an error. @@ -81,6 +81,6 @@ public final class MockStream: Stream { public func receiveError(_ error: EnvoyError) { self.mockStream.callbacks.onError(error.errorCode, error.message, Int32(error.attemptCount ?? 0), - EnvoyStreamIntel()) + EnvoyStreamIntel(), EnvoyFinalStreamIntel()) } } diff --git a/mobile/test/swift/integration/CancelStreamTest.swift b/mobile/test/swift/integration/CancelStreamTest.swift index 732a1f00bdaf..a8d89573159e 100644 --- a/mobile/test/swift/integration/CancelStreamTest.swift +++ b/mobile/test/swift/integration/CancelStreamTest.swift @@ -77,11 +77,13 @@ static_resources: return .continue(trailers: trailers) } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) {} + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} - func onCancel(streamIntel: StreamIntel) { + func onCancel(streamIntel: FinalStreamIntel) { self.expectation.fulfill() } + + func onComplete(streamIntel: FinalStreamIntel) {} } let runExpectation = self.expectation(description: "Run called with expected cancellation") diff --git a/mobile/test/swift/integration/FilterResetIdleTest.swift b/mobile/test/swift/integration/FilterResetIdleTest.swift index e23bbbe9c4e1..2a93d48ea878 100644 --- a/mobile/test/swift/integration/FilterResetIdleTest.swift +++ b/mobile/test/swift/integration/FilterResetIdleTest.swift @@ -168,11 +168,13 @@ static_resources: return .stopIteration } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) {} + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) {} - func onCancel(streamIntel: StreamIntel) { + func onCancel(streamIntel: FinalStreamIntel) { cancelExpectation.fulfill() } + + func onComplete(streamIntel: FinalStreamIntel) {} } let resetExpectation = self.expectation(description: "Stream idle timer reset 3 times") diff --git a/mobile/test/swift/integration/GRPCReceiveErrorTest.swift b/mobile/test/swift/integration/GRPCReceiveErrorTest.swift index 95cb189acf30..4433c9de8c59 100644 --- a/mobile/test/swift/integration/GRPCReceiveErrorTest.swift +++ b/mobile/test/swift/integration/GRPCReceiveErrorTest.swift @@ -66,15 +66,17 @@ static_resources: return .continue(trailers: trailers) } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) { + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) { XCTAssertEqual(error.errorCode, 2) // 503/Connection Failure self.receivedError.fulfill() } - func onCancel(streamIntel: StreamIntel) { + func onCancel(streamIntel: FinalStreamIntel) { XCTFail("Unexpected call to onCancel filter callback") self.notCancelled.fulfill() } + + func onComplete(streamIntel: FinalStreamIntel) {} } let callbackReceivedError = self.expectation(description: "Run called with expected error") diff --git a/mobile/test/swift/integration/IdleTimeoutTest.swift b/mobile/test/swift/integration/IdleTimeoutTest.swift index 9ff767ccdca0..cff72888e716 100644 --- a/mobile/test/swift/integration/IdleTimeoutTest.swift +++ b/mobile/test/swift/integration/IdleTimeoutTest.swift @@ -125,14 +125,16 @@ static_resources: return .stopIteration } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) { + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) { XCTAssertEqual(error.errorCode, 4) timeoutExpectation.fulfill() } - func onCancel(streamIntel: StreamIntel) { + func onCancel(streamIntel: FinalStreamIntel) { XCTFail("Unexpected call to onCancel filter callback") } + + func onComplete(streamIntel: FinalStreamIntel) {} } let filterExpectation = self.expectation(description: "Stream idle timeout received by filter") diff --git a/mobile/test/swift/integration/ReceiveErrorTest.swift b/mobile/test/swift/integration/ReceiveErrorTest.swift index cdd2e43f17a0..cdf1f779efc8 100644 --- a/mobile/test/swift/integration/ReceiveErrorTest.swift +++ b/mobile/test/swift/integration/ReceiveErrorTest.swift @@ -66,15 +66,17 @@ static_resources: return .continue(trailers: trailers) } - func onError(_ error: EnvoyError, streamIntel: StreamIntel) { + func onError(_ error: EnvoyError, streamIntel: FinalStreamIntel) { XCTAssertEqual(error.errorCode, 2) // 503/Connection Failure self.receivedError.fulfill() } - func onCancel(streamIntel: StreamIntel) { + func onCancel(streamIntel: FinalStreamIntel) { XCTFail("Unexpected call to onCancel filter callback") self.notCancelled.fulfill() } + + func onComplete(streamIntel: FinalStreamIntel) {} } let callbackReceivedError = self.expectation(description: "Run called with expected error")