Files
team-api/internal/logic/common/usage_log_writer.go
T
2026-05-10 12:21:27 +08:00

233 lines
4.9 KiB
Go

package common
import (
"sync"
"sync/atomic"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)
// OverflowPolicy 队列满时的处理策略
type OverflowPolicy int
const (
OverflowDrop OverflowPolicy = iota // 丢弃新记录
OverflowBlock // 阻塞等待
OverflowSyncFallback // 降级为同步写入
)
// UsageLogWriterConfig 写入器配置
type UsageLogWriterConfig struct {
Table string
QueueSize int
BatchSize int
FlushInterval time.Duration
Workers int
Overflow OverflowPolicy
}
// WriterStats 写入器统计
type WriterStats struct {
Submitted int64 `json:"submitted"`
Completed int64 `json:"completed"`
Dropped int64 `json:"dropped"`
Failed int64 `json:"failed"`
QueueLen int `json:"queue_len"`
}
// UsageLogWriter channel + 固定 worker 的异步批量写入器
type UsageLogWriter struct {
table string
queue chan any
batchSize int
flushInterval time.Duration
workers int
overflow OverflowPolicy
submitted atomic.Int64
completed atomic.Int64
dropped atomic.Int64
failed atomic.Int64
stopCh chan struct{}
wg sync.WaitGroup
}
// DefaultUsageLogWriter 全局实例
var DefaultUsageLogWriter *UsageLogWriter
// InitUsageLogWriter 初始化全局写入器
func InitUsageLogWriter() {
DefaultUsageLogWriter = NewUsageLogWriter(UsageLogWriterConfig{
Table: "bil_usage_logs",
QueueSize: 8192,
BatchSize: 64,
FlushInterval: 3 * time.Second,
Workers: 4,
Overflow: OverflowDrop,
})
DefaultUsageLogWriter.Start()
}
// CloseUsageLogWriter 关闭全局写入器
func CloseUsageLogWriter() {
if DefaultUsageLogWriter != nil {
DefaultUsageLogWriter.Close()
}
}
// NewUsageLogWriter 创建写入器实例
func NewUsageLogWriter(cfg UsageLogWriterConfig) *UsageLogWriter {
if cfg.QueueSize <= 0 {
cfg.QueueSize = 8192
}
if cfg.BatchSize <= 0 {
cfg.BatchSize = 64
}
if cfg.FlushInterval <= 0 {
cfg.FlushInterval = 3 * time.Second
}
if cfg.Workers <= 0 {
cfg.Workers = 4
}
return &UsageLogWriter{
table: cfg.Table,
queue: make(chan any, cfg.QueueSize),
batchSize: cfg.BatchSize,
flushInterval: cfg.FlushInterval,
workers: cfg.Workers,
overflow: cfg.Overflow,
stopCh: make(chan struct{}),
}
}
// Start 启动所有 worker
func (w *UsageLogWriter) Start() {
for i := range w.workers {
w.wg.Add(1)
go w.runWorker(i)
}
}
// Submit 提交一条记录到队列
func (w *UsageLogWriter) Submit(record any) {
w.submitted.Add(1)
switch w.overflow {
case OverflowBlock:
w.queue <- record
case OverflowSyncFallback:
select {
case w.queue <- record:
default:
ctx := gctx.New()
if _, err := g.DB().Model(w.table).Ctx(ctx).Data(record).Insert(); err != nil {
w.failed.Add(1)
g.Log().Errorf(ctx, "usage_log_writer: sync fallback insert failed: %v", err)
} else {
w.completed.Add(1)
}
}
default: // OverflowDrop
select {
case w.queue <- record:
default:
w.dropped.Add(1)
}
}
}
// Stats 返回当前统计
func (w *UsageLogWriter) Stats() WriterStats {
return WriterStats{
Submitted: w.submitted.Load(),
Completed: w.completed.Load(),
Dropped: w.dropped.Load(),
Failed: w.failed.Load(),
QueueLen: len(w.queue),
}
}
// Close 优雅关闭:通知 worker 停止,排空队列
func (w *UsageLogWriter) Close() {
close(w.stopCh)
w.wg.Wait()
}
// runWorker 单个 worker 循环
func (w *UsageLogWriter) runWorker(id int) {
defer w.wg.Done()
defer func() {
if r := recover(); r != nil {
g.Log().Errorf(gctx.New(), "usage_log_writer: worker %d panic: %v, restarting", id, r)
w.wg.Add(1)
go w.runWorker(id)
}
}()
buf := make([]any, 0, w.batchSize)
ticker := time.NewTicker(w.flushInterval)
defer ticker.Stop()
flush := func() {
if len(buf) == 0 {
return
}
w.flushBatch(buf)
buf = buf[:0]
}
for {
select {
case record, ok := <-w.queue:
if !ok {
flush()
return
}
buf = append(buf, record)
if len(buf) >= w.batchSize {
flush()
}
case <-ticker.C:
flush()
case <-w.stopCh:
// 排空 channel 中剩余记录
for {
select {
case record, ok := <-w.queue:
if !ok {
flush()
return
}
buf = append(buf, record)
if len(buf) >= w.batchSize {
flush()
}
default:
flush()
return
}
}
}
}
}
// flushBatch 批量写入数据库
func (w *UsageLogWriter) flushBatch(records []any) {
if len(records) == 0 {
return
}
batch := make([]any, len(records))
copy(batch, records)
ctx := gctx.New()
_, err := g.DB().Model(w.table).Ctx(ctx).Data(batch).Batch(len(batch)).Insert()
if err != nil {
w.failed.Add(int64(len(batch)))
g.Log().Errorf(ctx, "usage_log_writer: batch insert %d records failed: %v", len(batch), err)
return
}
w.completed.Add(int64(len(batch)))
}