Refactor the NSE to create a new UserSession every time a payload comes in and pass the room down to the SDK so it can optimize how many rooms it loads from the store and how much memory it's using.

- this will undo creating at most one user session pe process as that shoulnd't be a problem anymore
- it also renames some methods and improves the logging
This commit is contained in:
Stefan Ceriu
2025-04-03 15:47:23 +03:00
parent 346e4adde3
commit 0c8b3470e1
2 changed files with 53 additions and 68 deletions

View File

@@ -28,25 +28,24 @@ import UserNotifications
// but it will always be called on different threads. It may or may not be
// called on the same instance of `NotificationService` as a previous
// notification.
//
// We keep a global `environment` singleton to ensure that our app context,
// database, logging, etc. are only ever setup once per *process*
private let settings: CommonSettingsProtocol = AppSettings()
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), destination: .notification),
settings: settings)
private let keychainController = KeychainController(service: .sessions,
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
private let eventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()),
destination: .notification)
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder,
settings: settings)
class NotificationServiceExtension: UNNotificationServiceExtension {
private var handler: ((UNNotificationContent) -> Void)?
private var modifiedContent: UNMutableNotificationContent?
private let appHooks = AppHooks()
// Used to create one single UserSession across process/instances/runs
private static let serialQueue = DispatchQueue(label: "io.element.elementx.nse")
deinit {
cleanUp()
ExtensionLogger.logMemory(with: tag)
@@ -74,63 +73,49 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent
MXLog.info("\(tag) #########################################")
ExtensionLogger.logMemory(with: tag)
MXLog.info("\(tag) Payload came: \(request.content.userInfo)")
Self.serialQueue.sync {
// If the session directories have changed, the user has logged out and back in (even if they entered the same user ID).
// We can't do this comparison with the access token of the existing session here due to token refresh when using OIDC.
if Self.userSession == nil || Self.userSession?.sessionDirectories != credentials.restorationToken.sessionDirectories {
// This function might be run concurrently and from different processes
// It's imperative that we create **at most** one UserSession/Client per process
Task.synchronous { [appHooks] in
do {
Self.userSession = try await NSEUserSession(credentials: credentials,
clientSessionDelegate: keychainController,
appHooks: appHooks,
appSettings: settings)
} catch {
MXLog.error("Failed creating user session with error: \(error)")
}
}
}
if Self.userSession == nil {
return discard(unreadCount: request.unreadCount)
}
}
ExtensionLogger.logMemory(with: tag)
MXLog.info("\(tag) Received payload: \(request.content.userInfo)")
Task {
await run(with: credentials,
roomID: roomID,
eventID: eventID,
unreadCount: request.unreadCount)
do {
let userSession = try await NSEUserSession(credentials: credentials,
roomID: roomID,
clientSessionDelegate: keychainController,
appHooks: appHooks,
appSettings: settings)
ExtensionLogger.logMemory(with: tag)
MXLog.info("\(tag) Configured user session")
await processEvent(eventID,
roomID: roomID,
unreadCount: request.unreadCount,
userSession: userSession)
} catch {
MXLog.error("Failed creating user session with error: \(error)")
}
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
MXLog.warning("\(tag) serviceExtensionTimeWillExpire")
MXLog.warning("\(tag) Extension time will expire")
notify(unreadCount: nil)
}
// MARK: - Private
private func run(with credentials: KeychainCredentials,
roomID: String,
eventID: String,
unreadCount: Int?) async {
MXLog.info("\(tag) run with roomId: \(roomID), eventId: \(eventID)")
private func processEvent(_ eventID: String,
roomID: String,
unreadCount: Int?,
userSession: NSEUserSession) async {
MXLog.info("\(tag) Processing event: \(eventID) in room: \(roomID)")
guard let userSession = Self.userSession else {
MXLog.error("Invalid NSE User Session, discarding.")
return discard(unreadCount: unreadCount)
}
do {
guard let itemProxy = await userSession.notificationItemProxy(roomID: roomID, eventID: eventID) else {
MXLog.info("\(tag) no notification for the event, discard")
MXLog.error("\(tag) Failed retrieving notification item")
return discard(unreadCount: unreadCount)
}
@@ -141,40 +126,35 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
break
}
// After the first processing, update the modified content
modifiedContent = try await notificationContentBuilder.content(for: itemProxy, mediaProvider: nil)
guard itemProxy.hasMedia else {
MXLog.info("\(tag) no media needed")
// We've processed the item and no media operations needed, so no need to go further
MXLog.info("\(tag) Notification item doesn't contain media")
return notify(unreadCount: unreadCount)
}
MXLog.info("\(tag) process with media")
// There is some media to load, process it again
MXLog.info("\(tag) Processing media")
if let latestContent = try? await notificationContentBuilder.content(for: itemProxy, mediaProvider: userSession.mediaProvider) {
// Processing finished, hopefully with some media
modifiedContent = latestContent
} else {
MXLog.error("\(tag) Failed processing notification media")
}
// We still notify, but without the media attachment if it fails to load
return notify(unreadCount: unreadCount)
} catch {
MXLog.error("NSE run error: \(error)")
MXLog.error("Failed processing with error: \(error)")
return discard(unreadCount: unreadCount)
}
}
private func notify(unreadCount: Int?) {
MXLog.info("\(tag) notify")
guard let modifiedContent else {
MXLog.info("\(tag) notify: no modified content")
MXLog.error("\(tag) Notification modified content invalid")
return discard(unreadCount: unreadCount)
}
MXLog.info("\(tag) Displaying notification")
if let unreadCount {
modifiedContent.badge = NSNumber(value: unreadCount)
}
@@ -184,7 +164,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
}
private func discard(unreadCount: Int?) {
MXLog.info("\(tag) discard")
MXLog.info("\(tag) Discarding notification")
let content = UNMutableNotificationContent()
@@ -226,7 +206,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
}
case .roomRedaction(let redactedEventID, _):
guard let redactedEventID else {
MXLog.error("Unable to handle redact notification due to missing event ID.")
MXLog.error("Unable to handle redact notification due to missing event ID")
return .processedShouldDiscard
}

View File

@@ -19,7 +19,11 @@ final class NSEUserSession {
networkMonitor: nil)
private let delegateHandle: TaskHandle?
init(credentials: KeychainCredentials, clientSessionDelegate: ClientSessionDelegate, appHooks: AppHooks, appSettings: CommonSettingsProtocol) async throws {
init(credentials: KeychainCredentials,
roomID: String,
clientSessionDelegate: ClientSessionDelegate,
appHooks: AppHooks,
appSettings: CommonSettingsProtocol) async throws {
sessionDirectories = credentials.restorationToken.sessionDirectories
userID = credentials.userID
@@ -34,8 +38,7 @@ final class NSEUserSession {
slidingSync: .restored,
sessionDelegate: clientSessionDelegate,
appHooks: appHooks,
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
eventCacheEnabled: appSettings.eventCacheEnabled)
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode)
.sessionPaths(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
.username(username: credentials.userID)
@@ -44,7 +47,9 @@ final class NSEUserSession {
baseClient = try await clientBuilder.build()
delegateHandle = baseClient.setDelegate(delegate: ClientDelegateWrapper())
try await baseClient.restoreSession(session: credentials.restorationToken.session)
try await baseClient.restoreSessionWith(session: credentials.restorationToken.session,
roomLoadSettings: .one(roomId: roomID))
notificationClient = try await baseClient.notificationClient(processSetup: .multipleProcesses)
}