merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
291
internal/rpc/presence_last_seen_batch.go
Normal file
291
internal/rpc/presence_last_seen_batch.go
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPresenceLastSeenBatchMax = 512
|
||||
defaultPresenceLastSeenBatchWait = time.Second
|
||||
defaultPresenceLastSeenBatchQueue = 65_536
|
||||
defaultPresenceLastSeenBatchTimeout = 5 * time.Second
|
||||
defaultPresenceLastSeenShutdownDrain = 10 * time.Second
|
||||
presenceLastSeenRetryInitial = 50 * time.Millisecond
|
||||
presenceLastSeenRetryMaximum = 2 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
errPresenceLastSeenBatchFull = errors.New("presence last-seen batch queue full")
|
||||
errPresenceLastSeenBatchStopped = errors.New("presence last-seen batch dispatcher stopped")
|
||||
)
|
||||
|
||||
type presenceLastSeenBatchUpdater interface {
|
||||
UpdateLastSeenBatch(ctx context.Context, updates []store.UserLastSeenUpdate) error
|
||||
}
|
||||
|
||||
type presenceLastSeenBatchConfig struct {
|
||||
MaxSize int
|
||||
MaxWait time.Duration
|
||||
QueueSize int
|
||||
QueryTimeout time.Duration
|
||||
DrainTimeout time.Duration
|
||||
}
|
||||
|
||||
func normalizePresenceLastSeenBatchConfig(cfg presenceLastSeenBatchConfig) presenceLastSeenBatchConfig {
|
||||
if cfg.MaxSize <= 0 {
|
||||
cfg.MaxSize = defaultPresenceLastSeenBatchMax
|
||||
}
|
||||
if cfg.MaxWait <= 0 {
|
||||
cfg.MaxWait = defaultPresenceLastSeenBatchWait
|
||||
}
|
||||
if cfg.QueueSize <= 0 {
|
||||
cfg.QueueSize = defaultPresenceLastSeenBatchQueue
|
||||
}
|
||||
if cfg.QueryTimeout <= 0 {
|
||||
cfg.QueryTimeout = defaultPresenceLastSeenBatchTimeout
|
||||
}
|
||||
if cfg.DrainTimeout <= 0 {
|
||||
cfg.DrainTimeout = defaultPresenceLastSeenShutdownDrain
|
||||
}
|
||||
return cfg
|
||||
}
|
||||
|
||||
// presenceLastSeenBatchDispatcher is the single production writer for
|
||||
// asynchronous lifecycle last-seen watermarks. Accepted work is retried with a
|
||||
// bounded backoff and remains in memory until the database write plus exact
|
||||
// cache invalidation succeeds, or the bounded shutdown drain expires.
|
||||
type presenceLastSeenBatchDispatcher struct {
|
||||
updater presenceLastSeenBatchUpdater
|
||||
cfg presenceLastSeenBatchConfig
|
||||
queue chan store.UserLastSeenUpdate
|
||||
log *zap.Logger
|
||||
metrics Metrics
|
||||
|
||||
gate sync.RWMutex
|
||||
accepting bool
|
||||
}
|
||||
|
||||
func newPresenceLastSeenBatchDispatcher(
|
||||
updater presenceLastSeenBatchUpdater,
|
||||
cfg presenceLastSeenBatchConfig,
|
||||
log *zap.Logger,
|
||||
metrics Metrics,
|
||||
) *presenceLastSeenBatchDispatcher {
|
||||
if updater == nil {
|
||||
return nil
|
||||
}
|
||||
cfg = normalizePresenceLastSeenBatchConfig(cfg)
|
||||
if log == nil {
|
||||
log = zap.NewNop()
|
||||
}
|
||||
if metrics == nil {
|
||||
metrics = NopMetrics{}
|
||||
}
|
||||
return &presenceLastSeenBatchDispatcher{
|
||||
updater: updater,
|
||||
cfg: cfg,
|
||||
queue: make(chan store.UserLastSeenUpdate, cfg.QueueSize),
|
||||
log: log,
|
||||
metrics: metrics,
|
||||
accepting: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *presenceLastSeenBatchDispatcher) submit(update store.UserLastSeenUpdate) error {
|
||||
if d == nil || update.UserID == 0 || update.LastSeenAt <= 0 {
|
||||
return nil
|
||||
}
|
||||
d.gate.RLock()
|
||||
defer d.gate.RUnlock()
|
||||
if !d.accepting {
|
||||
return errPresenceLastSeenBatchStopped
|
||||
}
|
||||
d.metrics.PresenceLastSeenPending(1)
|
||||
select {
|
||||
case d.queue <- update:
|
||||
d.metrics.PresenceLastSeenSubmitted()
|
||||
return nil
|
||||
default:
|
||||
d.metrics.PresenceLastSeenPending(-1)
|
||||
d.metrics.PresenceLastSeenOverflow()
|
||||
return errPresenceLastSeenBatchFull
|
||||
}
|
||||
}
|
||||
|
||||
func (d *presenceLastSeenBatchDispatcher) stopAccepting() {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
d.gate.Lock()
|
||||
d.accepting = false
|
||||
d.gate.Unlock()
|
||||
}
|
||||
|
||||
func (d *presenceLastSeenBatchDispatcher) Run(ctx context.Context) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case first := <-d.queue:
|
||||
batch, canceled := d.collect(ctx, first)
|
||||
if canceled || !d.executeWithRetry(ctx, batch) {
|
||||
d.stopAccepting()
|
||||
d.drain(batch)
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
d.stopAccepting()
|
||||
d.drain(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *presenceLastSeenBatchDispatcher) collect(
|
||||
ctx context.Context,
|
||||
first store.UserLastSeenUpdate,
|
||||
) ([]store.UserLastSeenUpdate, bool) {
|
||||
batch := make([]store.UserLastSeenUpdate, 0, d.cfg.MaxSize)
|
||||
batch = append(batch, first)
|
||||
if len(batch) >= d.cfg.MaxSize {
|
||||
return batch, false
|
||||
}
|
||||
timer := time.NewTimer(d.cfg.MaxWait)
|
||||
defer func() {
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
for len(batch) < d.cfg.MaxSize {
|
||||
select {
|
||||
case update := <-d.queue:
|
||||
batch = append(batch, update)
|
||||
case <-timer.C:
|
||||
return batch, false
|
||||
case <-ctx.Done():
|
||||
return batch, true
|
||||
}
|
||||
}
|
||||
return batch, false
|
||||
}
|
||||
|
||||
func mergePresenceLastSeenBatch(updates []store.UserLastSeenUpdate) []store.UserLastSeenUpdate {
|
||||
latest := make(map[int64]int, len(updates))
|
||||
for _, update := range updates {
|
||||
if update.UserID == 0 || update.LastSeenAt <= 0 {
|
||||
continue
|
||||
}
|
||||
if current := latest[update.UserID]; update.LastSeenAt > current {
|
||||
latest[update.UserID] = update.LastSeenAt
|
||||
}
|
||||
}
|
||||
userIDs := make([]int64, 0, len(latest))
|
||||
for userID := range latest {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
|
||||
merged := make([]store.UserLastSeenUpdate, 0, len(userIDs))
|
||||
for _, userID := range userIDs {
|
||||
merged = append(merged, store.UserLastSeenUpdate{UserID: userID, LastSeenAt: latest[userID]})
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func (d *presenceLastSeenBatchDispatcher) executeWithRetry(
|
||||
ctx context.Context,
|
||||
updates []store.UserLastSeenUpdate,
|
||||
) bool {
|
||||
rawCount := len(updates)
|
||||
updates = mergePresenceLastSeenBatch(updates)
|
||||
if len(updates) == 0 {
|
||||
return true
|
||||
}
|
||||
backoff := presenceLastSeenRetryInitial
|
||||
for attempt := 1; ; attempt++ {
|
||||
queryCtx, cancel := context.WithTimeout(ctx, d.cfg.QueryTimeout)
|
||||
started := time.Now()
|
||||
err := d.updater.UpdateLastSeenBatch(queryCtx, updates)
|
||||
cancel()
|
||||
d.metrics.PresenceLastSeenBatch(len(updates), time.Since(started), err)
|
||||
if err == nil {
|
||||
d.metrics.PresenceLastSeenPending(-rawCount)
|
||||
return true
|
||||
}
|
||||
if attempt == 1 || attempt&(attempt-1) == 0 {
|
||||
d.log.Warn("presence last-seen batch failed; retrying",
|
||||
zap.Int("updates", len(updates)),
|
||||
zap.Int("attempt", attempt),
|
||||
zap.Duration("retry_after", backoff),
|
||||
zap.Error(err))
|
||||
}
|
||||
timer := time.NewTimer(backoff)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
backoff *= 2
|
||||
if backoff > presenceLastSeenRetryMaximum {
|
||||
backoff = presenceLastSeenRetryMaximum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *presenceLastSeenBatchDispatcher) drain(current []store.UserLastSeenUpdate) {
|
||||
drainCtx, cancel := context.WithTimeout(context.Background(), d.cfg.DrainTimeout)
|
||||
defer cancel()
|
||||
pending := append([]store.UserLastSeenUpdate(nil), current...)
|
||||
for {
|
||||
for len(pending) < d.cfg.MaxSize {
|
||||
select {
|
||||
case update := <-d.queue:
|
||||
pending = append(pending, update)
|
||||
default:
|
||||
break
|
||||
}
|
||||
if len(d.queue) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(pending) > 0 {
|
||||
if !d.executeWithRetry(drainCtx, pending) {
|
||||
d.reportDrainDropped(len(pending) + len(d.queue))
|
||||
return
|
||||
}
|
||||
pending = pending[:0]
|
||||
}
|
||||
if len(d.queue) == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *presenceLastSeenBatchDispatcher) reportDrainDropped(count int) {
|
||||
if count <= 0 {
|
||||
return
|
||||
}
|
||||
d.metrics.PresenceLastSeenDrainDropped(count)
|
||||
d.metrics.PresenceLastSeenPending(-count)
|
||||
d.log.Error("presence last-seen shutdown drain expired", zap.Int("updates", count))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue