Sources/XCMetricsClient/Mobius/Domain/MetricsUploaderLogic.swift (125 lines of code) (raw):

// Copyright (c) 2020 Spotify AB. // // Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. import Foundation import MobiusCore /// To avoid using too much CPU in case the user has many logs to parse and upload, /// we set a maximum number of logs to parse and send at each invocation. Other logs /// will be parsed and sent in future invocations. private let maximumNumberOfLogsToParseAndSend = 3 /// To avoid spending too much time uploading requests, we at most upload this value of requests previously saved. private let maximumNumberOfParsedRequestsToSend = 3 /// We have to wait for both `cleanedUpLogs` and `savedUploadRequests` events to happpen before finishing executing. private var expectedRequests = 2 /// Lock that manages read/write access to `expectedRequests`. private let expectedRequestsLock = NSLock() enum MetricsUploaderLogic { static func buildInitiator(with initEffect: MetricsUploaderEffect) -> (MetricsUploaderModel) -> First<MetricsUploaderModel, MetricsUploaderEffect> { return { model in return First( model: model, effects: [initEffect] ) } } static func update(model: MetricsUploaderModel, event: MetricsUploaderEvent) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { switch event { case .logsFound(let currentLog, let xcodeLogs, let cachedLogs): return onLogsFound(model, currentLog, xcodeLogs, cachedLogs) case .logsCached(let cachedCurrentLog, let cachedLogs, let cachedUploadRequest): return onLogsCached(model, cachedCurrentLog, cachedLogs, cachedUploadRequest) case .requestMetadataAppended(let currentRequest): return onRequestMetadataAppended(model, currentRequest) case .pluginsExecuted(let currentRequest): return onPluginsExecuted(model, currentRequest) case .logsUploaded(let uploadedLogs): return onLogsUploaded(model, uploadedLogs) case .logsTaggedAsUploaded(let taggedLogs): return onLogsTaggedAsUploaded(model, taggedLogs) case .cleanedUpLogs(let cleanedUpLogs): return onCleanedUpLogs(model, cleanedUpLogs) case .logsUploadFailed(let failedLogs): return onLogsUploadFailed(model, failedLogs) case .savedUploadRequests: return onSavedUploadRequests(model) } } private static func onLogsFound(_ model: MetricsUploaderModel, _ currentLog: URL?, _ xcodeLogs: Set<URL>, _ cachedLogs: Set<URL>) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { return .dispatchEffects([.cacheLogs(currentLog: currentLog, previousLogs: xcodeLogs, cachedLogs: cachedLogs, projectName: model.projectName)]) } private static func onLogsCached(_ model: MetricsUploaderModel, _ cachedCurrentLog: URL?, _ cachedLogs: Set<URL>, _ cachedUploadRequest: Set<MetricsUploadRequest>) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { var effects: [MetricsUploaderEffect] = [] if let cachedCurrentLog = cachedCurrentLog { effects.append(.appendMetadata(request: MetricsUploadRequest(fileURL: cachedCurrentLog, request: UploadBuildMetricsRequest()))) } // TODO(patrickb): maybe only send maximumNumberOfLogsToParseAndSend logs let uploadRequests = Set(cachedLogs.map { MetricsUploadRequest(fileURL: $0) }) if !uploadRequests.isEmpty { effects.append(.uploadLogs( serviceURL: model.serviceURL, additionalHeaders: model.additionalHeaders, projectName: model.projectName, isCI: model.isCI, skipNotes: model.skipNotes, truncLargeIssues: model.truncLargeIssues, logs: uploadRequests )) } let updatedModel = model.withChanged( parsedRequests: model.parsedRequests.union(cachedUploadRequest.prefix(maximumNumberOfParsedRequestsToSend)), awaitingParsingLogResponses: 0 ) // If no log to parse has been found, skip directly to upload cached logs if any. if effects.isEmpty { return .next(updatedModel, effects: [.uploadLogs( serviceURL: model.serviceURL, additionalHeaders: model.additionalHeaders, projectName: model.projectName, isCI: model.isCI, skipNotes: model.skipNotes, truncLargeIssues: model.truncLargeIssues, logs: updatedModel.parsedRequests )]) } return .next(updatedModel, effects: effects) } private static func onRequestMetadataAppended(_ model: MetricsUploaderModel, _ request: MetricsUploadRequest) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { return .dispatchEffects([.executePlugins(request: request, plugins: model.plugins)]) } private static func onPluginsExecuted(_ model: MetricsUploaderModel, _ request: MetricsUploadRequest) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { let updatedModel = model.withChanged(parsedRequests: model.parsedRequests.union([request])) return .next(updatedModel, effects: [ .uploadLogs( serviceURL: model.serviceURL, additionalHeaders: model.additionalHeaders, projectName: model.projectName, isCI: model.isCI, skipNotes: model.skipNotes, truncLargeIssues: model.truncLargeIssues, logs: updatedModel.parsedRequests ) ]) } private static func onLogsUploaded(_ model: MetricsUploaderModel, _ uploadedLogs: Set<URL>) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { if uploadedLogs.isEmpty { return .dispatchEffects([.cleanUpLogs]) } return .dispatchEffects([.tagLogsAsUploaded(logs: uploadedLogs)]) } private static func onLogsTaggedAsUploaded(_ model: MetricsUploaderModel, _ taggedLogs: Set<URL>) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { return .dispatchEffects([.cleanUpLogs]) } private static func onLogsUploadFailed(_ model: MetricsUploaderModel, _ failedLogs: [URL: Data]) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { return .dispatchEffects([.persistNonUploadedLogs(logs: failedLogs)]) } private static func onCleanedUpLogs(_ model: MetricsUploaderModel, _ cleandUpLogs: Set<URL>) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { expectedRequestsLock.lock() defer { expectedRequestsLock.unlock() } expectedRequests -= 1 if expectedRequests == 0 { NotificationCenter.default.post(name: .mobiusLoopCompleted, object: nil) } return .noChange } private static func onSavedUploadRequests(_ model: MetricsUploaderModel) -> Next<MetricsUploaderModel, MetricsUploaderEffect> { expectedRequestsLock.lock() defer { expectedRequestsLock.unlock() } expectedRequests -= 1 if expectedRequests == 0 { NotificationCenter.default.post(name: .mobiusLoopCompleted, object: nil) } return .noChange } }