Lab 图片后台批处理设计
接口、配置和示例见 Lab README。该样例展示上传校验、媒体处理和私有结果存储的组合,不为工具模块新增 Host、持久化调度器或数据库模型。
依赖和接入
ImageBatchesController 使用现有 FileUploadValidator 校验扩展名、声明 MIME 和签名;请求及 multipart 上限沿用 SampleUploadOptions 的 25 MiB。通过校验后,ImageBatchJobs 再按实际读取字节数检查单文件及整批预算,用 LocalFileStorage 的新键和原子写入保存私有输入。完整图片解码留在后台 Media.ImageProcessor 内完成,签名校验不能替代解码。
宿主只在允许 Lab 入口时注册队列,Controller 仍由 LabOnly 标记隔离,并要求已登录身份。Enabled 默认关闭。队列不持有请求流,接收完成前已复制所有输入;Controller 负责释放自己打开的上传流。所有业务操作都用 NameIdentifier 的精确字符串匹配 owner,客户端不能提交 owner 或磁盘路径。
状态和容量
状态按 Preparing → Queued → Running → Succeeded / Failed / Cancelled 转移。Preparing 已占用全局记录名额,避免并发上传绕过容量限制。所有集合和状态访问由一个锁协调;有界 Channel 只传递内部 Job,单消费者顺序处理,不以 DropWrite 静默丢任务。
每个文件结果先在受限内存流编码,再用 LocalFileStorage 发布到该任务私有目录。已完成数按文件递增;任一步失败或取消时,已有文件仍保留在私有目录,状态接口不暴露它们,下载也只接受整个任务 Succeeded。成功状态和取消请求在同一锁下确定,取消先到达时不能宣布成功;成功后取消不撤销结果。
完成任务仍占名额,必须显式删除才能释放。删除也清理输入和不可下载的中间结果。队列中已取消任务可能暂时保留一个 Channel 项,直到消费者取出;因此即使有记录名额,通道容量也可能短暂不足,提交仍明确失败。预算限制编码长度和记录数量,不是操作系统内存或磁盘配额。
取消、重试和读取
排队任务取消后直接进入 Cancelled,消费者跳过。运行任务取消令牌与宿主停止令牌关联,当前媒体步骤退出后才转为终态,不能提前删除仍在使用的目录。同步 Skia 解码和编码不会被强制抢占,宿主停止期限之外的强制进程终止仍可能留下目录。
重试只接受 Failed/Cancelled 的保留任务,复制原有输入并创建新 Id、新目录和独立取消令牌。复制期间增加读取租约,原任务不能删除;无论复制、提交还是流释放是否失败,租约必须释放。旧任务保持原状态,不会被覆盖或解释为同一次执行成功。
下载在同一次状态快照中取得结果键和 ContentType,随后在读取租约内打开流。文件已打开后释放租约;LocalFileStorage 的 FileShare.Delete 允许调用方继续读取已经打开的内容,终态目录可以随后删除。返回的流由 ASP.NET FileStreamResult 接管释放,不暴露存储键或物理路径。
清理与失败
提交失败只删除本次随机任务目录,并从记录集合移除;源请求流由 Controller 管理。目录删除前核对完整目标的直接父目录与配置存储根一致,配置根必须由可信宿主管理,不防御有权限修改该目录的本机攻击者。
处理异常保存在服务日志中,状态只返回 processing_failed,不将完整异常或物理路径暴露给其他用户。输入清理会尝试释放所有流;主操作和清理同时失败时聚合保留全部原因。重试复制租约在流清理失败时仍释放。容量或字节限制映射为 HTTP 响应时,也保留该原因为可能的清理异常上下文。
删除失败时保留任务记录以便重试删除,不改 ACL、不删除配置根或其他任务目录。没有自动保留期清理,重启后失去记录的目录需要宿主处置。该内存状态机不声称断电恢复、跨进程幂等或分布式取消;这些需求应使用仓库已有持久化与消息组件。
验证
测试通过可控的 IImageBatchOperation 暂停点验证排队、进行中取消、部分进度、失败结果隐藏、所属用户隔离、重试和删除的约束,并使用实际小 PNG 经过 ImageProcessor 生成可解码结果。预算矩阵检查单份和整批实际长度的等值/超一边界,以及同一结果流的 Write、WriteByte 和 SetLength 路径。下载回归覆盖状态从 Running 变成 Succeeded 的交界,确保文件及 MIME 来自同一份完成结果。