Refactor WorkerImpl to integrate logging, enhance testing with mocks, improve timeout and error handling, and add worker ID generation.
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user