Class CaldavShareSubscriptionService
Why eXo does it. On BlueMind a grant writes an access entry and
nothing else; the colleague's DAV home and webmail list
subscriptions, so until they click "Add" in BlueMind the calendar
appears nowhere — not in BlueMind, not under "Shared with me" in eXo. The
PO decided (2026-09-16) that eXo takes that step for them at grant time,
with their own stored credentials; BlueMind's access-change mail is their
notice. The reverse on revoke is load-bearing: BlueMind auto-unsubscribes on
an access removal for mailboxes only, and a dangling calendar subscription
keeps the href in the colleague's listing — a dead calendar shown, and a
hidden share (HIDDEN_SHARE, EXO-90239) never retired, since that
record is dropped only when the href leaves the listing.
Never in the owner's way. Both entry points are called from inside
the owner's grant or revoke, after the access list read back confirmed the
change, and they never throw: the owner's share succeeded on the server
whatever happens to the colleague's subscription. A change that does not
land is recorded as owed (CALDAV_PENDING_SUBSCRIPTION) and drained
later; a stale colleague password is a WARN here and a row, never a
failure of the grant and never a pause of the colleague's own account at
grant time — the drain, like their own pass, is where that verdict belongs.
Where the drain runs, and why it is table-driven. The sweep selects
accounts holding an ACTIVE pair and only its background pass runs outbound
work; a colleague who merely received shares holds no pair for them and is
never swept. So the owed rows are read from their own table — every sharee,
oldest first, one session per sharee — from the sweep job, and the
colleague's own rows are drained at the top of their own synchronisation
pass when they open their agenda (the PO's decision of 2026-09-16). The
attempt bound is the
meeting-copy one, exo.agenda.caldav.push.maxAttempts, on the PO's
decision that no new property is wanted.
What is given up on at once, what is argued with, and what waits. A server that cannot be reached and any other unexplained answer are counted and retried up to the bound. A login refused to the drain pauses the colleague's active pairs on that server, as their own synchronisation pass does at its first refusal, and their rows wait uncounted until the account is put back to work; the drain does not log in as a colleague whose account is paused. A colleague holding no pair on the server has nothing to pause, and a refused login counts against their rows like any retry. A session BlueMind authenticated as somebody other than the recorded colleague, a 403, a container BlueMind does not hold, a colleague no longer connected to that server, credentials that are not a login, and a login eXo cannot resolve any more are final: the row spends its whole budget and stays as the record that eXo gave up.
What is left alone by design. A colleague who unsubscribes in
BlueMind afterwards is not re-subscribed: eXo acts at grant and at revoke,
never on the listing. Hiding the calendar in eXo (HIDDEN_SHARE)
covers "subscribed for me, I do not want it shown" exactly as for a hand
subscription. One edge here is bounded: a row drained after they
unsubscribed by hand re-subscribes them, for at most maxAttempts
sweep periods after the grant.
A drain and a grant or revoke of the same obligation do not interleave, on one node. The grant's or the revoke's attempt and settle and the drain's re-read, post and settle of a row each run under the lock of the colleague, the server and the container. So a revoke cannot land between a drain's subscribe being posted and its settle, and a drain does not post a row a grant or a revoke has decided again since the drain read it. Every write the drain makes about a row is one statement that matches the kind it read, so a row renewed with the other change is never charged with the old change's verdict.
Six limits, named because each is a decision somebody may want to revisit rather than an oversight.
- Only a revoke made from eXo unsubscribes. An owner who removes
the access entry in BlueMind's own webmail never runs this service, and
BlueMind does not auto-unsubscribe for a calendar — so that colleague keeps
the dangling subscription this service exists to prevent. Covering it would
mean noticing, at the colleague's own pass, a subscribed collection they can
no longer read; the natural seam is
CaldavSyncService.forgetRevokedShares, which already watches the listing. Out of scope until the PO asks for it. - The grant-time attempt is synchronous, on the owner's thread, inside the share's stripe lock. It is one login, one POST and one logout, each bounded by the REST session's 30-second timeout, so a stalled BlueMind can hold the owner's Share or Unshare for up to a minute and a half beyond what the grant itself already costs, and up to 30 seconds more when a drain is posting the same obligation, whose lock the grant then waits for — and with it every other share hashing to the same one of the share service's 64 lock stripes, which is one share in 64 on that node, of any calendar on any server, not only of this one. It buys immediacy in BlueMind's own webmail and on the colleague's devices; the drain alone would put the calendar in their very next eXo pass anyway. The trade is the PO's and the Architect's, not this class's.
- The attempt bound is per node.
@Scheduledis node-local and every node fires; the owed rows carry no claim and no lease, so on an N-node cluster each sweep period runs N drains of the same rows — N logins as the colleague, N counted refusals — and the bound is reached in aboutmaxAttempts / Nperiods rather thanmaxAttempts. The account sweep beside it converges through its own last-sync column; this table has no equivalent, and adding one is an Ops-visible change. - A row given up on is retired by spending the configured bound, not
by a terminal marker (
CaldavPendingPushDAO's pattern, reused). Raisingexo.agenda.caldav.push.maxAttempts— a property shared with the meeting-copy push, so raised for reasons that have nothing to do with subscriptions — therefore makes every previously abandoned row attemptable again. For a meeting copy that is a harmless re-push; here it can re-subscribe a colleague who unsubscribed by hand. - The drain spends the account sweep's batch on sessions, not on
rows. The job hands it
exo.agenda.caldav.sync.sweep.batchSize(50 by default), and those 50 rows can be 50 distinct colleagues on 50 servers — 50 logins, each up to the session's three 30-second timeouts — on the scheduler's single thread, against a cron that fires every five minutes. A backlog on a server that swallows connections therefore delays this add-on's own account sweep for as many periods as it takes, and it is the larger of the two costs named here. Self-limiting aftermaxAttemptsperiods; a bound of its own, rather than the account sweep's, is the Architect's and Ops' call. The claim EXO-90920 adds bounds the sweep to one node per period. - Nodes share no lock. The lock above is this node's: a drain on
one node and a revoke on another can still interleave, and the revoke's
unsubscribe can be followed by the drain's subscribe landing, because
BlueMind performs no access check on a subscribe. What it leaves is one
stale calendar in that colleague's listing — not access to anything they
could not already see, since BlueMind enforces its own ACL on the contents
(
CalendarServicechecksVerb.Readon the container for every read, subscribed or not). Closing it across nodes needs a claim or a version column onCALDAV_PENDING_SUBSCRIPTION, an Ops-visible schema change, like the lease the third limit lacks: EXO-90920.
-
Field Summary
FieldsModifier and TypeFieldDescriptionstatic final intHow many owed rows the colleague's own pass drains before it lists anything. -
Constructor Summary
Constructors -
Method Summary
Modifier and TypeMethodDescriptionintretryOwed(int batch) Drains the changes owed to anybody, oldest first, one session per colleague and server: what the sweep job calls, because a colleague who holds no pair is never reached by the account sweep.intretryOwed(long userIdentityId, int batch) Drains the changes owed to one colleague: what their own synchronisation pass calls first, so a calendar shared with them is subscribed before that pass lists their home.voidSubscribes the colleague to the calendar just shared with them, as them.voidUnsubscribes the colleague from the calendar no longer shared with them, as them.
-
Field Details
-
OWN_DRAIN_BATCH
public static final int OWN_DRAIN_BATCHHow many owed rows the colleague's own pass drains before it lists anything.- See Also:
-
-
Constructor Details
-
CaldavShareSubscriptionService
public CaldavShareSubscriptionService()
-
-
Method Details
-
retryOwed
public int retryOwed(int batch) Drains the changes owed to anybody, oldest first, one session per colleague and server: what the sweep job calls, because a colleague who holds no pair is never reached by the account sweep.- Parameters:
batch- how many owed rows one run looks at- Returns:
- how many changes landed this run
-
retryOwed
public int retryOwed(long userIdentityId, int batch) Drains the changes owed to one colleague: what their own synchronisation pass calls first, so a calendar shared with them is subscribed before that pass lists their home.- Parameters:
userIdentityId- the colleaguebatch- how many owed rows to look at- Returns:
- how many changes landed