fix: sync scoped connection and outbox exclusion updates
This commit is contained in:
parent
aa21bd04e1
commit
cbccd6a8d9
58 changed files with 919 additions and 1435 deletions
|
|
@ -1,4 +1,4 @@
|
|||
// Package redisstore 用 Redis 实现高频易失态的存储接口(第一阶段:SessionStore)。
|
||||
// Package redisstore 用 Redis 实现高频、易失且可重建的短状态、缓存、计数器与限流。
|
||||
//
|
||||
// 职责边界见 docs/persistence-layer.md §1:Redis 存「态与计数」,丢失可由 PG/协议恢复。
|
||||
package redisstore
|
||||
|
|
|
|||
|
|
@ -1,76 +0,0 @@
|
|||
package redisstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// DefaultSessionTTL 是 session 记录的默认过期时间。
|
||||
// session 是连接态:过期或丢失后,客户端重连会触发 new_session_created / bad_server_salt 重建,
|
||||
// 因此 TTL 不必很长。每个随机 session_id 都落一条记录且断连不删,过长的 TTL
|
||||
// 只会堆积死 session(移动端每次重连一条)。7 天足够覆盖常规离线窗口。
|
||||
const DefaultSessionTTL = 7 * 24 * time.Hour
|
||||
|
||||
// SessionStore 用 Redis 实现 store.SessionStore。
|
||||
type SessionStore struct {
|
||||
c *redis.Client
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewSessionStore 创建 Redis SessionStore。ttl<=0 表示永不过期。
|
||||
func NewSessionStore(c *redis.Client, ttl time.Duration) *SessionStore {
|
||||
return &SessionStore{c: c, ttl: ttl}
|
||||
}
|
||||
|
||||
func sessionKey(id int64) string {
|
||||
return fmt.Sprintf("session:%d", id)
|
||||
}
|
||||
|
||||
// sessionValue 是 SessionData 在 Redis 中的序列化形态(不含 ID,ID 即 key)。
|
||||
type sessionValue struct {
|
||||
AuthKeyID [8]byte `json:"auth_key_id"`
|
||||
Salt int64 `json:"salt"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
}
|
||||
|
||||
// Save 实现 store.SessionStore。
|
||||
func (s *SessionStore) Save(ctx context.Context, d store.SessionData) error {
|
||||
v, err := json.Marshal(sessionValue{AuthKeyID: d.AuthKeyID, Salt: d.Salt, LastSeen: d.LastSeen})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal session: %w", err)
|
||||
}
|
||||
if err := s.c.Set(ctx, sessionKey(d.ID), v, s.ttl).Err(); err != nil {
|
||||
return fmt.Errorf("redis set session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 实现 store.SessionStore。不存在时 found=false。
|
||||
func (s *SessionStore) Get(ctx context.Context, id int64) (store.SessionData, bool, error) {
|
||||
raw, err := s.c.Get(ctx, sessionKey(id)).Bytes()
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.Nil) {
|
||||
return store.SessionData{}, false, nil
|
||||
}
|
||||
return store.SessionData{}, false, fmt.Errorf("redis get session: %w", err)
|
||||
}
|
||||
var v sessionValue
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
return store.SessionData{}, false, fmt.Errorf("unmarshal session: %w", err)
|
||||
}
|
||||
return store.SessionData{ID: id, AuthKeyID: v.AuthKeyID, Salt: v.Salt, LastSeen: v.LastSeen}, true, nil
|
||||
}
|
||||
|
||||
func (s *SessionStore) Delete(ctx context.Context, id int64) error {
|
||||
if err := s.c.Del(ctx, sessionKey(id)).Err(); err != nil {
|
||||
return fmt.Errorf("redis delete session: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package redisstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
// TestSessionStoreRoundTrip 验证 session 落 Redis 后能用全新 store 实例原样读回。
|
||||
// 未设 TELESRV_TEST_REDIS_ADDR 则跳过。
|
||||
func TestSessionStoreRoundTrip(t *testing.T) {
|
||||
addr := os.Getenv("TELESRV_TEST_REDIS_ADDR")
|
||||
if addr == "" {
|
||||
t.Skip("set TELESRV_TEST_REDIS_ADDR to run redis integration test")
|
||||
}
|
||||
ctx := context.Background()
|
||||
c, err := Open(ctx, addr, "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = c.Close() })
|
||||
|
||||
want := store.SessionData{
|
||||
ID: 0x1234beef,
|
||||
AuthKeyID: [8]byte{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
Salt: 42,
|
||||
LastSeen: 1000,
|
||||
}
|
||||
t.Cleanup(func() { _ = c.Del(ctx, sessionKey(want.ID)).Err() })
|
||||
|
||||
if err := NewSessionStore(c, time.Minute).Save(ctx, want); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
|
||||
got, found, err := NewSessionStore(c, time.Minute).Get(ctx, want.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("session not found after save")
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("round trip mismatch: got %+v want %+v", got, want)
|
||||
}
|
||||
|
||||
if _, found, _ := NewSessionStore(c, time.Minute).Get(ctx, 999999); found {
|
||||
t.Fatal("unexpected found for missing session")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue