Files
to-do/FINDINGS.md
T
Connor JohnstoneandClaude Opus 5 727813b5e9 Record that the NAS crystal is sound, the clock was just never set
chrony measures the hardware at 2.6 ppm, so the 124s was about eighteen
months of accumulation on a machine with no time daemon, not drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU5ZFfQFtDTFqqhvMTWdGH
2026-09-09 18:20:53 -04:00

149 lines
6.2 KiB
Markdown

# Findings
Empirical results that settle open questions in PLAN.md. Each entry records how
it was measured, so it can be re-checked when something changes.
## SFTPGo WebDAV does not honor `If-Match` on `PUT`
PLAN.md section 7 flagged this as "to verify in phase 1". Verified 2026-09-09
against the live endpoint.
```
PUT (create) -> 201
HEAD -> etag "18d3c65559dd89a43"
PUT with correct If-Match -> 201
PUT with If-Match "deadbeef0000..." -> 201 <- should have been 412
GET -> body from the bogus-If-Match write
```
The precondition is ignored, not evaluated. **The client must use
HEAD-compare-then-`PUT`**, which is the fallback PLAN.md already anticipates.
The remaining race window is milliseconds wide for a single user with one phone.
## ETag is exactly `hex(mtime_nanos) + hex(size)`
Not a hash. Measured by comparing the WebDAV ETag against `stat` over ssh on the
same file:
```
HEAD etag decoded 1788991618.521424162
disk mtime 1788991618.521424162 identical to the nanosecond
```
Two consequences:
- Rewriting identical content still produces a new ETag. Content equality has to
be checked by comparing bytes, never by comparing ETags.
- An ETag says only "the file was written at time T and is N bytes". That is
sufficient for change detection, which is all the sync design asks of it.
## The ETag returned by `PUT` is never the file's actual ETag
Reproduced 3/3 before the clock fix, and 3/3 after it. Do not cache the `PUT`
response ETag as the new base. **`HEAD` after `PUT`** to learn the authoritative
value, or the next sync will diagnose a phantom remote change on every save.
SFTPGo computes the response ETag roughly a millisecond before the file's final
mtime settles, and ETag is derived from mtime, so the two can never agree.
```
after the clock fix:
trial 1: PUT="18d3c711836b5e748" HEAD="18d3c7118361820b8" gap 0.646 ms
trial 2: PUT="18d3c711b9297f978" HEAD="18d3c711b91648318" gap 1.259 ms
trial 3: PUT="18d3c711cb79bac48" HEAD="18d3c711cb62efba8" gap 1.494 ms
```
Worth recording how this was nearly misdiagnosed. Before the NAS clock was
fixed the same gap measured about 113 **seconds**, which looked like a serious
protocol defect. It was the same one-millisecond write-path race the whole time,
with a broken clock amplifying it by five orders of magnitude. Fixing the clock
shrank the symptom by 100000x without changing the underlying cause, and without
changing what the client has to do about it.
## The NAS clock was ~124 seconds fast, now fixed
`/home/connor/docs` on mainframe is `nas.rcjohnstone.com:/docs` over NFSv4. NFS
stamps mtimes with the *server's* clock, so every file written to that tree, by
any means, was landing with an mtime about two minutes in the future.
Isolated to the filesystem, with SFTPGo entirely out of the path:
```
$ ssh mainframe
now: 1788991551.498123146
touch .probe-ssh.md
ssh-touch mtime: 1788991665.036420629 <- +113.5s, written locally over ssh
fs type: nfs
```
The offset was measured at a stable 124.125s over a 193s window. It was not
drifting at a measurable rate; the apparent growth between two early spot checks
was inside the ssh round-trip noise of those checks.
`nas` is Arch, and had no time daemon at all: `systemd-timesyncd` was installed
but disabled, and nothing else was present. Fixed by installing `chrony` and
enabling it. The shipped Arch `chrony.conf` already carries the two settings
this case needed, so it was left alone rather than rewritten:
- `makestep 1.0 3`, which steps a large initial offset instead of spending 25
minutes slewing it away.
- `rtcsync`, which disciplines the hardware clock. This mattered here, since the
RTC was fast by the same amount and would otherwise have reintroduced the
offset on the next boot.
`systemd-timesyncd` was masked so it cannot later race chronyd.
```
Sep 09 18:18:29 nas chronyd[1588]: System clock was stepped by -123.824615 seconds
```
The step matches the independently measured 124.125s offset to within the
measurement noise, which is the confirmation that the two figures describe the
same thing. Verified afterwards, end to end:
```
mainframe now : 1788992343.649697683
NFS file mtime: 1788992343.949308287
mtime minus clock: +0.300 s (was +113.5, and the 0.3 is the gap
between `date` and `touch` in one command)
```
Once disciplined, chrony measured the hardware clock at **2.643 ppm**, about
0.23 s/day, which is a perfectly good crystal. So this was never a drift problem.
At that rate 124 seconds is roughly a year and a half of accumulation, which is
simply how long the machine had been running with nothing to set its clock.
The NAS was serving NFS but running no containers and no databases, so stepping
the clock backward by two minutes was safe. One harmless after-effect: files
written to that tree before the fix carry mtimes up to ~124s in the future, and
stay that way until they are next rewritten.
This project never depended on the fix. Rollover selects the previous note by
**filename sort**, never by mtime, and the sync design compares ETags for
inequality only, never against a clock. It is recorded because the drift
silently affected everything else on that mount, `rsync --update` and
incremental backups being the obvious victims.
## Reconciliation, phase 0, completed 2026-09-09
| File | Resolution |
|---|---|
| `2024-10-21.md` | conflict copy was a strict superset, taken wholesale |
| `2024-11-12.md` | differed only in one checkbox, kept the `[x]` |
| `2025-01-17.md` | local was the superset, kept as-is |
| `2025-01-16.md` | genuine divergence, union merged |
Mainframe's copy was byte-identical to the local conflict file in both divergent
cases, so each was a two-way merge, not a three-way one.
All four `.sync-conflict-` files deleted. 228 files remain, all `YYYY-MM-DD.md`.
Laptop pushed to mainframe; both sides now hash identically:
```
laptop 228 files b9a39c9cdf0e0ba49080c6ba03629679
mainframe 228 files b9a39c9cdf0e0ba49080c6ba03629679
```
Backups taken first: `~/todo-notes-mainframe-backup-20260909.tar.gz` on
mainframe, and a matching archive in the session scratchpad on the laptop.