Skip to content

Commit

Permalink
[ch75316] Added maxCachedUsers to LDConfig and UserEnvironmentFlagCac…
Browse files Browse the repository at this point in the history
…he (#96)

* Added maxCachedUsers to LDConfig and UserEnvironmentFlagCache

* Changed specific -1 to 0

* Added unlimited users tests
  • Loading branch information
torchhound authored May 27, 2020
1 parent e157d45 commit ff0451d
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 80 deletions.
10 changes: 10 additions & 0 deletions LaunchDarkly/GeneratedCode/mocks.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,16 @@ final class EventReportingMock: EventReporting {
// MARK: - FeatureFlagCachingMock
final class FeatureFlagCachingMock: FeatureFlagCaching {

// MARK: maxCachedUsers
var maxCachedUsersSetCount = 0
var setMaxCachedUsersCallback: (() -> Void)?
var maxCachedUsers: Int = 5 {
didSet {
maxCachedUsersSetCount += 1
setMaxCachedUsersCallback?()
}
}

// MARK: retrieveFeatureFlags
var retrieveFeatureFlagsCallCount = 0
var retrieveFeatureFlagsCallback: (() -> Void)?
Expand Down
8 changes: 6 additions & 2 deletions LaunchDarkly/LaunchDarkly/LDClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ public class LDClient {
@available(*, deprecated, message: "Please use the startCompleteWhenFlagsReceived method instead")
public func start(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) {
Log.debug(typeName(and: #function, appending: ": ") + "starting")
flagCache.maxCachedUsers = config.maxCachedUsers
cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: config.maxCachedUsers)
let wasStarted = hasStarted
let wasOnline = isOnline
isStarting = true
Expand Down Expand Up @@ -338,6 +340,8 @@ public class LDClient {
*/
public func startCompleteWhenFlagsReceived(config: LDConfig, user: LDUser? = nil, completion: (() -> Void)? = nil) {
Log.debug(typeName(and: #function, appending: ": ") + "starting")
flagCache.maxCachedUsers = config.maxCachedUsers
cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: config.maxCachedUsers)
let wasStarted = hasStarted
let wasOnline = isOnline
isStarting = true
Expand Down Expand Up @@ -1015,9 +1019,9 @@ public class LDClient {
self.serviceFactory = serviceFactory
}
environmentReporter = self.serviceFactory.makeEnvironmentReporter()
flagCache = self.serviceFactory.makeFeatureFlagCache()
flagCache = self.serviceFactory.makeFeatureFlagCache(maxCachedUsers: LDConfig.Defaults.maxCachedUsers)
LDUserWrapper.configureKeyedArchiversToHandleVersion2_3_0AndOlderUserCacheFormat()
cacheConverter = self.serviceFactory.makeCacheConverter()
cacheConverter = self.serviceFactory.makeCacheConverter(maxCachedUsers: LDConfig.Defaults.maxCachedUsers)
flagChangeNotifier = self.serviceFactory.makeFlagChangeNotifier()
throttler = self.serviceFactory.makeThrottler(maxDelay: Throttler.Constants.defaultDelay, environmentReporter: environmentReporter)

Expand Down
7 changes: 7 additions & 0 deletions LaunchDarkly/LaunchDarkly/Models/LDConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ public struct LDConfig {

/// The default setting for whether we request evaluation reasons for all flags.
static let evaluationReasons = false

/// The default setting for the maximum number of locally cached users.
static let maxCachedUsers = 5
}

/// The minimum values allowed to be set into LDConfig.
Expand Down Expand Up @@ -179,6 +182,9 @@ public struct LDConfig {

/// Enables requesting evaluation reasons for all flags. (Default: false)
public var evaluationReasons: Bool = Defaults.evaluationReasons

/// An Integer that tells UserEnvironmentFlagCache the maximum number of users to locally cache. Can be set to -1 for unlimited cached users.
public var maxCachedUsers: Int = Defaults.maxCachedUsers

/// LaunchDarkly defined minima for selected configurable items
public let minima: Minima
Expand Down Expand Up @@ -241,6 +247,7 @@ extension LDConfig: Equatable {
&& lhs.inlineUserInEvents == rhs.inlineUserInEvents
&& lhs.isDebugMode == rhs.isDebugMode
&& lhs.evaluationReasons == rhs.evaluationReasons
&& lhs.maxCachedUsers == rhs.maxCachedUsers
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ final class CacheConverter: CacheConverting {
private(set) var deprecatedCaches = [DeprecatedCacheModel: DeprecatedCache]()
let maxAge: TimeInterval

init(serviceFactory: ClientServiceCreating, maxAge: TimeInterval = Constants.maxAge) {
currentCache = serviceFactory.makeFeatureFlagCache()
init(serviceFactory: ClientServiceCreating, maxCachedUsers: Int, maxAge: TimeInterval = Constants.maxAge) {
currentCache = serviceFactory.makeFeatureFlagCache(maxCachedUsers: maxCachedUsers)
self.maxAge = maxAge
DeprecatedCacheModel.allCases.forEach { version in
deprecatedCaches[version] = serviceFactory.makeDeprecatedCacheModel(version)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ enum FlagCachingStoreMode: CaseIterable {

//sourcery: autoMockable
protocol FeatureFlagCaching {
//sourcery: defaultMockValue = 5
var maxCachedUsers: Int { get set }
func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]?
func storeFeatureFlags(_ featureFlags: [LDFlagKey: FeatureFlag], forUser user: LDUser, andMobileKey mobileKey: String, lastUpdated: Date, storeMode: FlagCachingStoreMode)
}
Expand All @@ -21,19 +23,20 @@ final class UserEnvironmentFlagCache: FeatureFlagCaching {

struct Constants {
static let cacheStoreOperationQueueLabel = "com.launchDarkly.FeatureFlagCaching.cacheStoreOperationQueue"
static let maxCachedUsers = 5
}

struct CacheKeys {
static let cachedUserEnvironmentFlags = "com.launchDarkly.cachedUserEnvironmentFlags"
}

private(set) var keyedValueCache: KeyedValueCaching
var maxCachedUsers: Int

private static let cacheStoreOperationQueue = DispatchQueue(label: Constants.cacheStoreOperationQueueLabel, qos: .background)

init(withKeyedValueCache keyedValueCache: KeyedValueCaching) {
init(withKeyedValueCache keyedValueCache: KeyedValueCaching, maxCachedUsers: Int) {
self.keyedValueCache = keyedValueCache
self.maxCachedUsers = maxCachedUsers
}

func retrieveFeatureFlags(forUserWithKey userKey: String, andMobileKey mobileKey: String) -> [LDFlagKey: FeatureFlag]? {
Expand Down Expand Up @@ -85,15 +88,15 @@ final class UserEnvironmentFlagCache: FeatureFlagCaching {
}

private func removeOldestUsersIfNeeded(from cacheableUserEnvironmentsCollection: [UserKey: CacheableUserEnvironmentFlags]) -> [UserKey: CacheableUserEnvironmentFlags] {
guard cacheableUserEnvironmentsCollection.count > Constants.maxCachedUsers
guard cacheableUserEnvironmentsCollection.count > maxCachedUsers && maxCachedUsers >= 0
else {
return cacheableUserEnvironmentsCollection
}
//sort collection into key-value pairs in descending order...youngest to oldest
var userEnvironmentsCollection = cacheableUserEnvironmentsCollection.sorted { pair1, pair2 -> Bool in
pair2.value.lastUpdated.isEarlierThan(pair1.value.lastUpdated)
}
while userEnvironmentsCollection.count > Constants.maxCachedUsers {
while userEnvironmentsCollection.count > maxCachedUsers && maxCachedUsers >= 0 {
userEnvironmentsCollection.removeLast()
}
return [UserKey: CacheableUserEnvironmentFlags](userEnvironmentsCollection, uniquingKeysWith: { value1, _ in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import DarklyEventSource

protocol ClientServiceCreating {
func makeKeyedValueCache() -> KeyedValueCaching
func makeFeatureFlagCache() -> FeatureFlagCaching
func makeCacheConverter() -> CacheConverting
func makeFeatureFlagCache(maxCachedUsers: Int) -> FeatureFlagCaching
func makeCacheConverter(maxCachedUsers: Int) -> CacheConverting
func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache
func makeDarklyServiceProvider(config: LDConfig, user: LDUser) -> DarklyServiceProvider
func makeFlagSynchronizer(streamingMode: LDStreamingMode, pollingInterval: TimeInterval, useReport: Bool, service: DarklyServiceProvider) -> LDFlagSynchronizing
Expand All @@ -36,12 +36,12 @@ final class ClientServiceFactory: ClientServiceCreating {
UserDefaults.standard
}

func makeFeatureFlagCache() -> FeatureFlagCaching {
UserEnvironmentFlagCache(withKeyedValueCache: makeKeyedValueCache())
func makeFeatureFlagCache(maxCachedUsers: Int) -> FeatureFlagCaching {
UserEnvironmentFlagCache(withKeyedValueCache: makeKeyedValueCache(), maxCachedUsers: maxCachedUsers)
}

func makeCacheConverter() -> CacheConverting {
CacheConverter(serviceFactory: self)
func makeCacheConverter(maxCachedUsers: Int) -> CacheConverting {
CacheConverter(serviceFactory: self, maxCachedUsers: maxCachedUsers)
}

func makeDeprecatedCacheModel(_ model: DeprecatedCacheModel) -> DeprecatedCache {
Expand Down
6 changes: 3 additions & 3 deletions LaunchDarkly/LaunchDarklyTests/LDClientSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ final class LDClientSpec: QuickSpec {
expect(testContext.recordedEvent?.key) == newUser.key
}
it("converts cached data") {
expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2
expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 //only called once because CacheConverter is replaced during start
expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser
expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == newConfig
}
Expand Down Expand Up @@ -514,7 +514,7 @@ final class LDClientSpec: QuickSpec {
expect(testContext.recordedEvent?.key) == newUser.key
}
it("converts cached data") {
expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2
expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 //only called once because CacheConverter is replaced during start
expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == newUser
expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == newConfig
}
Expand Down Expand Up @@ -558,7 +558,7 @@ final class LDClientSpec: QuickSpec {
expect(testContext.recordedEvent?.key) == testContext.user.key
}
it("converts cached data") {
expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 2
expect(testContext.cacheConvertingMock.convertCacheDataCallCount) == 1 //only called once because CacheConverter is replaced during start
expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.user) == testContext.user
expect(testContext.cacheConvertingMock.convertCacheDataReceivedArguments?.config) == testContext.config
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ final class ClientServiceMockFactory: ClientServiceCreating {

var makeFeatureFlagCacheReturnValue = FeatureFlagCachingMock()
var makeFeatureFlagCacheCallCount = 0
func makeFeatureFlagCache() -> FeatureFlagCaching {
func makeFeatureFlagCache(maxCachedUsers: Int = 5) -> FeatureFlagCaching {
makeFeatureFlagCacheCallCount += 1
return makeFeatureFlagCacheReturnValue
}

func makeCacheConverter() -> CacheConverting {
func makeCacheConverter(maxCachedUsers: Int = 5) -> CacheConverting {
return CacheConvertingMock()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ final class CacheableUserEnvironmentFlagsSpec: QuickSpec {
describe("dictionaryValues") {
context("with multiple CacheableUserEnvironments") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)

cacheableUserEnvironmentsCollectionDictionary = testContext.userEnvironmentFlagsCollection.dictionaryValues
}
Expand Down Expand Up @@ -286,7 +286,7 @@ final class CacheableUserEnvironmentFlagsSpec: QuickSpec {
describe("makeCacheableUserEnvironmentsCollection") {
context("with multiple CacheableUserEnvironments dictionaries") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
cacheableUserEnvironmentsCollectionDictionary = testContext.userEnvironmentFlagsCollection.dictionaryValues

cacheableUserEnvironmentsCollection = CacheableUserEnvironmentFlags.makeCollection(from: cacheableUserEnvironmentsCollectionDictionary)
Expand Down Expand Up @@ -320,7 +320,7 @@ final class CacheableUserEnvironmentFlagsSpec: QuickSpec {
var badUserKey: String!
context("missing userKey") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
cacheableUserEnvironmentsCollectionDictionary = testContext.userEnvironmentFlagsCollection.dictionaryValues

//Create a new CacheableUserEnvironment
Expand All @@ -343,7 +343,7 @@ final class CacheableUserEnvironmentFlagsSpec: QuickSpec {
}
context("missing environmentFlags") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
cacheableUserEnvironmentsCollectionDictionary = testContext.userEnvironmentFlagsCollection.dictionaryValues

//Create a new CacheableUserEnvironment
Expand All @@ -369,7 +369,7 @@ final class CacheableUserEnvironmentFlagsSpec: QuickSpec {
var badUserKey: String!
context("type mismatched userKey") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
cacheableUserEnvironmentsCollectionDictionary = testContext.userEnvironmentFlagsCollection.dictionaryValues

//Create a new CacheableUserEnvironment
Expand All @@ -392,7 +392,7 @@ final class CacheableUserEnvironmentFlagsSpec: QuickSpec {
}
context("type mismatched environmentFlags") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
cacheableUserEnvironmentsCollectionDictionary = testContext.userEnvironmentFlagsCollection.dictionaryValues

//Create a new CacheableUserEnvironment
Expand Down Expand Up @@ -468,7 +468,7 @@ extension Int {
extension CacheableUserEnvironmentFlags {
struct Constants {
static let environmentCount = 3
static let userCount = UserEnvironmentFlagCache.Constants.maxCachedUsers
static let userCount = LDConfig.Defaults.maxCachedUsers
}

static func stubCollection(environmentCount: Int = Constants.environmentCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ final class CacheConverterSpec: QuickSpec {

init(maxAge: TimeInterval? = nil, createCacheData: Bool = false, deprecatedCacheData: DeprecatedCacheModel? = nil) {
if let maxAge = maxAge {
cacheConverter = CacheConverter(serviceFactory: clientServiceFactoryMock, maxAge: maxAge)
cacheConverter = CacheConverter(serviceFactory: clientServiceFactoryMock, maxCachedUsers: LDConfig.Defaults.maxCachedUsers, maxAge: maxAge)
} else {
cacheConverter = CacheConverter(serviceFactory: clientServiceFactoryMock)
cacheConverter = CacheConverter(serviceFactory: clientServiceFactoryMock, maxCachedUsers: LDConfig.Defaults.maxCachedUsers)
}
expiredCacheThreshold = Date().addingTimeInterval(maxAge ?? CacheConverter.Constants.maxAge)
if createCacheData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ final class DeprecatedCacheModelV2Spec: QuickSpec {
context("when cached data exists") {
context("and a cached user is requested") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
}
it("retrieves the cached data") {
testContext.users.forEach { user in
Expand All @@ -123,7 +123,7 @@ final class DeprecatedCacheModelV2Spec: QuickSpec {
}
context("and an uncached user is requested") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)

cachedData = testContext.modelV2cache.retrieveFlags(for: testContext.uncachedUser.key, and: testContext.uncachedMobileKey)
}
Expand All @@ -142,7 +142,7 @@ final class DeprecatedCacheModelV2Spec: QuickSpec {
describe("removeData") {
context("no modelV2 cached data expired") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
let oldestLastUpdatedDate = testContext.sortedLastUpdatedDates.first!
expirationDate = oldestLastUpdatedDate.lastUpdated.addingTimeInterval(-Constants.offsetInterval)

Expand All @@ -154,7 +154,7 @@ final class DeprecatedCacheModelV2Spec: QuickSpec {
}
context("some modelV2 cached data expired") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
let selectedLastUpdatedDate = testContext.sortedLastUpdatedDates[testContext.users.count / 2]
expirationDate = selectedLastUpdatedDate.lastUpdated.addingTimeInterval(-Constants.offsetInterval)

Expand All @@ -172,7 +172,7 @@ final class DeprecatedCacheModelV2Spec: QuickSpec {
}
context("all modelV2 cached data expired") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
let newestLastUpdatedDate = testContext.sortedLastUpdatedDates.last!
expirationDate = newestLastUpdatedDate.lastUpdated.addingTimeInterval(Constants.offsetInterval)

Expand All @@ -185,7 +185,7 @@ final class DeprecatedCacheModelV2Spec: QuickSpec {
}
context("no modelV2 cached data exists") {
beforeEach {
testContext = TestContext(userCount: UserEnvironmentFlagCache.Constants.maxCachedUsers)
testContext = TestContext(userCount: LDConfig.Defaults.maxCachedUsers)
let newestLastUpdatedDate = testContext.sortedLastUpdatedDates.last!
expirationDate = newestLastUpdatedDate.lastUpdated.addingTimeInterval(Constants.offsetInterval)
testContext.keyedValueCacheMock.dictionaryReturnValue = nil //mock simulates no modelV2 cached data
Expand Down
Loading

0 comments on commit ff0451d

Please sign in to comment.