From 641ebcac7f8c4c4383c48f9d4c342d6d5226d886 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 Nov 2023 10:57:05 -0600 Subject: [PATCH 01/10] - updated the ClientParameterProtocol to use an associatedType for the Model being used in then Handler - refactored the ClientParameterHandler with the above changes. Signed-off-by: Matt Bruce --- .../ClientParameterHandler.swift | 19 ++++++--- .../ClientParameterProtocol.swift | 42 +++++++++++++++---- 2 files changed, 47 insertions(+), 14 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 6f8c7e0..6c6549a 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -9,11 +9,20 @@ @objcMembers open class ClientParameterHandler: NSObject { public static let DefaultTimeout = 30.0 - - open func createParametersHandler(_ clientParameterModel: ClientParameterModelProtocol) -> ClientParameterProtocol? { + + open func createParametersHandler(_ clientParameterModel: ClientParameterModelProtocol) -> (any ClientParameterProtocol)? { do { - let parameterType = try ModelRegistry.getHandler(clientParameterModel) as! ClientParameterProtocol.Type - return parameterType.init(clientParameterModel) + //Ensure the handlerType return will be initable using ClientParameterHandler + let handlerType = try ModelRegistry.getHandler(clientParameterModel) as! AnyClientParameterProtocol.Type + + //init the handler + let handler = try handlerType.init(clientParameterModel: clientParameterModel) + + //Cast from AnyClientParameterProtocol to AnyClientParameterProtocol, if not throw + guard let castedHandler = handler as? any ClientParameterProtocol else { throw ClientParameterError.castingError } + + //Return the any ClientParameterProtocol + return castedHandler } catch { if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { MVMCoreLoggingHandler.addError(toLog: errorObject) @@ -21,7 +30,7 @@ return nil } } - + func getClientParameterModel(_ clientParameters: [String: Any]) throws -> ClientParameterModel? { let data = try JSONSerialization.data(withJSONObject: clientParameters) return try JSONDecoder.create().decode(ClientParameterModel.self, from: data) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift index e55fd15..ccaea3b 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift @@ -8,25 +8,49 @@ import Foundation -public protocol ClientParameterProtocol: ModelHandlerProtocol { - static var name: String { get } +/// Errors used for ClientParamter +public enum ClientParameterError: Error { + case castingError + case modelMismatch(message: String) +} - init(_ clientParameterModel: ClientParameterModelProtocol) +public protocol AnyClientParameterProtocol { + /// Original init using a ClientParameterModelProtocol to get around the self - associatedType issues with ClientParameterProtocol + /// - Parameter clientParameterModel: any Model using the ClientParameterModelProtocol + init(clientParameterModel: ClientParameterModelProtocol) throws +} + +public protocol ClientParameterProtocol: ModelHandlerProtocol, AnyClientParameterProtocol { + associatedtype Model: ClientParameterModelProtocol - var clientParameterModel: ClientParameterModelProtocol { get set } + static var name: String { get } + + var clientParameterModel: Model { get set } + + func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval, completionHandler:@escaping ([String: AnyHashable]?) -> ()) - func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval, completionHandler:@escaping ([String: AnyHashable]?) -> ()) + /// Default parameter for timeout scenarios. It will use the protocol extension method bydefault. Can override to send custom values. + func valueOnTimeout() -> [String: AnyHashable] - /// Default parameter for timeout scenarios. It will use the protocol extension method bydefault. Can override to send custom values. - func valueOnTimeout() -> [String: AnyHashable] + init(clientParameterModel: Model) } public extension ClientParameterProtocol { - + /// Default Implementation should never be overridden + init(clientParameterModel: ClientParameterModelProtocol) throws { + // Cast the clientParameterModel to ensure it matches the asscociatedType Model assigned to this Handler. + guard let castedModel = clientParameterModel as? Model else { + let message = "Expected \(Model.self) but received \(clientParameterModel.type)" + throw ClientParameterError.modelMismatch(message: message) + } + // Call init for the Handler Class and pass in the casted Model. + self.init(clientParameterModel: castedModel) + } + func valueOnTimeout() -> [String: AnyHashable] { return [Self.name: "failed_to_collect"] } - + /// The handler should call this method to pass the parameter back to the caller. /// If using isFlatMap, you must provide at least 1 element in parameters or it will result in triggering a timeout. func returnParameters(_ isFlatMap: Bool, _ parameter: [String: AnyHashable], completionHandler: @escaping ([String: AnyHashable]?) -> ()) { From 9549dca564cdf9322b932b71109bd9491c45392a Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Wed, 15 Nov 2023 13:25:02 -0500 Subject: [PATCH 02/10] casting error --- .../ActionType/Client Parameters/ClientParameterHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 6c6549a..18b91d9 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -13,7 +13,7 @@ open func createParametersHandler(_ clientParameterModel: ClientParameterModelProtocol) -> (any ClientParameterProtocol)? { do { //Ensure the handlerType return will be initable using ClientParameterHandler - let handlerType = try ModelRegistry.getHandler(clientParameterModel) as! AnyClientParameterProtocol.Type + guard let handlerType = try ModelRegistry.getHandler(clientParameterModel) as? AnyClientParameterProtocol.Type else { throw ClientParameterError.castingError } //init the handler let handler = try handlerType.init(clientParameterModel: clientParameterModel) From f5e8c0b2e68e0172f08f41db62ff231ceffe8f8d Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Wed, 17 Jan 2024 15:44:07 -0500 Subject: [PATCH 03/10] async await client parameters --- .../ClientParameterHandler.swift | 96 +++++++------------ .../ClientParameterModelProtocol.swift | 5 + .../ClientParameterProtocol.swift | 23 +++-- 3 files changed, 48 insertions(+), 76 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 42e8d6b..2773f59 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -58,78 +58,46 @@ getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId, completionHandler: completionHandler) } - open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: Any]?) -> ()) { - - let parametersWorkQueue = DispatchQueue(label: "com.mva.clientparameter") - let group = DispatchGroup() + open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { let timeout = model.timeout ?? Self.DefaultTimeout - let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } - let requestUUID = (0.. [String: AnyHashable] in - guard let parameter = element else { + return await withTaskGroup(of: [String: AnyHashable]?.self, returning: [String: AnyHashable].self) { group in + for handler in parameterHandlerList { + group.addTask{ + // Fetch the client parameter. + do { + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch( + name: handler.clientParameterModel.type, + uuid: handler.clientParameterModel.id, + actionId: actionId)) + let parameter: [String: AnyHashable]? = try await MVMCoreActionUtility.perform(withTimeout: .now() + .seconds(Int(timeout))) { + let parameter = await handler.fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) + + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete( + name: handler.clientParameterModel.type, + uuid: handler.clientParameterModel.id, + actionId: actionId)) + return parameter + } + return parameter + } catch MVMCoreActionUtilityError.timeOut { + // The client parameter timed out. MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( name: model.list[index].type, - uuid: requestUUID[index], + uuid: handler.clientParameterModel.id, actionId: actionId)) - return parameterHandlerList[index].valueOnTimeout() - } - return parameter - }.reduce(into: [String: AnyHashable]()) { partialResult, next in - partialResult.merge(next) { first, last in first } - } - } - - // Setup completion handlers. Barriered to ensure one happens after the other. - var complete = false - let timeoutWorkItem = DispatchWorkItem(qos: .userInitiated) { - completionHandler(mergedParametersList); - complete = true - } - let completionWorkItem = DispatchWorkItem(qos: .userInitiated) { - timeoutWorkItem.cancel() - if !complete { // In the case of firing after timeout. - completionHandler(mergedParametersList); - complete = true - } - } - - // Setup timeout. - parametersWorkQueue.asyncAfter(deadline: .now() + .seconds(Int(timeout)), execute: timeoutWorkItem) - - // Setup the parameter execution. - for (index, parameterHandler) in parameterHandlerList.enumerated() { - let parameterType = parameterHandler.clientParameterModel.type - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch(name: parameterType, uuid: requestUUID[index], actionId: actionId)) - group.enter() - parameterHandler.fetchClientParameters(requestParameters: requestParameters, - timingOutIn: timeout) { (receivedParameter) in - // Queue the results for merge. - parametersWorkQueue.async { - guard !complete else { - MVMCoreLoggingHandler.logDebugMessage(withDelegate: "Client \(parameterType) responded after task completion.") - return - } - guard returnedList[index] == nil else { - MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, message: "Client parameter \(parameterType) has already executed. The completion handler should only be called once!", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: String(describing: ClientParameterHandler.self))!) - return - } - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: requestUUID[index], actionId: actionId)) - returnedList[index] = receivedParameter - group.leave() // Leaving is only done after setup (barriered). + return handler.valueOnTimeout() + } catch { + return nil } } } - - // Callback when all parameters have been merged. - group.notify(queue: parametersWorkQueue, work: completionWorkItem); + + return await group.reduce(into: [String: AnyHashable]()) { partialResult, parameter in + if let parameter = parameter { + partialResult.merge(parameter) { first, last in first } + } + } } } } diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift index b742170..bbde12c 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterModelProtocol.swift @@ -8,8 +8,13 @@ import Foundation +/// A protocol defining models for client parameter fetching handlers. public protocol ClientParameterModelProtocol: ModelProtocol { + /// The type of client parameter. var type: String { get } + + /// A unique identifier for this instance. + var id: String { get } } public extension ClientParameterModelProtocol { diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift index ccaea3b..ead6837 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift @@ -20,23 +20,22 @@ public protocol AnyClientParameterProtocol { init(clientParameterModel: ClientParameterModelProtocol) throws } +/// A protocol defining client parameter fetching handlers. public protocol ClientParameterProtocol: ModelHandlerProtocol, AnyClientParameterProtocol { - associatedtype Model: ClientParameterModelProtocol - static var name: String { get } - - var clientParameterModel: Model { get set } - - func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval, completionHandler:@escaping ([String: AnyHashable]?) -> ()) + associatedtype Model: ClientParameterModelProtocol + + var clientParameterModel: Model { get set } + init(clientParameterModel: Model) - /// Default parameter for timeout scenarios. It will use the protocol extension method bydefault. Can override to send custom values. - func valueOnTimeout() -> [String: AnyHashable] + func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval) async -> [String: AnyHashable]? - init(clientParameterModel: Model) + /// Default parameter for timeout scenarios. It will use the protocol extension method bydefault. Can override to send custom values. + func valueOnTimeout() -> [String: AnyHashable] } public extension ClientParameterProtocol { - /// Default Implementation should never be overridden + /// Helper init to convert protocol to specific model. Default Implementation should never be overridden init(clientParameterModel: ClientParameterModelProtocol) throws { // Cast the clientParameterModel to ensure it matches the asscociatedType Model assigned to this Handler. guard let castedModel = clientParameterModel as? Model else { @@ -48,7 +47,7 @@ public extension ClientParameterProtocol { } func valueOnTimeout() -> [String: AnyHashable] { - return [Self.name: "failed_to_collect"] + return [clientParameterModel.type: "failed_to_collect"] } /// The handler should call this method to pass the parameter back to the caller. @@ -57,7 +56,7 @@ public extension ClientParameterProtocol { if isFlatMap { completionHandler(parameter) } else { - completionHandler([Self.name: parameter]) + completionHandler([clientParameterModel.type: parameter]) } } } From 03168287281fe618f5fc999fea9adb637c14d35f Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Thu, 18 Jan 2024 16:23:10 -0500 Subject: [PATCH 04/10] async await --- .../ClientParameterHandler.swift | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 2773f59..5d4d370 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -6,6 +6,22 @@ // Copyright © 2021 myverizon. All rights reserved. // +/// Sample clientParameters +///{ +/// "timeout": 30, +/// "list": [ +/// { +/// "type": "currentLocation", +/// "inputParameters": { +/// "accuracy": 10, +/// "timeThreshold": 600 +/// } +/// } +/// ] +///} +/// completionHandler can return flat dictinary or a map. It depends on the paramters handler + + @objcMembers open class ClientParameterHandler: NSObject { public static let DefaultTimeout = 30.0 @@ -36,30 +52,19 @@ return try JSONDecoder.create().decode(ClientParameterModel.self, from: data) } - /// Sample clientParameters - ///{ - /// "timeout": 30, - /// "list": [ - /// { - /// "type": "currentLocation", - /// "inputParameters": { - /// "accuracy": 10, - /// "timeThreshold": 600 - /// } - /// } - /// ] - ///} - /// completionHandler can return flat dictinary or a map. It depends on the paramters handler open func getParameters(with clientParameters: [String: Any], requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: Any]?) -> ()) throws { guard let clientParameterModel = try getClientParameterModel(clientParameters) else { completionHandler(nil) return } - getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId, completionHandler: completionHandler) + Task { + let parameters = await getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId) + completionHandler(parameters) + } } open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { - let timeout = model.timeout ?? Self.DefaultTimeout + let timeout = TimeInterval(model.timeout ?? Self.DefaultTimeout) let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } return await withTaskGroup(of: [String: AnyHashable]?.self, returning: [String: AnyHashable].self) { group in for handler in parameterHandlerList { @@ -70,7 +75,7 @@ name: handler.clientParameterModel.type, uuid: handler.clientParameterModel.id, actionId: actionId)) - let parameter: [String: AnyHashable]? = try await MVMCoreActionUtility.perform(withTimeout: .now() + .seconds(Int(timeout))) { + let parameter: [String: AnyHashable]? = try await MVMCoreActionUtility.perform(withTimeout: timeout) { let parameter = await handler.fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete( @@ -83,11 +88,12 @@ } catch MVMCoreActionUtilityError.timeOut { // The client parameter timed out. MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( - name: model.list[index].type, + name: handler.clientParameterModel.type, uuid: handler.clientParameterModel.id, actionId: actionId)) return handler.valueOnTimeout() } catch { + // Should be impossible. return nil } } From a8606e66412da20dceb611132c66f08cef1e021f Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 19 Jan 2024 10:44:14 -0500 Subject: [PATCH 05/10] Fix Open Page --- MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift index 2f79085..a889da1 100644 --- a/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift +++ b/MVMCore/MVMCore/ActionHandling/ActionOpenPageHandler.swift @@ -99,10 +99,6 @@ public extension ClientParameterHandler { MVMCoreLoadingOverlayHandler.sharedLoadingOverlay()?.stopLoading(true) } } - return try await withCheckedThrowingContinuation({ continuation in - getParameters(with: model, requestParameters: requestParameters, actionId: actionId) { parameters in - continuation.resume(returning: parameters) - } - }) + return await getParameters(with: model, requestParameters: requestParameters, actionId: actionId) } } From 7ccb191f6e88c8fe82408d25c2db2a632419908c Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 19 Jan 2024 12:00:08 -0500 Subject: [PATCH 06/10] change convenience return --- .../Client Parameters/ClientParameterProtocol.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift index ead6837..28c018b 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift @@ -50,13 +50,8 @@ public extension ClientParameterProtocol { return [clientParameterModel.type: "failed_to_collect"] } - /// The handler should call this method to pass the parameter back to the caller. - /// If using isFlatMap, you must provide at least 1 element in parameters or it will result in triggering a timeout. - func returnParameters(_ isFlatMap: Bool, _ parameter: [String: AnyHashable], completionHandler: @escaping ([String: AnyHashable]?) -> ()) { - if isFlatMap { - completionHandler(parameter) - } else { - completionHandler([clientParameterModel.type: parameter]) - } + /// Convenience function to format the return parameters consistently. + func formatReturnParameters(_ parameter: AnyHashable) -> [String: AnyHashable] { + return [clientParameterModel.type: parameter] } } From ec610690fb0d0238a9ffaf3d42cc3e211b9fd0b9 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Wed, 31 Jan 2024 15:55:11 -0500 Subject: [PATCH 07/10] update to protocol non optional --- .../ClientParameterHandler.swift | 15 +++++++-------- .../ClientParameterProtocol.swift | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 5d4d370..ac58a3c 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -66,7 +66,7 @@ open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { let timeout = TimeInterval(model.timeout ?? Self.DefaultTimeout) let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } - return await withTaskGroup(of: [String: AnyHashable]?.self, returning: [String: AnyHashable].self) { group in + return await withTaskGroup(of: [String: AnyHashable].self, returning: [String: AnyHashable].self) { group in for handler in parameterHandlerList { group.addTask{ // Fetch the client parameter. @@ -75,9 +75,8 @@ name: handler.clientParameterModel.type, uuid: handler.clientParameterModel.id, actionId: actionId)) - let parameter: [String: AnyHashable]? = try await MVMCoreActionUtility.perform(withTimeout: timeout) { + let parameter: [String: AnyHashable] = try await MVMCoreActionUtility.perform(withTimeout: timeout) { let parameter = await handler.fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete( name: handler.clientParameterModel.type, uuid: handler.clientParameterModel.id, @@ -93,16 +92,16 @@ actionId: actionId)) return handler.valueOnTimeout() } catch { - // Should be impossible. - return nil + if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { + MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) + } + return [handler.clientParameterModel.type: "failed_to_collect"] } } } return await group.reduce(into: [String: AnyHashable]()) { partialResult, parameter in - if let parameter = parameter { - partialResult.merge(parameter) { first, last in first } - } + partialResult.merge(parameter) { first, last in first } } } } diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift index 28c018b..6f9eacc 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterProtocol.swift @@ -28,7 +28,7 @@ public protocol ClientParameterProtocol: ModelHandlerProtocol, AnyClientP var clientParameterModel: Model { get set } init(clientParameterModel: Model) - func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval) async -> [String: AnyHashable]? + func fetchClientParameters(requestParameters: [String: Any], timingOutIn timeout: TimeInterval) async -> [String: AnyHashable] /// Default parameter for timeout scenarios. It will use the protocol extension method bydefault. Can override to send custom values. func valueOnTimeout() -> [String: AnyHashable] From 606dbf61319d471b646cc5a1a7a5ec26be531887 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 2 Feb 2024 15:15:22 -0500 Subject: [PATCH 08/10] client parameter handler refactoring --- .../ClientParameterHandler.swift | 220 +++++++++++++++--- 1 file changed, 186 insertions(+), 34 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index ac58a3c..62a4103 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -22,7 +22,7 @@ /// completionHandler can return flat dictinary or a map. It depends on the paramters handler -@objcMembers open class ClientParameterHandler: NSObject { +@objc open class ClientParameterHandler: NSObject { public static let DefaultTimeout = 30.0 @@ -47,62 +47,214 @@ } } - func getClientParameterModel(_ clientParameters: [String: Any]) throws -> ClientParameterModel? { + open func getClientParameterModel(_ clientParameters: [String: Any]) throws -> ClientParameterModel? { let data = try JSONSerialization.data(withJSONObject: clientParameters) return try JSONDecoder.create().decode(ClientParameterModel.self, from: data) } + @objc open func getParameters(with clientParameters: [String: Any], requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: Any]?) -> ()) throws { guard let clientParameterModel = try getClientParameterModel(clientParameters) else { completionHandler(nil) return } - Task { - let parameters = await getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId) - completionHandler(parameters) + getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId, completionHandler: completionHandler) + } + +// open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { +// let timeout = TimeInterval(10)//model.timeout ?? Self.DefaultTimeout) +// let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } +// let parameters = await withThrowingTaskGroup(of: (any ClientParameterProtocol, [String: AnyHashable]).self, returning: [String: AnyHashable].self) { group in +// +// // Task for time out. +// group.addTask { +// try Task.checkCancellation() +// try await Task.sleep(nanoseconds: UInt64(timeout * TimeInterval(NSEC_PER_SEC))) +// try Task.checkCancellation() +// throw MVMCoreActionUtilityError.timeOut +// } +// +// for handler in parameterHandlerList { +// group.addTask{ +// try Task.checkCancellation() +// print("vvvv start \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") +// MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch( +// name: handler.clientParameterModel.type, +// uuid: handler.clientParameterModel.id, +// actionId: actionId)) +// let parameter = await handler.fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) +// print("vvvv end \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") +// MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete( +// name: handler.clientParameterModel.type, +// uuid: handler.clientParameterModel.id, +// actionId: actionId)) +// return (handler, parameter) +// } +// } +// // +// // return parameter +// // } catch MVMCoreActionUtilityError.timeOut { +// // // The client parameter timed out. +// // MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( +// // name: handler.clientParameterModel.type, +// // uuid: handler.clientParameterModel.id, +// // actionId: actionId)) +// // return handler.valueOnTimeout() +// // } catch { +// // if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { +// // MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) +// // } +// // return [handler.clientParameterModel.type: "failed_to_collect"] +// // } +// +// var finishedHandlers: [String: [String: AnyHashable]] = [:] +// //var parameters = [String: AnyHashable]() +// do { +// for try await currentParameter in group { +// print("vvvv finished \(currentParameter.0.clientParameterModel.type) \(currentParameter.0.clientParameterModel.id)") +// finishedHandlers[currentParameter.0.clientParameterModel.id] = currentParameter.1 +// guard finishedHandlers.count >= parameterHandlerList.count else { continue } +// print("vvvv finished all handlers") +// // Cancel the timeout +// group.cancelAll() +// } +// } catch MVMCoreActionUtilityError.timeOut { +// // The client parameter timed out. Log each event and set failure values for unfinished handlers. +// print("vvvv timeout \(timeout)") +// group.cancelAll() +// for handler in parameterHandlerList { +// guard finishedHandlers[handler.clientParameterModel.id] == nil else { continue } +// print("vvvv timeout \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") +// MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( +// name: handler.clientParameterModel.type, +// uuid: handler.clientParameterModel.id, +// actionId: actionId)) +// finishedHandlers[handler.clientParameterModel.id] = handler.valueOnTimeout() +// } +// } catch { +// if !(error is CancellationError) { +// print("vvvv error \(error.localizedDescription)") +// // Log the error and set failure values for unfinished handlers. +// group.cancelAll() +// if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { +// MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) +// } +// for handler in parameterHandlerList { +// guard finishedHandlers[handler.clientParameterModel.id] == nil else { continue } +// finishedHandlers[handler.clientParameterModel.id] = handler.valueOnTimeout() +// } +// } +// } +// print("vvvv finishing group") +// return finishedHandlers.reduce(into: [String: AnyHashable]()) { partialResult, parameter in +// partialResult.merge(parameter.value) { first, last in first } +// } +// } +// print("vvvv parameters \(parameters)") +// +// return parameters +// } +// open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { +// } + + open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { + await withCheckedContinuation { continuation in + getParameters(with: model, requestParameters: requestParameters, actionId: actionId) { parameters in + continuation.resume(returning: parameters) + } } } - open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { - let timeout = TimeInterval(model.timeout ?? Self.DefaultTimeout) + private func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String, completionHandler:@escaping ([String: AnyHashable]) -> ()) { + + let parametersWorkQueue = DispatchQueue(label: "com.mva.clientparameter") + let group = DispatchGroup() + let timeout = model.timeout ?? Self.DefaultTimeout + let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } - return await withTaskGroup(of: [String: AnyHashable].self, returning: [String: AnyHashable].self) { group in - for handler in parameterHandlerList { - group.addTask{ - // Fetch the client parameter. - do { - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch( - name: handler.clientParameterModel.type, - uuid: handler.clientParameterModel.id, - actionId: actionId)) - let parameter: [String: AnyHashable] = try await MVMCoreActionUtility.perform(withTimeout: timeout) { - let parameter = await handler.fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete( - name: handler.clientParameterModel.type, - uuid: handler.clientParameterModel.id, - actionId: actionId)) - return parameter - } - return parameter - } catch MVMCoreActionUtilityError.timeOut { - // The client parameter timed out. + let requestUUID = (0.. [String: AnyHashable] in + guard let parameter = element else { + let handler = parameterHandlerList[index] + print("vvvv timeout \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( name: handler.clientParameterModel.type, uuid: handler.clientParameterModel.id, actionId: actionId)) return handler.valueOnTimeout() - } catch { - if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { - MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) - } - return [handler.clientParameterModel.type: "failed_to_collect"] } + return parameter + }.reduce(into: [String: AnyHashable]()) { partialResult, next in + partialResult.merge(next) { first, last in first } + } + } + + // Setup completion handlers. Barriered to ensure one happens after the other. + var complete = false + let timeoutWorkItem = DispatchWorkItem(qos: .userInitiated) { + let params = mergedParametersList + print("vvvv timeout parameters \(params)") + completionHandler(params); + complete = true + } + let completionWorkItem = DispatchWorkItem(qos: .userInitiated) { + timeoutWorkItem.cancel() + print("vvvv finishing group") + if !complete { // In the case of firing after timeout. + let params = mergedParametersList + print("vvvv finishing group \(params)") + completionHandler(params); + complete = true } } - return await group.reduce(into: [String: AnyHashable]()) { partialResult, parameter in - partialResult.merge(parameter) { first, last in first } + // Setup timeout. + parametersWorkQueue.asyncAfter(deadline: .now() + .seconds(Int(timeout)), execute: timeoutWorkItem) + + // Setup the parameter execution. + for (index, parameterHandler) in parameterHandlerList.enumerated() { + let parameterType = parameterHandler.clientParameterModel.type + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch(name: parameterType, uuid: requestUUID[index], actionId: actionId)) + group.enter() + print("vvvv start \(parameterHandler.clientParameterModel.type) \(parameterHandler.clientParameterModel.id)") + parameterHandler.fetchClientParametersBridge(requestParameters: requestParameters, + timingOutIn: timeout) { (receivedParameter) in + print("vvvv end \(parameterHandler.clientParameterModel.type) \(parameterHandler.clientParameterModel.id)") + // Queue the results for merge. + parametersWorkQueue.async { + guard !complete else { + MVMCoreLoggingHandler.logDebugMessage(withDelegate: "Client \(parameterType) responded after task completion.") + return + } + guard returnedList[index] == nil else { + MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, message: "Client parameter \(parameterType) has already executed. The completion handler should only be called once!", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: String(describing: ClientParameterHandler.self))!) + return + } + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: requestUUID[index], actionId: actionId)) + returnedList[index] = receivedParameter + group.leave() // Leaving is only done after setup (barriered). + } + } } + + // Callback when all parameters have been merged. + group.notify(queue: parametersWorkQueue, work: completionWorkItem); + } + } +} + +fileprivate extension ClientParameterProtocol { + func fetchClientParametersBridge(requestParameters: [String: Any], timingOutIn timeout: TimeInterval, completion: @escaping ([String: AnyHashable]?) -> ()) { + Task { + let parameters = await fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) + completion(parameters) } } } From 3bde28c2d28a93f63b9a9d03681a5795dbce669f Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 2 Feb 2024 15:28:32 -0500 Subject: [PATCH 09/10] Cleanup --- .../ClientParameterHandler.swift | 108 +----------------- 1 file changed, 3 insertions(+), 105 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index 62a4103..da1a93a 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -61,102 +61,6 @@ getParameters(with: clientParameterModel, requestParameters: requestParameters, actionId: actionId, completionHandler: completionHandler) } -// open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { -// let timeout = TimeInterval(10)//model.timeout ?? Self.DefaultTimeout) -// let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } -// let parameters = await withThrowingTaskGroup(of: (any ClientParameterProtocol, [String: AnyHashable]).self, returning: [String: AnyHashable].self) { group in -// -// // Task for time out. -// group.addTask { -// try Task.checkCancellation() -// try await Task.sleep(nanoseconds: UInt64(timeout * TimeInterval(NSEC_PER_SEC))) -// try Task.checkCancellation() -// throw MVMCoreActionUtilityError.timeOut -// } -// -// for handler in parameterHandlerList { -// group.addTask{ -// try Task.checkCancellation() -// print("vvvv start \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") -// MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch( -// name: handler.clientParameterModel.type, -// uuid: handler.clientParameterModel.id, -// actionId: actionId)) -// let parameter = await handler.fetchClientParameters(requestParameters: requestParameters, timingOutIn: timeout) -// print("vvvv end \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") -// MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete( -// name: handler.clientParameterModel.type, -// uuid: handler.clientParameterModel.id, -// actionId: actionId)) -// return (handler, parameter) -// } -// } -// // -// // return parameter -// // } catch MVMCoreActionUtilityError.timeOut { -// // // The client parameter timed out. -// // MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( -// // name: handler.clientParameterModel.type, -// // uuid: handler.clientParameterModel.id, -// // actionId: actionId)) -// // return handler.valueOnTimeout() -// // } catch { -// // if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { -// // MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) -// // } -// // return [handler.clientParameterModel.type: "failed_to_collect"] -// // } -// -// var finishedHandlers: [String: [String: AnyHashable]] = [:] -// //var parameters = [String: AnyHashable]() -// do { -// for try await currentParameter in group { -// print("vvvv finished \(currentParameter.0.clientParameterModel.type) \(currentParameter.0.clientParameterModel.id)") -// finishedHandlers[currentParameter.0.clientParameterModel.id] = currentParameter.1 -// guard finishedHandlers.count >= parameterHandlerList.count else { continue } -// print("vvvv finished all handlers") -// // Cancel the timeout -// group.cancelAll() -// } -// } catch MVMCoreActionUtilityError.timeOut { -// // The client parameter timed out. Log each event and set failure values for unfinished handlers. -// print("vvvv timeout \(timeout)") -// group.cancelAll() -// for handler in parameterHandlerList { -// guard finishedHandlers[handler.clientParameterModel.id] == nil else { continue } -// print("vvvv timeout \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") -// MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( -// name: handler.clientParameterModel.type, -// uuid: handler.clientParameterModel.id, -// actionId: actionId)) -// finishedHandlers[handler.clientParameterModel.id] = handler.valueOnTimeout() -// } -// } catch { -// if !(error is CancellationError) { -// print("vvvv error \(error.localizedDescription)") -// // Log the error and set failure values for unfinished handlers. -// group.cancelAll() -// if let errorObject = MVMCoreErrorObject.createErrorObject(for: error, location: #function) { -// MVMCoreLoggingHandler.shared()?.addError(toLog: errorObject) -// } -// for handler in parameterHandlerList { -// guard finishedHandlers[handler.clientParameterModel.id] == nil else { continue } -// finishedHandlers[handler.clientParameterModel.id] = handler.valueOnTimeout() -// } -// } -// } -// print("vvvv finishing group") -// return finishedHandlers.reduce(into: [String: AnyHashable]()) { partialResult, parameter in -// partialResult.merge(parameter.value) { first, last in first } -// } -// } -// print("vvvv parameters \(parameters)") -// -// return parameters -// } -// open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { -// } - open func getParameters(with model: ClientParameterModel, requestParameters: [String: Any], actionId: String) async -> [String: AnyHashable] { await withCheckedContinuation { continuation in getParameters(with: model, requestParameters: requestParameters, actionId: actionId) { parameters in @@ -172,7 +76,6 @@ let timeout = model.timeout ?? Self.DefaultTimeout let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } - let requestUUID = (0.. [String: AnyHashable] in guard let parameter = element else { let handler = parameterHandlerList[index] - print("vvvv timeout \(handler.clientParameterModel.type) \(handler.clientParameterModel.id)") MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterTimeout( name: handler.clientParameterModel.type, uuid: handler.clientParameterModel.id, @@ -200,16 +102,13 @@ var complete = false let timeoutWorkItem = DispatchWorkItem(qos: .userInitiated) { let params = mergedParametersList - print("vvvv timeout parameters \(params)") completionHandler(params); complete = true } let completionWorkItem = DispatchWorkItem(qos: .userInitiated) { timeoutWorkItem.cancel() - print("vvvv finishing group") if !complete { // In the case of firing after timeout. let params = mergedParametersList - print("vvvv finishing group \(params)") completionHandler(params); complete = true } @@ -221,12 +120,11 @@ // Setup the parameter execution. for (index, parameterHandler) in parameterHandlerList.enumerated() { let parameterType = parameterHandler.clientParameterModel.type - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch(name: parameterType, uuid: requestUUID[index], actionId: actionId)) + let id = parameterHandler.clientParameterModel.id + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterStartFetch(name: parameterType, uuid: id, actionId: actionId)) group.enter() - print("vvvv start \(parameterHandler.clientParameterModel.type) \(parameterHandler.clientParameterModel.id)") parameterHandler.fetchClientParametersBridge(requestParameters: requestParameters, timingOutIn: timeout) { (receivedParameter) in - print("vvvv end \(parameterHandler.clientParameterModel.type) \(parameterHandler.clientParameterModel.id)") // Queue the results for merge. parametersWorkQueue.async { guard !complete else { @@ -237,7 +135,7 @@ MVMCoreLoggingHandler.shared()?.addError(toLog: MVMCoreErrorObject(title: nil, message: "Client parameter \(parameterType) has already executed. The completion handler should only be called once!", code: ErrorCode.default.rawValue, domain: ErrorDomainNative, location: String(describing: ClientParameterHandler.self))!) return } - MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: requestUUID[index], actionId: actionId)) + MVMCoreLoggingHandler.shared()?.logCoreEvent(.clientParameterFetchComplete(name: parameterType, uuid: id, actionId: actionId)) returnedList[index] = receivedParameter group.leave() // Leaving is only done after setup (barriered). } From fc0bd385c605c533686febb43a69c7ccc487e8ff Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Wed, 14 Feb 2024 15:48:33 -0500 Subject: [PATCH 10/10] Revert Logging UUID for the run --- .../Client Parameters/ClientParameterHandler.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift index da1a93a..21149cf 100644 --- a/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift +++ b/MVMCore/MVMCore/Models/ActionType/Client Parameters/ClientParameterHandler.swift @@ -76,6 +76,7 @@ let timeout = model.timeout ?? Self.DefaultTimeout let parameterHandlerList = model.list.compactMap { createParametersHandler($0) } + let requestUUID = (0..