feat: complete shared database tenant sharding
This commit is contained in:
+74
-40
@@ -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 ©, false, nil
|
||||
}
|
||||
copy := *record
|
||||
copy.State = types.HTTPIdempotencyProcessing
|
||||
s.records[key] = ©
|
||||
return ©, 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)] = ©
|
||||
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 ©, 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()
|
||||
|
||||
Reference in New Issue
Block a user