Commit 5cb8e3a
feat(core): remote-existence audit heals files whose Drive objects vanished (#171)
## Why
Today a user deleted their backup's Drive folders and emptied the trash.
That left **4,526 `file_state` rows pointing at hard-deleted
`drive_file_id`s.**
#168 fixed half of it: a file that *later changes* plans an UPDATE, the
update 404s, and the stale id self-heals. But a file that **never
changes never plans an update** - so it would have stayed silently
un-backed-up **forever**: no error shown, restore impossible. This PR
adds the proactive pass that closes that gap, and verifies the adjacent
folder-deletion recovery path.
## Part 1: the audit
**Enumeration, not per-file GETs.** New required
`RemoteStore::list_source_object_ids`. Every object the executor creates
carries `appProperties driven.source_id`, so one paged `files.list`
returns a source's whole live footprint in ~N/1000 requests. Folders
carry only `driven.folder_marker`, so they are excluded for free. A new
`ID_ONLY_FIELDS` projection (`fields=nextPageToken,files(id)`) avoids
pulling nine fields per row and discarding eight on a large source.
`SOURCE_ID_KEY` now has **one** definition in `driven-drive`,
re-exported by the executor. Two copies that drifted would make the
audit match nothing, judge every id dead, and re-upload the whole
source.
**The heal.** Dead FILE: clear `drive_file_id` + `drive_md5` and stamp
the `REQUEUE_FORCE_RESCAN_MTIME_NS` sentinel (the exact remediation
applied by hand during the incident), so the next scan re-emits the path
in *either* scan mode and the executor re-creates it. Dead BUNDLE
object: `heal_dead_bundle` re-queues the members and drops the `bundles`
row in **one transaction** - members must be read before the row is
deleted, because the `bundle_id` FK cascades the membership rows away.
Members then re-upload individually, which is the same standalone
promotion a bundled member already gets whenever it changes.
**The safety property.** The audit infers "gone" from **absence**, so an
enumeration that cannot be completed writes **nothing** and returns
`Err`. A partial listing would name live objects as dead and churn the
entire source; retrying next cycle is free. This is also why the trait
method has **no default body** - "no live objects" is not a safe
degradation, it is that same failure arriving silently, so
`BreakerReportingStore` and every test double must delegate or error
explicitly.
**Scheduling.** Once per source per process, **plus** every deep-verify
cycle. Startup-once matters because the damage appeared overnight and
`deep_verify_interval_secs` defaults to a week. It runs **before** the
scan, so a heal completes in one cycle instead of two. A source is
marked done only on success, so a transient failure retries rather than
being skipped until restart; an audit error never fails the cycle.
**Reporting.** Capped at 20 per-file `drive.remote_file_missing` WARN
rows - the incident would have written 4,526 - plus one
`remote_audit_done` Info row whose `file_count` carries the **full**
healed count, so the cap hides nothing. A clean audit writes nothing at
all.
## Part 2: ancestor-deletion recovery - verified, and it found a real
wedge
The answer is yes, it recovers, but **not** for the reason the brief
guessed, and there was a genuine bug.
Not the mechanism: nothing in the orchestrator halts a source on
`dest_folder_missing` (grep returns no matches), so that was never the
failure mode.
The real one: an intermediate folder cached in `parent_dirs` can be dead
while a **deeper component is not cached** - a *new subdirectory* under
a deleted-but-cached parent. `ensure_folder` then 404s **inside**
`resolve_remote_target`, which maps to `UploadError::Fatal` and
**aborted the entire `execute()` for the source**. And because
`invalidate_parent_dirs` only ran *after* the target resolved, the
poisoned cache entry **survived for the process lifetime** - every later
file under that chain failed identically. A permanent wedge until
restart.
Fix: `ensure_parents` now drops the path's cached chain and re-ensures
**once** from the source root, gated on the invalidation having actually
removed something. That gate is what keeps a deleted destination
**root** on its unchanged fail-fast mass-delete guard - the root is
never in this cache, so it removes nothing, never retries, and is never
silently re-created. Only the subfolder chain self-heals.
Three cases are pinned: cold cache, warm cache with the whole chain
cached (recovers in two cycles via the pre-existing invalidate path - a
per-op failure, never an aborted cycle), and the new-subdirectory case
that was broken.
## Test-infrastructure notes
- The fake gains `delete_folder_tree` - real Drive **cascades** a folder
deletion, while `trash`/`delete_permanent` touch one object, and that
cascade is the incident.
- New `with_source_listing_broken` fault, scoped to the enumeration, so
a test can let its setup uploads land and break *only* the audit.
- One e2e fixture hand-created an orphan **without** the `source_id`
stamp - unfaithful to the executor, which has stamped it since v0.1.0
(verified present in `v1.0.0`, so there is no back-compat hazard for
existing installs). It is now stamped. A duplicate re-upload in that
test is what caught it.
## Gates
`cargo fmt`; `clippy -p driven-core -p driven-drive --all-targets -D
warnings` clean; `cargo test -p driven-core -p driven-drive` all green
(533 tests); `cargo check --workspace --all-targets` clean; vitest 515
passed; prettier clean.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JLB3E2Jm7knNJd37fVpH8X
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent 3792e47 commit 5cb8e3a
13 files changed
Lines changed: 2317 additions & 100 deletions
File tree
- crates
- driven-core
- src
- state
- tests
- driven-drive/src
- fake
- google
- design
- ui/src
- __tests__
- locales
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1152 | 1152 | | |
1153 | 1153 | | |
1154 | 1154 | | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
| 1193 | + | |
| 1194 | + | |
| 1195 | + | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
| 1201 | + | |
| 1202 | + | |
| 1203 | + | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
1155 | 1269 | | |
1156 | 1270 | | |
1157 | 1271 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1576 | 1576 | | |
1577 | 1577 | | |
1578 | 1578 | | |
| 1579 | + | |
| 1580 | + | |
| 1581 | + | |
| 1582 | + | |
| 1583 | + | |
| 1584 | + | |
| 1585 | + | |
| 1586 | + | |
| 1587 | + | |
| 1588 | + | |
| 1589 | + | |
| 1590 | + | |
| 1591 | + | |
| 1592 | + | |
| 1593 | + | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
| 1600 | + | |
| 1601 | + | |
| 1602 | + | |
| 1603 | + | |
| 1604 | + | |
| 1605 | + | |
| 1606 | + | |
| 1607 | + | |
| 1608 | + | |
| 1609 | + | |
| 1610 | + | |
| 1611 | + | |
| 1612 | + | |
| 1613 | + | |
| 1614 | + | |
| 1615 | + | |
| 1616 | + | |
| 1617 | + | |
| 1618 | + | |
| 1619 | + | |
| 1620 | + | |
| 1621 | + | |
| 1622 | + | |
| 1623 | + | |
| 1624 | + | |
| 1625 | + | |
| 1626 | + | |
| 1627 | + | |
| 1628 | + | |
| 1629 | + | |
| 1630 | + | |
| 1631 | + | |
| 1632 | + | |
| 1633 | + | |
| 1634 | + | |
| 1635 | + | |
| 1636 | + | |
| 1637 | + | |
| 1638 | + | |
| 1639 | + | |
| 1640 | + | |
| 1641 | + | |
| 1642 | + | |
| 1643 | + | |
| 1644 | + | |
| 1645 | + | |
| 1646 | + | |
| 1647 | + | |
| 1648 | + | |
| 1649 | + | |
| 1650 | + | |
| 1651 | + | |
| 1652 | + | |
| 1653 | + | |
| 1654 | + | |
| 1655 | + | |
| 1656 | + | |
| 1657 | + | |
| 1658 | + | |
| 1659 | + | |
| 1660 | + | |
| 1661 | + | |
| 1662 | + | |
| 1663 | + | |
| 1664 | + | |
| 1665 | + | |
| 1666 | + | |
| 1667 | + | |
| 1668 | + | |
| 1669 | + | |
| 1670 | + | |
| 1671 | + | |
| 1672 | + | |
| 1673 | + | |
| 1674 | + | |
| 1675 | + | |
| 1676 | + | |
| 1677 | + | |
| 1678 | + | |
1579 | 1679 | | |
1580 | 1680 | | |
1581 | 1681 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
44 | 44 | | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| |||
1039 | 1039 | | |
1040 | 1040 | | |
1041 | 1041 | | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
1042 | 1047 | | |
1043 | 1048 | | |
1044 | 1049 | | |
1045 | 1050 | | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
1046 | 1058 | | |
1047 | 1059 | | |
1048 | 1060 | | |
| |||
1057 | 1069 | | |
1058 | 1070 | | |
1059 | 1071 | | |
1060 | | - | |
1061 | | - | |
1062 | 1072 | | |
1063 | 1073 | | |
1064 | 1074 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
193 | 193 | | |
194 | 194 | | |
195 | 195 | | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
196 | 219 | | |
197 | 220 | | |
198 | 221 | | |
| |||
0 commit comments