dispatchChatAutoArchive audits every archived root chat and enqueues one notification per owner covering the roots archived in this tick. Children inherit their root's archival decision and are skipped for audit, matching the manual archive path (patchChat audits the root only). Enqueue is per-tick:
(auditCtx, enqueueCtx context.Context, tickStart time.Time, autoArchiveDays, retentionDays int32, archived []database.AutoArchiveInactiveChatsRow)
| 534 | // is the cancellable parent: on shutdown we abandon any remaining digests |
| 535 | // rather than blocking Close. |
| 536 | func (i *instance) dispatchChatAutoArchive(auditCtx, enqueueCtx context.Context, tickStart time.Time, autoArchiveDays, retentionDays int32, archived []database.AutoArchiveInactiveChatsRow) { |
| 537 | // Children inherit their root's archival decision and are skipped |
| 538 | // for both audit and digest. Partition once so the two loops |
| 539 | // cannot drift apart if the cascade shape ever changes. |
| 540 | roots := slice.Filter(archived, func(r database.AutoArchiveInactiveChatsRow) bool { |
| 541 | return !r.ParentChatID.Valid |
| 542 | }) |
| 543 | |
| 544 | auditor := *i.auditor.Load() |
| 545 | for _, row := range roots { |
| 546 | after := chatFromAutoArchiveRow(i.logger, row) |
| 547 | before := after |
| 548 | before.Archived = false |
| 549 | audit.BackgroundAudit(auditCtx, &audit.BackgroundAuditParams[database.Chat]{ |
| 550 | Audit: auditor, |
| 551 | Log: i.logger, |
| 552 | UserID: row.OwnerID, |
| 553 | OrganizationID: row.OrganizationID, |
| 554 | Action: database.AuditActionWrite, |
| 555 | Old: before, |
| 556 | New: after, |
| 557 | Status: http.StatusOK, |
| 558 | AdditionalFields: audit.BackgroundTaskFieldsBytes(auditCtx, i.logger, audit.BackgroundSubsystemChatAutoArchive), |
| 559 | }) |
| 560 | } |
| 561 | |
| 562 | // Group archived roots by owner. Inline because this is the |
| 563 | // only call site and the loop body is self-explanatory. |
| 564 | rootsByOwner := make(map[uuid.UUID][]database.AutoArchiveInactiveChatsRow, len(roots)) |
| 565 | for _, row := range roots { |
| 566 | rootsByOwner[row.OwnerID] = append(rootsByOwner[row.OwnerID], row) |
| 567 | } |
| 568 | |
| 569 | // Sort owner IDs so shutdown abandons a deterministic tail of the dispatch list. |
| 570 | ownerIDs := make([]uuid.UUID, 0, len(rootsByOwner)) |
| 571 | for id := range rootsByOwner { |
| 572 | ownerIDs = append(ownerIDs, id) |
| 573 | } |
| 574 | slices.SortFunc(ownerIDs, func(a, b uuid.UUID) int { |
| 575 | return cmp.Compare(a.String(), b.String()) |
| 576 | }) |
| 577 | |
| 578 | dispatched := 0 |
| 579 | for _, ownerID := range ownerIDs { |
| 580 | // Check between iterations so shutdown unblocks promptly. A |
| 581 | // hung in-flight enqueue is unblocked by enqueueCtx propagating |
| 582 | // cancellation into the DB call. Skipped owners are not |
| 583 | // re-notified on the next tick because AutoArchiveInactiveChats |
| 584 | // only returns rows with archived = false; we accept that |
| 585 | // tradeoff over hanging shutdown. |
| 586 | if err := enqueueCtx.Err(); err != nil { |
| 587 | i.logger.Warn(enqueueCtx, "chat auto-archive digest dispatch canceled", |
| 588 | slog.F("remaining_owners", len(ownerIDs)-dispatched), |
| 589 | slog.Error(err)) |
| 590 | return |
| 591 | } |
| 592 | dispatched++ |
| 593 |
no test coverage detected