Class CaldavShareSubscriptionService

java.lang.Object
org.exoplatform.caldav.service.CaldavShareSubscriptionService

@Service public class CaldavShareSubscriptionService extends Object
Makes a calendar shared from eXo on BlueMind appear to the colleague it was shared with, by subscribing their BlueMind account to it as they would themselves — and removes that subscription when the share is revoked (EXO-90277).

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. @Scheduled is 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 about maxAttempts / N periods rather than maxAttempts. 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). Raising exo.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 after maxAttempts periods; 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 (CalendarService checks Verb.Read on the container for every read, subscribed or not). Closing it across nodes needs a claim or a version column on CALDAV_PENDING_SUBSCRIPTION, an Ops-visible schema change, like the lease the third limit lacks: EXO-90920.
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final int
    How many owed rows the colleague's own pass drains before it lists anything.
  • Constructor Summary

    Constructors
    Constructor
    Description
     
  • Method Summary

    Modifier and Type
    Method
    Description
    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.
    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.
    void
    Subscribes the colleague to the calendar just shared with them, as them.
    void
    Unsubscribes the colleague from the calendar no longer shared with them, as them.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Field Details

    • OWN_DRAIN_BATCH

      public static final int OWN_DRAIN_BATCH
      How many owed rows the colleague's own pass drains before it lists anything.
      See Also:
  • Constructor Details

    • CaldavShareSubscriptionService

      public CaldavShareSubscriptionService()
  • Method Details

    • subscribeSharee

      public void subscribeSharee(ShareeSubscription share)
      Subscribes the colleague to the calendar just shared with them, as them. Never throws.
      Parameters:
      share - the share as granted
    • unsubscribeSharee

      public void unsubscribeSharee(ShareeSubscription share)
      Unsubscribes the colleague from the calendar no longer shared with them, as them. Never throws.
      Parameters:
      share - the share as revoked
    • 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 colleague
      batch - how many owed rows to look at
      Returns:
      how many changes landed