186 lines
5.5 KiB
Go
186 lines
5.5 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
const (
|
|
defaultBootstrapBatch = 100
|
|
defaultBootstrapInterval = 200 * time.Millisecond
|
|
defaultBootstrapLease = 30 * time.Second
|
|
)
|
|
|
|
type BootstrapUpdateDispatcher struct {
|
|
router *Router
|
|
log *zap.Logger
|
|
batch int
|
|
interval time.Duration
|
|
maxIdleInterval time.Duration
|
|
leaseTimeout time.Duration
|
|
}
|
|
|
|
type BootstrapUpdateOption func(*BootstrapUpdateDispatcher)
|
|
|
|
func WithBootstrapUpdateBatch(n int) BootstrapUpdateOption {
|
|
return func(d *BootstrapUpdateDispatcher) {
|
|
if n > 0 {
|
|
d.batch = n
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithBootstrapUpdateInterval(interval time.Duration) BootstrapUpdateOption {
|
|
return func(d *BootstrapUpdateDispatcher) {
|
|
if interval > 0 {
|
|
d.interval = interval
|
|
}
|
|
}
|
|
}
|
|
|
|
func WithBootstrapUpdateLease(timeout time.Duration) BootstrapUpdateOption {
|
|
return func(d *BootstrapUpdateDispatcher) {
|
|
if timeout > 0 {
|
|
d.leaseTimeout = timeout
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewBootstrapUpdateDispatcher(router *Router, log *zap.Logger, opts ...BootstrapUpdateOption) *BootstrapUpdateDispatcher {
|
|
if log == nil {
|
|
log = zap.NewNop()
|
|
}
|
|
d := &BootstrapUpdateDispatcher{
|
|
router: router,
|
|
log: log,
|
|
batch: defaultBootstrapBatch,
|
|
interval: defaultBootstrapInterval,
|
|
maxIdleInterval: defaultIdleDispatchMaxInterval,
|
|
leaseTimeout: defaultBootstrapLease,
|
|
}
|
|
for _, opt := range opts {
|
|
if opt != nil {
|
|
opt(d)
|
|
}
|
|
}
|
|
return d
|
|
}
|
|
|
|
func (d *BootstrapUpdateDispatcher) Run(ctx context.Context) {
|
|
if d == nil || d.router == nil {
|
|
return
|
|
}
|
|
runIdleBackoffLoop(ctx, d.interval, d.maxIdleInterval, d.DispatchOnce)
|
|
}
|
|
|
|
func (d *BootstrapUpdateDispatcher) DispatchOnce(ctx context.Context) bool {
|
|
return d.router.publishReadyBootstrapUpdates(ctx, d.batch, d.leaseTimeout, d.log) > 0
|
|
}
|
|
|
|
func (r *Router) enqueueLoginMessageBootstrap(ctx context.Context, msg domain.Message) {
|
|
if r.deps.BootstrapUpdates == nil || msg.OwnerUserID == 0 || msg.ID == 0 {
|
|
return
|
|
}
|
|
authKeyID, hasAuthKeyID := AuthKeyIDFrom(ctx)
|
|
sessionID, hasSessionID := SessionIDFrom(ctx)
|
|
if !hasAuthKeyID || !hasSessionID {
|
|
return
|
|
}
|
|
if _, err := r.deps.BootstrapUpdates.EnqueueLoginMessage(ctx, domain.BootstrapUpdateJob{
|
|
Kind: domain.BootstrapUpdateJobLoginMessage,
|
|
UserID: msg.OwnerUserID,
|
|
AuthKeyID: authKeyID,
|
|
SessionID: sessionID,
|
|
MessageBoxID: msg.ID,
|
|
Status: domain.BootstrapUpdateJobPending,
|
|
}); err != nil {
|
|
r.log.Warn("enqueue bootstrap login message", zap.Int64("user_id", msg.OwnerUserID), zap.Int("message_id", msg.ID), zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// publishBootstrapAfterBaseline runs only from the ordered post-response plan.
|
|
// It must never be called while the baseline rpc_result is merely encoded or
|
|
// queued, otherwise the bootstrap update can overtake that baseline on wire.
|
|
func (r *Router) publishBootstrapAfterBaseline(ctx context.Context, userID int64) bool {
|
|
if r.deps.BootstrapUpdates == nil || userID == 0 {
|
|
return false
|
|
}
|
|
authKeyID, hasAuthKeyID := AuthKeyIDFrom(ctx)
|
|
sessionID, hasSessionID := SessionIDFrom(ctx)
|
|
if !hasAuthKeyID || !hasSessionID {
|
|
return false
|
|
}
|
|
cbCtx, cancel := context.WithTimeout(ctx, updatesDeliveryPhaseTimeout)
|
|
defer cancel()
|
|
ready, err := r.deps.BootstrapUpdates.MarkReadyForSession(cbCtx, userID, authKeyID, sessionID)
|
|
if err != nil {
|
|
r.log.Warn("mark bootstrap updates ready", zap.Int64("user_id", userID), zap.Int64("session_id", sessionID), zap.Error(err))
|
|
return false
|
|
}
|
|
if ready == 0 {
|
|
return true
|
|
}
|
|
r.publishReadyBootstrapUpdates(cbCtx, ready, defaultBootstrapLease, r.log.Named("bootstrap"))
|
|
return true
|
|
}
|
|
|
|
func (r *Router) publishReadyBootstrapUpdates(ctx context.Context, batch int, leaseTimeout time.Duration, log *zap.Logger) int {
|
|
if r.deps.BootstrapUpdates == nil || r.deps.Updates == nil || r.deps.Messages == nil {
|
|
return 0
|
|
}
|
|
if log == nil {
|
|
log = zap.NewNop()
|
|
}
|
|
jobs, err := r.deps.BootstrapUpdates.ClaimReady(ctx, batch, leaseTimeout)
|
|
if err != nil {
|
|
log.Warn("claim bootstrap updates", zap.Error(err))
|
|
return 0
|
|
}
|
|
for _, job := range jobs {
|
|
if err := r.publishBootstrapUpdateJob(ctx, job); err != nil {
|
|
log.Warn("publish bootstrap update",
|
|
zap.Int64("job_id", job.ID),
|
|
zap.Int64("user_id", job.UserID),
|
|
zap.String("kind", string(job.Kind)),
|
|
zap.Error(err),
|
|
)
|
|
_ = r.deps.BootstrapUpdates.MarkFailed(ctx, job.ID, err.Error())
|
|
}
|
|
}
|
|
return len(jobs)
|
|
}
|
|
|
|
func (r *Router) publishBootstrapUpdateJob(ctx context.Context, job domain.BootstrapUpdateJob) error {
|
|
switch job.Kind {
|
|
case domain.BootstrapUpdateJobLoginMessage:
|
|
return r.publishBootstrapLoginMessage(ctx, job)
|
|
default:
|
|
return fmt.Errorf("unknown bootstrap update kind %q", job.Kind)
|
|
}
|
|
}
|
|
|
|
func (r *Router) publishBootstrapLoginMessage(ctx context.Context, job domain.BootstrapUpdateJob) error {
|
|
list, err := r.deps.Messages.GetMessages(ctx, job.UserID, []int{job.MessageBoxID})
|
|
if err != nil {
|
|
return fmt.Errorf("load bootstrap login message: %w", err)
|
|
}
|
|
if len(list.Messages) == 0 {
|
|
return fmt.Errorf("bootstrap login message missing: box_id=%d", job.MessageBoxID)
|
|
}
|
|
msg := list.Messages[0]
|
|
if msg.OwnerUserID != job.UserID || msg.ID != job.MessageBoxID {
|
|
return fmt.Errorf("bootstrap login message mismatch: owner=%d id=%d", msg.OwnerUserID, msg.ID)
|
|
}
|
|
if _, _, err := r.deps.Updates.PublishNewMessage(ctx, job.UserID, msg); err != nil {
|
|
return fmt.Errorf("publish bootstrap login message update: %w", err)
|
|
}
|
|
if err := r.deps.BootstrapUpdates.MarkPublished(ctx, job.ID); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|