Repository navigation
fix(rules): roll back a rule delivery that fails mid-write - #635
Conversation
|
@claude /review |
|
Claude finished @theCodeDrift's task in 4m 24s —— View job Review completeReview mode: incremental — read 0 prior review thread(s) before reviewing.
Summary: I traced Nothing new to report as a defect. Two very minor, non-blocking observations, neither of which I think needs a change:
The implementation matches what the |
Thanks. Leaving both as they are. (1) To fail the into-place rename of a rule that is replacing an older version, the move-aside rename has to succeed in the same directory just before it. A real-filesystem failure cannot separate the two (they need the same permission on the same parent), and doing it through a mocked — AI Coding Agent |
ddf6b2e to
489842f
Compare
2a94840 to
1e49588
Compare
Stage every rule of a request under .taskless/ and move each into place with rename, moving any older version aside first. An IO failure while staging or moving undoes the moves already made, so .taskless/rules/ is left as it was and the command fails with RULE_GENERATION_FAILED. Refs #623
1e49588 to
efecc58
Compare
Stack (root → tip):
Problem
#632 makes
rule create/rule improvecheck every rule a request produced before writing the first. It leaves out one case, and says so in thedeliverRevisionsdoc comment and thecli-generated-rule-deliveryspec: an IO failure part-way through the writes (a full disk,EACCESon the second rule). In that case rules 1..N-1 stay on disk while the command fails and reports no rules. Under--jsonthe failure was alsoINTERNAL_ERROR.What changed
writePreparedRulesinrules/files.ts, used bydeliverRevisions:.taskless/.tmp-delivery-*/(mkdtempinside.taskless/, so it is on the same filesystem as.taskless/rules/andrenameis never cross-device). It goes through the samewriteDeliveredFileSetas before, with the staging dir standing in for the project root. A full disk or unwritable file stops here, before anything under.taskless/rules/has moved.renamed aside into the staging dir, then the staged directory isrenamed onto the rule path. If a rename fails, the moves already made are undone in reverse order: the new directory is removed, the old one renamed back, and any parent directories the commit created arermdired. Then the staging dir is removed.CLIErrorRULE_GENERATION_FAILED, naming the rule and the errno, and saying.taskless/rules/is as it was.assertRuleDirectoryIsNotALink, now exported fromdeliver.ts). That check now runs before anything is staged, rather than part-way through the writes.writePreparedRuledid.rule restore/rule rollback(writeServedRule, one rule each) are unchanged. TheirPurgeIncompleteErrorhandling inrecover.tsdepends on the in-place write, and moving them over is a separate decision.Residual window (not atomic)
Each
renameis atomic on one filesystem, but the sequence of renames is not. If the process is killed outright (SIGKILL, power loss) between two renames, no rollback runs..taskless/rules/can then hold some rules at their new version and the rest at their old one, and one rule can be absent for the instant between its two renames. Every replaced version is still on disk under.taskless/.tmp-delivery-*, but nothing restores it automatically. Nothing is fsynced, so after a power loss the filesystem decides the order the renames reached the disk. All of this is written down in thewritePreparedRulesanddeliverRevisionsdoc comments and in the spec requirement.Testing
rule-create-multi-rule.test.tshas two new cases, both driven through the realrule create --json. In each, the second rule is a Vale rule whose engine directory.taskless/rules/vale/is read-only (0o555), so moving it into place fails with a realEACCES:RULE_GENERATION_FAILED, that.taskless/rules/is unchanged, and that no.tmp-delivery-*is left behind. They are skipped as root or on Windows, where a read-only directory does not refuse the write.expected 'INTERNAL_ERROR' to be 'RULE_GENERATION_FAILED').pnpm typecheck,pnpm lint,pnpm --filter @taskless/cli test(2972 passed) all pass.patch).git merge-tree --write-tree origin/refactor/626-explicit-schema-policy HEAD: clean, no conflicts with refactor(schema): explicit scaffold state and per-command migrate/refuse policy #634.Merge order
Stacked on #632; land after it.
Refs #623
Built on top of #632
A rule-generation request can produce several rules.
deliverRevisionsverified every rule's signatures up front, but the other pre-write checks (rule id, known engine, andassessDelivery: unsafe paths, case-folded duplicates, incomplete sets) ran inside the write loop. So when rule 2 was refused, rule 1 was already in.taskless/rules/while the command failed and reported no rules. Under--jsonthose refusals also surfaced asINTERNAL_ERROR.What changed
writeServedRuleis split intoprepareServedRule(every check, no filesystem writes) andwritePreparedRule(ensureTasklessDirectory, the write, the missing-fixtures and id-collision warnings).writeServedRulestays as prepare + write, sorule restore/rollbackbehave exactly as before, error types included.deliverRevisionsnow verifies and prepares every rule before writing the first. A prepare refusal that is a plainErroris wrapped asRULE_GENERATION_FAILEDthere (not at the source, so restore's reporting is unchanged);RULE_UNSUPPORTEDkeeps its code. Per-rule notices are held and released in request order, so output ordering is unchanged.deliverRevisionsnow states what it guarantees, and names the remaining gap: an IO failure part-way through the writes (out of scope, per the issue).cli-generated-rule-delivery, archived in this PR (21 scenarios before, 23 after).Testing
packages/cli/test/rule-create-multi-rule.test.tsdrives the realrule create --jsonagainst the v2 stub with two rules: second rule with an unknown engine, an unsafe../path, or an incomplete set. Each asserts the error code and that.taskless/rules/(pre-seeded with an older version of rule 1) is byte-identical afterwards. A fourth case checks both rules are written when both pass. The three refusal cases fail against the previousgenerate.ts.pnpm typecheck,pnpm lint,pnpm --filter @taskless/cli test(2970 passed).Fixes #623