perf: expand gateway capacity and prevent receipt replay
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
package protocollog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
const defaultStream = "gateway.protocol.logs"
|
||||
|
||||
type Event struct {
|
||||
EventID string `json:"eventId"`
|
||||
GatewayInstanceID string `json:"gatewayInstanceId,omitempty"`
|
||||
ConnectionID string `json:"connectionId,omitempty"`
|
||||
Protocol string `json:"protocol"`
|
||||
Direction string `json:"direction"`
|
||||
EventType string `json:"eventType"`
|
||||
Status string `json:"status"`
|
||||
TenantID string `json:"tenantId,omitempty"`
|
||||
ApplicationID string `json:"applicationId,omitempty"`
|
||||
ChannelID string `json:"channelId,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
MessageID string `json:"messageId,omitempty"`
|
||||
SubmitID string `json:"submitId,omitempty"`
|
||||
GatewayMessageID string `json:"gatewayMessageId,omitempty"`
|
||||
Phone string `json:"phone,omitempty"`
|
||||
ResultCode string `json:"resultCode,omitempty"`
|
||||
DurationMs int `json:"durationMs,omitempty"`
|
||||
PayloadBytes int `json:"payloadBytes,omitempty"`
|
||||
Detail map[string]any `json:"detail,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type Publisher struct {
|
||||
Redis *redis.Client
|
||||
Stream string
|
||||
GatewayInstanceID string
|
||||
SuccessSampleRate int
|
||||
MaxLen int64
|
||||
published atomic.Int64
|
||||
sampled atomic.Int64
|
||||
errors atomic.Int64
|
||||
}
|
||||
|
||||
func New(redisURL string) (*Publisher, error) {
|
||||
if strings.TrimSpace(redisURL) == "" {
|
||||
redisURL = "redis://127.0.0.1:6379"
|
||||
}
|
||||
options, err := redis.ParseURL(redisURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Publisher{Redis: redis.NewClient(options)}, nil
|
||||
}
|
||||
|
||||
func (p *Publisher) Publish(ctx context.Context, event Event) error {
|
||||
if p == nil || p.Redis == nil {
|
||||
return fmt.Errorf("protocol log Redis publisher is unavailable")
|
||||
}
|
||||
if event.CreatedAt.IsZero() {
|
||||
event.CreatedAt = time.Now().UTC()
|
||||
}
|
||||
if event.GatewayInstanceID == "" {
|
||||
event.GatewayInstanceID = p.GatewayInstanceID
|
||||
}
|
||||
if event.EventID == "" {
|
||||
event.EventID = fmt.Sprintf("PL-%d-%d", event.CreatedAt.UnixNano(), p.published.Load()+p.sampled.Load()+1)
|
||||
}
|
||||
if !p.mustKeep(event) && !p.sample(event) {
|
||||
p.sampled.Add(1)
|
||||
return nil
|
||||
}
|
||||
data, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
p.errors.Add(1)
|
||||
return err
|
||||
}
|
||||
args := &redis.XAddArgs{Stream: p.stream(), Values: map[string]any{"data": string(data)}}
|
||||
if p.MaxLen > 0 {
|
||||
args.MaxLen = p.MaxLen
|
||||
args.Approx = true
|
||||
}
|
||||
if err := p.Redis.XAdd(ctx, args).Err(); err != nil {
|
||||
p.errors.Add(1)
|
||||
return err
|
||||
}
|
||||
p.published.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Publisher) mustKeep(event Event) bool {
|
||||
if event.Status != "success" {
|
||||
return true
|
||||
}
|
||||
code := strings.TrimSpace(event.ResultCode)
|
||||
return code != "" && code != "0"
|
||||
}
|
||||
|
||||
func (p *Publisher) sample(event Event) bool {
|
||||
rate := p.SuccessSampleRate
|
||||
if rate <= 0 {
|
||||
rate = 10
|
||||
}
|
||||
if rate >= 100 {
|
||||
return true
|
||||
}
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(event.ChannelID + "|" + event.MessageID + "|" + event.EventType + "|" + strconv.FormatInt(event.CreatedAt.UnixNano()/int64(time.Millisecond), 10)))
|
||||
return int(h.Sum32()%100) < rate
|
||||
}
|
||||
|
||||
func (p *Publisher) stream() string {
|
||||
if strings.TrimSpace(p.Stream) != "" {
|
||||
return p.Stream
|
||||
}
|
||||
return defaultStream
|
||||
}
|
||||
func (p *Publisher) StreamName() string { return p.stream() }
|
||||
func (p *Publisher) Counts() (int64, int64, int64) {
|
||||
return p.published.Load(), p.sampled.Load(), p.errors.Load()
|
||||
}
|
||||
Reference in New Issue
Block a user