Refactor WorkerImpl to integrate logging, enhance testing with mocks, improve timeout and error handling, and add worker ID generation.

This commit is contained in:
2025-09-22 20:04:37 +02:00
parent 64343bbb80
commit 3a4b360f42
4 changed files with 458 additions and 138 deletions

View File

@@ -1,20 +1,20 @@
import 'dart:async';
import 'dart:isolate';
import 'package:flutter/services.dart'
show ServicesBinding, RootIsolateToken, BackgroundIsolateBinaryMessenger;
import 'package:fluttery/fluttery.dart';
import 'package:fluttery/logger.dart';
import 'package:fluttery/worker.dart';
class WorkerImpl implements Worker {
final Logger _logger;
WorkerImpl({
this.defaultTimeout,
this.maxHistory = 100,
RootIsolateToken? rootToken, // optional for tests
}) : _rootToken =
rootToken ?? ServicesBinding.rootIsolateToken; // <— static getter
RootIsolateToken? rootToken,
}) : _rootToken = rootToken ?? ServicesBinding.rootIsolateToken,
_logger = App.service<Logger>();
final Duration? defaultTimeout;
final int maxHistory;
@@ -22,7 +22,6 @@ class WorkerImpl implements Worker {
// Captured from the root isolate (may be null in some test envs)
final RootIsolateToken? _rootToken;
int _seq = 0;
final Map<String, WorkerInfo> _active = {};
final List<WorkerInfo> _history = [];
@@ -33,62 +32,110 @@ class WorkerImpl implements Worker {
void Function()? preTask,
Duration? timeout,
}) {
final id = (++_seq).toString().padLeft(6, '0');
final id = _generateWorkerId();
final started = DateTime.now();
_logger.debug('Spawning worker "$debugName" ($id)');
_registerActiveWorker(id, debugName, started);
final future = _executeWithTimeout(
id,
debugName,
task,
preTask,
timeout ?? defaultTimeout,
);
_attachCompletionHandlers(id, debugName, future);
return future;
}
String _generateWorkerId() {
return 'iso-${DateTime.now().millisecondsSinceEpoch}';
}
void _registerActiveWorker(String id, String debugName, DateTime started) {
_active[id] = WorkerInfo(
id: id,
name: debugName,
startedAt: started,
status: WorkerStatus.running,
);
_logger.debug('Registered worker "$debugName" ($id)');
}
Future<T> inner() async {
final token = _rootToken; // captured into closure
Future<T> _executeWithTimeout<T>(
String id,
String debugName,
FutureOr<T> Function() task,
void Function()? preTask,
Duration? timeout,
) {
_logger.debug(
'Executing worker "$debugName" ($id) with timeout: ${timeout?.inSeconds ?? "none"} seconds',
);
final future = _executeInIsolate(debugName, task, preTask);
return Isolate.run<T>(() async {
// Initialize platform channels for this background isolate.
if (token != null) {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
}
return timeout == null ? future : future.timeout(timeout);
}
// Now it's safe to touch plugins (e.g., SharedPreferences).
App.registerDefaultServices();
Future<T> _executeInIsolate<T>(
String debugName,
FutureOr<T> Function() task,
void Function()? preTask,
) {
final token = _rootToken; // captured into closure
_logger.debug('Starting isolate for worker "$debugName"');
preTask?.call();
return Isolate.run<T>(() async {
// Initialize platform channels for this background isolate.
if (token != null) {
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
}
// Now it's safe to touch plugins (e.g., SharedPreferences).
App.registerDefaultServices();
if (preTask != null) {
_logger.debug('Executing pre-task for worker "$debugName"');
preTask();
}
return await Future.sync(task);
}, debugName: debugName);
}
return await Future.sync(task);
}, debugName: debugName);
}
final effectiveTimeout = timeout ?? defaultTimeout;
final fut = effectiveTimeout == null
? inner()
: inner().timeout(effectiveTimeout);
fut
void _attachCompletionHandlers<T>(
String id,
String debugName,
Future<T> future,
) {
future
.then((_) {
_logger.debug('Worker "$debugName" ($id) completed successfully');
_finish(id, status: WorkerStatus.completed);
})
.catchError((e, st) {
_finish(
id,
status: e is TimeoutException
? WorkerStatus.timedOut
: WorkerStatus.failed,
error: e,
stack: st,
);
// Best-effort logging
try {
App.service<Logger>().error(
'Worker job "$debugName" ($id) failed: $e',
st,
);
} catch (_) {}
});
final status = e is TimeoutException
? WorkerStatus.timedOut
: WorkerStatus.failed;
return fut;
_finish(id, status: status, error: e, stack: st);
_logWorkerError(debugName, id, e, st);
});
}
void _logWorkerError(
String debugName,
String id,
Object error,
StackTrace stackTrace,
) {
// Best-effort logging
try {
App.service<Logger>().error(
'Worker job "$debugName" ($id) failed: $error',
stackTrace,
);
} catch (_) {}
}
void _finish(
@@ -99,7 +146,6 @@ class WorkerImpl implements Worker {
}) {
final prev = _active.remove(id);
final endedAt = DateTime.now();
final info = WorkerInfo(
id: prev?.id ?? id,
name: prev?.name ?? 'unknown',
@@ -109,11 +155,11 @@ class WorkerImpl implements Worker {
error: error,
stackTrace: stack,
);
_history.insert(0, info);
if (_history.length > maxHistory) {
_history.removeRange(maxHistory, _history.length);
}
_logger.debug('Worker "${prev?.name}" ($id) finished with status: $status');
}
@override
@@ -137,6 +183,7 @@ class WorkerImpl implements Worker {
@override
void purge({Duration maxAge = const Duration(minutes: 30)}) {
final cutoff = DateTime.now().subtract(maxAge);
_logger.debug('Purging workers older than $maxAge');
_history.removeWhere((w) => (w.endedAt ?? w.startedAt).isBefore(cutoff));
}
}