feat: complete shared database tenant sharding
This commit is contained in:
+123
-29
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user