sync: check the reply's origin instead of keying on it
Three corrections to 7fac4bd, all found in review.
Folding the origin into the match key was wrong: the REPORT body carries
only our paths, so a reply's href resolves against the collection's
post-redirect location while our stored hrefs still hold the origin we
had before it. Every resource of a redirected collection would have
landed in both buckets -- the failure the commit was meant to remove.
The key is the decoded path; the origin is checked separately, and a
reply may come from the host we asked or from the collection's own.
Two request hrefs can decode to one identity, and keeping the last of
them silently reported the other as missing and applied one row's body
to the other. Only identities naming exactly one href are matched
loosely now; where we cannot tell two spellings apart, the exact one is
the only honest answer.
And the syncer's amnesty was excused by any stray at all, though servers
volunteer siblings as a matter of course -- so a genuinely omitted
resource would never have been counted and would be re-requested for
ever. Only a stray naming the same path can be this href under a
spelling we failed to read.
This commit is contained in:
@@ -662,11 +662,20 @@ class CollectionSyncer(
|
||||
// no refund: at THRESHOLD the href is stripped before the fetch,
|
||||
// so `apply` can never run to clear it. A wasteful re-request is
|
||||
// recoverable; a permanent silent drop of a good task is not.
|
||||
val unmatched = fetched.unsolicited.isNotEmpty()
|
||||
// ⚠️ Paired by path, not excused wholesale. Real servers answer
|
||||
// with hrefs nobody asked for as a matter of course — a sibling,
|
||||
// something from another collection — so "any stray at all"
|
||||
// would mean a genuinely omitted resource is never counted and
|
||||
// is re-requested for ever, which is the loop the count exists
|
||||
// to break. Only a stray naming the *same path* is plausibly
|
||||
// this href under a spelling we failed to recognise, and that is
|
||||
// the one case where counting could permanently drop a resource
|
||||
// that is really there.
|
||||
val strayPaths = fetched.unsolicited.map { it.pathSegments }.toSet()
|
||||
fetched.missing.forEach {
|
||||
val href = it.toString()
|
||||
if (unmatched) {
|
||||
skip(href, "listed but not returned, among hrefs we did not ask for")
|
||||
if (it.pathSegments in strayPaths) {
|
||||
skip(href, "answered under a spelling we did not recognise")
|
||||
} else {
|
||||
fail(href, "listed but not returned")
|
||||
}
|
||||
|
||||
@@ -524,9 +524,9 @@ class CollectionSyncerTest {
|
||||
resources = emptyList(),
|
||||
missing = hrefs,
|
||||
failed = emptyList(),
|
||||
// The shape an href we failed to recognise takes: one
|
||||
// resource in both buckets.
|
||||
unsolicited = listOf("http://server/dav/tasks/one.ics".toHttpUrl()),
|
||||
// The shape an href we failed to recognise takes: the same
|
||||
// path, in both buckets.
|
||||
unsolicited = listOf("http://elsewhere/dav/tasks/one.ics".toHttpUrl()),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -539,6 +539,28 @@ class CollectionSyncerTest {
|
||||
assertThat(report.quarantined.single().failures).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test fun `a stray for another path does not excuse a missing one`() {
|
||||
remote.put("one.ics", vtodo("a", "Fine"))
|
||||
remote.onFetch = { hrefs ->
|
||||
Result.success(
|
||||
FetchResult(
|
||||
resources = emptyList(),
|
||||
missing = hrefs,
|
||||
failed = emptyList(),
|
||||
// Servers volunteer siblings routinely. Excusing every
|
||||
// missing href on that basis would mean nothing is ever
|
||||
// counted, and the loop never breaks.
|
||||
unsolicited = listOf("http://server/dav/tasks/somebody-else.ics".toHttpUrl()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val report = sync()
|
||||
|
||||
assertThat(quarantine.values.single()).isEqualTo(1)
|
||||
assertThat(report.quarantined.single().failures).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test fun `a missing href is still counted when nothing was unmatched`() {
|
||||
remote.put("one.ics", vtodo("a", "Fine"))
|
||||
remote.onFetch = { hrefs ->
|
||||
|
||||
Reference in New Issue
Block a user