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: Event sender engine #88

Merged
merged 8 commits into from
Apr 12, 2024
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
106 changes: 106 additions & 0 deletions Sources/Confidence/EventSenderEngine.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import Combine
import Foundation

protocol EventsUploader {
func upload(request: [Event]) async -> Bool
}

struct Event: Encodable, Equatable {
let name: String
let payload: [String: ConfidenceValue]
let eventTime: Date
}

protocol FlushPolicy {
func reset()
func hit(event: Event)
func shouldFlush() -> Bool
}

protocol Clock {
func now() -> Date
}

protocol EventSenderEngine {
func send(name: String, message: [String: ConfidenceValue])
func shutdown()
}

final class EventSenderEngineImpl: EventSenderEngine {
private static let sendSignalName: String = "FLUSH"
private let storage: any EventStorage
private let writeReqChannel = PassthroughSubject<Event, Never>()
private let uploadReqChannel = PassthroughSubject<String, Never>()
private var cancellables = Set<AnyCancellable>()
private let flushPolicies: [FlushPolicy]
private let uploader: EventsUploader
private let clientSecret: String
private let clock: Clock

init(
clientSecret: String,
uploader: EventsUploader,
clock: Clock,
storage: EventStorage,
flushPolicies: [FlushPolicy]
) {
self.clock = clock
self.uploader = uploader
self.clientSecret = clientSecret
self.storage = storage
self.flushPolicies = flushPolicies

writeReqChannel.sink(receiveValue: { [weak self] event in

Check warning on line 53 in Sources/Confidence/EventSenderEngine.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)
guard let self = self else { return }
do {
try self.storage.writeEvent(event: event)
} catch {

}

self.flushPolicies.forEach({ policy in policy.hit(event: event) })

Check warning on line 61 in Sources/Confidence/EventSenderEngine.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)
let shouldFlush = self.flushPolicies.contains(where: { policy in policy.shouldFlush() })

Check warning on line 62 in Sources/Confidence/EventSenderEngine.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)

if shouldFlush {
self.uploadReqChannel.send(EventSenderEngineImpl.sendSignalName)
self.flushPolicies.forEach({ policy in policy.reset() })

Check warning on line 66 in Sources/Confidence/EventSenderEngine.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Trailing Closure Violation: Trailing closure syntax should be used whenever possible (trailing_closure)
}

}).store(in: &cancellables)

Check warning on line 69 in Sources/Confidence/EventSenderEngine.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)

uploadReqChannel.sink(receiveValue: { [weak self] _ in
do {
guard let self = self else { return }
try self.storage.startNewBatch()
let ids = storage.batchReadyIds()
for id in ids {
let events = try self.storage.eventsFrom(id: id)
let shouldCleanup = await self.uploader.upload(request: events)
if shouldCleanup {
try storage.remove(id: id)
}
}
} catch {

}
}).store(in: &cancellables)

Check warning on line 86 in Sources/Confidence/EventSenderEngine.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)
}

func send(name: String, message: [String: ConfidenceValue]) {
writeReqChannel.send(Event(name: name, payload: message, eventTime: Date()))
}

func shutdown() {
cancellables.removeAll()
}
}

private extension Publisher where Self.Failure == Never {
func sink(receiveValue: @escaping ((Self.Output) async -> Void)) -> AnyCancellable {

Check warning on line 99 in Sources/Confidence/EventSenderEngine.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)
sink { value in
Task {

Check warning on line 101 in Sources/Confidence/EventSenderEngine.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)
await receiveValue(value)
}

Check warning on line 103 in Sources/Confidence/EventSenderEngine.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Indentation Width Violation: Code should be unindented by multiples of one tab or multiples of 4 spaces (indentation_width)
}
}

Check warning on line 105 in Sources/Confidence/EventSenderEngine.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Indentation Width Violation: Code should be unindented by multiples of one tab or multiples of 4 spaces (indentation_width)
}
15 changes: 15 additions & 0 deletions Sources/Confidence/EventSenderStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Foundation

struct EventBatchRequest: Encodable {
let clientSecret: String
let sendTime: Date
let events: [Event]
}

internal protocol EventStorage {
func startNewBatch() throws
func writeEvent(event: Event) throws
func batchReadyIds() -> [String]
func eventsFrom(id: String) throws -> [Event]
func remove(id: String) throws
}
57 changes: 57 additions & 0 deletions Tests/ConfidenceProviderTests/EventSenderEngineTest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Foundation
import XCTest

@testable import Confidence

final class MinSizeFlushPolicy: FlushPolicy {
private var maxSize = 5
private var size = 0
func reset() {
size = 0
}

func hit(event: Event) {
size += 1
}

func shouldFlush() -> Bool {
return size >= maxSize
}


}

final class EventSenderEngineTest: XCTestCase {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about test cases and what do you think about something pseudo like this:

// given that engine is initialized with storage that contains 3 events to be uploaded

// when no "sends" happen (in some specific time?)

// expect the 3 events to be uploaded

We don't have support for this yet but I think we need it (on both android and ios).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean introducing flush intervals?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes 👍 but for later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ticket is created here

func testAddingEventsWithSizeFlushPolicyWorks() throws {
let flushPolicies = [MinSizeFlushPolicy()]
let uploader = EventUploaderMock()
let eventSenderEngine = EventSenderEngineImpl(
clientSecret: "CLIENT_SECRET",
uploader: uploader,
clock: ClockMock(),
storage: EventStorageMock(),
flushPolicies: flushPolicies
)

let expectation = XCTestExpectation(description: "Upload finished")
let cancellable = uploader.subject.sink { value in
expectation.fulfill()
}

var events: [Event] = []
for i in 0..<5 {
events.append(Event(name: "\(i)", payload: [:], eventTime: Date()))
eventSenderEngine.send(name: "\(i)", message: [:])
}

wait(for: [expectation], timeout: 5)
let uploadRequest = try XCTUnwrap(uploader.calledRequest)
XCTAssertTrue(uploadRequest.map { $0.name } == events.map { $0.name })

uploader.reset()
eventSenderEngine.send(name: "Hello", message: [:])
XCTAssertNil(uploader.calledRequest)
cancellable.cancel()
}
}

49 changes: 49 additions & 0 deletions Tests/ConfidenceProviderTests/EventUploaderMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Foundation
import Combine
@testable import Confidence

final class EventUploaderMock: EventsUploader {
var calledRequest: [Event]? = nil
let subject: PassthroughSubject<Int, Never> = PassthroughSubject()
func upload(request: [Event]) async -> Bool {
calledRequest = request
subject.send(1)
return true
}

func reset() {
calledRequest = nil
}
}

final class ClockMock: Clock {
func now() -> Date {
return Date()
}
}

final class EventStorageMock: EventStorage {
private var events: [Event] = []
private var batches: [String: [Event]] = [:]
func startNewBatch() throws {
batches[("\(batches.count)")] = events
events.removeAll()
}

func writeEvent(event: Event) throws {
events.append(event)
}

func batchReadyIds() -> [String] {
return batches.map({ batch in batch.0})
}

func eventsFrom(id: String) throws -> [Event] {
return batches[id]!
}

func remove(id: String) throws {
batches.removeValue(forKey: id)
}

}
Loading