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
+123 -29
View File
@@ -52,6 +52,7 @@ const (
imReasonSchemaUnsupported = "SCHEMA_UNSUPPORTED"
imReasonIdempotencyConflict = "IDEMPOTENCY_CONFLICT"
imReasonIdempotencyPending = "IDEMPOTENCY_IN_PROGRESS"
imReasonIdempotencyService = "IDEMPOTENCY_UNAVAILABLE"
imReasonInternal = "INTERNAL_ERROR"
imReasonUnsupportedMedia = "UNSUPPORTED_MEDIA_TYPE"
imReasonFileUnavailable = "FILE_UNAVAILABLE"
@@ -69,6 +70,75 @@ const (
registrationTypePersonal = 1
)
const (
imHTTPIdempotencyTTL = 24 * time.Hour
imHTTPIdempotencyNamespace = "http_idempotency"
)
type imHTTPIdempotencyReplay struct {
BodyHash string `json:"body_hash"`
ResponseStatus int `json:"response_status"`
ResponseBody []byte `json:"response_body"`
}
var errIMHTTPIdempotencyConflict = errors.New("http idempotency conflict")
var (
beginIMHTTPIdempotency = redisBeginIMHTTPIdempotency
completeIMHTTPIdempotency = redisCompleteIMHTTPIdempotency
abortIMHTTPIdempotency = redisAbortIMHTTPIdempotency
)
func imHTTPIdempotencyID(tenantID types.TenantID, uid types.Uid, method, route, key string) string {
return strconv.FormatInt(int64(tenantID), 10) + ":" + uid.String() + ":" + method + ":" + route + ":" + key
}
func redisBeginIMHTTPIdempotency(tenantID types.TenantID, uid types.Uid, method, route, key, bodyHash string) (*imHTTPIdempotencyReplay, bool, bool, error) {
id := imHTTPIdempotencyID(tenantID, uid, method, route, key)
claimed, sameValue, enabled, err := store.ClaimShortStateValue(imHTTPIdempotencyNamespace, id, bodyHash, imHTTPIdempotencyTTL)
if err != nil || !enabled || claimed {
return nil, claimed, enabled, err
}
if !sameValue {
return nil, false, true, errIMHTTPIdempotencyConflict
}
raw, found, enabled, err := store.GetShortStateValue(imHTTPIdempotencyNamespace, id+":response")
if err != nil || !enabled || !found {
return nil, false, enabled, err
}
var replay imHTTPIdempotencyReplay
if err = json.Unmarshal([]byte(raw), &replay); err != nil {
_, _ = store.DeleteShortState(imHTTPIdempotencyNamespace, id+":response")
return nil, false, true, err
}
if replay.BodyHash != bodyHash {
return nil, false, true, nil
}
return &replay, false, true, nil
}
func redisCompleteIMHTTPIdempotency(tenantID types.TenantID, uid types.Uid, method, route, key, bodyHash string, status int, body []byte) error {
id := imHTTPIdempotencyID(tenantID, uid, method, route, key)
payload, err := json.Marshal(imHTTPIdempotencyReplay{BodyHash: bodyHash, ResponseStatus: status, ResponseBody: body})
if err != nil {
return err
}
enabled, err := store.SetShortStateValue(imHTTPIdempotencyNamespace, id+":response", string(payload), imHTTPIdempotencyTTL)
if !enabled {
return errors.New("http idempotency redis unavailable")
}
return err
}
func redisAbortIMHTTPIdempotency(tenantID types.TenantID, uid types.Uid, method, route, key string) error {
id := imHTTPIdempotencyID(tenantID, uid, method, route, key)
if _, err := store.DeleteShortState(imHTTPIdempotencyNamespace, id); err != nil {
return err
}
_, err := store.DeleteShortState(imHTTPIdempotencyNamespace, id+":response")
return err
}
func reviewIMHTTPText(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, scene, topic, text string) bool {
text = strings.TrimSpace(text)
if text == "" {
@@ -232,53 +302,54 @@ func serveIMHTTPIdempotent(wrt http.ResponseWriter, req *http.Request, next http
req.Body = io.NopCloser(bytes.NewReader(body))
hashInput := append([]byte(req.URL.RawQuery+"\x00"), body...)
bodyHash := sha256.Sum256(hashInput)
record := &types.HTTPIdempotencyRecord{
UserID: principal.Uid, Method: req.Method, Route: req.URL.Path, Key: key,
BodyHash: fmt.Sprintf("%x", bodyHash[:]), ExpiresAt: time.Now().UTC().Add(24 * time.Hour),
bodyHashText := fmt.Sprintf("%x", bodyHash[:])
existing, acquired, enabled, err := beginIMHTTPIdempotency(principal.TenantID, principal.Uid, req.Method, req.URL.Path, key, bodyHashText)
if errors.Is(err, errIMHTTPIdempotencyConflict) {
writeIMError(wrt, req, http.StatusConflict, 30007, imReasonIdempotencyConflict, "Idempotency-Key was used with a different request", nil)
return
}
existing, acquired, err := store.HTTPInfrastructure.BeginIdempotency(principal.TenantID, record)
if err != nil {
logs.Warn.Printf("im http idempotency begin failed request_id=%s tenant=%d user=%s method=%s route=%s err=%v",
logs.Warn.Printf("im http redis idempotency begin failed request_id=%s tenant=%d user=%s method=%s route=%s err=%v",
req.Header.Get("X-Request-ID"), principal.TenantID, principal.Uid.UserId(), req.Method, req.URL.Path, err)
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
writeIMError(wrt, req, http.StatusServiceUnavailable, 30050, imReasonIdempotencyService, "idempotency service unavailable", nil)
return
}
if !enabled {
logs.Warn.Printf("im http redis idempotency unavailable request_id=%s tenant=%d user=%s method=%s route=%s",
req.Header.Get("X-Request-ID"), principal.TenantID, principal.Uid.UserId(), req.Method, req.URL.Path)
writeIMError(wrt, req, http.StatusServiceUnavailable, 30050, imReasonIdempotencyService, "idempotency service unavailable", nil)
return
}
if !acquired {
if existing == nil || existing.BodyHash != record.BodyHash {
writeIMError(wrt, req, http.StatusConflict, 30007, imReasonIdempotencyConflict, "Idempotency-Key was used with a different request", nil)
if existing == nil {
writeIMError(wrt, req, http.StatusConflict, 30008, imReasonIdempotencyPending, "request with this Idempotency-Key is still processing", nil)
return
}
if existing.State == types.HTTPIdempotencyCompleted {
wrt.Header().Set("Content-Type", "application/json; charset=utf-8")
wrt.Header().Set("X-Idempotent-Replay", "true")
wrt.WriteHeader(existing.ResponseStatus)
_, _ = wrt.Write(existing.ResponseBody)
return
}
writeIMError(wrt, req, http.StatusConflict, 30008, imReasonIdempotencyPending, "request with this Idempotency-Key is still processing", nil)
wrt.Header().Set("Content-Type", "application/json; charset=utf-8")
wrt.Header().Set("X-Idempotent-Replay", "true")
wrt.WriteHeader(existing.ResponseStatus)
_, _ = wrt.Write(existing.ResponseBody)
return
}
buffered := newIMBufferedResponseWriter()
next.ServeHTTP(buffered, req)
record.ResponseStatus = buffered.status
record.ResponseBody = append([]byte(nil), buffered.body.Bytes()...)
if buffered.status >= http.StatusInternalServerError {
if abortErr := store.HTTPInfrastructure.AbortIdempotency(principal.TenantID, record); abortErr != nil {
logs.Warn.Printf("im http idempotency abort failed request_id=%s tenant=%d user=%s method=%s route=%s err=%v",
if abortErr := abortIMHTTPIdempotency(principal.TenantID, principal.Uid, req.Method, req.URL.Path, key); abortErr != nil {
logs.Warn.Printf("im http redis idempotency abort failed request_id=%s tenant=%d user=%s method=%s route=%s err=%v",
req.Header.Get("X-Request-ID"), principal.TenantID, principal.Uid.UserId(), req.Method, req.URL.Path, abortErr)
}
buffered.flushTo(wrt)
return
}
if err = store.HTTPInfrastructure.CompleteIdempotency(principal.TenantID, record); err != nil {
logs.Warn.Printf("im http idempotency complete failed request_id=%s tenant=%d user=%s method=%s route=%s status=%d err=%v",
if err = completeIMHTTPIdempotency(principal.TenantID, principal.Uid, req.Method, req.URL.Path, key, bodyHashText, buffered.status, append([]byte(nil), buffered.body.Bytes()...)); err != nil {
logs.Warn.Printf("im http redis idempotency complete failed request_id=%s tenant=%d user=%s method=%s route=%s status=%d err=%v",
req.Header.Get("X-Request-ID"), principal.TenantID, principal.Uid.UserId(), req.Method, req.URL.Path, buffered.status, err)
if abortErr := store.HTTPInfrastructure.AbortIdempotency(principal.TenantID, record); abortErr != nil {
logs.Warn.Printf("im http idempotency abort after complete failed request_id=%s tenant=%d user=%s method=%s route=%s err=%v",
if abortErr := abortIMHTTPIdempotency(principal.TenantID, principal.Uid, req.Method, req.URL.Path, key); abortErr != nil {
logs.Warn.Printf("im http redis idempotency abort after complete failed request_id=%s tenant=%d user=%s method=%s route=%s err=%v",
req.Header.Get("X-Request-ID"), principal.TenantID, principal.Uid.UserId(), req.Method, req.URL.Path, abortErr)
}
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
writeIMError(wrt, req, http.StatusServiceUnavailable, 30050, imReasonIdempotencyService, "idempotency service unavailable", nil)
return
}
buffered.flushTo(wrt)
@@ -2412,7 +2483,8 @@ func serveIMSystemNotificationRead(wrt http.ResponseWriter, req *http.Request) {
}
readAt = parsed.UTC().Round(time.Millisecond)
}
if err := store.Security.MarkSystemNotificationRead(principal.TenantID, principal.Uid, eventID, readAt); err != nil {
notification, err := store.Security.MarkSystemNotificationRead(principal.TenantID, principal.Uid, eventID, readAt)
if err != nil {
if errors.Is(err, types.ErrNotFound) {
writeIMError(wrt, req, http.StatusNotFound, 30044, imReasonChallengeExpired, "notification not found", nil)
return
@@ -2420,7 +2492,9 @@ func serveIMSystemNotificationRead(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return
}
writeIMSuccess(wrt, req, map[string]any{"status": "ok", "id": eventID, "read_at": readAt.Format(time.RFC3339Nano)})
payload := systemNotificationPayload(*notification)
payload["status"] = "ok"
writeIMSuccess(wrt, req, payload)
}
func serveIMFavorites(wrt http.ResponseWriter, req *http.Request) {
@@ -7978,7 +8052,7 @@ func favoritePayload(item types.FavoriteItem) map[string]any {
"favorite_id": item.FavoriteID,
"type": item.ItemType,
"snapshot": item.Snapshot,
"source": map[string]any{"topic": item.SourceTopic, "seq": item.SourceSeq},
"source": favoriteSourcePayload(item.SourceTopic, item.SourceSeq, types.ZeroUid),
"collected_at": item.CollectedAt.UTC().Format(time.RFC3339Nano),
"availability": item.Availability,
}
@@ -8015,7 +8089,27 @@ func favoritePayloadForPrincipal(principal *imPrincipal, item types.FavoriteItem
item.Availability = "removed"
}
}
return favoritePayload(item), true
return favoritePayloadForViewer(principal.Uid, item), true
}
func favoritePayloadForViewer(viewer types.Uid, item types.FavoriteItem) map[string]any {
payload := favoritePayload(item)
payload["source"] = favoriteSourcePayload(item.SourceTopic, item.SourceSeq, viewer)
return payload
}
func favoriteSourcePayload(topic string, seq int64, viewer types.Uid) map[string]any {
source := map[string]any{"topic": topic, "seq": seq}
if viewer.IsZero() || types.GetTopicCat(topic) != types.TopicCatP2P {
return source
}
peerTopic, err := types.P2PNameForUser(viewer, topic)
if err != nil || strings.TrimSpace(peerTopic) == "" {
return source
}
source["peer_user_id"] = peerTopic
source["display_topic"] = peerTopic
return source
}
func legacyFavoriteSnapshotOnly(item types.FavoriteItem) bool {