mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
96 lines
2.0 KiB
Go
96 lines
2.0 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/snip/backend/internal/port"
|
|
)
|
|
|
|
// AsyncClickRecorder coalesces clicks in memory and flushes them to the
|
|
// repository in batches. This keeps the redirect path free of synchronous DB
|
|
// writes and collapses bursts (e.g. 1000 clicks/sec on one code) into a single
|
|
// periodic UPDATE.
|
|
type AsyncClickRecorder struct {
|
|
repo port.LinkRepository
|
|
interval time.Duration
|
|
|
|
mu sync.Mutex
|
|
pending map[string]int64
|
|
|
|
stop chan struct{}
|
|
done chan struct{}
|
|
}
|
|
|
|
func NewAsyncClickRecorder(repo port.LinkRepository, interval time.Duration) *AsyncClickRecorder {
|
|
if interval <= 0 {
|
|
interval = 10 * time.Second
|
|
}
|
|
return &AsyncClickRecorder{
|
|
repo: repo,
|
|
interval: interval,
|
|
pending: make(map[string]int64),
|
|
stop: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Record is non-blocking: it just bumps an in-memory counter.
|
|
func (r *AsyncClickRecorder) Record(code string) {
|
|
r.mu.Lock()
|
|
r.pending[code]++
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
// Start runs the flush loop until ctx is cancelled or Stop is called.
|
|
func (r *AsyncClickRecorder) Start(ctx context.Context) {
|
|
go func() {
|
|
defer close(r.done)
|
|
t := time.NewTicker(r.interval)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
r.flush(context.Background())
|
|
return
|
|
case <-r.stop:
|
|
r.flush(context.Background())
|
|
return
|
|
case <-t.C:
|
|
r.flush(ctx)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Stop flushes and halts the loop.
|
|
func (r *AsyncClickRecorder) Stop() {
|
|
select {
|
|
case <-r.stop:
|
|
default:
|
|
close(r.stop)
|
|
}
|
|
<-r.done
|
|
}
|
|
|
|
func (r *AsyncClickRecorder) flush(ctx context.Context) {
|
|
r.mu.Lock()
|
|
batch := r.pending
|
|
r.pending = make(map[string]int64)
|
|
r.mu.Unlock()
|
|
|
|
if len(batch) == 0 {
|
|
return
|
|
}
|
|
day := time.Now().UTC().Truncate(24 * time.Hour)
|
|
for code, n := range batch {
|
|
if err := r.repo.RecordClicks(ctx, code, day, n); err != nil {
|
|
// Re-queue on failure so clicks aren't lost.
|
|
r.mu.Lock()
|
|
r.pending[code] += n
|
|
r.mu.Unlock()
|
|
}
|
|
}
|
|
}
|