Skip to content

Commit

Permalink
fix: reduce memory and cpu usage while running background queue (#379)
Browse files Browse the repository at this point in the history
  • Loading branch information
levibostian committed Sep 7, 2023
1 parent f304189 commit 87a7eed
Show file tree
Hide file tree
Showing 22 changed files with 173 additions and 361 deletions.
98 changes: 70 additions & 28 deletions Sources/Common/Background Queue/QueueRunner.swift
Expand Up @@ -13,20 +13,13 @@ public protocol QueueRunner: AutoMockable {

// sourcery: InjectRegister = "QueueRunner"
public class CioQueueRunner: ApiSyncQueueRunner, QueueRunner {
private let hooks: HooksManager
// store currently running queue hook in memory so it doesn't get garbage collected.
// hook instance needs to call completion handler so hold strong reference
private var currentlyRunningHook: QueueRunnerHook?

init(
jsonAdapter: JsonAdapter,

This comment has been minimized.

Copy link
@lizzydavis695

lizzydavis695 Sep 30, 2023

Run

logger: Logger,
httpClient: HttpClient,
hooksManager: HooksManager,
sdkConfig: SdkConfig
) {
self.hooks = hooksManager

super.init(
jsonAdapter: jsonAdapter,
logger: logger,
Expand All @@ -36,30 +29,20 @@ public class CioQueueRunner: ApiSyncQueueRunner, QueueRunner {
}

public func runTask(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void) {
if let queueTaskType = QueueTaskType(rawValue: task.type) {
switch queueTaskType {
case .trackDeliveryMetric: trackDeliveryMetric(task, onComplete: onComplete)
}

return
}
guard let queueTaskType = QueueTaskType(rawValue: task.type) else {
// not being able to compose a QueueTaskType is unexpected. All types are expected to be handled by this runner. Log an error so we get notified of this event.
logger.error("task \(task.type) not handled by the queue runner.")

var hookHandled = false

hooks.queueRunnerHooks.forEach { hook in
if hook.runTask(task, onComplete: { result in
self.currentlyRunningHook = nil
onComplete(result)
}) {
self.currentlyRunningHook = hook
hookHandled = true
}
return onComplete(.failure(.noRequestMade(nil)))
}

if !hookHandled {
logger.error("task \(task.type) not handled by any module")

onComplete(.failure(.noRequestMade(nil)))
switch queueTaskType {
case .trackDeliveryMetric: trackDeliveryMetric(task, onComplete: onComplete)
case .identifyProfile: identify(task, onComplete: onComplete)
case .trackEvent: track(task, onComplete: onComplete)
case .registerPushToken: registerPushToken(task, onComplete: onComplete)
case .deletePushToken: deletePushToken(task, onComplete: onComplete)
case .trackPushMetric: trackPushMetric(task, onComplete: onComplete)
}
}
}
Expand All @@ -79,4 +62,63 @@ private extension CioQueueRunner {

performHttpRequest(endpoint: .trackDeliveryMetrics, requestBody: bodyData, onComplete: onComplete)
}

private func identify(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void) {
guard let taskData = getTaskData(task, type: IdentifyProfileQueueTaskData.self) else {
return onComplete(failureIfDontDecodeTaskData)
}

performHttpRequest(
endpoint: .identifyCustomer(identifier: taskData.identifier),
requestBody: taskData.attributesJsonString?.data,
onComplete: onComplete
)
}

private func track(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void) {
guard let taskData = getTaskData(task, type: TrackEventQueueTaskData.self) else {
return onComplete(failureIfDontDecodeTaskData)
}

performHttpRequest(
endpoint: .trackCustomerEvent(identifier: taskData.identifier),
requestBody: taskData.attributesJsonString.data,
onComplete: onComplete
)
}

private func registerPushToken(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void) {
guard let taskData = getTaskData(task, type: RegisterPushNotificationQueueTaskData.self) else {
return onComplete(failureIfDontDecodeTaskData)
}

performHttpRequest(
endpoint: .registerDevice(identifier: taskData.profileIdentifier),
requestBody: taskData.attributesJsonString?.data,
onComplete: onComplete
)
}

private func deletePushToken(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void) {
guard let taskData = getTaskData(task, type: DeletePushNotificationQueueTaskData.self) else {
return onComplete(failureIfDontDecodeTaskData)
}

performHttpRequest(endpoint: .deleteDevice(
identifier: taskData.profileIdentifier,
deviceToken: taskData.deviceToken
), requestBody: nil, onComplete: onComplete)
}

private func trackPushMetric(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void) {
guard let taskData = getTaskData(task, type: MetricRequest.self) else {
return onComplete(failureIfDontDecodeTaskData)
}

guard let bodyData = jsonAdapter.toJson(taskData) else {
return
}

performHttpRequest(endpoint: .pushMetrics, requestBody: bodyData, onComplete: onComplete)
}
}
@@ -0,0 +1,16 @@
import Foundation

public struct DeletePushNotificationQueueTaskData: Codable {
public let profileIdentifier: String
public let deviceToken: String

public init(profileIdentifier: String, deviceToken: String) {
self.profileIdentifier = profileIdentifier
self.deviceToken = deviceToken
}

enum CodingKeys: String, CodingKey {
case profileIdentifier = "profile_identifier"
case deviceToken = "device_token"
}
}
@@ -0,0 +1,17 @@
import Foundation

public struct IdentifyProfileQueueTaskData: Codable {
public let identifier: String
/// JSON string: '{"foo": "bar"}'
public let attributesJsonString: String?

public init(identifier: String, attributesJsonString: String?) {
self.identifier = identifier
self.attributesJsonString = attributesJsonString
}

enum CodingKeys: String, CodingKey {
case identifier
case attributesJsonString = "attributes_json_string"
}
}
@@ -0,0 +1,16 @@
import Foundation

public struct RegisterPushNotificationQueueTaskData: Codable {
public let profileIdentifier: String
public let attributesJsonString: String?

public init(profileIdentifier: String, attributesJsonString: String?) {
self.profileIdentifier = profileIdentifier
self.attributesJsonString = attributesJsonString
}

enum CodingKeys: String, CodingKey {
case profileIdentifier = "profile_identifier"
case attributesJsonString = "attributes_json_string"
}
}
@@ -0,0 +1,17 @@
import Foundation

public struct TrackEventQueueTaskData: Codable {
public let identifier: String
/// JSON string: '{"foo": "bar"}'
public let attributesJsonString: String

public init(identifier: String, attributesJsonString: String) {
self.identifier = identifier
self.attributesJsonString = attributesJsonString
}

enum CodingKeys: String, CodingKey {
case identifier
case attributesJsonString = "attributes_json_string"
}
}
7 changes: 6 additions & 1 deletion Sources/Common/Background Queue/Type/QueueTaskType.swift
@@ -1,5 +1,10 @@
import Foundation

enum QueueTaskType: String {
public enum QueueTaskType: String {
case trackDeliveryMetric
case identifyProfile
case trackEvent
case registerPushToken
case deletePushToken
case trackPushMetric
}
Expand Up @@ -2,11 +2,18 @@ import CioInternalCommon
import Foundation

// https://customer.io/docs/api/#operation/pushMetrics
struct MetricRequest: Codable {
let deliveryId: String
let event: Metric
let deviceToken: String
let timestamp: Date
public struct MetricRequest: Codable {
public let deliveryId: String
public let event: Metric
public let deviceToken: String
public let timestamp: Date

public init(deliveryId: String, event: Metric, deviceToken: String, timestamp: Date) {
self.deliveryId = deliveryId
self.event = event
self.deviceToken = deviceToken
self.timestamp = timestamp
}

enum CodingKeys: String, CodingKey {
case deliveryId = "delivery_id"
Expand Down
132 changes: 0 additions & 132 deletions Sources/Common/autogenerated/AutoMockable.generated.swift
Expand Up @@ -832,42 +832,6 @@ public class HooksManagerMock: HooksManager, Mock {
}
}

/**
When setter of the property called, the value given to setter is set here.
When the getter of the property called, the value set here will be returned. Your chance to mock the property.
*/
public var underlyingQueueRunnerHooks: [QueueRunnerHook] = []
/// `true` if the getter or setter of property is called at least once.
public var queueRunnerHooksCalled: Bool {
queueRunnerHooksGetCalled || queueRunnerHooksSetCalled
}

/// `true` if the getter called on the property at least once.
public var queueRunnerHooksGetCalled: Bool {
queueRunnerHooksGetCallsCount > 0
}

public var queueRunnerHooksGetCallsCount = 0
/// `true` if the setter called on the property at least once.
public var queueRunnerHooksSetCalled: Bool {
queueRunnerHooksSetCallsCount > 0
}

public var queueRunnerHooksSetCallsCount = 0
/// The mocked property with a getter and setter.
public var queueRunnerHooks: [QueueRunnerHook] {
get {
mockCalled = true
queueRunnerHooksGetCallsCount += 1
return underlyingQueueRunnerHooks
}
set(value) {
mockCalled = true
queueRunnerHooksSetCallsCount += 1
underlyingQueueRunnerHooks = value
}
}

/**
When setter of the property called, the value given to setter is set here.
When the getter of the property called, the value set here will be returned. Your chance to mock the property.
Expand Down Expand Up @@ -907,8 +871,6 @@ public class HooksManagerMock: HooksManager, Mock {
public func resetMock() {
profileIdentifyHooksGetCallsCount = 0
profileIdentifyHooksSetCallsCount = 0
queueRunnerHooksGetCallsCount = 0
queueRunnerHooksSetCallsCount = 0
screenViewHooksGetCallsCount = 0
screenViewHooksSetCallsCount = 0
addCallsCount = 0
Expand Down Expand Up @@ -1368,42 +1330,6 @@ public class ModuleHookProviderMock: ModuleHookProvider, Mock {
}
}

/**
When setter of the property called, the value given to setter is set here.
When the getter of the property called, the value set here will be returned. Your chance to mock the property.
*/
public var underlyingQueueRunnerHook: QueueRunnerHook? = nil
/// `true` if the getter or setter of property is called at least once.
public var queueRunnerHookCalled: Bool {
queueRunnerHookGetCalled || queueRunnerHookSetCalled
}

/// `true` if the getter called on the property at least once.
public var queueRunnerHookGetCalled: Bool {
queueRunnerHookGetCallsCount > 0
}

public var queueRunnerHookGetCallsCount = 0
/// `true` if the setter called on the property at least once.
public var queueRunnerHookSetCalled: Bool {
queueRunnerHookSetCallsCount > 0
}

public var queueRunnerHookSetCallsCount = 0
/// The mocked property with a getter and setter.
public var queueRunnerHook: QueueRunnerHook? {
get {
mockCalled = true
queueRunnerHookGetCallsCount += 1
return underlyingQueueRunnerHook
}
set(value) {
mockCalled = true
queueRunnerHookSetCallsCount += 1
underlyingQueueRunnerHook = value
}
}

/**
When setter of the property called, the value given to setter is set here.
When the getter of the property called, the value set here will be returned. Your chance to mock the property.
Expand Down Expand Up @@ -1444,9 +1370,6 @@ public class ModuleHookProviderMock: ModuleHookProvider, Mock {
profileIdentifyHook = nil
profileIdentifyHookGetCallsCount = 0
profileIdentifyHookSetCallsCount = 0
queueRunnerHook = nil
queueRunnerHookGetCallsCount = 0
queueRunnerHookSetCallsCount = 0
screenTrackingHook = nil
screenTrackingHookGetCallsCount = 0
screenTrackingHookSetCallsCount = 0
Expand Down Expand Up @@ -2033,61 +1956,6 @@ public class QueueRunnerMock: QueueRunner, Mock {
}
}

/**
Class to easily create a mocked version of the `QueueRunnerHook` class.
This class is equipped with functions and properties ready for you to mock!
Note: This file is automatically generated. This means the mocks should always be up-to-date and has a consistent API.
See the SDK documentation to learn the basics behind using the mock classes in the SDK.
*/
public class QueueRunnerHookMock: QueueRunnerHook, Mock {
/// If *any* interactions done on mock. `true` if any method or property getter/setter called.
public var mockCalled: Bool = false //

public init() {
Mocks.shared.add(mock: self)
}

public func resetMock() {
runTaskCallsCount = 0
runTaskReceivedArguments = nil
runTaskReceivedInvocations = []

mockCalled = false // do last as resetting properties above can make this true
}

// MARK: - runTask

/// Number of times the function was called.
public private(set) var runTaskCallsCount = 0
/// `true` if the function was ever called.
public var runTaskCalled: Bool {
runTaskCallsCount > 0
}

/// The arguments from the *last* time the function was called.
public private(set) var runTaskReceivedArguments: (task: QueueTask, onComplete: (Result<Void, HttpRequestError>) -> Void)?
/// Arguments from *all* of the times that the function was called.
public private(set) var runTaskReceivedInvocations: [(task: QueueTask, onComplete: (Result<Void, HttpRequestError>) -> Void)] = []
/// Value to return from the mocked function.
public var runTaskReturnValue: Bool!
/**
Set closure to get called when function gets called. Great way to test logic or return a value for the function.
The closure has first priority to return a value for the mocked function. If the closure returns `nil`,
then the mock will attempt to return the value for `runTaskReturnValue`
*/
public var runTaskClosure: ((QueueTask, @escaping (Result<Void, HttpRequestError>) -> Void) -> Bool)?

/// Mocked function for `runTask(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void)`. Your opportunity to return a mocked value and check result of mock in test code.
public func runTask(_ task: QueueTask, onComplete: @escaping (Result<Void, HttpRequestError>) -> Void) -> Bool {
mockCalled = true
runTaskCallsCount += 1
runTaskReceivedArguments = (task: task, onComplete: onComplete)
runTaskReceivedInvocations.append((task: task, onComplete: onComplete))
return runTaskClosure.map { $0(task, onComplete) } ?? runTaskReturnValue
}
}

/**
Class to easily create a mocked version of the `QueueStorage` class.
This class is equipped with functions and properties ready for you to mock!
Expand Down

0 comments on commit 87a7eed

Please sign in to comment.