Skip to content

Commit 45a5a4d

Browse files
authored
[Core] Make storage class conform to Sendable and subsequent changes (#13939)
1 parent 00674c8 commit 45a5a4d

File tree

10 files changed

+135
-106
lines changed

10 files changed

+135
-106
lines changed

FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatController.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import Foundation
1616

1717
/// An object that provides API to log and flush heartbeats from a synchronized storage container.
18-
public final class HeartbeatController {
18+
public final class HeartbeatController: Sendable {
1919
/// Used for standardizing dates for calendar-day comparison.
2020
private enum DateStandardizer {
2121
private static let calendar: Calendar = {
@@ -31,18 +31,18 @@ public final class HeartbeatController {
3131
}
3232

3333
/// The thread-safe storage object to log and flush heartbeats from.
34-
private let storage: HeartbeatStorageProtocol
34+
private let storage: any HeartbeatStorageProtocol
3535
/// The max capacity of heartbeats to store in storage.
36-
private let heartbeatsStorageCapacity: Int = 30
36+
private static let heartbeatsStorageCapacity: Int = 30
3737
/// Current date provider. It is used for testability.
38-
private let dateProvider: () -> Date
38+
private let dateProvider: @Sendable () -> Date
3939
/// Used for standardizing dates for calendar-day comparison.
4040
private static let dateStandardizer = DateStandardizer.self
4141

4242
/// Public initializer.
4343
/// - Parameter id: The `id` to associate this controller's heartbeat storage with.
4444
public convenience init(id: String) {
45-
self.init(id: id, dateProvider: Date.init)
45+
self.init(id: id, dateProvider: { Date() })
4646
}
4747

4848
/// Convenience initializer. Mirrors the semantics of the public initializer with the added
@@ -51,7 +51,7 @@ public final class HeartbeatController {
5151
/// - Parameters:
5252
/// - id: The id to associate this controller's heartbeat storage with.
5353
/// - dateProvider: A date provider.
54-
convenience init(id: String, dateProvider: @escaping () -> Date) {
54+
convenience init(id: String, dateProvider: @escaping @Sendable () -> Date) {
5555
let storage = HeartbeatStorage.getInstance(id: id)
5656
self.init(storage: storage, dateProvider: dateProvider)
5757
}
@@ -61,7 +61,7 @@ public final class HeartbeatController {
6161
/// - storage: A heartbeat storage container.
6262
/// - dateProvider: A date provider. Defaults to providing the current date.
6363
init(storage: HeartbeatStorageProtocol,
64-
dateProvider: @escaping () -> Date = Date.init) {
64+
dateProvider: @escaping @Sendable () -> Date = { Date() }) {
6565
self.storage = storage
6666
self.dateProvider = { Self.dateStandardizer.standardize(dateProvider()) }
6767
}
@@ -76,7 +76,7 @@ public final class HeartbeatController {
7676

7777
storage.readAndWriteAsync { heartbeatsBundle in
7878
var heartbeatsBundle = heartbeatsBundle ??
79-
HeartbeatsBundle(capacity: self.heartbeatsStorageCapacity)
79+
HeartbeatsBundle(capacity: Self.heartbeatsStorageCapacity)
8080

8181
// Filter for the time periods where the last heartbeat to be logged for
8282
// that time period was logged more than one time period (i.e. day) ago.
@@ -109,7 +109,7 @@ public final class HeartbeatController {
109109
// The new value that's stored will use the old's cache to prevent the
110110
// logging of duplicates after flushing.
111111
return HeartbeatsBundle(
112-
capacity: self.heartbeatsStorageCapacity,
112+
capacity: Self.heartbeatsStorageCapacity,
113113
cache: oldHeartbeatsBundle.lastAddedHeartbeatDates
114114
)
115115
}
@@ -126,15 +126,15 @@ public final class HeartbeatController {
126126
}
127127
}
128128

129-
public func flushAsync(completionHandler: @escaping (HeartbeatsPayload) -> Void) {
130-
let resetTransform = { (heartbeatsBundle: HeartbeatsBundle?) -> HeartbeatsBundle? in
129+
public func flushAsync(completionHandler: @escaping @Sendable (HeartbeatsPayload) -> Void) {
130+
let resetTransform = { @Sendable (heartbeatsBundle: HeartbeatsBundle?) -> HeartbeatsBundle? in
131131
guard let oldHeartbeatsBundle = heartbeatsBundle else {
132132
return nil // Storage was empty.
133133
}
134134
// The new value that's stored will use the old's cache to prevent the
135135
// logging of duplicates after flushing.
136136
return HeartbeatsBundle(
137-
capacity: self.heartbeatsStorageCapacity,
137+
capacity: Self.heartbeatsStorageCapacity,
138138
cache: oldHeartbeatsBundle.lastAddedHeartbeatDates
139139
)
140140
}

FirebaseCore/Internal/Sources/HeartbeatLogging/HeartbeatStorage.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,22 @@
1515
import Foundation
1616

1717
/// A type that can perform atomic operations using block-based transformations.
18-
protocol HeartbeatStorageProtocol {
18+
protocol HeartbeatStorageProtocol: Sendable {
1919
func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?)
20-
func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?)
20+
func readAndWriteAsync(using transform: @escaping @Sendable (HeartbeatsBundle?)
21+
-> HeartbeatsBundle?)
2122
func getAndSet(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) throws
2223
-> HeartbeatsBundle?
23-
func getAndSetAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?,
24-
completion: @escaping (Result<HeartbeatsBundle?, Error>) -> Void)
24+
func getAndSetAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) -> HeartbeatsBundle?,
25+
completion: @escaping @Sendable (Result<HeartbeatsBundle?, Error>) -> Void)
2526
}
2627

2728
/// Thread-safe storage object designed for transforming heartbeat data that is persisted to disk.
28-
final class HeartbeatStorage: HeartbeatStorageProtocol {
29+
final class HeartbeatStorage: Sendable, HeartbeatStorageProtocol {
2930
/// The identifier used to differentiate instances.
3031
private let id: String
3132
/// The underlying storage container to read from and write to.
32-
private let storage: Storage
33+
private let storage: any Storage
3334
/// The encoder used for encoding heartbeat data.
3435
private let encoder: JSONEncoder = .init()
3536
/// The decoder used for decoding heartbeat data.
@@ -130,7 +131,8 @@ final class HeartbeatStorage: HeartbeatStorageProtocol {
130131
/// Asynchronously reads from and writes to storage using the given transform block.
131132
/// - Parameter transform: A block to transform the currently stored heartbeats bundle to a new
132133
/// heartbeats bundle value.
133-
func readAndWriteAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?) {
134+
func readAndWriteAsync(using transform: @escaping @Sendable (HeartbeatsBundle?)
135+
-> HeartbeatsBundle?) {
134136
queue.async { [self] in
135137
let oldHeartbeatsBundle = try? load(from: storage)
136138
let newHeartbeatsBundle = transform(oldHeartbeatsBundle)
@@ -166,8 +168,8 @@ final class HeartbeatStorage: HeartbeatStorageProtocol {
166168
/// - completion: An escaping block used to process the heartbeat data that
167169
/// was stored (before the `transform` was applied); otherwise, the error
168170
/// that occurred.
169-
func getAndSetAsync(using transform: @escaping (HeartbeatsBundle?) -> HeartbeatsBundle?,
170-
completion: @escaping (Result<HeartbeatsBundle?, Error>) -> Void) {
171+
func getAndSetAsync(using transform: @escaping @Sendable (HeartbeatsBundle?) -> HeartbeatsBundle?,
172+
completion: @escaping @Sendable (Result<HeartbeatsBundle?, Error>) -> Void) {
171173
queue.async {
172174
do {
173175
let oldHeartbeatsBundle = try? self.load(from: self.storage)

FirebaseCore/Internal/Sources/HeartbeatLogging/Storage.swift

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import Foundation
1616

1717
/// A type that reads from and writes to an underlying storage container.
18-
protocol Storage {
18+
protocol Storage: Sendable {
1919
/// Reads and returns the data stored by this storage type.
2020
/// - Returns: The data read from storage.
2121
/// - Throws: An error if the read failed.
@@ -38,16 +38,12 @@ enum StorageError: Error {
3838
final class FileStorage: Storage {
3939
/// A file system URL to the underlying file resource.
4040
private let url: URL
41-
/// The file manager used to perform file system operations.
42-
private let fileManager: FileManager
4341

4442
/// Designated initializer.
4543
/// - Parameters:
4644
/// - url: A file system URL for the underlying file resource.
47-
/// - fileManager: A file manager. Defaults to `default` manager.
48-
init(url: URL, fileManager: FileManager = .default) {
45+
init(url: URL) {
4946
self.url = url
50-
self.fileManager = fileManager
5147
}
5248

5349
/// Reads and returns the data from this object's associated file resource.
@@ -90,7 +86,7 @@ final class FileStorage: Storage {
9086
/// - Parameter url: The URL to create directories in.
9187
private func createDirectories(in url: URL) throws {
9288
do {
93-
try fileManager.createDirectory(
89+
try FileManager.default.createDirectory(
9490
at: url,
9591
withIntermediateDirectories: true
9692
)
@@ -104,17 +100,26 @@ final class FileStorage: Storage {
104100

105101
/// A object that provides API for reading and writing to a user defaults resource.
106102
final class UserDefaultsStorage: Storage {
107-
/// The underlying defaults container.
108-
private let defaults: UserDefaults
103+
/// The suite name for the underlying defaults container.
104+
private let suiteName: String
105+
109106
/// The key mapping to the object's associated resource in `defaults`.
110107
private let key: String
111108

109+
/// The underlying defaults container.
110+
private var defaults: UserDefaults {
111+
// It's safe to force unwrap the below defaults instance because the
112+
// initializer only returns `nil` when the bundle id or `globalDomain`
113+
// is passed in as the `suiteName`.
114+
UserDefaults(suiteName: suiteName)!
115+
}
116+
112117
/// Designated initializer.
113118
/// - Parameters:
114-
/// - defaults: The defaults container.
119+
/// - suiteName: The suite name for the defaults container.
115120
/// - key: The key mapping to the value stored in the defaults container.
116-
init(defaults: UserDefaults, key: String) {
117-
self.defaults = defaults
121+
init(suiteName: String, key: String) {
122+
self.suiteName = suiteName
118123
self.key = key
119124
}
120125

FirebaseCore/Internal/Sources/HeartbeatLogging/StorageFactory.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,7 @@ extension FileManager {
5656
extension UserDefaultsStorage: StorageFactory {
5757
static func makeStorage(id: String) -> Storage {
5858
let suiteName = Constants.heartbeatUserDefaultsSuiteName
59-
// It's safe to force unwrap the below defaults instance because the
60-
// initializer only returns `nil` when the bundle id or `globalDomain`
61-
// is passed in as the `suiteName`.
62-
let defaults = UserDefaults(suiteName: suiteName)!
6359
let key = "heartbeats-\(id)"
64-
return UserDefaultsStorage(defaults: defaults, key: key)
60+
return UserDefaultsStorage(suiteName: suiteName, key: key)
6561
}
6662
}

FirebaseCore/Internal/Sources/HeartbeatLogging/_ObjC_HeartbeatController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public class _ObjC_HeartbeatController: NSObject {
4949
///
5050
/// - Note: This API is thread-safe.
5151
/// - Returns: A heartbeats payload for the flushed heartbeat(s).
52-
public func flushAsync(completionHandler: @escaping (_ObjC_HeartbeatsPayload) -> Void) {
52+
public func flushAsync(completionHandler: @escaping @Sendable (_ObjC_HeartbeatsPayload) -> Void) {
5353
// TODO: When minimum version moves to iOS 13.0, restore the async version
5454
// removed in #13952.
5555
heartbeatController.flushAsync { heartbeatsPayload in
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
/// Used to manipulate a date across multiple concurrent contexts for simulation purposes.
18+
final class AdjustableDate: @unchecked Sendable {
19+
var date: Date
20+
init(date: Date) {
21+
self.date = date
22+
}
23+
}

FirebaseCore/Internal/Tests/Integration/HeartbeatLoggingIntegrationTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ class HeartbeatLoggingIntegrationTests: XCTestCase {
232232

233233
func testLogRepeatedly_WithoutFlushing_LimitsOnWrite() throws {
234234
// Given
235-
var testdate = date
236-
let heartbeatController = HeartbeatController(id: #function, dateProvider: { testdate })
235+
let testDate = AdjustableDate(date: date)
236+
let heartbeatController = HeartbeatController(id: #function, dateProvider: { testDate.date })
237237

238238
// When
239239
// Iterate over 35 days and log a heartbeat each day.
@@ -252,7 +252,7 @@ class HeartbeatLoggingIntegrationTests: XCTestCase {
252252
heartbeatController.log("dummy_agent_3")
253253
}
254254

255-
testdate.addTimeInterval(60 * 60 * 24) // Advance the test date by 1 day.
255+
testDate.date.addTimeInterval(60 * 60 * 24) // Advance the test date by 1 day.
256256
}
257257

258258
// Then

FirebaseCore/Internal/Tests/Unit/HeartbeatControllerTests.swift

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ class HeartbeatControllerTests: XCTestCase {
9999

100100
func testLogAtEndOfTimePeriodAndAcceptAtStartOfNextOne() throws {
101101
// Given
102-
var testDate = date
102+
let testDate = AdjustableDate(date: date)
103103

104104
let controller = HeartbeatController(
105105
storage: HeartbeatStorageFake(),
106-
dateProvider: { testDate }
106+
dateProvider: { testDate.date }
107107
)
108108

109109
assertHeartbeatControllerFlushesEmptyPayload(controller)
@@ -113,12 +113,12 @@ class HeartbeatControllerTests: XCTestCase {
113113
controller.log("dummy_agent")
114114

115115
// - Advance to 2021-11-01 @ 23:59:59 (EST)
116-
testDate.addTimeInterval(60 * 60 * 24 - 1)
116+
testDate.date.addTimeInterval(60 * 60 * 24 - 1)
117117

118118
controller.log("dummy_agent")
119119

120120
// - Advance to 2021-11-02 @ 00:00:00 (EST)
121-
testDate.addTimeInterval(1)
121+
testDate.date.addTimeInterval(1)
122122

123123
controller.log("dummy_agent")
124124

@@ -271,18 +271,18 @@ class HeartbeatControllerTests: XCTestCase {
271271
).date // 2021-11-02 @ 11 PM (Tokyo time zone)
272272
)
273273

274-
var testDate = newYorkDate
274+
let testDate = AdjustableDate(date: newYorkDate)
275275

276276
let heartbeatController = HeartbeatController(
277277
storage: HeartbeatStorageFake(),
278-
dateProvider: { testDate }
278+
dateProvider: { testDate.date }
279279
)
280280

281281
// When
282282
heartbeatController.log("dummy_agent")
283283

284284
// Device travels from NYC to Tokyo.
285-
testDate = tokyoDate
285+
testDate.date = tokyoDate
286286

287287
heartbeatController.log("dummy_agent")
288288

@@ -308,22 +308,22 @@ class HeartbeatControllerTests: XCTestCase {
308308

309309
func testLoggingDependsOnDateNotUserAgent() throws {
310310
// Given
311-
var testDate = date
311+
let testDate = AdjustableDate(date: date)
312312
let heartbeatController = HeartbeatController(
313313
storage: HeartbeatStorageFake(),
314-
dateProvider: { testDate }
314+
dateProvider: { testDate.date }
315315
)
316316

317317
// When
318318
// - Day 1
319319
heartbeatController.log("dummy_agent")
320320

321321
// - Day 2
322-
testDate.addTimeInterval(60 * 60 * 24)
322+
testDate.date.addTimeInterval(60 * 60 * 24)
323323
heartbeatController.log("some_other_agent")
324324

325325
// - Day 3
326-
testDate.addTimeInterval(60 * 60 * 24)
326+
testDate.date.addTimeInterval(60 * 60 * 24)
327327
heartbeatController.log("dummy_agent")
328328

329329
// Then
@@ -359,20 +359,20 @@ class HeartbeatControllerTests: XCTestCase {
359359
let todaysDate = date
360360
let tomorrowsDate = date.addingTimeInterval(60 * 60 * 24)
361361

362-
var testDate = yesterdaysDate
362+
let testDate = AdjustableDate(date: yesterdaysDate)
363363

364364
let heartbeatController = HeartbeatController(
365365
storage: HeartbeatStorageFake(),
366-
dateProvider: { testDate }
366+
dateProvider: { testDate.date }
367367
)
368368

369369
// When
370370
heartbeatController.log("yesterdays_dummy_agent")
371-
testDate = todaysDate
371+
testDate.date = todaysDate
372372
heartbeatController.log("todays_dummy_agent")
373-
testDate = tomorrowsDate
373+
testDate.date = tomorrowsDate
374374
heartbeatController.log("tomorrows_dummy_agent")
375-
testDate = todaysDate
375+
testDate.date = todaysDate
376376

377377
// Then
378378
let payload = heartbeatController.flushHeartbeatFromToday()
@@ -426,7 +426,10 @@ class HeartbeatControllerTests: XCTestCase {
426426

427427
// MARK: - Fakes
428428

429-
private class HeartbeatStorageFake: HeartbeatStorageProtocol {
429+
private final class HeartbeatStorageFake: HeartbeatStorageProtocol, @unchecked Sendable {
430+
// The unchecked Sendable conformance is used to prevent warnings for the below var, which
431+
// violates the class's Sendable conformance. Ignoring this violation should be okay for
432+
// testing purposes.
430433
private var heartbeatsBundle: HeartbeatsBundle?
431434

432435
func readAndWriteSync(using transform: (HeartbeatsBundle?) -> HeartbeatsBundle?) {

0 commit comments

Comments
 (0)