From 8f4b0fce49051122636d6a79e7b4bddad49da6b2 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Tue, 12 May 2026 11:08:17 +0700 Subject: [PATCH] fix: limit android background worker duration (#23566) * fix: limit each android background run to 20 mins # Conflicts: # mobile/lib/domain/services/background_worker.service.dart * review chages --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../immich/background/BackgroundWorker.g.kt | 4 +- .../immich/background/BackgroundWorker.kt | 2 +- .../Background/BackgroundWorker.g.swift | 6 +- .../services/background_worker.service.dart | 64 +++++++++++-------- .../lib/platform/background_worker_api.g.dart | 6 +- mobile/pigeon/background_worker_api.dart | 2 +- 6 files changed, 50 insertions(+), 34 deletions(-) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt index 0ae49f87f6..3fcaed34bc 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.g.kt @@ -416,12 +416,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p } } } - fun onAndroidUpload(callback: (Result) -> Unit) + fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result) -> Unit) { val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix" val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(null) { + channel.send(listOf(maxMinutesArg)) { if (it is List<*>) { if (it.size > 1) { callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt index 7dce1f6edf..716477904c 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/background/BackgroundWorker.kt @@ -107,7 +107,7 @@ class BackgroundWorker(context: Context, params: WorkerParameters) : * This method acts as a bridge between the native Android background task system and Flutter. */ override fun onInitialized() { - flutterApi?.onAndroidUpload { handleHostResult(it) } + flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) } } // TODO: Move this to a separate NotificationManager class diff --git a/mobile/ios/Runner/Background/BackgroundWorker.g.swift b/mobile/ios/Runner/Background/BackgroundWorker.g.swift index 40553441a6..bd01e953f9 100644 --- a/mobile/ios/Runner/Background/BackgroundWorker.g.swift +++ b/mobile/ios/Runner/Background/BackgroundWorker.g.swift @@ -348,7 +348,7 @@ class BackgroundWorkerBgHostApiSetup { /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. protocol BackgroundWorkerFlutterApiProtocol { func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result) -> Void) - func onAndroidUpload(completion: @escaping (Result) -> Void) + func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) func cancel(completion: @escaping (Result) -> Void) } class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { @@ -379,10 +379,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { } } } - func onAndroidUpload(completion: @escaping (Result) -> Void) { + func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result) -> Void) { let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)" let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) - channel.sendMessage(nil) { response in + channel.sendMessage([maxMinutesArg] as [Any?]) { response in guard let listResponse = response as? [Any?] else { completion(.failure(createConnectionError(withChannelName: channelName))) return diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index a2e96f2313..0c8746700c 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -105,46 +105,58 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } @override - Future onAndroidUpload() async { - _logger.info('Android background processing started'); - final sw = Stopwatch()..start(); - try { - if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) { - _logger.warning("Remote sync did not complete successfully, skipping backup"); - return; - } - await _handleBackup(); - } catch (error, stack) { - _logger.severe("Failed to complete Android background processing", error, stack); - } finally { - sw.stop(); - _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); - await _cleanup(); - } + Future onAndroidUpload(int? maxMinutes) async { + final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6); + final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null; + return _backgroundLoop( + hashTimeout: hashTimeout, + backupTimeout: backupTimeout, + debugLabel: 'Android background upload', + ); } @override Future onIosUpload(bool isRefresh, int? maxSeconds) async { - _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); + final hashTimeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); + final backupTimeout = maxSeconds != null ? Duration(seconds: maxSeconds - 1) : null; + return _backgroundLoop(hashTimeout: hashTimeout, backupTimeout: backupTimeout, debugLabel: 'iOS background upload'); + } + + Future _backgroundLoop({ + required Duration hashTimeout, + required Duration? backupTimeout, + required String debugLabel, + }) async { + _logger.info( + '$debugLabel started hashTimeout: ${hashTimeout.inSeconds}s, backupTimeout: ${backupTimeout?.inMinutes ?? '~'}m', + ); final sw = Stopwatch()..start(); try { - final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); - if (!await _syncAssets(hashTimeout: timeout)) { + if (!await _syncAssets(hashTimeout: hashTimeout)) { _logger.warning("Remote sync did not complete successfully, skipping backup"); return; } final backupFuture = _handleBackup(); - if (maxSeconds != null) { - await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); - } else { + Timer? cancelTimer; + if (backupTimeout != null) { + cancelTimer = Timer(backupTimeout, () { + if (!_cancellationToken.isCompleted) { + _logger.warning("$debugLabel timed out after ${backupTimeout.inMinutes}m, cancelling backup"); + _cancellationToken.complete(); + } + }); + } + try { await backupFuture; + } finally { + cancelTimer?.cancel(); } } catch (error, stack) { - _logger.severe("Failed to complete iOS background upload", error, stack); + _logger.severe("Failed to complete $debugLabel", error, stack); } finally { sw.stop(); - _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); + _logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s"); await _cleanup(); } } @@ -177,7 +189,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { final nativeSyncApi = _ref?.read(nativeSyncApiProvider); _logger.info("Cleaning up background worker"); - _cancellationToken.complete(); + if (!_cancellationToken.isCompleted) { + _cancellationToken.complete(); + } final cleanupFutures = [ nativeSyncApi?.cancelHashing(), workerManagerPatch.dispose().catchError((_) async { diff --git a/mobile/lib/platform/background_worker_api.g.dart b/mobile/lib/platform/background_worker_api.g.dart index 580531b0f0..34f4c41b48 100644 --- a/mobile/lib/platform/background_worker_api.g.dart +++ b/mobile/lib/platform/background_worker_api.g.dart @@ -277,7 +277,7 @@ abstract class BackgroundWorkerFlutterApi { Future onIosUpload(bool isRefresh, int? maxSeconds); - Future onAndroidUpload(); + Future onAndroidUpload(int? maxMinutes); Future cancel(); @@ -323,8 +323,10 @@ abstract class BackgroundWorkerFlutterApi { pigeonVar_channel.setMessageHandler(null); } else { pigeonVar_channel.setMessageHandler((Object? message) async { + final List args = message! as List; + final int? arg_maxMinutes = args[0] as int?; try { - await api.onAndroidUpload(); + await api.onAndroidUpload(arg_maxMinutes); return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); diff --git a/mobile/pigeon/background_worker_api.dart b/mobile/pigeon/background_worker_api.dart index a40d290199..06395fae7b 100644 --- a/mobile/pigeon/background_worker_api.dart +++ b/mobile/pigeon/background_worker_api.dart @@ -47,7 +47,7 @@ abstract class BackgroundWorkerFlutterApi { // Android Only: Called when the Android background upload is triggered @async - void onAndroidUpload(); + void onAndroidUpload(int? maxMinutes); @async void cancel();