Kotlin 23:文件读写失败后旧任务还在吗
任务输入之前只存在于 双模块工程的内存中。现在要从文件读入任务行,再写一份文本文件:读到文件末尾、提前遇到结束标记、读取过程中抛异常,三条路径会不会把 Reader 留着?已经有一份旧文件时,新文件只写了一半便失败,旧内容还能否读到?
这里使用冻结的 Kotlin/JVM 2.2.21、JDK 21、Gradle 8.14,输入只包含合成任务 ID 与文本,不处理用户传来的文件路径或外部命令。Kotlin stdlib use API提供带关闭的作用域;JDK 21 Files、Process 和 ProcessBuilder是文件及子进程边界的依据。下文的关闭次数、退出码与文件内容来自本次运行,不是文档对所有文件系统的承诺。
从 Reader 提前返回时谁来关流
文件格式有意保持简单:每行是 id<TAB>text,整行 #stop 表示后续输入不再导入。readTaskRows(reader: Reader) 先调用 reader.buffered().use { ... };遇到 EOF 返回行列表,遇到 #stop 用 return@use 早退,缺字段或空值则抛 IllegalArgumentException。返回的 TaskFileImport 记录列表和是否提前停止;它是内存里的解析结果,不是已经提交的任务。
TaskFilePortTest 使用底层可计数 Reader 检查 EOF 与 #stop,两条路径各关闭一次。第三条使用先返回一行、随后抛出同一个合成 IOException 的 Reader;断言异常对象未被换掉,底层也只关闭一次。这不是“看见 use 就假设成功”,而是分别控制输入位置,再在底层统计 close。若将错误行交给解析器,use 同样会在校验失败时关闭;本篇保存的定向故障实验是读取时的 IOException,不能拿它替代所有编码和设备错误的测试。
真实文件的正例使用 UTF-8 写出 task-1 / alpha、task-2 / 中文 两行,再从临时文件导入相同的两个 TaskFileRow。测试在导入完成以后才逐行调用已有 submitValidated,确认 task-2 的文本存入 InMemoryTaskStore。解析成功本身不会隐式提交任务;如果将来要保证多个记录同时提交或同时回滚,还要另建事务契约。这里的 TaskFileRow 暴露 List 只读接口也不等于元素或所有调用路径天然不可变,不能覆盖 06 的别名边界。
为什么直接覆盖原文件会混淆失败与旧内容
导出使用同一目标目录的临时文件。Files.newBufferedWriter 在 use 中写入任务行并关闭;只有写入步骤返回,才调用 Files.move(staged, target, ATOMIC_MOVE, REPLACE_EXISTING),最后尝试清理暂存文件。没有将原文件先截断,也没有把 writer 的 close 推迟到移动之后。
| 本次分支 | 实际观察 | 还不能推出 |
|---|---|---|
| 本地 UTF-8 正常导出并导入 | 两行完整读回,再显式提交到内存 Store | 进程重启后业务任务会自动恢复 |
| 暂存 writer 部分写入后注入 IOException | 原始异常透出;writer 已关闭;旧目标保持 task-1<TAB>original,暂存文件被删 |
任意时刻断电后文件一定完好 |
要求 ATOMIC_MOVE |
本次临时文件所在本地文件系统的成功路径可运行 | 所有 provider 均支持,或移动异常必然已回滚 |
失败实验先在目标路径放入旧文本,注入的回调向暂存 writer 写 task-1<TAB>partial 后抛异常;之后尝试再向这个 writer 写内容得到 IOException,确认其关闭;读取目标仍是旧字节,目录只剩目标文件。断言检查的是写入尚未完成时不会触及目标。Files.move 还可能因不支持原子移动而失败;不同 provider 对已存在目标的处理及崩溃持久性不能由一次 /tmp 的结果推广。本篇没有 fsync 或断电恢复实验,不能写成“具备持久事务”。文件的 staged write、移动成功与任务在 TaskStore 中生效是不同的完成点。
子进程退出能证明什么
为了让进程边界也有可运行证据,probeJdkProcess 只启动当前 java.home/bin/java,参数固定为 -version 或一个固定的无效选项,不接受外部任意命令。它合并 stderr、关闭 stdin、在有界等待后读取这个已知短输出并记录退出码。真实 JDK 21 子进程的 -version 路径退出 0,无效选项路径退出 1,后者第一行是 Unrecognized option: --kotlin-task-invalid-switch。完整输出在 verification/23/TaskProcessProbeTest.txt 的 JUnit XML system-out,不是伪造的示意字符串。
这段探针不能照搬为“运行任意任务命令”的通用进程控制器:若子进程持续产生大量输出,在等待读取前可能塞满管道;若超时或调用线程被中断,还需针对真实进程树细化关闭策略。固定 JDK 短输出的 waitFor 返回,只证明那个子进程已经结束,既不证明写文件成功,也不证明外部副作用生效,更不是后台任务的业务 Succeeded。这些生命周期以后由 状态转移和协程任务树分别约束。
bash examples/kotlin-language/verification/23.sh 实际运行 00–23 的二十五组 JUnit,各 2/2;旧 CLI submit alpha / submit 分别退出 0/1。最初将无限 while 放在 use lambda 最末,编译器认为 lambda 返回 Unit,build-initial.txt 保存这个退出 1;显式在 EOF 后构造 TaskFileImport 才通过。修改后的命令、运行环境、文件系统类型、进程输出和断言见 writing-plans/kotlin/verification/23/。旧 CLI 尚未接入文件端口与进程探针,现有 run 命令仍只接收合成文本,不会悄悄执行外部程序。
同名素材提供 完整累计双模块工程、Wrapper、RUN/VERSIONS 与原始证据(SHA-256 校验)。离线解包复跑需要 JDK 21 和缓存好的依赖,页面生成还需博客的 Node 22 环境;素材不是线上持久化服务。
练习:在 #stop 分支提早 return@use,底层 Reader 是否因没走到 EOF 而漏关?不会,本次可计数 Reader 的关闭次数为 1。换成“先对目标文件 writeString,写一半就抛异常”,能直接拿本篇的旧文件保留结果背书吗?不能,本篇只在暂存文件上写失败;直接覆盖原路径不经过该保护。最后,看到子进程退出 0,能把任务状态直接记为 Succeeded 吗?也不能,还缺任务语义、存储提交和外部效果各自的验收。
[PATTERN] 有资源参与时,分开核查何时取得、哪条退出路径关闭、哪一步写入结果对外可见。用注入失败和前后内容对照测试具体边界,别用一个“命令成功”覆盖 Reader 关闭、文件替换和业务任务完成。下一篇才引入 suspend,继续区分挂起和阻塞;本篇没有协程调度或取消证据。






