merged from gramsrv upstream
This commit is contained in:
parent
79c64ee916
commit
21a0856587
651 changed files with 54774 additions and 4590 deletions
288
internal/rpc/welcome_delivery_dispatcher.go
Normal file
288
internal/rpc/welcome_delivery_dispatcher.go
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/proto"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tlprofile"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWelcomeDeliveryBatch = 100
|
||||
defaultWelcomeDeliveryLease = 15 * time.Second
|
||||
defaultWelcomeDeliveryInterval = 250 * time.Millisecond
|
||||
defaultWelcomeDeliveryMaxRetry = time.Minute
|
||||
defaultWelcomeDeliverySweep = time.Minute
|
||||
)
|
||||
|
||||
var errWelcomeDeliveryMembershipSuperseded = errors.New("welcome delivery membership epoch is no longer active")
|
||||
|
||||
type WelcomeDeliveryDispatcher struct {
|
||||
router *Router
|
||||
store store.WelcomeMessageDeliveryStore
|
||||
log *zap.Logger
|
||||
owner string
|
||||
batch int
|
||||
lease time.Duration
|
||||
nextSweep time.Time
|
||||
}
|
||||
|
||||
func NewWelcomeDeliveryDispatcher(router *Router, deliveries store.WelcomeMessageDeliveryStore, log *zap.Logger) *WelcomeDeliveryDispatcher {
|
||||
if log == nil {
|
||||
log = zap.NewNop()
|
||||
}
|
||||
return &WelcomeDeliveryDispatcher{
|
||||
router: router, store: deliveries, log: log,
|
||||
owner: welcomeDeliveryOwner(), batch: defaultWelcomeDeliveryBatch,
|
||||
lease: defaultWelcomeDeliveryLease,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *WelcomeDeliveryDispatcher) Run(ctx context.Context) {
|
||||
if d == nil || d.router == nil || d.store == nil {
|
||||
return
|
||||
}
|
||||
runIdleBackoffLoop(ctx, defaultWelcomeDeliveryInterval, defaultIdleDispatchMaxInterval, d.DispatchOnce)
|
||||
}
|
||||
|
||||
func (d *WelcomeDeliveryDispatcher) DispatchOnce(ctx context.Context) bool {
|
||||
if d == nil || d.router == nil || d.store == nil {
|
||||
return false
|
||||
}
|
||||
now := d.router.clock.Now()
|
||||
deleted := 0
|
||||
if d.nextSweep.IsZero() || !now.Before(d.nextSweep) {
|
||||
var err error
|
||||
deleted, err = d.store.DeleteExpiredWelcomeMessageDeliveries(ctx, now, d.batch*10)
|
||||
switch {
|
||||
case err != nil:
|
||||
d.nextSweep = now.Add(defaultWelcomeDeliveryInterval)
|
||||
d.log.Warn("delete expired welcome deliveries", zap.Error(err))
|
||||
case deleted == d.batch*10:
|
||||
d.nextSweep = now
|
||||
default:
|
||||
d.nextSweep = now.Add(defaultWelcomeDeliverySweep)
|
||||
}
|
||||
}
|
||||
deliveries, err := d.store.ClaimWelcomeMessageDeliveries(ctx, d.owner, now, d.batch, d.lease)
|
||||
if err != nil {
|
||||
d.log.Warn("claim welcome deliveries", zap.Error(err))
|
||||
return deleted > 0
|
||||
}
|
||||
for _, group := range groupWelcomeMessageDeliveries(deliveries) {
|
||||
d.dispatchGroup(ctx, group)
|
||||
}
|
||||
return deleted > 0 || len(deliveries) > 0
|
||||
}
|
||||
|
||||
func (d *WelcomeDeliveryDispatcher) dispatchGroup(ctx context.Context, deliveries []domain.WelcomeMessageDelivery) {
|
||||
if len(deliveries) == 0 {
|
||||
return
|
||||
}
|
||||
delivery := deliveries[0]
|
||||
ids := welcomeDeliveryIDs(deliveries)
|
||||
now := d.router.clock.Now()
|
||||
if !delivery.ExpiresAt.After(now) {
|
||||
return
|
||||
}
|
||||
if online, ok := d.router.deps.Sessions.(OnlineUserProvider); ok && !online.IsUserOnline(delivery.TargetUserID) {
|
||||
d.retry(ctx, deliveries, now, "target has no online session")
|
||||
return
|
||||
}
|
||||
binder, ok := d.router.deps.Sessions.(SemanticTransientSessionBinder)
|
||||
if !ok {
|
||||
d.retry(ctx, deliveries, now, "semantic transient session binder is unavailable")
|
||||
return
|
||||
}
|
||||
updates, err := d.router.welcomeDeliveryUpdates(ctx, deliveries)
|
||||
if errors.Is(err, errWelcomeDeliveryMembershipSuperseded) {
|
||||
if _, ackErr := d.store.AckWelcomeMessageDeliveries(ctx, d.owner, ids, now); ackErr != nil {
|
||||
d.log.Warn("discard superseded welcome delivery", zap.Int64("join_event_id", delivery.JoinEventID), zap.Error(ackErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
d.retry(ctx, deliveries, now, err.Error())
|
||||
return
|
||||
}
|
||||
sent, sendErr := binder.PushToUserTransientCompatible(
|
||||
ctx, delivery.TargetUserID, tlprofile.SemanticTypeUpdateNewEphemeralMessage,
|
||||
proto.MessageFromServer, updates, d.router.cfg.OutboundPushTimeout,
|
||||
)
|
||||
if sent <= 0 {
|
||||
reason := "no ready exact profile can represent welcome delivery"
|
||||
if sendErr != nil {
|
||||
reason = sendErr.Error()
|
||||
}
|
||||
d.retry(ctx, deliveries, now, reason)
|
||||
return
|
||||
}
|
||||
acked, err := d.store.AckWelcomeMessageDeliveries(ctx, d.owner, ids, now)
|
||||
if err != nil || acked != len(ids) {
|
||||
d.log.Warn("ack welcome delivery",
|
||||
zap.Int64("join_event_id", delivery.JoinEventID), zap.Int64("target_user_id", delivery.TargetUserID),
|
||||
zap.Int("templates", len(ids)), zap.Int("sent_sessions", sent), zap.Int("acked", acked), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *WelcomeDeliveryDispatcher) retry(ctx context.Context, deliveries []domain.WelcomeMessageDelivery, now time.Time, reason string) {
|
||||
if len(deliveries) == 0 {
|
||||
return
|
||||
}
|
||||
delivery := deliveries[0]
|
||||
attempt := delivery.AttemptCount
|
||||
expiresAt := delivery.ExpiresAt
|
||||
for _, item := range deliveries[1:] {
|
||||
if item.AttemptCount > attempt {
|
||||
attempt = item.AttemptCount
|
||||
}
|
||||
if item.ExpiresAt.Before(expiresAt) {
|
||||
expiresAt = item.ExpiresAt
|
||||
}
|
||||
}
|
||||
delay := welcomeDeliveryRetryDelay(attempt)
|
||||
next := now.Add(delay)
|
||||
if next.After(expiresAt) {
|
||||
next = expiresAt
|
||||
}
|
||||
ids := welcomeDeliveryIDs(deliveries)
|
||||
updated, err := d.store.RetryWelcomeMessageDeliveries(ctx, d.owner, ids, next, reason)
|
||||
if err != nil || updated != len(ids) {
|
||||
d.log.Warn("retry welcome delivery",
|
||||
zap.Int64("join_event_id", delivery.JoinEventID), zap.Int64("target_user_id", delivery.TargetUserID),
|
||||
zap.Int("templates", len(ids)), zap.Int("updated", updated), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) welcomeDeliveryUpdates(ctx context.Context, deliveries []domain.WelcomeMessageDelivery) (*tg.Updates, error) {
|
||||
if r == nil || r.deps.Channels == nil {
|
||||
return nil, errors.New("channel projection is unavailable")
|
||||
}
|
||||
if err := validateWelcomeDeliveryGroup(deliveries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
delivery := deliveries[0]
|
||||
view, err := r.deps.Channels.ResolveChannel(ctx, delivery.TargetUserID, delivery.ChannelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if view.Self.Status != domain.ChannelMemberActive || view.Self.JoinedAt != delivery.JoinedAt {
|
||||
return nil, errWelcomeDeliveryMembershipSuperseded
|
||||
}
|
||||
updates := make([]tg.UpdateClass, 0, len(deliveries))
|
||||
for _, item := range deliveries {
|
||||
wire := tg.EphemeralMessage{
|
||||
Out: false, WelcomeTemplate: false,
|
||||
InvertMedia: item.Content.InvertMedia, Noforwards: item.Content.NoForwards,
|
||||
ID: item.EphemeralID,
|
||||
FromID: tgPeer(domain.Peer{Type: domain.PeerTypeChannel, ID: item.ChannelID}),
|
||||
PeerID: tgPeer(domain.Peer{Type: domain.PeerTypeChannel, ID: item.ChannelID}),
|
||||
ReceiverID: 0,
|
||||
Date: item.JoinedAt,
|
||||
Message: item.Content.Message,
|
||||
}
|
||||
if len(item.Content.Entities) != 0 {
|
||||
wire.SetEntities(tgMessageEntities(item.Content.Entities))
|
||||
}
|
||||
if item.Content.Media != nil && !item.Content.Media.IsZero() {
|
||||
wire.SetMedia(tgMessageMedia(item.Content.Media))
|
||||
}
|
||||
if item.Content.ReplyMarkup != nil && !item.Content.ReplyMarkup.IsZero() {
|
||||
wire.SetReplyMarkup(tgReplyMarkup(item.Content.ReplyMarkup))
|
||||
}
|
||||
rich, err := tgRichMessage(item.Content.RichMessage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("project welcome rich message: %w", err)
|
||||
}
|
||||
if rich != nil {
|
||||
wire.SetRichMessage(*rich)
|
||||
}
|
||||
updates = append(updates, &tg.UpdateNewEphemeralMessage{Message: wire})
|
||||
}
|
||||
return &tg.Updates{
|
||||
Updates: updates,
|
||||
Chats: []tg.ChatClass{tgChannelChatForView(delivery.TargetUserID, view)},
|
||||
Date: delivery.JoinedAt,
|
||||
Seq: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func groupWelcomeMessageDeliveries(deliveries []domain.WelcomeMessageDelivery) [][]domain.WelcomeMessageDelivery {
|
||||
groups := make([][]domain.WelcomeMessageDelivery, 0, len(deliveries))
|
||||
positions := make(map[int64]int, len(deliveries))
|
||||
for _, delivery := range deliveries {
|
||||
position, ok := positions[delivery.JoinEventID]
|
||||
if !ok {
|
||||
position = len(groups)
|
||||
positions[delivery.JoinEventID] = position
|
||||
groups = append(groups, nil)
|
||||
}
|
||||
groups[position] = append(groups[position], delivery)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func validateWelcomeDeliveryGroup(deliveries []domain.WelcomeMessageDelivery) error {
|
||||
if len(deliveries) == 0 || len(deliveries) > domain.MaxWelcomeMessagesPerPeer {
|
||||
return errors.New("invalid welcome delivery group size")
|
||||
}
|
||||
first := deliveries[0]
|
||||
seenTemplates := make(map[int]struct{}, len(deliveries))
|
||||
seenEphemeral := make(map[int]struct{}, len(deliveries))
|
||||
for _, delivery := range deliveries {
|
||||
if delivery.JoinEventID != first.JoinEventID || delivery.ChannelID != first.ChannelID ||
|
||||
delivery.TargetUserID != first.TargetUserID || delivery.JoinedAt != first.JoinedAt {
|
||||
return errors.New("inconsistent welcome delivery group")
|
||||
}
|
||||
if _, duplicate := seenTemplates[delivery.TemplateID]; duplicate {
|
||||
return errors.New("duplicate welcome delivery template")
|
||||
}
|
||||
if _, duplicate := seenEphemeral[delivery.EphemeralID]; duplicate {
|
||||
return errors.New("duplicate welcome delivery ephemeral id")
|
||||
}
|
||||
seenTemplates[delivery.TemplateID] = struct{}{}
|
||||
seenEphemeral[delivery.EphemeralID] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func welcomeDeliveryIDs(deliveries []domain.WelcomeMessageDelivery) []int64 {
|
||||
ids := make([]int64, len(deliveries))
|
||||
for i, delivery := range deliveries {
|
||||
ids[i] = delivery.ID
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func welcomeDeliveryRetryDelay(attempt int) time.Duration {
|
||||
if attempt < 1 {
|
||||
attempt = 1
|
||||
}
|
||||
shift := attempt - 1
|
||||
if shift > 6 {
|
||||
shift = 6
|
||||
}
|
||||
delay := time.Second * time.Duration(1<<shift)
|
||||
if delay > defaultWelcomeDeliveryMaxRetry {
|
||||
return defaultWelcomeDeliveryMaxRetry
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
func welcomeDeliveryOwner() string {
|
||||
var value [16]byte
|
||||
if _, err := rand.Read(value[:]); err == nil {
|
||||
return "welcome-" + hex.EncodeToString(value[:])
|
||||
}
|
||||
return fmt.Sprintf("welcome-%d", time.Now().UnixNano())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue