From cbe24cfa3f6bc43f94e9269a74326099b3ffade5 Mon Sep 17 00:00:00 2001 From: AKosylo Date: Thu, 15 Feb 2024 09:40:49 +0300 Subject: [PATCH] Promise: test and documentation --- .../Native/Promise/NativePromise.swift | 103 ++++++++------- DXFeedFramework/Promise/Promise.swift | 110 +++++++++++----- DXFeedFrameworkTests/DXPromiseTest.swift | 122 +++++++++++++++++- 3 files changed, 251 insertions(+), 84 deletions(-) diff --git a/DXFeedFramework/Native/Promise/NativePromise.swift b/DXFeedFramework/Native/Promise/NativePromise.swift index e320310c9..e6856bcc1 100644 --- a/DXFeedFramework/Native/Promise/NativePromise.swift +++ b/DXFeedFramework/Native/Promise/NativePromise.swift @@ -8,21 +8,15 @@ import Foundation @_implementationOnly import graal_api -class NativePromise { - class HandlerBox { - private var _value: AnyObject? - var value: T? { - return _value as? T - } - init(value: T) { - self._value = value as AnyObject - } - func resetValue() { - _value = nil - } - } +internal protocol PromiseListener: AnyObject { + func finished() +} - public typealias PromiseHandler = (_: NativePromise) -> Void +/// Native wrapper over the Java com.dxfeed.promise.Promise class. +/// The location of the imported functions is in the header files "dxfg_feed.h". +class NativePromise { + private class WeakListener: WeakBox { } + typealias PromiseHandler = (_: NativePromise) -> Void let promise: UnsafeMutablePointer? @@ -31,22 +25,26 @@ class NativePromise { private var result: MarketEvent? private var results: [MarketEvent]? - private static let listeners = ConcurrentArray>() + private static let listeners = ConcurrentArray() static let listenerCallback: dxfg_promise_handler_function = { _, promise, context in if let context = context { + let listener: AnyObject = bridge(ptr: context) - if let weakListener = listener as? HandlerBox { + if let weakListener = listener as? WeakListener { + defer { + NativePromise.listeners.removeAll(where: { + return $0 === weakListener + }) + } guard let listener = weakListener.value else { return } promise?.withMemoryRebound(to: dxfg_promise_event_t.self, capacity: 1, { pointer in let native = NativePromise(promise: &pointer.pointee.handler) - listener(native) - }) - NativePromise.listeners.removeAll(where: { - return $0 === weakListener + listener.finished() }) + } } } @@ -103,6 +101,15 @@ class NativePromise { return res } + func isDone() -> Bool { + let thread = currentThread() + if let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_isDone(thread, promise)) { + return result == 1 + } else { + return false + } + } + func hasResult() -> Bool { let thread = currentThread() if let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_hasResult(thread, promise)) { @@ -142,10 +149,10 @@ class NativePromise { return nil } - func await() throws -> MarketEvent? { + func await() throws -> Bool { let thread = currentThread() - try ErrorCheck.nativeCall(thread, dxfg_Promise_await(thread, promise)) - return nil + let success = try ErrorCheck.nativeCall(thread, dxfg_Promise_await(thread, promise)) + return success == ErrorCheck.Result.success.rawValue } func await(millis timeOut: Int32) throws -> Bool { @@ -154,9 +161,10 @@ class NativePromise { return success == ErrorCheck.Result.success.rawValue } - func awaitWithoutException(millis timeOut: Int32) { + func awaitWithoutException(millis timeOut: Int32) -> Bool { let thread = currentThread() - _ = try? ErrorCheck.nativeCall(thread, dxfg_Promise_awaitWithoutException(thread, promise, timeOut)) + let success = try? ErrorCheck.nativeCall(thread, dxfg_Promise_awaitWithoutException(thread, promise, timeOut)) + return success == ErrorCheck.Result.success.rawValue } func cancel() { @@ -166,12 +174,20 @@ class NativePromise { func complete(result: MarketEvent) throws { let thread = currentThread() - let nativeEvent = try NativePromise.mapper.toNative(event: result) - try ErrorCheck.nativeCall(thread, dxfg_Promise_EventType_complete(thread, promise, nativeEvent)) + if let nativeEvent = try NativePromise.mapper.toNative(event: result) { + defer { + NativePromise.mapper.releaseNative(native: nativeEvent) + } + try ErrorCheck.nativeCall(thread, dxfg_Promise_EventType_complete(thread, promise, nativeEvent)) + } } func completeExceptionally(_ exception: GraalException) throws { if let nativeException = exception.toNative() { +// defer { +// nativeException.deinitialize(count: 1) +// nativeException.deallocate() +// } let thread = currentThread() try ErrorCheck.nativeCall(thread, dxfg_Promise_completeExceptionally(thread, promise, nativeException)) } else { @@ -179,9 +195,9 @@ class NativePromise { } } - func whenDone(handler: @escaping NativePromise.PromiseHandler) { + func whenDone(handler: PromiseListener) { let thread = currentThread() - let weakListener = HandlerBox(value: handler) + let weakListener = WeakListener(value: handler) NativePromise.listeners.append(newElement: weakListener) let voidPtr = bridge(obj: weakListener) _ = try? ErrorCheck.nativeCall(thread, @@ -191,29 +207,6 @@ class NativePromise { voidPtr)) } - static func completed(result: MarketEvent) -> NativePromise? { - let thread = currentThread() - let promise = UnsafeMutablePointer.allocate(capacity: 1) - if let nativeEvent = try? NativePromise.mapper.toNative(event: result) { - let handler = nativeEvent.withMemoryRebound(to: dxfg_java_object_handler.self, capacity: 1) { pointer in - return pointer - } - let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_completed(thread, promise, handler)) - return NativePromise(promise: result) - } - return nil - } - - static func failed(exception: GraalException) -> NativePromise? { - let thread = currentThread() - let promise = UnsafeMutablePointer.allocate(capacity: 1) - if let nativeEvent = exception.toNative() { - let result = try? ErrorCheck.nativeCall(thread, dxfg_Promise_failed(thread, promise, nativeEvent)) - return NativePromise(promise: result) - } - return nil - } - static func allOf(promises: [NativePromise]) throws -> NativePromise? { let promiseList = UnsafeMutablePointer.allocate(capacity: 1) let nativeList = UnsafeMutablePointer.allocate(capacity: 1) @@ -243,4 +236,10 @@ class NativePromise { let result = try ErrorCheck.nativeCall(thread, dxfg_Promises_allOf(thread, promiseList)) return NativePromise(promise: result) } + + static func removeListener(listener: PromiseListener) { + listeners.removeAll { listener in + listener.value === listener + } + } } diff --git a/DXFeedFramework/Promise/Promise.swift b/DXFeedFramework/Promise/Promise.swift index de402c0d1..a0849a0ea 100644 --- a/DXFeedFramework/Promise/Promise.swift +++ b/DXFeedFramework/Promise/Promise.swift @@ -7,94 +7,144 @@ import Foundation +/// Result of a computation that will be completed normally or exceptionally in the future. +/// This class is designed to represent a promise to deliver certain result. public class Promise { public typealias PromiseHandler = (_: Promise) -> Void private let native: NativePromise + private var handlers = [PromiseHandler]() + + deinit { + NativePromise.removeListener(listener: self) + } internal init(native: NativePromise) { self.native = native } + /// Returns results of computation. If computation has no result, then this method returns nil. public func getResults() throws -> [MarketEvent]? { return try native.getResults() } + /// Returns results of computation. If computation has no result, then this method returns nil. public func getResult() throws -> MarketEvent? { return try native.getResult() } - + /// Returns **true** when computation has + /// ``complete(result:)`` completed normally, + /// or ``completeExceptionally(_:)`` exceptionally, + /// or was ``cancel()`` canceled + public func isDone() -> Bool { + return native.isDone() + } + /// Returns **true** when computation has completed normally. + /// Use ``hasResult()`` method to get the result of the computation. public func hasResult() -> Bool { return native.hasResult() } - + /// Returns **true** when computation has completed exceptionally or was cancelled. + /// Use ``getException()`` method to get the exceptional outcome of the computation. public func hasException() -> Bool { return native.hasException() } - + /// Returns **true** when computation was canceled. + /// Use ``getException()`` method to get the corresponding exception. public func isCancelled() -> Bool { return native.isCancelled() } + /// Returns exceptional outcome of computation. If computation has no ``hasException()`` exception, + /// then this method returns ``nil``. If computation has completed exceptionally or was cancelled, then + /// the result of this method is not ``nil``. + /// If computation was ``isCancelled()`` cancelled, then this method returns an + // instance of GraalException. + /// - Returns: GraalException. Rethrows exception from Java. public func getException() -> GraalException? { return native.getException() } - + + /// Wait for computation to complete and return its result or throw an exception in case of exceptional completion. + /// This method waits forever. + /// - Returns: result of computation. + /// - Throws : GraalException. Rethrows exception from Java public func await() throws -> MarketEvent? { - return try native.await() + if try native.await() { + return try getResult() + } + return nil } + /// Wait for computation to complete and return its result or throw an exception in case of exceptional completion. + /// If the wait times out, then the computation is ``cancel()`` cancelled and exception is thrown. + /// - Returns: result of computation. + /// - Throws : GraalException. Rethrows exception from Java public func await(millis timeOut: Int32) throws -> MarketEvent? { if try native.await(millis: timeOut) { return try getResult() } return nil } - - public func awaitWithoutException(millis timeOut: Int32) { + /// Wait for computation to complete and return its result or throw an exception in case of exceptional completion. + /// If the wait times out, then the computation is ``cancel()`` cancelled and exception is thrown. + /// - Returns: If the wait times out, then the computation is ``cancel()`` cancelled and this method returns **false**. + /// Use this method in the code that shall continue normal execution in case of timeout. + public func awaitWithoutException(millis timeOut: Int32) -> Bool { return native.awaitWithoutException(millis: timeOut) } - + /// Cancels computation. This method does nothing if computation has already ``isDone()`` completed. public func cancel() { native.cancel() } - + /// Completes computation normally with a specified result. + /// This method does nothing if computation has already ``isDone()`` completed + /// (normally, exceptionally, or was cancelled), + /// - Throws : GraalException. Rethrows exception from Java public func complete(result: MarketEvent) throws { try native.complete(result: result) } - + /// Completes computation exceptionally with a specified exception. + /// This method does nothing if computation has already ``isDone()`` completed, + /// otherwise ``getException()`` will return the specified exception. + /// - Throws : GraalException. Rethrows exception from Java public func completeExceptionally(_ exception: GraalException) throws { try native.completeExceptionally(exception) } + /// Registers a handler to be invoked exactly once when computation ``isDone()`` completes. + /// The handler's method is invoked immediately when this computation has already completed, + /// otherwise it will be invoked **synchronously** in the future when computation + /// ``complete(result:)`` completes normally, + /// or ``completeExceptionally(_:)`` exceptionally, + /// or is ``cancel()`` cancelled from the same thread that had invoked one of the completion methods. public func whenDone(handler: @escaping PromiseHandler) { - native.whenDone { nativePromise in - handler(Promise(native: nativePromise)) - } - } - - public static func completed(result: MarketEvent) -> Promise? { - if let native = NativePromise.completed(result: result) { + handlers.append(handler) + native.whenDone(handler: self) + } + + /// Returns a new promise that ``isDone()`` completes when all promises from the given array + /// complete normally or exceptionally. + /// The results of the given promises are not reflected in the returned promise, but may be + /// obtained by inspecting them individually. If no promises are provided, returns a promise completed + /// with the value null. + /// When the resulting promise completes for any reason ``cancel()`` canceled, for example) + /// then all of the promises from the given array are canceled. + /// - Throws : GraalException. Rethrows exception from Java + public static func allOf(promises: [Promise]) throws -> Promise? { + if let native = try NativePromise.allOf(promises: promises.map { $0.native }) { return Promise(native: native) } else { return nil } } - public static func failed(exception: GraalException) -> Promise? { - if let native = NativePromise.failed(exception: exception) { - return Promise(native: native) - } else { - return nil - } - } +} - public static func allOf(promises: [Promise]) throws -> Promise? { - if let native = try NativePromise.allOf(promises: promises.map { $0.native }) { - return Promise(native: native) - } else { - return nil +extension Promise: PromiseListener { + func finished() { + handlers.forEach { handler in + handler(self) } } - } diff --git a/DXFeedFrameworkTests/DXPromiseTest.swift b/DXFeedFrameworkTests/DXPromiseTest.swift index 6ca61d631..05f5417c7 100644 --- a/DXFeedFrameworkTests/DXPromiseTest.swift +++ b/DXFeedFrameworkTests/DXPromiseTest.swift @@ -25,7 +25,19 @@ final class DXPromiseTest: XCTestCase { return promise } - func testGetResult() { + func testGetAsyncResultWithTimeout() { + getAsyncResult(timeOut: 1000) + } + + func testGetAsyncResultWithTimeoutWithoutException() { + getAsyncResult(timeOut: 1000, withException: false) + } + + func testGetAsyncResultNoTimeout() { + getAsyncResult(timeOut: nil) + } + + func getAsyncResult(timeOut: Int32?, withException: Bool = true) { do { let endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") let feed = endpoint.getFeed() @@ -33,8 +45,21 @@ final class DXPromiseTest: XCTestCase { symbol: "ETH/USD:GDAX", feed: feed!) XCTAssert(promise.hasResult() == false) - let result = try promise.await(millis: 1000) + var result: MarketEvent? + if let timeOut = timeOut { + if withException { + result = try promise.await(millis: timeOut) + } else { + if promise.awaitWithoutException(millis: timeOut) { + result = try promise.getResult() + } + } + } else { + result = try promise.await() + } XCTAssert(result != nil) + XCTAssert(promise.hasResult() == true) + XCTAssert(result === (try? promise.getResult())) } catch { XCTAssert(false, "testGetResult \(error)") @@ -225,4 +250,97 @@ final class DXPromiseTest: XCTestCase { } + func testCompletePromise() { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + let receivedEventExp = expectation(description: "Received promise") + promise.whenDone { [weak promise]_ in + if (try? promise?.getResult()) != nil { + receivedEventExp.fulfill() + } + } + try promise.complete(result: Quote("AAPL")) + wait(for: [receivedEventExp], timeout: 1) + + } catch { + XCTAssert(false, "testCompletePromise \(error)") + } + } + + func testCompleteExceptPromise() throws { + throw XCTSkip("Graal doesn't have impl for ExceptionMapper.toJava and always throws exception illegalStateException") + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + let receivedEventExp = expectation(description: "Received promise") + promise.whenDone { [weak promise]_ in + receivedEventExp.fulfill() + } + try promise.completeExceptionally(GraalException.fail(message: "Failed from iOS", className: "TestClas", stack: "Stack empty")) + wait(for: [receivedEventExp], timeout: 1) + + } catch { + XCTAssert(false, "testCompleteExceptPromise \(error)") + } + } + + func testIsCanceled() throws { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + let isCanceled = promise.isCancelled() + XCTAssert(isCanceled == false) + } catch { + XCTAssert(false, "testIsCanceled \(error)") + } + } + + func testGetException() throws { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + let exception = promise.getException() + XCTAssert(exception == nil) + } catch { + XCTAssert(false, "testGetException \(error)") + } + } + + func testCancelPromise() { + do { + let endpoint = try DXEndpoint.create() + let feed = endpoint.getFeed() + let promise = try eventPromise(type: Quote.self, + symbol: "ETH/USD:GDAX", + feed: feed!) + XCTAssert(promise.hasResult() == false) + let receivedEventExp = expectation(description: "Received promise") + promise.whenDone { [weak promise]_ in + if promise?.isCancelled() == true { + receivedEventExp.fulfill() + } + } + try promise.cancel() + wait(for: [receivedEventExp], timeout: 1) + + } catch { + XCTAssert(false, "testCompletePromise \(error)") + } + } + }