ConfigContent.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  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. import FirebaseCore
  15. import Foundation
  16. @objc(RCNDBSource) public enum DBSource: Int {
  17. case active
  18. case `default`
  19. case fetched
  20. }
  21. /// The AtomicConfig class for the config variables enables atomic accesses to support multiple
  22. /// namespace usage of RemoteConfig.
  23. private class AtomicConfig {
  24. private var value: [String: [String: RemoteConfigValue]]
  25. private let lock = NSLock()
  26. init(_ value: [String: [String: RemoteConfigValue]]) {
  27. self.value = value
  28. }
  29. var wrappedValue: [String: [String: RemoteConfigValue]] {
  30. get { return load() }
  31. set { store(newValue: newValue) }
  32. }
  33. func load() -> [String: [String: RemoteConfigValue]] {
  34. lock.lock()
  35. defer { lock.unlock() }
  36. return value
  37. }
  38. func store(newValue: [String: [String: RemoteConfigValue]]) {
  39. lock.lock()
  40. defer { lock.unlock() }
  41. value = newValue
  42. }
  43. func update(namespace: String, newValue: [String: RemoteConfigValue]) {
  44. lock.lock()
  45. defer { lock.unlock() }
  46. value[namespace] = newValue
  47. }
  48. func update(namespace: String, key: String, rcValue: RemoteConfigValue) {
  49. lock.lock()
  50. defer { lock.unlock() }
  51. value[namespace]?[key] = rcValue
  52. }
  53. }
  54. /// This class handles all the config content that is fetched from the server, cached in local
  55. /// config or persisted in database.
  56. @objc(RCNConfigContent) public
  57. class ConfigContent: NSObject {
  58. /// Active config data that is currently used.
  59. private var _activeConfig = AtomicConfig([:])
  60. /// Pending config (aka Fetched config) data that is latest data from server that might or might
  61. /// not be applied.
  62. private var _fetchedConfig = AtomicConfig([:])
  63. /// Default config provided by user.
  64. private var _defaultConfig = AtomicConfig([:])
  65. /// Active Personalization metadata that is currently used.
  66. private var _activePersonalization: [String: Any] = [:]
  67. /// Pending Personalization metadata that is latest data from server that might or might not be
  68. /// applied.
  69. private var _fetchedPersonalization: [String: Any] = [:]
  70. /// Active Rollout metadata that is currently used.
  71. private var _activeRolloutMetadata: [[String: Any]] = []
  72. /// Pending Rollout metadata that is latest data from server that might or might not be applied.
  73. private var _fetchedRolloutMetadata: [[String: Any]] = []
  74. /// DBManager
  75. private var dbManager: ConfigDBManager?
  76. /// Current bundle identifier;
  77. private var bundleIdentifier: String
  78. /// Blocks all config reads until we have read from the database. This only
  79. /// potentially blocks on the first read. Should be a no-wait for all subsequent reads once we
  80. /// have data read into memory from the database.
  81. private let dispatchGroup: DispatchGroup
  82. /// Boolean indicating if initial DB load of fetched,active and default config has succeeded.
  83. private var isConfigLoadFromDBCompleted: Bool
  84. /// Boolean indicating that the load from database has initiated at least once.
  85. private var isDatabaseLoadAlreadyInitiated: Bool
  86. /// Default timeout when waiting to read data from database.
  87. private let databaseLoadTimeoutSecs = 30.0
  88. /// Shared Singleton Instance
  89. @objc public
  90. static let sharedInstance = ConfigContent(dbManager: ConfigDBManager.sharedInstance)
  91. /// Designated initializer
  92. @objc(initWithDBManager:) public
  93. init(dbManager: ConfigDBManager) {
  94. self.dbManager = dbManager
  95. bundleIdentifier = Bundle.main.bundleIdentifier ?? ""
  96. if bundleIdentifier.isEmpty {
  97. RCLog.notice("I-RCN000038",
  98. "Main bundle identifier is missing. Remote Config might not work properly.")
  99. }
  100. dispatchGroup = DispatchGroup()
  101. isConfigLoadFromDBCompleted = false
  102. isDatabaseLoadAlreadyInitiated = false
  103. super.init()
  104. loadConfigFromMainTable()
  105. }
  106. // Blocking call that returns true/false once database load completes / times out.
  107. // @return Initialization status.
  108. @objc public
  109. func initializationSuccessful() -> Bool {
  110. assert(!Thread.isMainThread, "Must not be executing on the main thread.")
  111. return checkAndWaitForInitialDatabaseLoad()
  112. }
  113. /// We load the database async at init time. Block all further calls to active/fetched/default
  114. /// configs until load is done.
  115. @discardableResult
  116. private func checkAndWaitForInitialDatabaseLoad() -> Bool {
  117. /// Wait until load is done. This should be a no-op for subsequent calls.
  118. if !isConfigLoadFromDBCompleted {
  119. let waitResult = dispatchGroup.wait(timeout: .now() + databaseLoadTimeoutSecs)
  120. if waitResult == .timedOut {
  121. RCLog.error("I-RCN000048", "Timed out waiting for fetched config to be loaded from DB")
  122. return false
  123. }
  124. isConfigLoadFromDBCompleted = true
  125. }
  126. return true
  127. }
  128. // MARK: - Database
  129. /// This method is only meant to be called at init time. The underlying logic will need to be
  130. /// reevaluated if the assumption changes at a later time.
  131. private func loadConfigFromMainTable() {
  132. guard let dbManager = dbManager else { return }
  133. assert(!isDatabaseLoadAlreadyInitiated, "Database load has already been initiated")
  134. isDatabaseLoadAlreadyInitiated = true
  135. dispatchGroup.enter()
  136. dbManager.loadMain(withBundleIdentifier: bundleIdentifier) { [weak self] success,
  137. fetched, active, defaults, rolloutMetadata in
  138. guard let self else { return }
  139. self._fetchedConfig.store(newValue: fetched)
  140. self._activeConfig.store(newValue: active)
  141. self._defaultConfig.store(newValue: defaults)
  142. self
  143. ._fetchedRolloutMetadata =
  144. rolloutMetadata[ConfigConstants.rolloutTableKeyFetchedMetadata] ?? []
  145. self
  146. ._activeRolloutMetadata =
  147. rolloutMetadata[ConfigConstants.rolloutTableKeyActiveMetadata] ?? []
  148. self.dispatchGroup.leave()
  149. }
  150. // TODO(karenzeng): Refactor personalization to be returned in loadMainWithBundleIdentifier above
  151. dispatchGroup.enter()
  152. dbManager.loadPersonalization { [weak self] success, fetchedPersonalization,
  153. activePersonalization in
  154. guard let self else { return }
  155. self._fetchedPersonalization = fetchedPersonalization
  156. self._activePersonalization = activePersonalization
  157. self.dispatchGroup.leave()
  158. }
  159. }
  160. /// Update the current config result to main table.
  161. /// @param values Values in a row to write to the table.
  162. /// @param source The source the config data is coming from. It determines which table to write
  163. /// to.
  164. private func updateMainTable(withValues values: [Any], fromSource source: DBSource) {
  165. dbManager?.insertMainTable(withValues: values, fromSource: source, completionHandler: nil)
  166. }
  167. // MARK: - Update
  168. /// This function is for copying dictionary when user set up a default config or when user clicks
  169. /// activate. For now the DBSource can only be Active or Default.
  170. @objc public
  171. func copy(fromDictionary dictionary: [String: [String: Any]],
  172. toSource dbSource: DBSource,
  173. forNamespace firebaseNamespace: String) {
  174. // Make sure database load has completed.
  175. checkAndWaitForInitialDatabaseLoad()
  176. var source: RemoteConfigSource = .remote
  177. var toDictionary: [String: [String: RemoteConfigValue]]
  178. switch dbSource {
  179. case .default:
  180. toDictionary = defaultConfig()
  181. source = .default
  182. case .fetched:
  183. RCLog.warning("I-RCN000008",
  184. "This shouldn't happen. Destination dictionary should never be pending type.")
  185. return
  186. case .active:
  187. toDictionary = activeConfig()
  188. source = .remote
  189. toDictionary.removeValue(forKey: firebaseNamespace)
  190. }
  191. // Completely wipe out DB first.
  192. dbManager?.deleteRecord(fromMainTableWithNamespace: firebaseNamespace,
  193. bundleIdentifier: bundleIdentifier,
  194. fromSource: dbSource)
  195. toDictionary[firebaseNamespace] = [:]
  196. guard let config = dictionary[firebaseNamespace] else { return }
  197. for (key, value) in config {
  198. if dbSource == .default {
  199. guard let value = value as? NSObject else { continue }
  200. var valueData: Data?
  201. if let value = value as? Data {
  202. valueData = value
  203. } else if let value = value as? String {
  204. valueData = value.data(using: .utf8)
  205. } else if let value = value as? NSNumber {
  206. let stringValue = value.stringValue
  207. valueData = stringValue.data(using: .utf8)
  208. } else if let value = value as? Date {
  209. let dateFormatter = DateFormatter()
  210. dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
  211. let stringValue = dateFormatter.string(from: value)
  212. valueData = stringValue.data(using: .utf8)
  213. } else if let value = value as? [Any] {
  214. do {
  215. valueData = try JSONSerialization.data(withJSONObject: value, options: [])
  216. } catch {
  217. RCLog.error("I-RCN000076", "Invalid array value for key '\(key)'")
  218. }
  219. } else if let value = value as? [String: Any] {
  220. do {
  221. valueData = try JSONSerialization.data(withJSONObject: value, options: [])
  222. } catch {
  223. RCLog.error("I-RCN000077",
  224. "Invalid dictionary value for key '\(key)'")
  225. }
  226. } else {
  227. continue
  228. }
  229. guard let data = valueData else { continue }
  230. toDictionary[firebaseNamespace]?[key] = RemoteConfigValue(data: data, source: source)
  231. let values: [Any] = [bundleIdentifier, firebaseNamespace, key, data]
  232. updateMainTable(withValues: values, fromSource: dbSource)
  233. } else {
  234. guard let value = value as? RemoteConfigValue else { continue }
  235. toDictionary[firebaseNamespace]?[key] = RemoteConfigValue(
  236. data: value.dataValue,
  237. source: source
  238. )
  239. let values: [Any] = [bundleIdentifier, firebaseNamespace, key, value.dataValue]
  240. updateMainTable(withValues: values, fromSource: dbSource)
  241. }
  242. }
  243. if dbSource == .default {
  244. _defaultConfig.store(newValue: toDictionary)
  245. } else {
  246. _activeConfig.store(newValue: toDictionary)
  247. }
  248. }
  249. @objc public
  250. func updateConfigContent(withResponse response: [String: Any],
  251. forNamespace firebaseNamespace: String) {
  252. // Make sure database load has completed.
  253. checkAndWaitForInitialDatabaseLoad()
  254. guard let state = response[ConfigConstants.fetchResponseKeyState] as? String else {
  255. RCLog.error("I-RCN000049", "State field in fetch response is nil.")
  256. return
  257. }
  258. RCLog.debug("I-RCN000059",
  259. "Updating config content from Response for namespace: \(firebaseNamespace) with state: \(state)")
  260. if state == ConfigConstants.fetchResponseKeyStateNoChange {
  261. handleNoChangeState(forConfigNamespace: firebaseNamespace)
  262. return
  263. }
  264. /// Handle empty config state
  265. if state == ConfigConstants.fetchResponseKeyStateEmptyConfig {
  266. handleEmptyConfigState(forConfigNamespace: firebaseNamespace)
  267. return
  268. }
  269. /// Handle no template state.
  270. if state == ConfigConstants.fetchResponseKeyStateNoTemplate {
  271. handleNoTemplateState(forConfigNamespace: firebaseNamespace)
  272. return
  273. }
  274. /// Handle update state
  275. if state == ConfigConstants.fetchResponseKeyStateUpdate {
  276. let entries = response[ConfigConstants.fetchResponseKeyEntries] as? [String: String] ?? [:]
  277. handleUpdateState(forConfigNamespace: firebaseNamespace, withEntries: entries)
  278. handleUpdatePersonalization(response[ConfigConstants
  279. .fetchResponseKeyPersonalizationMetadata] as? [String: Any])
  280. handleUpdateRolloutFetchedMetadata(response[ConfigConstants
  281. .fetchResponseKeyRolloutMetadata] as? [[String: Any]])
  282. return
  283. }
  284. }
  285. @objc public
  286. func activatePersonalization() {
  287. _activePersonalization = _fetchedPersonalization
  288. dbManager?.insertOrUpdatePersonalizationConfig(_activePersonalization, fromSource: .active)
  289. }
  290. @objc public
  291. func activateRolloutMetadata(_ completionHandler: @escaping (Bool) -> Void) {
  292. _activeRolloutMetadata = _fetchedRolloutMetadata
  293. dbManager?.insertOrUpdateRolloutTable(withKey: ConfigConstants.rolloutTableKeyActiveMetadata,
  294. value: _activeRolloutMetadata,
  295. completionHandler: { success, _ in
  296. completionHandler(success)
  297. })
  298. }
  299. // MARK: - State Handling
  300. func handleNoChangeState(forConfigNamespace firebaseNamespace: String) {
  301. if fetchedConfig()[firebaseNamespace] == nil {
  302. _fetchedConfig.update(namespace: firebaseNamespace, newValue: [:])
  303. }
  304. }
  305. func handleEmptyConfigState(forConfigNamespace firebaseNamespace: String) {
  306. // If namespace has empty status and it doesn't exist in _fetchedConfig, we will
  307. // still add an entry for that namespace. Even if it will not be persisted in database.
  308. _fetchedConfig.update(namespace: firebaseNamespace, newValue: [:])
  309. dbManager?.deleteRecord(fromMainTableWithNamespace: firebaseNamespace,
  310. bundleIdentifier: bundleIdentifier,
  311. fromSource: .fetched)
  312. }
  313. func handleNoTemplateState(forConfigNamespace firebaseNamespace: String) {
  314. // Remove the namespace.
  315. _fetchedConfig.update(namespace: firebaseNamespace, newValue: [:])
  316. dbManager?.deleteRecord(fromMainTableWithNamespace: firebaseNamespace,
  317. bundleIdentifier: bundleIdentifier,
  318. fromSource: .fetched)
  319. }
  320. func handleUpdateState(forConfigNamespace firebaseNamespace: String,
  321. withEntries entries: [String: String]) {
  322. RCLog.debug("I-RCN000058",
  323. "Update config in DB for namespace: \(firebaseNamespace)")
  324. // Clear before updating
  325. dbManager?.deleteRecord(fromMainTableWithNamespace: firebaseNamespace,
  326. bundleIdentifier: bundleIdentifier,
  327. fromSource: .fetched)
  328. _fetchedConfig.update(namespace: firebaseNamespace, newValue: [:])
  329. // Store the fetched config values.
  330. for (key, value) in entries {
  331. guard let valueData = value.data(using: .utf8) else { continue }
  332. _fetchedConfig
  333. .update(namespace: firebaseNamespace, key: key,
  334. rcValue: RemoteConfigValue(data: valueData, source: .remote))
  335. let values: [Any] = [bundleIdentifier, firebaseNamespace, key, valueData]
  336. updateMainTable(withValues: values, fromSource: .fetched)
  337. }
  338. }
  339. func handleUpdatePersonalization(_ metadata: [String: Any]?) {
  340. guard let metadata = metadata else { return }
  341. _fetchedPersonalization = metadata
  342. dbManager?.insertOrUpdatePersonalizationConfig(metadata, fromSource: .fetched)
  343. }
  344. func handleUpdateRolloutFetchedMetadata(_ metadata: [[String: Any]]?) {
  345. _fetchedRolloutMetadata = metadata ?? []
  346. dbManager?.insertOrUpdateRolloutTable(withKey: ConfigConstants.rolloutTableKeyFetchedMetadata,
  347. value: _fetchedRolloutMetadata,
  348. completionHandler: nil)
  349. }
  350. // MARK: - Getters/Setters
  351. @objc public
  352. func fetchedConfig() -> [String: [String: RemoteConfigValue]] {
  353. /// If this is the first time reading the fetchedConfig, we might still be reading it from the
  354. /// database.
  355. checkAndWaitForInitialDatabaseLoad()
  356. return _fetchedConfig.wrappedValue
  357. }
  358. @objc public
  359. func activeConfig() -> [String: [String: RemoteConfigValue]] {
  360. /// If this is the first time reading the activeConfig, we might still be reading it from the
  361. /// database.
  362. checkAndWaitForInitialDatabaseLoad()
  363. return _activeConfig.wrappedValue
  364. }
  365. @objc public
  366. func defaultConfig() -> [String: [String: RemoteConfigValue]] {
  367. /// If this is the first time reading the defaultConfig, we might still be reading it from the
  368. /// database.
  369. checkAndWaitForInitialDatabaseLoad()
  370. return _defaultConfig.wrappedValue
  371. }
  372. @objc public
  373. func activePersonalization() -> [String: Any] {
  374. /// If this is the first time reading the activePersonalization, we might still be reading it
  375. /// from the
  376. /// database.
  377. checkAndWaitForInitialDatabaseLoad()
  378. return _activePersonalization
  379. }
  380. @objc public
  381. func activeRolloutMetadata() -> [[String: Any]] {
  382. /// If this is the first time reading the activeRolloutMetadata, we might still be reading it
  383. /// from the
  384. /// database.
  385. checkAndWaitForInitialDatabaseLoad()
  386. return _activeRolloutMetadata
  387. }
  388. @objc public
  389. func getConfigAndMetadata(forNamespace firebaseNamespace: String) -> [String: Any] {
  390. // If this is the first time reading the active metadata, we might still be reading it from the
  391. // database.
  392. checkAndWaitForInitialDatabaseLoad()
  393. return [
  394. ConfigConstants.fetchResponseKeyEntries: activeConfig()[firebaseNamespace] as Any,
  395. ConfigConstants.fetchResponseKeyPersonalizationMetadata: activePersonalization,
  396. ]
  397. }
  398. // Compare fetched config with active config and output what has changed
  399. @objc public
  400. func getConfigUpdate(forNamespace firebaseNamespace: String) -> RemoteConfigUpdate? {
  401. // TODO: handle diff in experiment metadata.
  402. var updatedKeys = Set<String>()
  403. let fetchedConfig = fetchedConfig()[firebaseNamespace] ?? [:]
  404. let activeConfig = activeConfig()[firebaseNamespace] ?? [:]
  405. let fetchedP13n = _fetchedPersonalization
  406. let activeP13n = _activePersonalization
  407. let fetchedRolloutMetadata = _fetchedRolloutMetadata
  408. let activeRolloutMetadata = _activeRolloutMetadata
  409. // Add new/updated params
  410. for key in fetchedConfig.keys {
  411. if activeConfig[key] == nil ||
  412. activeConfig[key]?.stringValue != fetchedConfig[key]?.stringValue {
  413. updatedKeys.insert(key)
  414. }
  415. }
  416. // Add deleted params
  417. for key in activeConfig.keys {
  418. if fetchedConfig[key] == nil {
  419. updatedKeys.insert(key)
  420. }
  421. }
  422. // Add params with new/updated p13n metadata
  423. for key in fetchedP13n.keys {
  424. if activeP13n[key] == nil ||
  425. !isEqual(activeP13n[key], fetchedP13n[key]) {
  426. updatedKeys.insert(key)
  427. }
  428. }
  429. // Add params with deleted p13n metadata
  430. for key in activeP13n.keys {
  431. if fetchedP13n[key] == nil {
  432. updatedKeys.insert(key)
  433. }
  434. }
  435. let fetchedRollouts = parameterKeyToRolloutMetadata(rolloutMetadata: fetchedRolloutMetadata)
  436. let activeRollouts = parameterKeyToRolloutMetadata(rolloutMetadata: activeRolloutMetadata)
  437. // Add params with new/updated rollout metadata
  438. for key in fetchedRollouts.keys {
  439. if activeRollouts[key] == nil ||
  440. !isEqual(activeRollouts[key], fetchedRollouts[key]) {
  441. updatedKeys.insert(key)
  442. }
  443. }
  444. // Add params with deleted rollout metadata
  445. for key in activeRollouts.keys {
  446. if fetchedRollouts[key] == nil {
  447. updatedKeys.insert(key)
  448. }
  449. }
  450. return RemoteConfigUpdate(updatedKeys: updatedKeys)
  451. }
  452. private func isEqual(_ object1: Any?, _ object2: Any?) -> Bool {
  453. guard let object1 = object1, let object2 = object2 else {
  454. return object1 == nil && object2 == nil // consider nil equal to nil.
  455. }
  456. // Attempt to compare as dictionaries.
  457. if let dict1 = object1 as? [String: Any], let dict2 = object2 as? [String: Any] {
  458. return NSDictionary(dictionary: dict1).isEqual(to: dict2)
  459. }
  460. return String(describing: object1) == String(describing: object2)
  461. }
  462. private func parameterKeyToRolloutMetadata(rolloutMetadata: [[String: Any]]) -> [String: Any] {
  463. var result = [String: [String: String]]()
  464. for metadata in rolloutMetadata {
  465. guard let rolloutID = metadata[ConfigConstants.fetchResponseKeyRolloutID] as? String,
  466. let variantID = metadata[ConfigConstants.fetchResponseKeyVariantID] as? String,
  467. let affectedKeys =
  468. metadata[ConfigConstants.fetchResponseKeyAffectedParameterKeys] as? [String]
  469. else { continue }
  470. for key in affectedKeys {
  471. if var rolloutIdToVariantId = result[key] {
  472. rolloutIdToVariantId[rolloutID] = variantID
  473. result[key] = rolloutIdToVariantId
  474. } else {
  475. result[key] = [rolloutID: variantID]
  476. }
  477. }
  478. }
  479. return result
  480. }
  481. }