owpengram-server/internal/rpc/account_lifecycle_worker.go
2026-09-01 12:06:31 +03:00

68 lines
2 KiB
Go

package rpc
import (
"context"
"time"
"go.uber.org/zap"
"telesrv/internal/domain"
)
type accountLifecycleWorkerService interface {
SweepDueAccountDeletions(ctx context.Context, now time.Time, limit int) ([]domain.AccountDeletionResult, error)
}
// RunAccountLifecycle executes all due account deletion sources through one
// tombstone path. Deleted-user projections converge from authoritative reads;
// updateUser is non-PTS and therefore is not queued as a correctness signal.
func (r *Router) RunAccountLifecycle(ctx context.Context, interval time.Duration, batch int) {
if interval <= 0 {
interval = time.Minute
}
if batch <= 0 {
batch = 500
}
r.runAccountLifecycleOnce(ctx, batch)
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
r.runAccountLifecycleOnce(ctx, batch)
}
}
}
func (r *Router) runAccountLifecycleOnce(ctx context.Context, batch int) {
svc, ok := r.deps.Account.(accountLifecycleWorkerService)
if !ok {
return
}
now := r.clock.Now().UTC()
sweepCtx, cancel := context.WithTimeout(ctx, 45*time.Second)
results, err := svc.SweepDueAccountDeletions(sweepCtx, now, batch)
cancel()
changed := false
for _, result := range results {
if !result.Changed {
continue
}
changed = true
r.finishDeletedAccountAuthorizations(context.Background(), result.User.ID, result.RevokedAuthorizations)
}
if changed {
// One flush covers the entire due batch. Per-user predicate invalidation
// would scan the same large projection maps four times for every account.
r.flushRPCProjectionCache()
}
if err != nil {
// SweepDueAccountDeletions may return already-committed results before a
// later candidate fails. Always finish those sessions/caches; the failed
// and remaining candidates are retried from their authoritative due rows
// on the next tick.
r.log.Warn("account lifecycle deletion sweep partially failed", zap.Int("completed", len(results)), zap.Error(err))
}
}