Raw
 1package session
 2
 3import (
 4	"context"
 5	"sync"
 6	"sync/atomic"
 7	"time"
 8
 9	"tug/internal/storage"
10)
11
12const (
13	SeenInterval   = 5 * time.Minute
14	SeenFlushEvery = 5 * time.Second
15	SeenPendingMax = 1024
16)
17
18type seenGate struct {
19	interval time.Duration
20	mu       sync.Mutex
21	pending  map[string]time.Time
22	attempts atomic.Int64
23	dropped  atomic.Int64
24}
25
26func (s *Service) touch(row storage.ForwardSession, now time.Time) {
27	if row.LastSeenAt != nil && now.Sub(*row.LastSeenAt) < s.seen.interval {
28		return
29	}
30	s.seen.mu.Lock()
31	defer s.seen.mu.Unlock()
32	if _, ok := s.seen.pending[row.ID]; ok {
33		return
34	}
35	if len(s.seen.pending) >= SeenPendingMax {
36		s.seen.dropped.Add(1)
37		return
38	}
39	if s.seen.pending == nil {
40		s.seen.pending = make(map[string]time.Time)
41	}
42	s.seen.pending[row.ID] = now
43}
44
45func (s *Service) flushSeen(ctx context.Context) {
46	s.seen.mu.Lock()
47	batch := s.seen.pending
48	s.seen.pending = nil
49	s.seen.mu.Unlock()
50	if len(batch) == 0 {
51		return
52	}
53	seen := make([]storage.SessionSeen, 0, len(batch))
54	for id, at := range batch {
55		seen = append(seen, storage.SessionSeen{ID: id, At: at, StaleBefore: at.Add(-s.seen.interval)})
56	}
57	s.seen.attempts.Add(1)
58	s.store.TouchForwardSessions(ctx, seen)
59}