perf: expand gateway capacity and prevent receipt replay

This commit is contained in:
hectorzhao
2026-08-25 16:08:30 +08:00
parent 761c123b65
commit 9292352be1
48 changed files with 2001 additions and 144 deletions
+128
View File
@@ -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()
}
@@ -0,0 +1,34 @@
package protocollog
import (
"context"
"testing"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
)
func TestNewUsesLocalRedisWhenURLIsEmpty(t *testing.T) {
publisher, err := New("")
if err != nil {
t.Fatalf("New returned error: %v", err)
}
if publisher == nil || publisher.Redis == nil {
t.Fatal("expected an initialized Redis client")
}
if got := publisher.Redis.Options().Addr; got != "127.0.0.1:6379" {
t.Fatalf("expected local Redis fallback, got %q", got)
}
}
func TestFailuresAreNeverSampledOut(t *testing.T) {
mr := miniredis.RunT(t)
client := redis.NewClient(&redis.Options{Addr: mr.Addr()})
p := &Publisher{Redis: client, SuccessSampleRate: 1, MaxLen: 100}
if err := p.Publish(context.Background(), Event{Protocol: "cmpp", EventType: "submit", Status: "failed", ResultCode: "TIMEOUT"}); err != nil {
t.Fatal(err)
}
if client.XLen(context.Background(), p.StreamName()).Val() != 1 {
t.Fatal("failed protocol event must be retained")
}
}