Class CaldavSyncService
The outward half already exists: eXo calendars get collections of their own. This adds the inward one — a calendar the user made in their own client becomes an eXo personal calendar — and the entry point that runs both.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic enumThe pieces of outward work a pass may do once the calendars themselves are in step — each named, and each governed on its own. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionvoidestablishDestinations(long userIdentityId) Establishes, there and then, the collections this user's copies are written into.voidforgetThrottle(long userIdentityId) Forgets when this user last synchronised, and what was said about the server they were synchronising with.lastSyncEnd(long userIdentityId) When the connected account was last synchronised through to the end.voidrunOutboundBurst(long userIdentityId, String username) Settles what eXo owes this account, copies what it has never been given, and reads back the copies it already holds.voidStops accepting outward work when the platform is going down.intsweepDueAccounts(long staleMinutes, int batchSize) Synchronises the accounts that have gone longest without one.voidsyncAfterCalendarCreated(long userIdentityId) Synchronises because a calendar has just been created.voidsyncAfterCalendarUpdated(long userIdentityId) Synchronises because a calendar has just been edited — renamed, above all.voidSynchronises a user's calendars if they have not been synchronised recently.voidsyncInBackground(long userIdentityId, String username) Synchronises in the background, where the slow housekeeping belongs.voidSynchronises now, whatever the throttle says.voidsyncNowAndWait(long userIdentityId, String username) Synchronises now and does not return until it has.
-
Constructor Details
-
CaldavSyncService
public CaldavSyncService()
-
-
Method Details
-
sweepDueAccounts
public int sweepDueAccounts(long staleMinutes, int batchSize) Synchronises the accounts that have gone longest without one.Called by the sweep so that an agenda opened afterwards finds the work already done, rather than waiting on a server outside the platform before it can list anything.
Bounded on purpose: one run takes a page of the stalest bindings and stops. A sweep that tries to cover every account in one pass is one that eventually cannot finish, and the accounts it never reached are exactly the ones that needed it.
Bindings are grouped by account before anything is synchronised — synchronisation is per account, not per collection, and an account with five stale calendars must cost one pass rather than five.
- Parameters:
staleMinutes- how long since a successful sync makes a binding duebatchSize- how many stale bindings one run looks at- Returns:
- how many accounts were synchronised
-
lastSyncEnd
When the connected account was last synchronised through to the end.Read from the bindings rather than from the in-memory throttle: the throttle is a courtesy to the server and is emptied by a restart, so a user opening their settings after one would be told they had never synchronised.
lastSyncEndis written where the work finished and survives.The latest across bindings, not the earliest: what the user wants to know is when eXo last spoke to their account. A binding that has been failing on its own is a different question, and one this line would only muddle.
- Parameters:
userIdentityId- whose account- Returns:
- the instant, or null when nothing has ever synchronised
-
syncIfDue
Synchronises a user's calendars if they have not been synchronised recently.The trigger for opening the agenda. Doing nothing is the common answer and has to be cheap: a page load must not wait on a calendar server that has nothing new to say.
- Parameters:
userIdentityId- identity of the userusername- the user's login, which agenda's ACL reads
-
syncNow
Synchronises now, whatever the throttle says.What a "sync now" button calls. It bypasses the throttle because a user pressing it has a reason the throttle cannot know — they just changed something on their phone and want to see it.
- Parameters:
userIdentityId- identity of the userusername- the user's login
-
syncInBackground
Synchronises in the background, where the slow housekeeping belongs.Verifies the copies eXo pushed, and does it on the calling thread because that thread is the sweep's own. The check lists a whole collection, which on a real calendar can take longer than the request timeout allows — measured at a full 30 seconds against one account, every pass — so no request may ever be blocked on it: pressing Synchronise now once took 25 seconds, almost all of it spent on a listing whose result the user would never see.
That rule is about waiting, not about the check. A user pressing the button also starts it (EXO-89821), on the executor thread and without waiting for it — see
startOutboundBurst(long, String). Both paths take the sameoutboundInFlightkey, so an account is never verified twice at once.- Parameters:
userIdentityId- identity of the userusername- the user's login
-
syncNowAndWait
Synchronises now and does not return until it has.The difference from
syncNow(long, String)is what happens when a pass is already running for this user. Returning then — as every caller used to — reports a synchronisation that has not happened yet, and a caller that refreshes its display on the strength of that reads the account mid-pass: collections not yet materialised still count as remote, so they are shown under the heading for calendars eXo is not holding, and stay there until the page is reloaded. Waiting for the pass in flight makes the answer true when it is given.Bounded, and never waits on itself: a pass that re-enters through a listener is recognised by its thread and returns immediately, as it always did.
This is where connecting an account lands, and it is why the seeding is started from here. Agenda's connect sequence is: the drawer stores the credentials, then agenda records the connected account in the user's agenda settings, then it calls the connector's
sync(), which is this. Only the last of the three runs with the account recorded — and recorded is whatCaldavPendingInvitationServicereads to know the user consented to copies at all, so a seeding hung on the earlier steps would run, find no consent, and copy nothing. The user's own Synchronise now reaches the same entry, and seeds too: on a converged account that is one query answering "everything is already copied".And it is where the copies are read back from (EXO-89821). The burst started here settles, seeds and verifies, so an answer the user gave in their own calendar is read the moment they ask for it rather than whenever the next sweep comes round. Started, never awaited: see
startOutboundBurst(long, String)below.- Parameters:
userIdentityId- identity of the userusername- the user's login
-
runOutboundBurst
Settles what eXo owes this account, copies what it has never been given, and reads back the copies it already holds.The body of the background burst, on its own method because it is what carries the container: an executor thread has none bound, and
@ContainerTransactionalis what establishes one around the calls that read agenda and write the mapping rows.The key is not taken here. It is taken in
startOutboundBurst(long, String)before the hand-over, so that an account queued behind another one is already spoken for; taking it here as well would find it held by this very burst.Public because the aspect is woven on method execution and the platform weaves what it can see; and because it is the seam a test runs to pin what the burst does — and what the caller's thread did not do. A test that runs it states a container first, exactly as the sweep job's test does — the annotation building a portal in a unit-test JVM is it working, not failing.
- Parameters:
userIdentityId- identity of the user whose account receives copiesusername- their eXo login, which the credentials provider maps to their account on the server
-
syncAfterCalendarCreated
public void syncAfterCalendarCreated(long userIdentityId) Synchronises because a calendar has just been created.The engine needs the owner's login to satisfy agenda's ACL, and a listener has only their identity — resolving it is this method's reason to exist, and it keeps the listener the glue it is meant to be.
Nothing happens for a user with no connected account, which is most of them: a calendar created by someone who never set up CalDAV must not cost an identity lookup and a wasted pass.
- Parameters:
userIdentityId- identity of the calendar's owner
-
syncAfterCalendarUpdated
public void syncAfterCalendarUpdated(long userIdentityId) Synchronises because a calendar has just been edited — renamed, above all.The same pass as after a creation, for the same reason: the user who just renamed a calendar should see the name on their phone without waiting for the throttle, and the binding step of a pass is where the collection's name is reconciled and read back (EXO-89528).
- Parameters:
userIdentityId- identity of the calendar's owner
-
establishDestinations
public void establishDestinations(long userIdentityId) Establishes, there and then, the collections this user's copies are written into.What connecting an account calls, and the half of it that was missing. Connecting used to store the credentials, thaw whatever disconnecting had frozen, and forget the throttle — three pieces of bookkeeping, none of which gives a copy anywhere to go. The destinations were established later and elsewhere: the personal collections by the first synchronisation, the mirror by the calendar step of the agenda connector panel. Both are the browser's doing, and neither is guaranteed — an account connected from a page that does not offer the step, or through the REST API, ended up connected with no destination at all.
Until one exists, every copy is refused in a way nobody can see:
CaldavPushService.pushAgendaEvent(long, String, long)answers null for an event of a calendar with no collection — a 204 the page renders as nothing — and refuses a space meeting with a known-state code the seeding pass logs at debug. No error, no queue, no trace: exactly the "it does not work" this is reported as.And the wait was not merely long, it could be endless. The sweep picks its accounts from the bindings that already exist (
findDueAccountsselects user identities out ofCALDAV_CALENDAR_SYNC), so an account that never acquired one is invisible to it for ever. The wait people saw — the account-due interval — is what happens once a first binding exists; without one, nothing in the platform was ever going to make it.Narrow on purpose: this binds the calendars and establishes the mirror, and does not import events or seed the meetings the user already has. Those are the expensive halves of a pass, they are what the first synchronisation and the sweep are for, and nobody clicking Connect should wait on a download to find out whether their account was accepted. What this buys is that the next event they create has somewhere to go.
Neither step is allowed to fail the connection, and they fail independently: a server that refuses to create a collection for a personal calendar may still hold a perfectly good mirror, and the user is better off with the half that worked.
Independently, but not blindly. Two failures are not properties of the step that met them but of the server itself — its credentials refused, or nothing there at all — and are therefore settled after one attempt. Running the second step after either of them cannot succeed, and it is not free: it is a second authenticated request against a calendar server that may be counting failures and banning the source address for them (EXO-89806). So on those two, and only those two, this stops after the first step and says so once.
- Parameters:
userIdentityId- identity of the user who has just connected
-
forgetThrottle
public void forgetThrottle(long userIdentityId) Forgets when this user last synchronised, and what was said about the server they were synchronising with.What connecting an account calls. Someone who has just entered their credentials is owed their calendars now, not in a quarter of an hour — and a throttle stamped by the previous account's run has nothing to say about the new one.
The outage this user's previous server was in has nothing to say about the new one either, and keeping it is worse than losing it: the first pass that succeeds would announce that a server is answering again naming the account's current server and the former one's outage, dated to it. A recovery notice exists to be trusted, so it is not printed for a server that was never seen to be down. Same rule, and the same reason, as the identities and destinations the connect path forgets beside this call (EXO-90446).
- Parameters:
userIdentityId- identity of the user
-
stopSeeding
@PreDestroy public void stopSeeding()Stops accepting outward work when the platform is going down.shutdownand notshutdownNow: a copy half written is a copy the verification pass has to reconcile later, so the burst in flight is let finish rather than interrupted. Anything still queued is dropped, which costs nothing — the sweep seeds those accounts on the next start.
-