feat(app): bounded-parallel prepare (default 3) replaces serial queue

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-28 13:43:07 +08:00
parent dc5ae4a24a
commit b79edf447b
2 changed files with 114 additions and 16 deletions
+48 -11
View File
@@ -7,8 +7,8 @@
/// [UploadPreparer.retry]),lastError 携带客户端伪码([PrepErrorCodes])。
///
/// 设计取舍(对齐计划 Task 6 评审口径):
/// - **串行执行**:文件复制是最容易造成 IO 风暴的环节(连拍场景一次
/// 落盘多张),队列化逐张处理;传输并发由 UploadEngine 另行控制;
/// - **有界并发执行(默认 3)**:文件复制仍是 IO 最重环节,上限钳位
/// 防连拍(一次落盘多张)IO 风暴;传输并发由 UploadEngine 另行控制;
/// - **磁盘空间不显式预检**:引入 disk_space 插件成本(新依赖 + 平台
/// 误差)高于收益——复制失败本身即失败信号(ENOSPC 走 copy 失败分支
/// → preparationFailed,用户可见可重试)。此为有意裁剪,登记为计划
@@ -19,6 +19,7 @@
library;
import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:math';
@@ -45,15 +46,21 @@ abstract final class PrepErrorCodes {
static const String copyFailed = 'PREP_COPY_FAILED';
}
/// 准备阶段有界并发上限(dart-define PREPARE_CONCURRENCY 覆盖;1=串行)。
const int kMaxConcurrentPrepares =
int.fromEnvironment('PREPARE_CONCURRENCY', defaultValue: 3);
/// 上传准备器:入队唯一入口(队列 UI/连拍页调用;引擎消费产出)。
///
/// 所有操作串行(连拍入队逐张复制,避免 IO 风暴,见库级注释)。
/// 所有操作有界并发执行(默认 3):文件复制仍是 IO 最重环节,上限钳位
/// 防连拍风暴;传输并发由 UploadEngine 另行控制(见库级注释)。
class UploadPreparer {
UploadPreparer(
this._store, {
Future<Directory> Function()? baseDirProvider,
Random? random,
this.maxBytes = kMaxUploadBytes,
this.maxConcurrentPrepares = kMaxConcurrentPrepares,
this.onEnqueued,
}) : _baseDirProvider = baseDirProvider ?? getApplicationSupportDirectory,
_random = random ?? Random.secure();
@@ -71,8 +78,11 @@ class UploadPreparer {
/// 入队成功回调(唤醒传输引擎立即出队)。
final void Function()? onEnqueued;
/// 串行队列尾部。
Future<void> _tail = Future<void>.value();
/// 准备并发上限(运行时 <1 钳位为 1)。
final int maxConcurrentPrepares;
int get _effectivePrepareConcurrency =>
maxConcurrentPrepares < 1 ? 1 : maxConcurrentPrepares;
/// 准备一张图片并入队。
///
@@ -88,7 +98,7 @@ class UploadPreparer {
required String sourcePath,
String? filename,
}) {
return _serialize(
return _runBounded(
() => _prepare(
accountId: accountId,
albumId: albumId,
@@ -108,19 +118,46 @@ class UploadPreparer {
required String taskId,
required String sourcePath,
}) {
return _serialize(
return _runBounded(
() =>
_retry(accountId: accountId, taskId: taskId, sourcePath: sourcePath),
);
}
/// 串行化:所有准备操作首尾相接(错误吞掉以不断链,错误经返回值上抛)。
Future<T> _serialize<T>(Future<T> Function() op) {
final result = _tail.then((_) => op());
_tail = result.then<void>((_) {}, onError: (Object _) {});
/// 在途准备计数 + FIFO 等待队列(有界并发信号量)。
int _activePrepares = 0;
final Queue<Completer<void>> _prepareWaiters = Queue<Completer<void>>();
/// 有界并发执行:超额排队(FIFO);错误由调用方经返回值处理,不设全局断链。
Future<T> _runBounded<T>(Future<T> Function() op) {
final ticket = _acquirePrepareSlot();
final result = ticket.then((_) => op());
result.whenComplete(_releasePrepareSlot).ignore();
return result;
}
Future<void> _acquirePrepareSlot() {
if (_activePrepares < _effectivePrepareConcurrency) {
_activePrepares++;
return Future<void>.value();
}
final waiter = Completer<void>();
_prepareWaiters.addLast(waiter);
return waiter.future;
}
void _releasePrepareSlot() {
final waiter = _prepareWaiters.isEmpty
? null
: _prepareWaiters.removeFirst();
if (waiter == null) {
_activePrepares--;
return;
}
// 名额直接移交队首等待者(计数不变)。
waiter.complete();
}
Future<UploadTask> _prepare({
required String accountId,
required String albumId,