Skip to content

Commit

Permalink
Merge pull request #1940 from DataDog/ganeshnj/feat/clear-feature-sto…
Browse files Browse the repository at this point in the history
…rage-data

RUM-5084 feat: add ability to clear data in feature data storage
  • Loading branch information
ganeshnj authored Jul 15, 2024
2 parents eaafb61 + 6f1b3c6 commit 216873d
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 14 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

- [FEATURE] Enable DatadogCore, DatadogLogs and DatadogTrace to compile on watchOS platform. See [#1918][] (Thanks [@jfiser-paylocity][]) [#1946][]
- [IMPROVEMENT] Ability to clear feature data storage using `clearAllData` API. See [#1940][]

# 2.14.1 / 09-07-2024

Expand Down Expand Up @@ -721,6 +722,7 @@ Release `2.0` introduces breaking changes. Follow the [Migration Guide](MIGRATIO
[#1938]: https://github.com/DataDog/dd-sdk-ios/pull/1938
[#1947]: https://github.com/DataDog/dd-sdk-ios/pull/1947
[#1948]: https://github.com/DataDog/dd-sdk-ios/pull/1948
[#1940]: https://github.com/DataDog/dd-sdk-ios/pull/1940
[@00fa9a]: https://github.com/00FA9A
[@britton-earnin]: https://github.com/Britton-Earnin
[@hengyu]: https://github.com/Hengyu
Expand Down
16 changes: 14 additions & 2 deletions DatadogCore/Sources/Core/DataStore/FeatureDataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import DatadogInternal

/// A concrete implementation of the `DataStore` protocol using file storage.
internal final class FeatureDataStore: DataStore {
private enum Constants {
enum Constants {
/// The version of this data store implementation.
/// If a breaking change is introduced to the format of managed files, the version must be upgraded and old data should be deleted.
static let dataStoreVersion = 1
Expand All @@ -35,7 +35,7 @@ internal final class FeatureDataStore: DataStore {
) {
self.feature = feature
self.coreDirectory = directory
self.directoryPath = "\(Constants.dataStoreVersion)/" + feature
self.directoryPath = coreDirectory.getDataStorePath(forFeatureNamed: feature)
self.queue = queue
self.telemetry = telemetry
}
Expand Down Expand Up @@ -87,6 +87,18 @@ internal final class FeatureDataStore: DataStore {
}
}

func clearAllData() {
queue.async {
do {
let directory = try self.coreDirectory.coreDirectory.subdirectory(path: self.directoryPath)
try directory.deleteAllFiles()
} catch let error {
DD.logger.error("[Data Store] Error on clearing all data for `\(self.feature)`", error: error)
self.telemetry.error("[Data Store] Error on clearing all data for `\(self.feature)`", error: DDError(error: error))
}
}
}

// MARK: - Persistence

private func write(data: Data, forKey key: String, version: DataStoreKeyVersion) throws {
Expand Down
13 changes: 9 additions & 4 deletions DatadogCore/Sources/Core/DatadogCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ internal final class DatadogCore {

/// Registry for Features.
@ReadWriteLock
private(set) var stores: [String: (
storage: FeatureStorage,
upload: FeatureUpload
)] = [:]
private(set) var stores: [String: (storage: FeatureStorage, upload: FeatureUpload)] = [:]

/// Registry for Features.
@ReadWriteLock
Expand Down Expand Up @@ -171,6 +168,7 @@ internal final class DatadogCore {
/// Clears all data that has not already yet been uploaded Datadog servers.
func clearAllData() {
allStorages.forEach { $0.clearAllData() }
allDataStores.forEach { $0.clearAllData() }
}

/// Adds a message receiver to the bus.
Expand All @@ -197,6 +195,13 @@ internal final class DatadogCore {
stores.values.map { $0.upload }
}

private var allDataStores: [DataStore] {
features.values.compactMap { feature in
let featureType = type(of: feature) as DatadogFeature.Type
return scope(for: featureType).dataStore
}
}

/// Awaits completion of all asynchronous operations, forces uploads (without retrying) and deinitializes
/// this instance of the SDK. It **blocks the caller thread**.
///
Expand Down
13 changes: 12 additions & 1 deletion DatadogCore/Sources/Core/Storage/Directories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,22 @@ internal struct CoreDirectory {
authorized: try coreDirectory.createSubdirectory(path: "\(name)/v2")
)
}

/// Obtains the path to the data store for given Feature.
///
/// Note: `FeatureDataStore` directory is created on-demand which may happen before `FeatureDirectories` are created.
/// Hence, this method only returns the path and let the caller decide if the directory should be created.
///
/// - Parameter name: The given Feature name.
/// - Returns: The path to the data store for given Feature.
func getDataStorePath(forFeatureNamed name: String) -> String {
return "\(FeatureDataStore.Constants.dataStoreVersion)/" + name
}
}

internal extension CoreDirectory {
/// Creates the core directory.
///
///
/// - Parameters:
/// - osDirectory: the root OS directory (`/Library/Caches`) to create core directory inside.
/// - instanceName: The core instance name.
Expand Down
2 changes: 1 addition & 1 deletion DatadogCore/Sources/Core/Storage/Files/File.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ private enum FileError: Error {

/// An immutable `struct` designed to provide optimized and thread safe interface for file manipulation.
/// It doesn't own the file, which means the file presence is not guaranteed - the file can be deleted by OS at any time (e.g. due to memory pressure).
internal struct File: WritableFile, ReadableFile, FileProtocol {
internal struct File: WritableFile, ReadableFile, FileProtocol, Equatable {
let url: URL
let name: String

Expand Down
18 changes: 15 additions & 3 deletions DatadogCore/Tests/Datadog/DatadogTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,16 @@ class DatadogTests: XCTestCase {
try core.directory.getFeatureDirectories(forFeatureNamed: "tracing"),
]

let allDirectories: [Directory] = featureDirectories.flatMap { [$0.authorized, $0.unauthorized] }
let scope = core.scope(for: TraceFeature.self)
scope.dataStore.setValue("foo".data(using: .utf8)!, forKey: "bar")

// Wait for async clear completion in all features:
core.readWriteQueue.sync {}
let tracingDataStoreDir = try core.directory.coreDirectory.subdirectory(path: core.directory.getDataStorePath(forFeatureNamed: "tracing"))
XCTAssertTrue(tracingDataStoreDir.hasFile(named: "bar"))

var allDirectories: [Directory] = featureDirectories.flatMap { [$0.authorized, $0.unauthorized] }
allDirectories.append(.init(url: tracingDataStoreDir.url))
try allDirectories.forEach { directory in _ = try directory.createFile(named: .mockRandom()) }

// When
Expand All @@ -346,8 +355,11 @@ class DatadogTests: XCTestCase {
core.readWriteQueue.sync {}

// Then
let newNumberOfFiles = try allDirectories.reduce(0, { acc, nextDirectory in return try acc + nextDirectory.files().count })
XCTAssertEqual(newNumberOfFiles, 0, "All files must be removed")
let files: [File] = allDirectories.reduce([], { acc, nextDirectory in
let next = try? nextDirectory.files()
return acc + (next ?? [])
})
XCTAssertEqual(files, [], "All files must be removed")

Datadog.flushAndDeinitialize()
}
Expand Down
8 changes: 8 additions & 0 deletions DatadogInternal/Sources/DataStore/DataStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ public protocol DataStore {
///
/// - Parameter key: The unique identifier for the value to be deleted. Must be a valid file name, as it will be persisted in files.
func removeValue(forKey key: String)

/// Clears all data that has not already yet been uploaded Datadog servers.
///
/// Note: This may impact the SDK's ability to detect App Hangs and Watchdog Terminations
/// or other features that rely on data persisted in the data store.
func clearAllData()
}

public extension DataStore {
Expand All @@ -83,4 +89,6 @@ public struct NOPDataStore: DataStore {
public func value(forKey key: String, callback: @escaping (DataStoreValueResult) -> Void) {}
/// no-op
public func removeValue(forKey key: String) {}
/// no-op
public func clearAllData() {}
}
10 changes: 7 additions & 3 deletions TestUtilities/Mocks/DataStoreMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ public class DataStoreMock: DataStore {
public func setValue(_ value: Data, forKey key: String, version: DataStoreKeyVersion) {
storage[key] = .value(value, version)
}

public func value(forKey key: String, callback: @escaping (DataStoreValueResult) -> Void) {
callback(storage[key] ?? .noValue)
}

public func removeValue(forKey key: String) {
storage[key] = nil
}


public func clearAllData() {
storage.removeAll()
}

// MARK: - Side Effects Observation

public func value(forKey key: String) -> DataStoreValueResult? {
Expand Down

0 comments on commit 216873d

Please sign in to comment.