mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
feat: first commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user