Commit f5230d1
fix(core): commit file_state for a create that skipped post-upload so the next scan updates instead of re-creating (#146)
## Mechanism (#144)
When a plain first-CREATE upload's local file changes during the
hash->upload->post-check window, the executor returned `SkipPostUpload`
and kept the pending op with **no** `file_state` row, relying on the
startup-gated reconcile pass to adopt the just-created object by
op-uuid. Mid-run nothing adopts it, so the next scan saw a row-less path
and planned a **second** create - duplicating live objects until the
next app restart. The chaos `frequent-edits` gate saw ~20 duplicate
`hot.txt` objects under a 3ms injected store delay (found during #141's
CI run; pre-existing in v2.0.1, not introduced by #141).
## Fix - symmetry with reconcile's adopt-requeue arm
`upload_and_commit`'s post-upload identity recheck now routes through
`settle_post_upload_change`. For a **plain create** (no pre-existing
row, not a versioned change) it durably commits the *same* force-rescan
`file_state` row that reconcile's `adopt_reconciled` requeue arm would
eventually write:
- `drive_file_id` = the just-created object,
- `mtime_ns = REQUEUE_FORCE_RESCAN_MTIME_NS` (`i64::MIN`) so the next
FastPath scan always re-emits the path,
- the uploaded (now-stale) blake3, the on-Drive md5/size, status
`Pending`,
via `commit_create_result`, which upserts the row **and** deletes the op
in one transaction. The next scan re-uploads as an **UPDATE** against
the same object: exactly one live object per path, no restart required.
### Design note
The issue sketched "thread the created id through the error path". The
id is already in scope at the detection point, so the helper commits in
place and returns `Ok(Skipped)` - a smaller seam into the hardened
`UploadError` enum with an identical durable outcome.
## Invariants preserved
- **Atomic durable commit**: row upsert + op delete are one transaction
(`commit_create_result`) - no split write.
- **Crash-safety fallback**: a crash *before* the commit leaves the op
(with its op-uuid) and no row, so the pre-#144 orphan+reconcile adoption
still runs on the next boot (covered by
`crash_mid_upload_adopts_orphan_without_duplicate`).
- **Reconcile idempotency**: the commit deletes the op atomically with
the row, so reconcile never finds this op again - no double-adopt
(verified vacuous + covered by a restart test).
- **UPDATE and versioned-create** keep the pre-#144 `SkipPostUpload`
path (their existing row already points at an object; the next scan
updates/versions against it, never re-creating the path).
- **Encryption**: the created id + ciphertext `encrypted_remote_path`
flow into the row identically to a plaintext source.
- **DeferToReconcile** (ambiguous create) is untouched - it has no known
id to point at, so it stays reconcile-driven (comment updated to explain
the distinction).
## Tests
- `create_changed_after_upload_commits_force_rescan_row_no_duplicate` -
deterministic race, then the NEXT scan in the same process UPDATEs the
same id (exactly one live object).
- `create_skip_post_upload_row_survives_reconcile_no_double_adopt` -
restart mid-window: committed row + reconcile pass stays one object.
- `update_changed_after_upload_keeps_existing_id_no_duplicate` - update
carve-out unchanged.
- `create_changed_after_upload_encrypted_commits_force_rescan_row` -
encrypted path.
- `frequent_edits_slow_store_holds_one_object_144` - chaos
frequent-edits with a 3ms injected store delay holds exactly one object.
Full `driven-core` lib suite (336) + `e2e_fake` acceptance suite (24) +
clippy green.
Closes #144
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 62d543a commit f5230d1
2 files changed
Lines changed: 551 additions & 67 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1716 | 1716 | | |
1717 | 1717 | | |
1718 | 1718 | | |
| 1719 | + | |
| 1720 | + | |
| 1721 | + | |
| 1722 | + | |
| 1723 | + | |
| 1724 | + | |
| 1725 | + | |
| 1726 | + | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
| 1730 | + | |
| 1731 | + | |
| 1732 | + | |
| 1733 | + | |
| 1734 | + | |
| 1735 | + | |
| 1736 | + | |
| 1737 | + | |
| 1738 | + | |
| 1739 | + | |
| 1740 | + | |
| 1741 | + | |
| 1742 | + | |
| 1743 | + | |
| 1744 | + | |
| 1745 | + | |
| 1746 | + | |
| 1747 | + | |
| 1748 | + | |
| 1749 | + | |
| 1750 | + | |
| 1751 | + | |
| 1752 | + | |
| 1753 | + | |
| 1754 | + | |
| 1755 | + | |
| 1756 | + | |
| 1757 | + | |
| 1758 | + | |
| 1759 | + | |
| 1760 | + | |
| 1761 | + | |
| 1762 | + | |
| 1763 | + | |
| 1764 | + | |
| 1765 | + | |
| 1766 | + | |
| 1767 | + | |
| 1768 | + | |
| 1769 | + | |
| 1770 | + | |
| 1771 | + | |
| 1772 | + | |
| 1773 | + | |
| 1774 | + | |
| 1775 | + | |
| 1776 | + | |
| 1777 | + | |
| 1778 | + | |
| 1779 | + | |
| 1780 | + | |
| 1781 | + | |
| 1782 | + | |
| 1783 | + | |
1719 | 1784 | | |
1720 | 1785 | | |
1721 | 1786 | | |
| |||
0 commit comments