Skip to content

Commit

Permalink
Fixing background sessions logic in SessionController
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoEmbrace committed Sep 25, 2024
1 parent 394bd68 commit 57edf06
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 95 deletions.
5 changes: 4 additions & 1 deletion Sources/EmbraceCore/Embrace.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
self.upload = Embrace.createUpload(options: options, deviceId: deviceId.hex)
self.captureServices = try CaptureServices(options: options, storage: storage, upload: upload)
self.config = Embrace.createConfig(options: options, deviceId: deviceId.hex)
self.sessionController = SessionController(storage: storage, upload: upload)
self.sessionController = SessionController(storage: storage, upload: upload, config: config)
self.sessionLifecycle = Embrace.createSessionLifecycle(controller: sessionController)
self.metadata = MetadataHandler(storage: storage, sessionController: sessionController)
self.logController = logControllable ?? LogController(
Expand Down Expand Up @@ -182,6 +182,9 @@ To start the SDK you first need to configure it using an `Embrace.Options` insta
throw EmbraceSetupError.invalidThread("Embrace must be started on the main thread")
}

// must be called on main thread in order to fetch the app state
sessionLifecycle.setup()

Embrace.synchronizationQueue.sync {
guard started == false else {
Embrace.logger.warning("Embrace was already started!")
Expand Down
1 change: 1 addition & 0 deletions Sources/EmbraceCore/Internal/Embrace+Setup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import Foundation
import EmbraceCommonInternal
import EmbraceConfigInternal
import EmbraceOTelInternal
import EmbraceStorageInternal
import EmbraceUploadInternal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#if os(iOS)
import Foundation
import EmbraceCommonInternal
import EmbraceConfigInternal
import UIKit

// ignoring linting rule to have a lowercase letter first on the class name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Copyright © 2024 Embrace Mobile, Inc. All rights reserved.
//

import Foundation

class DefaultProcessUptimeProvider: ProcessUptimeProvider {
func uptime(since date: Date = Date()) -> TimeInterval? {
return ProcessMetadata.uptime(since: date)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//
// Copyright © 2024 Embrace Mobile, Inc. All rights reserved.
//

import Foundation

protocol ProcessUptimeProvider {
func uptime(since date: Date) -> TimeInterval?
}
2 changes: 1 addition & 1 deletion Sources/EmbraceCore/Session/SessionControllable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ protocol SessionControllable: AnyObject {
var currentSession: SessionRecord? { get }

@discardableResult
func startSession(state: SessionState) -> SessionRecord
func startSession(state: SessionState) -> SessionRecord?

@discardableResult
func endSession() -> Date
Expand Down
74 changes: 65 additions & 9 deletions Sources/EmbraceCore/Session/SessionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import Foundation
import EmbraceCommonInternal
import EmbraceConfigInternal
import EmbraceStorageInternal
import EmbraceUploadInternal
import EmbraceOTelInternal
Expand Down Expand Up @@ -35,23 +36,35 @@ class SessionController: SessionControllable {

weak var storage: EmbraceStorage?
weak var upload: EmbraceUpload?
weak var config: EmbraceConfig?

private var backgroundSessionsEnabled: Bool {
return config?.isBackgroundSessionEnabled == true
}

let heartbeat: SessionHeartbeat
let queue: DispatchQueue
let processUptimeProvider: ProcessUptimeProvider

internal var notificationCenter = NotificationCenter.default

init(
storage: EmbraceStorage,
upload: EmbraceUpload?,
heartbeatInterval: TimeInterval = SessionHeartbeat.defaultInterval
config: EmbraceConfig?,
heartbeatInterval: TimeInterval = SessionHeartbeat.defaultInterval,
processUptimeProvider: ProcessUptimeProvider = DefaultProcessUptimeProvider()
) {
self.storage = storage
self.upload = upload
self.config = config

let heartbeatQueue = DispatchQueue(label: "com.embrace.session_heartbeat")
self.heartbeat = SessionHeartbeat(queue: heartbeatQueue, interval: heartbeatInterval)
self.queue = DispatchQueue(label: "com.embrace.session_controller_upload")

self.processUptimeProvider = processUptimeProvider

self.heartbeat.callback = { [weak self] in
let heartbeat = Date()
self?.currentSession?.lastHeartbeatTime = heartbeat
Expand All @@ -65,24 +78,40 @@ class SessionController: SessionControllable {
}

@discardableResult
func startSession(state: SessionState) -> SessionRecord {
func startSession(state: SessionState) -> SessionRecord? {
return startSession(state: state, startTime: Date())
}

@discardableResult
func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord {
func startSession(state: SessionState, startTime: Date = Date()) -> SessionRecord? {
// end current session first
if currentSession != nil {
endSession()
}

// detect cold start
let isColdStart = withinColdStartInterval(startTime: startTime)

// Don't start background session if the config is disabled.
//
// Note: There's an exception for the cold start session:
// We start the session anyways and we drop it when it ends if
// it's still considered a background session.
// Due to how iOS works we can't know for sure the state when the
// app starts, so we need to delay the logic!
//
// +
if isColdStart == false &&
state == .background &&
backgroundSessionsEnabled == false {
return nil
}
// -

// we lock after end session to avoid a deadlock

return lock.locked {

// detect cold start
let isColdStart = withinColdStartInterval(startTime: startTime)

// create session span
let newId = SessionIdentifier.random
let span = SessionSpanUtils.span(id: newId, startTime: startTime, state: state, coldStart: isColdStart)
Expand Down Expand Up @@ -121,11 +150,22 @@ class SessionController: SessionControllable {
return lock.locked {
// stop heartbeat
heartbeat.stop()
let now = Date()

// If the session is a background session and background sessions
// are disabled in the config, we drop the session!
// +
if currentSession?.coldStart == true &&
currentSession?.state == SessionState.background.rawValue &&
backgroundSessionsEnabled == false {
delete()
return now
}
// -

// post notification
notificationCenter.post(name: .embraceSessionWillEnd, object: currentSession)

let now = Date()
currentSessionSpan?.end(time: now)
SessionSpanUtils.setCleanExit(span: currentSessionSpan, cleanExit: true)

Expand Down Expand Up @@ -173,9 +213,9 @@ class SessionController: SessionControllable {
extension SessionController {
static let allowedColdStartInterval: TimeInterval = 5.0

/// - Returns: `true` if ``ProcessMetadata.uptime`` is less than or equal to the allowed cold start interval. See ``iOSAppListener.minimumColdStartInterval``
/// - Returns: `true` if ``ProcessMetadata.uptime`` is less than or equal to the allowed cold start interval.
private func withinColdStartInterval(startTime: Date) -> Bool {
guard let uptime = ProcessMetadata.uptime(since: startTime), uptime >= 0 else {
guard let uptime = processUptimeProvider.uptime(since: startTime), uptime >= 0 else {
return false
}

Expand All @@ -194,4 +234,20 @@ extension SessionController {
Embrace.logger.warning("Error trying to update session:\n\(error.localizedDescription)")
}
}

private func delete() {
guard let storage = storage,
let session = currentSession else {
return
}

do {
try storage.delete(record: session)
} catch {
Embrace.logger.warning("Error trying to delete session:\n\(error.localizedDescription)")
}

currentSession = nil
currentSessionSpan = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import EmbraceCommonInternal
class EmbraceLogAttributesBuilderTests: XCTestCase {
private var sut: EmbraceLogAttributesBuilder!
private var storage: MockMetadataFetcher!
private var controller: SpySessionController!
private var controller: MockSessionController!
private var result: [String: String]!

// MARK: - Test Build Alone
Expand Down Expand Up @@ -177,11 +177,12 @@ private extension EmbraceLogAttributesBuilderTests {
sessionWithId sessionId: SessionIdentifier = .random,
sessionState: SessionState = .foreground
) {
controller = SpySessionController(currentSession: .with(id: sessionId, state: sessionState))
controller = MockSessionController()
controller.currentSession = .with(id: sessionId, state: sessionState)
}

func givenSessionControllerWithNoSession() {
controller = SpySessionController(currentSession: nil)
controller = MockSessionController()
}

func givenMetadataFetcher(with metadata: [MetadataRecord]? = nil) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import EmbraceCommonInternal
class LogControllerTests: XCTestCase {
private var sut: LogController!
private var storage: SpyStorage?
private var sessionController: SpySessionController!
private var sessionController: MockSessionController!
private var upload: SpyEmbraceLogUploader!

override func setUp() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"background": {
"threshold": 100
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ final class MetadataHandlerTests: XCTestCase {

// start new session
let newSession = sessionController.startSession(state: .foreground)
let secondSessionId = newSession.id
let secondSessionId = newSession!.id
try storage.addSession(
id: secondSessionId,
state: .foreground,
Expand Down
Loading

0 comments on commit 57edf06

Please sign in to comment.