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>
This commit is contained in:
shenlong
2026-05-12 11:08:17 +07:00
committed by GitHub
parent c6b3127b35
commit 8f4b0fce49
6 changed files with 50 additions and 34 deletions
@@ -416,12 +416,12 @@ class BackgroundWorkerFlutterApi(private val binaryMessenger: BinaryMessenger, p
} }
} }
} }
fun onAndroidUpload(callback: (Result<Unit>) -> Unit) fun onAndroidUpload(maxMinutesArg: Long?, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(null) { channel.send(listOf(maxMinutesArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -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. * This method acts as a bridge between the native Android background task system and Flutter.
*/ */
override fun onInitialized() { override fun onInitialized() {
flutterApi?.onAndroidUpload { handleHostResult(it) } flutterApi?.onAndroidUpload(maxMinutesArg = 20) { handleHostResult(it) }
} }
// TODO: Move this to a separate NotificationManager class // TODO: Move this to a separate NotificationManager class
+3 -3
View File
@@ -348,7 +348,7 @@ class BackgroundWorkerBgHostApiSetup {
/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift. /// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
protocol BackgroundWorkerFlutterApiProtocol { protocol BackgroundWorkerFlutterApiProtocol {
func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) func onIosUpload(isRefresh isRefreshArg: Bool, maxSeconds maxSecondsArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void)
func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void) func cancel(completion: @escaping (Result<Void, PigeonError>) -> Void)
} }
class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol { class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
@@ -379,10 +379,10 @@ class BackgroundWorkerFlutterApi: BackgroundWorkerFlutterApiProtocol {
} }
} }
} }
func onAndroidUpload(completion: @escaping (Result<Void, PigeonError>) -> Void) { func onAndroidUpload(maxMinutes maxMinutesArg: Int64?, completion: @escaping (Result<Void, PigeonError>) -> Void) {
let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)" let channelName: String = "dev.flutter.pigeon.immich_mobile.BackgroundWorkerFlutterApi.onAndroidUpload\(messageChannelSuffix)"
let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec) 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 { guard let listResponse = response as? [Any?] else {
completion(.failure(createConnectionError(withChannelName: channelName))) completion(.failure(createConnectionError(withChannelName: channelName)))
return return
@@ -105,46 +105,58 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
} }
@override @override
Future<void> onAndroidUpload() async { Future<void> onAndroidUpload(int? maxMinutes) async {
_logger.info('Android background processing started'); final hashTimeout = Duration(minutes: _isBackupEnabled ? 3 : 6);
final sw = Stopwatch()..start(); final backupTimeout = maxMinutes != null ? Duration(minutes: maxMinutes - 1) : null;
try { return _backgroundLoop(
if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) { hashTimeout: hashTimeout,
_logger.warning("Remote sync did not complete successfully, skipping backup"); backupTimeout: backupTimeout,
return; debugLabel: 'Android background upload',
} );
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();
}
} }
@override @override
Future<void> onIosUpload(bool isRefresh, int? maxSeconds) async { Future<void> 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<void> _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(); final sw = Stopwatch()..start();
try { try {
final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); if (!await _syncAssets(hashTimeout: hashTimeout)) {
if (!await _syncAssets(hashTimeout: timeout)) {
_logger.warning("Remote sync did not complete successfully, skipping backup"); _logger.warning("Remote sync did not complete successfully, skipping backup");
return; return;
} }
final backupFuture = _handleBackup(); final backupFuture = _handleBackup();
if (maxSeconds != null) { Timer? cancelTimer;
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {}); if (backupTimeout != null) {
} else { cancelTimer = Timer(backupTimeout, () {
if (!_cancellationToken.isCompleted) {
_logger.warning("$debugLabel timed out after ${backupTimeout.inMinutes}m, cancelling backup");
_cancellationToken.complete();
}
});
}
try {
await backupFuture; await backupFuture;
} finally {
cancelTimer?.cancel();
} }
} catch (error, stack) { } catch (error, stack) {
_logger.severe("Failed to complete iOS background upload", error, stack); _logger.severe("Failed to complete $debugLabel", error, stack);
} finally { } finally {
sw.stop(); sw.stop();
_logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); _logger.info("$debugLabel completed in ${sw.elapsed.inSeconds}s");
await _cleanup(); await _cleanup();
} }
} }
@@ -177,7 +189,9 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final nativeSyncApi = _ref?.read(nativeSyncApiProvider); final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
_logger.info("Cleaning up background worker"); _logger.info("Cleaning up background worker");
_cancellationToken.complete(); if (!_cancellationToken.isCompleted) {
_cancellationToken.complete();
}
final cleanupFutures = [ final cleanupFutures = [
nativeSyncApi?.cancelHashing(), nativeSyncApi?.cancelHashing(),
workerManagerPatch.dispose().catchError((_) async { workerManagerPatch.dispose().catchError((_) async {
+4 -2
View File
@@ -277,7 +277,7 @@ abstract class BackgroundWorkerFlutterApi {
Future<void> onIosUpload(bool isRefresh, int? maxSeconds); Future<void> onIosUpload(bool isRefresh, int? maxSeconds);
Future<void> onAndroidUpload(); Future<void> onAndroidUpload(int? maxMinutes);
Future<void> cancel(); Future<void> cancel();
@@ -323,8 +323,10 @@ abstract class BackgroundWorkerFlutterApi {
pigeonVar_channel.setMessageHandler(null); pigeonVar_channel.setMessageHandler(null);
} else { } else {
pigeonVar_channel.setMessageHandler((Object? message) async { pigeonVar_channel.setMessageHandler((Object? message) async {
final List<Object?> args = message! as List<Object?>;
final int? arg_maxMinutes = args[0] as int?;
try { try {
await api.onAndroidUpload(); await api.onAndroidUpload(arg_maxMinutes);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
+1 -1
View File
@@ -47,7 +47,7 @@ abstract class BackgroundWorkerFlutterApi {
// Android Only: Called when the Android background upload is triggered // Android Only: Called when the Android background upload is triggered
@async @async
void onAndroidUpload(); void onAndroidUpload(int? maxMinutes);
@async @async
void cancel(); void cancel();