Class CaldavMirrorVerificationService
The mirror calendar is a projection: eXo is authoritative and nothing ever syncs back from it, with a single deliberate exception since EXO-89681 — the owner's own PARTSTAT, so a meeting can be answered from the calendar on their phone. Until this ran, even the projection was a claim rather than a guarantee — a copy deleted from someone's phone stayed deleted, and a copy a server rewrote stayed rewritten, both silently. Nothing in eXo would ever notice, because nothing ever looked.
One ETag listing of the collection answers both questions at once, and it is the same confirm-by-reading-back discipline the mirror's own creation already needs: an href that is gone means the copy was deleted, and an ETag that moved means somebody rewrote it. Only the second case costs a fetch, and only when the ETag actually moved.
What "what eXo wrote" means. Not bytes — meaning. The copy is
compared against the object eXo would render for the same event now, by
IcsEquivalence, and nothing is recorded to
compare against. Two byte baselines were tried and neither can be captured:
a digest of what eXo sent made every copy on a re-serialising server
look tampered with from the first pass, and a digest of what the server was
seen to store, read back after the write, caught BlueMind mid-settle —
it finished afterwards without moving the ETag, so all 19 copies of a live
account were judged altered and rewritten every five minutes, for ever. A
CalDAV object is a structured document and a server re-serialising it is
behaving normally; the fix is to understand the content rather than to learn
one server's serialisation.
The ETag also decides direction. Comparing PARTSTAT values alone cannot tell "answered on the phone" from "answered in eXo and not pushed yet" — both read as a difference. An ETag that moved away from the one this connector recorded proves the client wrote the object after eXo did, so the answer on it is the user's latest word and is adopted; an ETag that still matches proves the copy is untouched, so any difference is eXo-side and the ordinary push simply overwrites the copy. And the answer is read before the object is repaired, deliberately: when eXo changed the meeting while the user accepted it on their phone, both differ at once, and a repair-first pass would overwrite the acceptance before anything read it.
It gives up, and giving up stops the work and not only the writing. An object that keeps disappearing — a server refusing writes it pretends to accept, a rule on the account deleting what eXo sends — is left alone after a few attempts and said out loud, rather than re-pushed on every sync for ever. The count is held in memory on purpose: a restart forgives, which is the right bias when the cause is usually temporary and the alternative is a database column recording that a server misbehaved once.
That check used to sit after the object had been fetched, re-rendered,
compared and named in the log, so an abandoned copy still cost a round trip
and a log line every five minutes for ever — everything except the one step
that could have changed something. The version each abandoned object settled
at is now remembered (settled), and a pass that finds the listing
still publishing it moves on without a fetch. It is the version rather than a
flag deliberately: a copy the user answers on their phone moves its ETag, and
that copy is examined again and its answer read, because abandonment is a
statement about eXo's writing and never about the user's.
And once, when a setting changes, it looks at everything. Everything
above converges towards the current render only when something moves the
remote ETag — which is exactly what an administrator changing a setting does
not do. Flip the answer-links switch and every copy already on the
server keeps the links it was given, for ever, because no version ever moves
and this pass never looks. So the registration carries a stamp of when a
copy-affecting setting last changed, the mirror pair carries the value it has
already applied, and a pair that is behind gets one round with the ETag gate
removed: every mapped object is rendered, fetched and compared on its content.
Which settings move that stamp is one decision in one place —
CopySettingsFingerprint.
And when the setting that changed was where the copies go, they are
moved rather than stranded. The destination is part of the same
fingerprint, so the round a destination change owes is this one — and before
a single content comparison runs, CaldavMirrorRelocationService points
the pair at the new collection and carries the backlog across, object by
object, reading any answer off a copy before the mapping stops watching it.
The stamp is then written only if that finished: a copy the server would not
let go of leaves the change owed, and the next sweep tries again.
Compare then repair; never a mass re-push. The round is this pass minus one gate, and nothing else. It does not write what it has not first found different: on a server that re-serialises or stamps what it stores — which is every server this add-on has met — an unconditional rewrite of everything is the churn incident of EXO-89716 and EXO-89756 back again, at the scale of a whole account at once. A converged mirror comes out of a settings round having written nothing at all, and the price it does pay is honest and bounded: one fetch per copy, once, instead of one listing.
Idempotent and resumable by construction. The stamp is written after the walk, so a crash halfway leaves it where it was and the next sweep re-runs a comparison that rewrites nothing already converged. The repair cap and the answer-before-repair ordering come for free, being the same code. Two things the round deliberately does not change: an abandoned copy at its settled version stays skipped — giving up is a statement about eXo's writing that a setting does not retract — and an answer is still only adopted off a copy whose ETag actually moved, because the direction rule is the ETag's other job and the round removes it only as a gate, never as evidence.
-
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidforgetRepairs(long userIdentityId) Forgets what is known about an account's repairs.org.exoplatform.caldav.model.MirrorVerificationCompares every copy eXo pushed for a user against what the server holds.
-
Constructor Details
-
CaldavMirrorVerificationService
public CaldavMirrorVerificationService()
-
-
Method Details
-
verify
Compares every copy eXo pushed for a user against what the server holds.- Parameters:
userIdentityId- identity of the user whose mirror is checkedusername- their eXo login, which the credentials provider maps to their account on the server- Returns:
- what the pass found and did
-
forgetRepairs
public void forgetRepairs(long userIdentityId) Forgets what is known about an account's repairs.The settled versions go with them: they are the second half of the same state, and a settled version left behind after the repair counts were cleared would be consulted by
hasSettled(long, org.exoplatform.caldav.model.ObjectSync, java.util.Map<java.lang.String, java.lang.String>)for an object that is no longer abandoned — harmless today because the count is checked first, and a trap for the next change that reorders those two checks.Both maps stay keyed by user and href, and the statement the budget is now keyed on (EXO-89863) lives in the value rather than in the key. That was the deciding argument for putting it there: a key carrying a statement would have to be matched by prefix here too, and any spelling of it that this sweep did not anticipate would survive the account that owned it — an entry attributed to a user who no longer exists, in a map nothing else ever prunes. One key shape, one prefix, nothing left behind.
- Parameters:
userIdentityId- identity of the user
-