package session import ( "context" "sync" "sync/atomic" "time" "tug/internal/storage" ) const ( SeenInterval = 5 * time.Minute SeenFlushEvery = 5 * time.Second SeenPendingMax = 1024 ) type seenGate struct { interval time.Duration mu sync.Mutex pending map[string]time.Time attempts atomic.Int64 dropped atomic.Int64 } func (s *Service) touch(row storage.ForwardSession, now time.Time) { if row.LastSeenAt != nil && now.Sub(*row.LastSeenAt) < s.seen.interval { return } s.seen.mu.Lock() defer s.seen.mu.Unlock() if _, ok := s.seen.pending[row.ID]; ok { return } if len(s.seen.pending) >= SeenPendingMax { s.seen.dropped.Add(1) return } if s.seen.pending == nil { s.seen.pending = make(map[string]time.Time) } s.seen.pending[row.ID] = now } func (s *Service) flushSeen(ctx context.Context) { s.seen.mu.Lock() batch := s.seen.pending s.seen.pending = nil s.seen.mu.Unlock() if len(batch) == 0 { return } seen := make([]storage.SessionSeen, 0, len(batch)) for id, at := range batch { seen = append(seen, storage.SessionSeen{ID: id, At: at, StaleBefore: at.Add(-s.seen.interval)}) } s.seen.attempts.Add(1) s.store.TouchForwardSessions(ctx, seen) }