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

feat: Storage check #61

Merged
merged 3 commits into from
Nov 15, 2023
Merged
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
10 changes: 9 additions & 1 deletion ConfidenceDemoApp/ConfidenceDemoApp/ConfidenceDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,18 @@ extension ConfidenceDemoApp {
guard let secret = ProcessInfo.processInfo.environment["CLIENT_SECRET"] else {
return
}

// If we have no cache, then do a fetch first.
var initializationStratgey: InitializationStrategy = .activateAndFetchAsync
if ConfidenceFeatureProvider.isStorageEmpty() {
initializationStratgey = .fetchAndActivate
}

let provider = ConfidenceFeatureProvider
.Builder(credentials: .clientSecret(secret: secret))
.with(initializationStrategy: .activateAndFetchAsync)
.with(initializationStrategy: initializationStratgey)
.build()
// NOTE: Using a random UUID for each app start is not advised and can result in getting stale values.
let ctx = MutableContext(targetingKey: UUID.init().uuidString, structure: MutableStructure())
OpenFeatureAPI.shared.setProvider(provider: provider, initialContext: ctx)
}
Expand Down
2 changes: 1 addition & 1 deletion ConfidenceDemoApp/ConfidenceDemoApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct ContentView: View {
text.text = OpenFeatureAPI
.shared
.getClient()
.getStringValue(key: "hawkflag.color", defaultValue: "ERROR")
.getStringValue(key: "swift-demoapp.color", defaultValue: "ERROR")
if text.text == "Green" {
color.color = .green
} else if text.text == "Yellow" {
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ The evaluation context is the way for the client to specify contextual data that

The `setProvider()` function is synchronous and returns immediately, however this does not mean that the provider is ready to be used. An asynchronous network request to the Confidence backend to fetch all the flags configured for your application must be completed by the provider first. The provider will then emit a _READY_ event indicating you can start resolving flags.

A ultity function is available on the provider to check if the current storage has any values stored - this can be used to determine the best initialization strategy.
```swift
// If we have no cache, then do a fetch first.
var initializationStratgey: InitializationStrategy = .activateAndFetchAsync
if ConfidenceFeatureProvider.isStorageEmpty() {
initializationStratgey = .fetchAndActivate
}
```

To listen for the _READY_ event, you can add an event handler via the `OpenFeatureAPI` shared instance:
```swift
func providerReady(notification: Notification) {
Expand Down
66 changes: 48 additions & 18 deletions Sources/ConfidenceProvider/Cache/DefaultStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,14 @@ public class DefaultStorage: Storage {
}

public func load<T>(defaultValue: T) throws -> T where T: Decodable {
try storageQueue.sync {
let configUrl = try getConfigUrl()
guard FileManager.default.fileExists(atPath: configUrl.backport.path) else {
return defaultValue
}

let data = try {
do {
return try Data(contentsOf: configUrl)
} catch {
throw ConfidenceError.cacheError(message: "Unable to load cache file: \(error)")
}
}()
guard let data = try read() else {
return defaultValue
}

do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw ConfidenceError.corruptedCache(message: "Unable to decode: \(error)")
}
do {
return try JSONDecoder().decode(T.self, from: data)
} catch {
throw ConfidenceError.corruptedCache(message: "Unable to decode: \(error)")
}
}

Expand All @@ -65,6 +54,33 @@ public class DefaultStorage: Storage {
}
}

public func isEmpty() -> Bool {
guard let data = try? read() else {
return true
}

return data.isEmpty
}

func read() throws -> Data? {
try storageQueue.sync {
let configUrl = try getConfigUrl()
guard FileManager.default.fileExists(atPath: configUrl.backport.path) else {
return nil
}

let data = try {
do {
return try Data(contentsOf: configUrl)
} catch {
throw ConfidenceError.cacheError(message: "Unable to load cache file: \(error)")
}
}()

return data
}
}

func getConfigUrl() throws -> URL {
guard
let applicationSupportUrl = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)
Expand All @@ -81,3 +97,17 @@ public class DefaultStorage: Storage {
components: resolverCacheBundleId, "\(bundleIdentifier)", filePath)
}
}

extension DefaultStorage {
public static func resolverFlagsCache() -> DefaultStorage {
DefaultStorage(filePath: "resolver.flags.cache")
}

public static func resolverApplyCache() -> DefaultStorage {
DefaultStorage(filePath: "resolver.apply.cache")
}

public static func applierFlagsCache() -> DefaultStorage {
DefaultStorage(filePath: "applier.flags.cache")
}
}
2 changes: 2 additions & 0 deletions Sources/ConfidenceProvider/Cache/Storage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ public protocol Storage {
func load<T>(defaultValue: T) throws -> T where T: Decodable

func clear() throws

func isEmpty() -> Bool
}
18 changes: 15 additions & 3 deletions Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,16 +378,28 @@ public class ConfidenceFeatureProvider: FeatureProvider {
}
}

// MARK: Storage

extension ConfidenceFeatureProvider {
public static func isStorageEmpty(
storage: Storage = DefaultStorage.resolverFlagsCache()
) -> Bool {
storage.isEmpty()
}
}

// MARK: Builder

extension ConfidenceFeatureProvider {
public struct Builder {
var options: ConfidenceClientOptions
var session: URLSession?
var localOverrides: [String: LocalOverride] = [:]
var storage: Storage = DefaultStorage(filePath: "resolver.flags.cache")
var storage: Storage = DefaultStorage.resolverFlagsCache()
var cache: ProviderCache?
var flagApplier: (any FlagApplier)?
var initializationStrategy: InitializationStrategy = .fetchAndActivate
var applyStorage: Storage = DefaultStorage(filePath: "resolver.apply.cache")
var applyStorage: Storage = DefaultStorage.resolverApplyCache()

/// Initializes the builder with the given credentails.
///
Expand Down Expand Up @@ -561,7 +573,7 @@ extension ConfidenceFeatureProvider {
flagApplier
?? FlagApplierWithRetries(
httpClient: NetworkClient(region: options.region),
storage: DefaultStorage(filePath: "applier.flags.cache"),
storage: DefaultStorage.applierFlagsCache(),
options: options
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ final class CacheDataInteractorTests: XCTestCase {

Task {
// When cache data add method is called
await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date())
_ = await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date())

// Then event is added with
let cache = await cacheDataInteractor.cache
Expand All @@ -49,7 +49,7 @@ final class CacheDataInteractorTests: XCTestCase {

Task {
// When cache data add method is called
await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date())
_ = await cacheDataInteractor.add(resolveToken: "token", flagName: "name", applyTime: Date())

// Then event is added with
let cache = await cacheDataInteractor.cache
Expand Down
20 changes: 10 additions & 10 deletions Tests/ConfidenceProviderTests/CacheDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class CacheDataTests: XCTestCase {

// When add event is called
let applyTime = Date()
cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime)
_ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime)

// Then cache data has one resolve event
XCTAssertEqual(cacheData.resolveEvents.count, 1)
Expand All @@ -29,7 +29,7 @@ final class CacheDataTests: XCTestCase {
var cacheData = CacheData(resolveToken: "token1", events: [])

// When add event is called with token that already exist in cache data
cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime)
_ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime)

// Then cache data has one resolve event
XCTAssertEqual(cacheData.resolveEvents.count, 1)
Expand All @@ -46,9 +46,9 @@ final class CacheDataTests: XCTestCase {
var cacheData = try CacheDataUtility.prefilledCacheData()

// When add event is called 3 times with token that already exist in cache data
cacheData.add(resolveToken: "token0", flagName: "flagName", applyTime: Date())
cacheData.add(resolveToken: "token0", flagName: "flagName2", applyTime: Date())
cacheData.add(resolveToken: "token0", flagName: "flagName3", applyTime: Date())
_ = cacheData.add(resolveToken: "token0", flagName: "flagName", applyTime: Date())
_ = cacheData.add(resolveToken: "token0", flagName: "flagName2", applyTime: Date())
_ = cacheData.add(resolveToken: "token0", flagName: "flagName3", applyTime: Date())

// Then cache data has 6 apply events
XCTAssertEqual(cacheData.resolveEvents.first?.events.count, 6)
Expand All @@ -58,11 +58,11 @@ final class CacheDataTests: XCTestCase {
// Given pre filled cache data
let applyTime = Date(timeIntervalSince1970: 1000)
var cacheData = CacheData(resolveToken: "token1", events: [])
cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime)
_ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTime)

// When add event is called with token and flagName that already exist in cache
let applyTimeOther = Date(timeIntervalSince1970: 3000)
cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTimeOther)
_ = cacheData.add(resolveToken: "token1", flagName: "flagName", applyTime: applyTimeOther)

// Then apply record is not overriden
let applyEvent = try XCTUnwrap(cacheData.resolveEvents.first?.events.first)
Expand All @@ -75,9 +75,9 @@ final class CacheDataTests: XCTestCase {
let date = Date(timeIntervalSince1970: 2000)

// When add event is called 3 times with different tokens
cacheData.add(resolveToken: "token1", flagName: "prefilled", applyTime: date)
cacheData.add(resolveToken: "token2", flagName: "prefilled", applyTime: date)
cacheData.add(resolveToken: "token3", flagName: "prefilled", applyTime: date)
_ = cacheData.add(resolveToken: "token1", flagName: "prefilled", applyTime: date)
_ = cacheData.add(resolveToken: "token2", flagName: "prefilled", applyTime: date)
_ = cacheData.add(resolveToken: "token3", flagName: "prefilled", applyTime: date)

// Then cache data has 4 resolve event
XCTAssertEqual(cacheData.resolveEvents.count, 4)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ enum CacheDataUtility {
let applyEvent = FlagApply(name: flagName, applyTime: date)
applyEvents.append(applyEvent)
} else {
cacheData.add(resolveToken: resolveToken, flagName: flagName, applyTime: date)
_ = cacheData.add(resolveToken: resolveToken, flagName: flagName, applyTime: date)
}
}

Expand Down
28 changes: 20 additions & 8 deletions Tests/ConfidenceProviderTests/Helpers/StorageMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,41 @@ import XCTest

class StorageMock: Storage {
var data = ""

var saveExpectation: XCTestExpectation?
private let storageQueue = DispatchQueue(label: "com.confidence.storagemock")

convenience init(data: Encodable) throws {
self.init()
try self.save(data: data)
}

func save(data: Encodable) throws {
let dataB = try JSONEncoder().encode(data)
self.data = String(data: dataB, encoding: .utf8) ?? ""
try storageQueue.sync {
let dataB = try JSONEncoder().encode(data)
self.data = String(data: dataB, encoding: .utf8) ?? ""

saveExpectation?.fulfill()
saveExpectation?.fulfill()
}
}

func load<T>(defaultValue: T) throws -> T where T: Decodable {
if data.isEmpty {
return defaultValue
try storageQueue.sync {
if data.isEmpty {
return defaultValue
}
return try JSONDecoder().decode(T.self, from: data.data)
}
return try JSONDecoder().decode(T.self, from: data.data)
}

func clear() throws {
data = ""
storageQueue.sync {
data = ""
}
}

func isEmpty() -> Bool {
storageQueue.sync {
return data.isEmpty
}
}
}
Loading