-
Notifications
You must be signed in to change notification settings - Fork 0
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
Changes from all commits
16a7cf9
6d5c91a
19d7e7d
f80b603
e04cb00
5ded5cd
2f1d979
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed |
||
/// 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: Maybe change |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. QoS of |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This call been synchronous, I removed the |
||
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 | ||
} | ||
} |
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is something in |
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} |
There was a problem hiding this comment.
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.