feat(chat): implement sequence pre-allocation, versioned sync, and push worker
Introduce several performance and synchronization enhancements: - Implement `SeqBufferManager` to allow sequence number pre-allocation via Redis Lua scripts, reducing atomic increment overhead. - Add `PushWorker` to handle asynchronous message pushing using Redis Streams. - Implement incremental conversation synchronization via `ConversationVersionLog` to allow clients to fetch only recent changes. - Add support for Gzip compression in WebSocket communications to reduce bandwidth usage. - Update dependency injection and configuration to support these new components.
This commit is contained in:
71
internal/pkg/ws/compress.go
Normal file
71
internal/pkg/ws/compress.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const gzipMagicByte = 0x1F
|
||||
|
||||
// Compressor 管理 gzip Writer/Reader 的 sync.Pool 复用
|
||||
type Compressor struct {
|
||||
writerPool sync.Pool
|
||||
readerPool sync.Pool
|
||||
level int
|
||||
}
|
||||
|
||||
// NewCompressor 创建 Compressor(level: gzip.DefaultCompression/BestSpeed/BestCompression 等)
|
||||
func NewCompressor(level int) *Compressor {
|
||||
return &Compressor{
|
||||
level: level,
|
||||
writerPool: sync.Pool{
|
||||
New: func() any {
|
||||
w, _ := gzip.NewWriterLevel(io.Discard, level)
|
||||
return w
|
||||
},
|
||||
},
|
||||
readerPool: sync.Pool{
|
||||
New: func() any { return new(gzip.Reader) },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Compress 压缩数据,返回 gzip 字节
|
||||
func (c *Compressor) Compress(data []byte) ([]byte, error) {
|
||||
buf := &bytes.Buffer{}
|
||||
w := c.writerPool.Get().(*gzip.Writer)
|
||||
w.Reset(buf)
|
||||
if _, err := w.Write(data); err != nil {
|
||||
c.writerPool.Put(w)
|
||||
return nil, err
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
c.writerPool.Put(w)
|
||||
return nil, err
|
||||
}
|
||||
c.writerPool.Put(w)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Decompress 解压 gzip 数据
|
||||
func (c *Compressor) Decompress(data []byte) ([]byte, error) {
|
||||
r := c.readerPool.Get().(*gzip.Reader)
|
||||
if err := r.Reset(bytes.NewReader(data)); err != nil {
|
||||
c.readerPool.Put(r)
|
||||
return nil, err
|
||||
}
|
||||
out, err := io.ReadAll(r)
|
||||
r.Close()
|
||||
c.readerPool.Put(r)
|
||||
return out, err
|
||||
}
|
||||
|
||||
// IsCompressed 检查数据是否以 gzip magic byte 开头
|
||||
func IsCompressed(data []byte) bool {
|
||||
return len(data) > 0 && data[0] == gzipMagicByte
|
||||
}
|
||||
|
||||
// DefaultCompressor 全局默认压缩器(gzip.DefaultCompression = level 6)
|
||||
var DefaultCompressor = NewCompressor(gzip.DefaultCompression)
|
||||
94
internal/pkg/ws/compress_test.go
Normal file
94
internal/pkg/ws/compress_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompressDecompressRoundTrip(t *testing.T) {
|
||||
c := NewCompressor(6)
|
||||
|
||||
original := []byte(`{"type":"chat_message","payload":{"conversation_id":"conv1","message":"hello world"}}`)
|
||||
|
||||
compressed, err := c.Compress(original)
|
||||
if err != nil {
|
||||
t.Fatalf("Compress failed: %v", err)
|
||||
}
|
||||
|
||||
if len(compressed) >= len(original) {
|
||||
t.Logf("warning: compressed size (%d) >= original size (%d), but this can happen for small payloads", len(compressed), len(original))
|
||||
}
|
||||
|
||||
if !IsCompressed(compressed) {
|
||||
t.Fatal("IsCompressed should return true for gzip data")
|
||||
}
|
||||
|
||||
decompressed, err := c.Decompress(compressed)
|
||||
if err != nil {
|
||||
t.Fatalf("Decompress failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(decompressed, original) {
|
||||
t.Fatalf("round-trip mismatch: got %q, want %q", decompressed, original)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCompressed(t *testing.T) {
|
||||
if IsCompressed([]byte{}) {
|
||||
t.Fatal("empty data should not be considered compressed")
|
||||
}
|
||||
if IsCompressed([]byte{0x00}) {
|
||||
t.Fatal("0x00 should not be considered gzip magic byte")
|
||||
}
|
||||
if !IsCompressed([]byte{0x1F, 0x8B}) {
|
||||
t.Fatal("gzip magic bytes should be detected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecompressNonGzipData(t *testing.T) {
|
||||
c := NewCompressor(6)
|
||||
_, err := c.Decompress([]byte("not gzip data"))
|
||||
if err == nil {
|
||||
t.Fatal("expected error when decompressing non-gzip data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCompressor(t *testing.T) {
|
||||
data := []byte("test data for default compressor")
|
||||
compressed, err := DefaultCompressor.Compress(data)
|
||||
if err != nil {
|
||||
t.Fatalf("DefaultCompressor.Compress failed: %v", err)
|
||||
}
|
||||
decompressed, err := DefaultCompressor.Decompress(compressed)
|
||||
if err != nil {
|
||||
t.Fatalf("DefaultCompressor.Decompress failed: %v", err)
|
||||
}
|
||||
if !bytes.Equal(decompressed, data) {
|
||||
t.Fatalf("DefaultCompressor round-trip mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompress(b *testing.B) {
|
||||
c := NewCompressor(6)
|
||||
data := bytes.Repeat([]byte(`{"type":"chat_message","payload":{"message":"hello"}}`), 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := c.Compress(data)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecompress(b *testing.B) {
|
||||
c := NewCompressor(6)
|
||||
data := bytes.Repeat([]byte(`{"type":"chat_message","payload":{"message":"hello"}}`), 10)
|
||||
compressed, _ := c.Compress(data)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := c.Decompress(compressed)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,12 @@ type Event struct {
|
||||
|
||||
// Client 表示一个WebSocket客户端连接
|
||||
type Client struct {
|
||||
ID uint64
|
||||
UserID string
|
||||
Send chan []byte
|
||||
Quit chan struct{}
|
||||
PendingAcks map[string]time.Time
|
||||
ID uint64
|
||||
UserID string
|
||||
Send chan []byte
|
||||
Quit chan struct{}
|
||||
PendingAcks map[string]time.Time
|
||||
CompressEnabled bool // 客户端是否启用 gzip 压缩
|
||||
}
|
||||
|
||||
// ErrConnectionLimit 连接数限制错误
|
||||
|
||||
Reference in New Issue
Block a user