Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Reworked background helpers #100

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 0 additions & 56 deletions Sources/InfomaniakCore/Asynchronous/BackgroundExecutor.swift

This file was deleted.

13 changes: 7 additions & 6 deletions Sources/InfomaniakCore/Asynchronous/FlowToAsyncResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,14 @@ import Foundation
/// The *first* event received will be forwarded. Thread safe.
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class FlowToAsyncResult<Success> {

/// Something to deal with live observation
typealias CompletionClosure = (Result<Success, Error>) -> Void

// MARK: Private

/// Serial locking queue
private let lock = DispatchQueue(label: "com.infomaniak.core.FlowToAsyncResult.lock")
private let lock: DispatchQueue

/// The internal state of `FlowToAsyncResult`
private enum State {
/// Waiting for input events
Expand Down Expand Up @@ -111,8 +110,10 @@ public final class FlowToAsyncResult<Success> {

// MARK: Init

public init() {
// META keep SonarCloud happy
/// Init method
/// - Parameter qos: Optionally set a custom QoS used by underlying queues
public init(qos: DispatchQoS = .default) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can specify QoS on FlowToAsyncResult to be consistent with other core types.

lock = DispatchQueue(label: "com.infomaniak.core.FlowToAsyncResult.lock", qos: qos)
}
}

Expand Down
78 changes: 78 additions & 0 deletions Sources/InfomaniakCore/Background/ClosureExpiringActivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Infomaniak Core - iOS
Copyright (C) 2023 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import CocoaLumberjackSwift
import Foundation

public typealias TaskCompletion = () -> Void

/// Something that can perform arbitrary short background tasks, with closure API.
protocol ClosureExpiringActivable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed BackgroundExecutor into ClosureExpiringActivity with a linked protocol ClosureExpiringActivable to move to something testable.

/// Perform a short task in the background, be notified when the system wants to expire the task.
/// - Parameters:
/// - block: The work to be performed on the background
/// - onExpired: The closure called by the system when we should end.
func executeWithBackgroundTask(_ block: @escaping (@escaping TaskCompletion) -> Void,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Maybe change executeWithBackgroundTask for executeWithShortBackground ?

onExpired: @escaping () -> Void)
}

/// Something to wrap arbitrary code that should be performed on the background.
public struct ClosureExpiringActivity: ClosureExpiringActivable {
private let processInfo = ProcessInfo.processInfo

private let qos: DispatchQoS

/// Init method of `BackgroundExecutor`
/// - Parameter qos: QoS used by the underlying queues. Defaults to `.userInitiated` to prevent most priority inversions.
public init(qos: DispatchQoS = .userInitiated) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QoS of ClosureExpiringActivity can be specified at init.

self.qos = qos
}

// MARK: - BackgroundExecutable

public func executeWithBackgroundTask(_ block: @escaping (@escaping TaskCompletion) -> Void,
onExpired: @escaping () -> Void) {
let taskName = "executeWithBackgroundTask \(UUID().uuidString)"

#if os(macOS)
DDLogDebug("Starting task \(taskName) (No expiration handler as we are running on macOS)")
processInfo.performActivity(options: .suddenTerminationDisabled, reason: taskName) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call been synchronous, I removed the DispatchGroup on macOS , it works out of the box.

block {
DDLogDebug("Ending task \(taskName)")
}
}
#else
DDLogDebug("Starting task \(taskName)")
let group = TolerantDispatchGroup(qos: qos)
group.enter()
processInfo.performExpiringActivity(withReason: taskName) { expired in
if expired {
onExpired()
DDLogDebug("Expired task \(taskName)")
group.leave()
} else {
block {
DDLogDebug("Ending task \(taskName)")
group.leave()
}
group.wait()
}
}
#endif
}
}
123 changes: 123 additions & 0 deletions Sources/InfomaniakCore/Background/ExpiringActivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// Delegation mechanism to notify the end of an `ExpiringActivity`
public protocol ExpiringActivityDelegate: AnyObject {
/// Called when the system is requiring us to terminate an expiring activity
func backgroundActivityExpiring()
}

/// Something that can perform arbitrary short background tasks, with a super simple API.
public protocol ExpiringActivityable {
/// Common init method
/// - Parameters:
/// - id: Something to identify the background activity in debug
/// - qos: QoS used by the underlying queues
/// - delegate: The delegate to notify we should terminate
init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?)

/// init method
/// - Parameters:
/// - id: Something to identify the background activity in debug
/// - delegate: The delegate to notify we should terminate
init(id: String, delegate: ExpiringActivityDelegate?)

/// Register with the system an expiring activity
func start()

/// Terminate all the expiring activities
func endAll()
}

public final class ExpiringActivity: ExpiringActivityable {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something in Drive Core that I brought here, also cleaning it a bit, adding some tests, documenting the protocol. The whole 9 yards.

private let qos: DispatchQoS

private let queue: DispatchQueue

private let processInfo = ProcessInfo.processInfo

var locks = [TolerantDispatchGroup]()

let id: String

weak var delegate: ExpiringActivityDelegate?

// MARK: Lifecycle

public init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?) {
self.id = id
self.qos = qos
self.delegate = delegate
queue = DispatchQueue(label: "com.infomaniak.ExpiringActivity.sync", qos: qos)
}

public convenience init(id: String, delegate: ExpiringActivityDelegate?) {
self.init(id: id, qos: .userInitiated, delegate: delegate)
}

deinit {
queue.sync {
assert(locks.isEmpty, "please make sure to call 'endAll()' once explicitly before releasing this object")
}
}

public func start() {
let group = TolerantDispatchGroup(qos: qos)

queue.sync {
self.locks.append(group)
}

#if os(macOS)
// We block a non cooperative queue that matches current QoS
DispatchQueue.global(qos: qos.qosClass).async {
self.processInfo.performActivity(options: .suddenTerminationDisabled, reason: self.id) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call in synchronous, therefore I jump queue, unlike iOS.

// No expiration handler as we are running on macOS
group.enter()
group.wait()
}
}
#else
// Make sure to not lock an unexpected thread that would deinit()
processInfo.performExpiringActivity(withReason: id) { [weak self] shouldTerminate in
guard let self else {
return
}

if shouldTerminate {
delegate?.backgroundActivityExpiring()
}

group.enter()
group.wait()
}
#endif
}

public func endAll() {
queue.sync {
// Release locks, oldest first
for group in locks.reversed() {
group.leave()
}
locks.removeAll()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import InfomaniakCore
import XCTest

final class ITClosureExpiringActivity: XCTestCase {
func testWorkClosureExecuted() {
// GIVEN
let expectation = CountedFulfillmentTestExpectation(description: "Closure is called")
let closureActivity = ClosureExpiringActivity()

// WHEN
closureActivity.executeWithBackgroundTask { completion in
expectation.fulfill()
completion()
} onExpired: {
XCTFail("Unexpected")
}

// THEN
wait(for: [expectation], timeout: 10.0)
}
}
Loading
Loading