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

FlagApplier alignment between iOS and Android #42

Merged
merged 9 commits into from
Aug 14, 2023
95 changes: 57 additions & 38 deletions Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,69 +21,88 @@
self.storage = storage
self.httpClient = httpClient
self.options = options
self.cacheDataInteractor = cacheDataInteractor ?? CacheDataInteractor(storage: storage)

let storedData = try? storage.load(defaultValue: CacheData.empty())
alina-v1 marked this conversation as resolved.
Show resolved Hide resolved
self.cacheDataInteractor = cacheDataInteractor ?? CacheDataInteractor(cacheData: storedData ?? .empty())

if triggerBatch {
self.triggerBatch()
Task(priority: .utility) {
await self.triggerBatch()
}
}
}

public func apply(flagName: String, resolveToken: String) async {
let applyTime = Date.backport.now
let eventExists = await cacheDataInteractor.applyEventExists(resolveToken: resolveToken, name: flagName)
guard eventExists == false else {
let (data, added) = await cacheDataInteractor.add(
resolveToken: resolveToken,
flagName: flagName,
applyTime: applyTime
)
guard added == true else {
// If record is found in the cache, early return (de-duplication).
// Triggerring batch apply in case if there are any unsent events stored
triggerBatch()
await triggerBatch()
return
}

await cacheDataInteractor.add(resolveToken: resolveToken, flagName: flagName, applyTime: applyTime)
let flagApply = FlagApply(name: flagName, applyTime: applyTime)
executeApply(resolveToken: resolveToken, items: [flagApply]) { success in
guard success else {
self.write(resolveToken: resolveToken, name: flagName, applyTime: applyTime)
return
}
self.triggerBatch()
}
self.writeToFile(data: data)
await triggerBatch()
}

// MARK: private

private func triggerBatch() {
guard let storedData = try? storage.load(defaultValue: CacheData.empty()), storedData.isEmpty == false else {
return
}
private func triggerBatch() async {
async let cacheData = await cacheDataInteractor.cache
await cacheData.resolveEvents.forEach { resolveEvent in
let appliesToSend = resolveEvent.events.filter { entry in
return entry.status == .created
}.chunk(size: 20)

Check warning on line 60 in Sources/ConfidenceProvider/Apply/FlagApplierWithRetries.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Multiline Function Chains Violation: Chained function calls should be either on the same line, or one per line (multiline_function_chains)

storedData.resolveEvents.forEach { resolveEvent in
executeApply(
resolveToken: resolveEvent.resolveToken,
items: resolveEvent.events
) { success in
guard success else {
return
guard appliesToSend.isEmpty == false else {
return
}

appliesToSend.forEach { chunk in
self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .sending)
executeApply(
resolveToken: resolveEvent.resolveToken,
items: chunk
) { success in
guard success else {
self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .created)
return
}
// Set 'sent' property of apply events to true
self.writeStatus(resolveToken: resolveEvent.resolveToken, events: chunk, status: .sent)
}
// Remove events from storage that were successfully sent
self.remove(resolveToken: resolveEvent.resolveToken)
}
}
}

private func write(resolveToken: String, name: String, applyTime: Date) {
guard var storedData = try? storage.load(defaultValue: CacheData.empty()) else {
return
private func writeStatus(resolveToken: String, events: [FlagApply], status: ApplyEventStatus) {
let lastIndex = events.count - 1
events.enumerated().forEach { index, event in
Task(priority: .medium) {
var data = await self.cacheDataInteractor.setEventStatus(
resolveToken: resolveToken,
name: event.name,
status: status
)

if index == lastIndex {
let unsentFlagApplies = data.resolveEvents.filter {
$0.isSent == false
}
data.resolveEvents = unsentFlagApplies
try? self.storage.save(data: data)
}
}
}
storedData.add(resolveToken: resolveToken, flagName: name, applyTime: applyTime)
try? storage.save(data: storedData)
}

private func remove(resolveToken: String) {
guard var storedData = try? storage.load(defaultValue: CacheData.empty()) else {
return
}
storedData.remove(resolveToken: resolveToken)
try? storage.save(data: storedData)
private func writeToFile(data: CacheData) {
try? storage.save(data: data)
}

private func executeApply(
Expand Down
9 changes: 8 additions & 1 deletion Sources/ConfidenceProvider/Cache/CacheDataActor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
var cache: CacheData { get }

/// Adds single data entry to the cache.
func add(resolveToken: String, flagName: String, applyTime: Date)
func add(resolveToken: String, flagName: String, applyTime: Date) -> (CacheData, Bool)

/// Removes data from the cache.
/// - Note: This method removes all flag apply entries from cache for given resolve token.
Expand All @@ -17,4 +17,11 @@

/// Removes single apply event from the cache.
func applyEventExists(resolveToken: String, name: String) -> Bool

/// Sets Flag Apply Event `status`.
func setEventStatus(resolveToken: String, name: String, status: ApplyEventStatus) -> CacheData

/// Sets Resolve Apply Event `status` property.
func setEventStatus(resolveToken: String, status: ApplyEventStatus) -> CacheData

Check warning on line 26 in Sources/ConfidenceProvider/Cache/CacheDataActor.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Vertical Whitespace before Closing Braces Violation: Don't include vertical whitespace (empty line) before closing braces (vertical_whitespace_closing_braces)
}
30 changes: 14 additions & 16 deletions Sources/ConfidenceProvider/Cache/CacheDataInteractor.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import Foundation

final actor CacheDataInteractor: CacheDataActor {
private let storage: Storage
var cache = CacheData.empty()

init(storage: Storage) {
self.storage = storage

Task {
await loadCacheFromStorage()
}
init(cacheData: CacheData) {
cache = cacheData
}

func add(resolveToken: String, flagName: String, applyTime: Date) {
func add(resolveToken: String, flagName: String, applyTime: Date) -> (CacheData, Bool) {
if cache.isEmpty == false {
cache.add(resolveToken: resolveToken, flagName: flagName, applyTime: applyTime)
let added = cache.add(resolveToken: resolveToken, flagName: flagName, applyTime: applyTime)
return (cache, added)
} else {
cache = CacheData(
resolveToken: resolveToken,
flagName: flagName,
applyTime: applyTime
)
return (cache, true)
}
}

Expand All @@ -36,12 +33,13 @@ final actor CacheDataInteractor: CacheDataActor {
cache.applyEventExists(resolveToken: resolveToken, name: name)
}

private func loadCacheFromStorage() {
guard let storedData = try? storage.load(defaultValue: cache),
storedData.isEmpty == false
else {
return
}
self.cache = storedData
func setEventStatus(resolveToken: String, name: String, status: ApplyEventStatus) -> CacheData {
cache.setEventStatus(resolveToken: resolveToken, name: name, status: status)
return cache
}

func setEventStatus(resolveToken: String, status: ApplyEventStatus) -> CacheData {
cache.setEventStatus(resolveToken: resolveToken, status: status)
return cache
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Foundation

enum ApplyEventStatus: Codable {
case created
case sending
case sent
}
28 changes: 25 additions & 3 deletions Sources/ConfidenceProvider/Cache/Models/CacheData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,27 @@
return resolveTokenIndex != nil
}

mutating func add(resolveToken: String, flagName: String, applyTime: Date) {
mutating func setEventStatus(resolveToken: String, name: String, status: ApplyEventStatus = .sent) {
let flagEventIndexes = flagEventIndex(resolveToken: resolveToken, name: name)
guard let resolveIndex = flagEventIndexes.resolveEventIndex,
let flagIndex = flagEventIndexes.flagEventIndex else {

Check warning on line 39 in Sources/ConfidenceProvider/Cache/Models/CacheData.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Indentation Width Violation: Code should be indented using one tab or 4 spaces (indentation_width)
return
}

resolveEvents[resolveIndex].events[flagIndex].status = status
}

mutating func setEventStatus(resolveToken: String, status: ApplyEventStatus = .sent) {
guard let resolveIndex = resolveEventIndex(resolveToken: resolveToken) else {
return
}

for i in 0..<resolveEvents[resolveIndex].events.count {
resolveEvents[resolveIndex].events[i].status = status
}
}

mutating func add(resolveToken: String, flagName: String, applyTime: Date) -> Bool {
let resolveEventIndex = resolveEventIndex(resolveToken: resolveToken)

if let resolveEventIndex {
Expand All @@ -46,12 +66,15 @@
// No flag apply event for given resolve token, adding new record
let flagEvent = FlagApply(name: flagName, applyTime: applyTime)
resolveEvents[resolveEventIndex].events.append(flagEvent)
return true
}
} else {
// No resolve event for given resolve token, adding new record
let event = ResolveApply(resolveToken: resolveToken, flagName: flagName, applyTime: applyTime)
resolveEvents.append(event)
return true
}
return false
}

mutating func remove(resolveToken: String) {
Expand Down Expand Up @@ -93,8 +116,7 @@

func flagEvent(resolveToken: String, name: String) -> FlagApply? {
guard let resolveTokenIndex = resolveEventIndex(resolveToken: resolveToken),
let flagEventIndex = applyEventIndex(resolveToken: resolveToken, name: name)
else {
let flagEventIndex = applyEventIndex(resolveToken: resolveToken, name: name) else {

Check warning on line 119 in Sources/ConfidenceProvider/Cache/Models/CacheData.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Indentation Width Violation: Code should be indented using one tab or 4 spaces (indentation_width)
return nil
}

Expand Down
6 changes: 4 additions & 2 deletions Sources/ConfidenceProvider/Cache/Models/FlagApply.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import Foundation

struct FlagApply: Codable {
let name: String
var applyTime: Date
let applyTime: Date
var status: ApplyEventStatus

init(name: String, applyTime: Date) {
init(name: String, applyTime: Date, status: ApplyEventStatus = .created) {
self.name = name
self.applyTime = applyTime
self.status = status
}
}
4 changes: 4 additions & 0 deletions Sources/ConfidenceProvider/Cache/Models/ResolveApply.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ struct ResolveApply: Codable {
resolveToken.isEmpty || events.isEmpty
}

var isSent: Bool {
events.allSatisfy { $0.status == .sent }
}

init(resolveToken: String, flagName: String, applyTime: Date) {
self.resolveToken = resolveToken
self.events = [FlagApply(name: flagName, applyTime: applyTime)]
Expand Down
12 changes: 5 additions & 7 deletions Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
///
///
///
// swiftlint:disable type_body_length

Check warning on line 9 in Sources/ConfidenceProvider/ConfidenceFeatureProvider.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Blanket Disable Command Violation: The disabled 'type_body_length' rule should be re-enabled before the end of the file (blanket_disable_command)
// swiftlint:disable file_length
public class ConfidenceFeatureProvider: FeatureProvider {
public var hooks: [any Hook] = []
Expand Down Expand Up @@ -471,13 +471,11 @@

/// Creates the `ConfidenceFeatureProvider` according to the settings specified in the builder.
public func build() -> ConfidenceFeatureProvider {
let flagApplier =
flagApplier
?? FlagApplierWithRetries(
httpClient: NetworkClient(region: options.region),
storage: DefaultStorage(filePath: "applier.flags.cache"),
options: options
)
let flagApplier = flagApplier ?? FlagApplierWithRetries(
httpClient: NetworkClient(region: options.region),
storage: DefaultStorage(filePath: "applier.flags.cache"),
options: options
)

let client = RemoteConfidenceClient(
options: options,
Expand Down
9 changes: 9 additions & 0 deletions Sources/ConfidenceProvider/Utils/Array+Chunks.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

extension Array {
func chunk(size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ final class CacheDataInteractorTests: XCTestCase {
resolveEventCount: 10,
applyEventCount: 20
)
let prefilledStorage = try StorageMock(data: prefilledCache)

// When cache data interactor is initialised
let cacheDataInteractor = CacheDataInteractor(storage: prefilledStorage)
let cacheDataInteractor = CacheDataInteractor(cacheData: prefilledCache)

// Then cache data is loaded from storage
Task {
Expand All @@ -27,8 +26,7 @@ final class CacheDataInteractorTests: XCTestCase {

func testCacheDataInteractor_addEventToEmptyCache() async throws {
// Given cache data interactor with no previously stored data
let storage = StorageMock()
let cacheDataInteractor = CacheDataInteractor(storage: storage)
let cacheDataInteractor = CacheDataInteractor(cacheData: .empty())
Task {
let cache = await cacheDataInteractor.cache
XCTAssertEqual(cache.resolveEvents.count, 0)
Expand All @@ -47,8 +45,7 @@ final class CacheDataInteractorTests: XCTestCase {
func testCacheDataInteractor_addEventToPreFilledCache() async throws {
// Given cache data interactor with previously stored data (1 resolve token and 2 apply event)
let prefilledCacheData = try CacheDataUtility.prefilledCacheData(applyEventCount: 2)
let prefilledStorage = try StorageMock(data: prefilledCacheData)
let cacheDataInteractor = CacheDataInteractor(storage: prefilledStorage)
let cacheDataInteractor = CacheDataInteractor(cacheData: prefilledCacheData)

Task {
// When cache data add method is called
Expand Down
35 changes: 35 additions & 0 deletions Tests/ConfidenceProviderTests/CacheDataTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,41 @@ final class CacheDataTests: XCTestCase {
XCTAssertEqual(cacheData.resolveEvents.isEmpty, true)
}

func testCacheData_eventExist_isEventSent() throws {
// Given prefilled cached data
// and all apply events has sent property set to false
var cacheData = try CacheDataUtility.prefilledCacheData()
let resolve = try XCTUnwrap(cacheData.resolveEvents.first)
let sentEvents = resolve.events.filter { $0.status == .sent }
XCTAssertEqual(sentEvents.count, 0)

// When set event sent is called for item that exists in cache
cacheData.setEventStatus(resolveToken: "token0", name: "prefilled2")
let resolveAfterUpdate = try XCTUnwrap(cacheData.resolveEvents.first)

// Then apply event sent property has been set to true
let sentEventsAfterUpdate = resolveAfterUpdate.events.filter { $0.status == .sent }
XCTAssertEqual(sentEventsAfterUpdate.count, 1)
XCTAssertEqual(sentEventsAfterUpdate.first?.name, "prefilled2")
}

func testCacheData_eventDoesNotExist_isEventSent() throws {
// Given prefilled cached data
// and all apply events has sent property set to false
var cacheData = try CacheDataUtility.prefilledCacheData()
let resolve = try XCTUnwrap(cacheData.resolveEvents.first)
let sentEvents = resolve.events.filter { $0.status == .sent }
XCTAssertEqual(sentEvents.count, 0)

// When set event sent is called for item that does not exists in cache
cacheData.setEventStatus(resolveToken: "token0", name: "prefilled45")
let resolveAfterUpdate = try XCTUnwrap(cacheData.resolveEvents.first)

// Then apply event sent property has not been changed
let sentEventsAfterUpdate = resolveAfterUpdate.events.filter { $0.status == .sent }
XCTAssertEqual(sentEventsAfterUpdate.count, 0)
}

func testCacheData_isEmpty() {
// Given empty cached data
let cacheData = CacheData.empty()
Expand Down
Loading
Loading