feat: complete shared database tenant sharding

This commit is contained in:
richjohnrambo
2026-09-23 02:19:07 +08:00
parent 1ec2feee47
commit e37b52c3c0
37 changed files with 946 additions and 1420 deletions
+74 -40
View File
@@ -41,7 +41,6 @@ type testVerificationStore struct {
}
type testHTTPInfrastructureStore struct {
records map[string]*types.HTTPIdempotencyRecord
deadLetters []types.KafkaDeadLetter
}
@@ -104,39 +103,6 @@ func (h *testPublicMediaHandler) UploadPublic(def *media.PublicFileDef, file io.
return def.Location, int64(len(data)), nil
}
func (s *testHTTPInfrastructureStore) recordKey(record *types.HTTPIdempotencyRecord) string {
return record.UserID.String() + "|" + record.Method + "|" + record.Route + "|" + record.Key
}
func (s *testHTTPInfrastructureStore) BeginIdempotency(_ types.TenantID, record *types.HTTPIdempotencyRecord) (*types.HTTPIdempotencyRecord, bool, error) {
if s.records == nil {
s.records = make(map[string]*types.HTTPIdempotencyRecord)
}
key := s.recordKey(record)
if existing := s.records[key]; existing != nil {
copy := *existing
copy.ResponseBody = append([]byte(nil), existing.ResponseBody...)
return &copy, false, nil
}
copy := *record
copy.State = types.HTTPIdempotencyProcessing
s.records[key] = &copy
return &copy, true, nil
}
func (s *testHTTPInfrastructureStore) CompleteIdempotency(_ types.TenantID, record *types.HTTPIdempotencyRecord) error {
copy := *record
copy.State = types.HTTPIdempotencyCompleted
copy.ResponseBody = append([]byte(nil), record.ResponseBody...)
s.records[s.recordKey(record)] = &copy
return nil
}
func (s *testHTTPInfrastructureStore) AbortIdempotency(_ types.TenantID, record *types.HTTPIdempotencyRecord) error {
delete(s.records, s.recordKey(record))
return nil
}
func (s *testHTTPInfrastructureStore) BeginKafkaConsume(_ types.TenantID, record *types.KafkaConsumeRecord, _ time.Time) (*types.KafkaConsumeRecord, bool, error) {
copy := *record
copy.Status = types.KafkaConsumeProcessing
@@ -692,8 +658,8 @@ func (s *testUserSecurityStore) CountUnreadSystemNotificationsByEventPrefix(_ ty
return s.unreadByPrefix[prefix], nil
}
func (s *testUserSecurityStore) MarkSystemNotificationRead(_ types.TenantID, _ types.Uid, _ string, _ time.Time) error {
return nil
func (s *testUserSecurityStore) MarkSystemNotificationRead(_ types.TenantID, _ types.Uid, eventID string, readAt time.Time) (*types.SystemNotification, error) {
return &types.SystemNotification{EventID: eventID, EventType: "system.notice", Title: "通知", Summary: "通知", ReadAt: &readAt, CreatedAt: readAt}, nil
}
func (s *testProfilesStore) Get(_ types.TenantID, uid types.Uid) (*types.UserProfile, error) {
@@ -1229,10 +1195,45 @@ func TestIMHTTPIdempotencyReplayAndConflict(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
expectIMBearer(t, ctrl, "token-1", 7, 42)
oldInfrastructure := store.HTTPInfrastructure
infrastructure := &testHTTPInfrastructureStore{}
store.HTTPInfrastructure = infrastructure
defer func() { store.HTTPInfrastructure = oldInfrastructure }()
oldBegin, oldComplete, oldAbort := beginIMHTTPIdempotency, completeIMHTTPIdempotency, abortIMHTTPIdempotency
type cachedResponse struct {
bodyHash string
replay *imHTTPIdempotencyReplay
}
records := map[string]*cachedResponse{}
recordKey := func(tenantID types.TenantID, uid types.Uid, method, route, key string) string {
return imHTTPIdempotencyID(tenantID, uid, method, route, key)
}
beginIMHTTPIdempotency = func(tenantID types.TenantID, uid types.Uid, method, route, key, bodyHash string) (*imHTTPIdempotencyReplay, bool, bool, error) {
id := recordKey(tenantID, uid, method, route, key)
if existing := records[id]; existing != nil {
if existing.bodyHash != bodyHash {
return nil, false, true, errIMHTTPIdempotencyConflict
}
if existing.replay == nil {
return nil, false, true, nil
}
copy := *existing.replay
copy.ResponseBody = append([]byte(nil), existing.replay.ResponseBody...)
return &copy, false, true, nil
}
records[id] = &cachedResponse{bodyHash: bodyHash}
return nil, true, true, nil
}
completeIMHTTPIdempotency = func(tenantID types.TenantID, uid types.Uid, method, route, key, bodyHash string, status int, body []byte) error {
records[recordKey(tenantID, uid, method, route, key)] = &cachedResponse{
bodyHash: bodyHash,
replay: &imHTTPIdempotencyReplay{BodyHash: bodyHash, ResponseStatus: status, ResponseBody: append([]byte(nil), body...)},
}
return nil
}
abortIMHTTPIdempotency = func(tenantID types.TenantID, uid types.Uid, method, route, key string) error {
delete(records, recordKey(tenantID, uid, method, route, key))
return nil
}
defer func() {
beginIMHTTPIdempotency, completeIMHTTPIdempotency, abortIMHTTPIdempotency = oldBegin, oldComplete, oldAbort
}()
called := 0
handler := withIMHTTPProtocol("/v1/im/", http.HandlerFunc(func(wrt http.ResponseWriter, req *http.Request) {
@@ -3085,6 +3086,26 @@ func TestServeIMSystemNotificationsListAndRead(t *testing.T) {
if security.readTenant != 7 || security.readUser != 42 || security.readNotificationID != "evt-1" || security.notifications[0].ReadAt == nil {
t.Fatalf("notification was not marked read: %#v", security)
}
readData := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if readData["id"] != "evt-1" || readData["status"] != "ok" || readData["read_at"] != security.notifications[0].ReadAt.UTC().Format(time.RFC3339Nano) {
t.Fatalf("read response did not return persisted notification state: %#v", readData)
}
expectIMBearer(t, ctrl, "token-read-again", 7, 42)
req = httptest.NewRequest(http.MethodPost, "/v1/im/system-notifications/evt-2/read", bytes.NewBufferString(`{"read_at":"2026-09-01T00:00:00Z"}`))
req.Header.Set("Authorization", "Bearer token-read-again")
req.Header.Set("Idempotency-Key", "read-2")
rec = httptest.NewRecorder()
serveIMSystemNotificationRead(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("read again status: got %d, want %d; body=%s", rec.Code, http.StatusOK, rec.Body.String())
}
readAgainData := decodeIMHTTPResponse(t, rec).Data.(map[string]any)
if readAgainData["id"] != "evt-2" || readAgainData["read_at"] != readAt.UTC().Format(time.RFC3339Nano) {
t.Fatalf("read again response should keep persisted read_at: %#v", readAgainData)
}
}
func TestServeIMMessageReceipts(t *testing.T) {
@@ -5211,6 +5232,19 @@ func TestServeIMFavoriteBatchCreateNormalizesP2PUserTopic(t *testing.T) {
}
}
func TestFavoriteSourcePayloadIncludesP2PPeer(t *testing.T) {
viewer := types.Uid(42)
peer := types.Uid(43)
topic := viewer.P2PName(peer)
source := favoriteSourcePayload(topic, 17, viewer)
if source["topic"] != topic || source["seq"] != int64(17) {
t.Fatalf("favorite source should keep stored topic and seq: %#v", source)
}
if source["peer_user_id"] != peer.UserId() || source["display_topic"] != peer.UserId() {
t.Fatalf("favorite source should expose addressable p2p peer: %#v", source)
}
}
func TestServeIMFavoriteForwardSavesMessages(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()