Skip to content

feat(common-contacts): reconcile the org instance with Sabre - #4977

Merged
shepilov merged 8 commits into
masterfrom
feat/common-contacts-reconcile-org
Oct 27, 2026
Merged

shepilov merged 8 commits into
masterfrom
feat/common-contacts-reconcile-org

Conversation

@MontaGhanmy

@MontaGhanmy MontaGhanmy commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add cozy-stack fix org-contacts <org-id> and POST /instances/fixers/org-contacts/:org_id to delete feed-owned organization contacts absent from a completed source republication.
  • Require since, the completed domain republication's processedContactCount as expected_contacts (CLI: --expected-contacts), and its exact CardDAV path list (CLI: --paths-file; API body: {"paths":[...]}). The side service currently supplies the count, so the corresponding paths must be exported separately from the source domain address books.
  • Refuse deletion until the distinct observed paths match the complete source list. Missing, duplicate or unexpected paths, consumer lag, and unrelated live updates cannot be hidden by a matching total count. Explicit zero plus an empty list supports a completed empty republication.
  • Store observations on contacts in CouchDB in the same revision as the card. Cache loss cannot make a contact stale, and concurrent updates cause a revision conflict instead of deletion. Republication now updates this bookkeeping field and the contact revision even when card fields are unchanged.
  • Reject missing inputs, invalid counts, future timestamps, windows older than seven days, missing organization instances, and contexts without common_contacts.
  • Keep contacts without a carddavPath. Use normal contact deletion so sharing-group events retain their existing behavior. Dry runs report candidates, and repeating a successful cleanup removes nothing more.
  • Reconciliation requires common_contacts independently of centralized_org_directory, including while replication is retained. Organization-wide sharing updates remain tracked in Propagate organization group changes to member-instance sharings #5102.
  • Cover cache resets, partial consumption, unrelated updates, duplicate paths, empty snapshots, API validation, and both storage modes in regression tests.
  • Depends on feat(common-contacts): groups on the organization instance #4976.

Part of linagora/twake-workplace-private#1781.

@MontaGhanmy
MontaGhanmy force-pushed the feat/common-contacts-reconcile-org branch from d445b70 to bd95911 Compare October 14, 2026 15:40

@rezk2ll rezk2ll left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice approach, the seen marker plus reconcile fits well, and going through DeleteDoc keeps share-group working.

Nothing blocking. My main worry is the guard, it only refuses when nothing at all was seen. Details inline.

// stack, not that Sabre is empty.
if len(stale) > 0 && report.Kept == 0 {
return nil, fmt.Errorf("no contact was sent since %s, refusing to remove %d contacts", since.Format(time.RFC3339), len(stale))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only refuses when Kept is 0.

If the consumer is still behind, or Redis restarted mid republication, Kept is above 0 and everything not seen yet gets deleted. share-group then removes those people from their group sharings.

I tried it locally: republished 1 contact out of 3, the fixer deleted the other 2. The late ADDs then recreated them with new ids and no group ref.

Could it refuse above some removal ratio (or take a --max-removals) unless --force is passed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what --dry-run is for: it lists what would go and how many were kept, so your 2 out of 3 shows up before anything is deleted. A ratio would also refuse a real cleanup after a big offboarding, and then --force becomes the default.

Comment thread model/orgdirectory/reconcile.go Outdated

func seenKey(inst *instance.Instance, path string) string {
return "common-contacts-seen:" + inst.Domain + ":" + path
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Redis Cluster the MGET spans several slots, gets CROSSSLOT, and MultiGet returns all nil. So the fixer always refuses there.

A hash tag fixes it for free: "common-contacts-seen:{" + inst.Domain + "}:" + path

Low priority, the job scheduler doesn't run on a cluster today either.

Comment thread web/instances/fixers.go
return err
}
return c.JSON(http.StatusOK, report)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: every refusal comes back as a 500 Unqualified error.

BadRequest / NotFound / Conflict would let a script tell misuse from a crash.

Comment thread model/orgdirectory/reconcile.go Outdated
var stale []*contact.Contact
for i, doc := range docs {
at, _ := strconv.ParseInt(string(seen[i]), 10, 64)
if at >= since.UnixMilli() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: at comes from the stack's clock, since from the side service. A small skew deletes the first contacts.

One line in docs/admin.md saying to take since a few minutes earlier would cover it.

@MontaGhanmy
MontaGhanmy force-pushed the feat/common-contacts-reconcile-org branch from 45c09a4 to 6231f0b Compare October 20, 2026 09:10
@MontaGhanmy
MontaGhanmy requested a review from rezk2ll October 20, 2026 09:47
@shepilov
shepilov added this pull request to stack #5114 October 27, 2026 16:40
@shepilov
shepilov force-pushed the feat/common-contacts-reconcile-org branch 4 times, most recently from 9584414 to 5b1a577 Compare October 27, 2026 18:35
Base automatically changed from feat/common-contacts-groups-on-org to master October 27, 2026 18:37
MontaGhanmy and others added 8 commits October 27, 2026 19:37
… Redis Cluster slot

The reconciliation reads them with one MGET, which fails with CROSSSLOT
on a cluster, so the fixer always refused.
…ixer

A since too far back, an unknown organization, a missing organization
instance and an organization instance that is not ready all came back as
a 500.
The stack dates what the feed sends with its own clock, so a side service
clock ahead of it removed the first contacts sent.
@shepilov
shepilov force-pushed the feat/common-contacts-reconcile-org branch from 5b1a577 to c4d5d7c Compare October 27, 2026 18:38
@shepilov
shepilov merged commit aebdc57 into master Oct 27, 2026
1 check passed
@shepilov
shepilov deleted the feat/common-contacts-reconcile-org branch October 27, 2026 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants