From 606dbf61319d471b646cc5a1a7a5ec26be531887 Mon Sep 17 00:00:00 2001 From: Scott Pfeil Date: Fri, 2 Feb 2024 15:15:22 -0500 Subject: [PATCH] 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) } } }