-
Notifications
You must be signed in to change notification settings - Fork 85
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: Add support for external cache storage #412
base: v9
Are you sure you want to change the base?
Changes from all commits
f1326ad
4133d70
215f655
82418bb
4517e05
503e8a7
54ac4c5
2d41c91
72fbaae
66fa65a
354412f
e238ab9
560b689
889d9ce
01ee468
43bac26
84673e0
cc331e3
3cf5cae
8465906
5f54439
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -253,6 +253,11 @@ public struct LDConfig { | |
/// The default logger for the SDK. Can be overridden to provide customization. | ||
static let logger: OSLog = OSLog(subsystem: "com.launchdarkly", category: "ios-client-sdk") | ||
|
||
/// The default cache for feature flags is UserDefaults | ||
static let cacheFactory: CacheFactory = { cacheKey, _ in | ||
UserDefaults(suiteName: cacheKey)! | ||
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. I realize we were previously forcing the unwrap here and you just moved this bit of code. Now that we have an in memory cache that can't fail, what about having this fall back to that if this instantiation fails? Then we don't have to worry about the force unwrap anymore. |
||
} | ||
|
||
/// The default behavior for event payload compression. | ||
static let enableCompression: Bool = false | ||
} | ||
|
@@ -427,6 +432,9 @@ public struct LDConfig { | |
/// Configure the logger that will be used by the rest of the SDK. | ||
public var logger: OSLog = Defaults.logger | ||
|
||
/// Configure the persistent storage for caching flags locally | ||
public var cacheFactory: CacheFactory = Defaults.cacheFactory | ||
|
||
/// LaunchDarkly defined minima for selected configurable items | ||
public let minima: Minima | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -57,18 +57,24 @@ protocol FeatureFlagCaching { | |
func saveCachedData(_ storedItems: StoredItems, cacheKey: String, contextHash: String, lastUpdated: Date, etag: String?) | ||
} | ||
|
||
extension MobileKey { | ||
func cacheKey() -> String { | ||
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. What do you think about having the cache define it's own key? I don't see any reason why completely different implementations need to use the same format as this one. 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. I didn't consider this as an important thing to add (trying to make the PR smaller), but this certainly can be done. Ideally, a wider refactoring is needed here. The parameter name
use cacheKey that look different, and are generated differently. The first one usually looks like 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. That's fair. It may be something we add ourselves then after this lands. @tanderson-ld that is something you and I can decide on then if we want this now, later, or not at all. |
||
let cacheKey: String | ||
if let bundleId = Bundle.main.bundleIdentifier { | ||
cacheKey = "\(Util.sha256base64(bundleId)).\(Util.sha256base64(self))" | ||
} else { | ||
cacheKey = Util.sha256base64(self) | ||
} | ||
return "com.launchdarkly.client.\(cacheKey)" | ||
} | ||
} | ||
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. Is this extension organizational or is there another reason for adding it? 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. Code reuse |
||
|
||
final class FeatureFlagCache: FeatureFlagCaching { | ||
let keyedValueCache: KeyedValueCaching | ||
let maxCachedContexts: Int | ||
|
||
init(serviceFactory: ClientServiceCreating, mobileKey: MobileKey, maxCachedContexts: Int) { | ||
let cacheKey: String | ||
if let bundleId = Bundle.main.bundleIdentifier { | ||
cacheKey = "\(Util.sha256base64(bundleId)).\(Util.sha256base64(mobileKey))" | ||
} else { | ||
cacheKey = Util.sha256base64(mobileKey) | ||
} | ||
self.keyedValueCache = serviceFactory.makeKeyedValueCache(cacheKey: "com.launchdarkly.client.\(cacheKey)") | ||
self.keyedValueCache = serviceFactory.makeKeyedValueCache(cacheKey: mobileKey.cacheKey()) | ||
self.maxCachedContexts = maxCachedContexts | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,25 +1,14 @@ | ||
import Foundation | ||
|
||
import OSLog | ||
// sourcery: autoMockable | ||
protocol KeyedValueCaching { | ||
public protocol KeyedValueCaching { | ||
func set(_ value: Data, forKey: String) | ||
func data(forKey: String) -> Data? | ||
func dictionary(forKey: String) -> [String: Any]? | ||
func removeObject(forKey: String) | ||
func removeAll() | ||
func keys() -> [String] | ||
} | ||
|
||
extension UserDefaults: KeyedValueCaching { | ||
func set(_ value: Data, forKey: String) { | ||
set(value as Any?, forKey: forKey) | ||
} | ||
|
||
func removeAll() { | ||
dictionaryRepresentation().keys.forEach { removeObject(forKey: $0) } | ||
} | ||
|
||
func keys() -> [String] { | ||
dictionaryRepresentation().keys.map { String($0) } | ||
} | ||
public extension LDConfig { | ||
typealias CacheFactory = (_ cacheKey: String?, _ logger: OSLog) -> KeyedValueCaching | ||
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. Is there any reason to make the cache key optional? It forces a decision on what a default key would be in your cache implementations which I would prefer to avoid. 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 was dictated by the 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. I think that's actually in line with my later comment about this conversion being something associated with a specific cache implementation. Our existing implementation could take its cache key, but when doing the conversion bit, it can ignore that key then since it knows it didn't use to use it. Also, at this point, I think we could argue that our cache converter could probably be cleaned up some. I don't think we have to support a v5 -> v9 migration path. So we could probably remove those steps entirely. What do you think @tanderson-ld 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. I agree. Dropping support for converting v5 to remove the optionality of the cacheKey is a good tradeoff. I think these are the pertinent lines.
|
||
} |
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.
With caching implementations being as wildly different as they are, it seem like the
convertCacheData
should be implemented alongside the cache implementation.So I think the cache prototype probably needs a method to retrieve an optional converter. If provided, then we pass in the list of mobile keys, and the max cached contexts option. From there, the implementation is cache specific.
We should also conversion migration and conversion as the same concept. If someone wants to migrate between two completely different storage technologies, they can write a custom cacher to handle that. Seems like it would be straight forward enough.