From 36730f68cc78b682e625f9ca244ca94a086fe386 Mon Sep 17 00:00:00 2001 From: ice-cronus <105345303+ice-cronus@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:02:55 +0300 Subject: [PATCH] Revert "email queue processing (#176)" This reverts commit 7cb452b9eec3c5dd3da316de7e0a4e6f055c8dce. --- application.yaml | 2 - auth/email_link/.testdata/docker-compose.yaml | 12 - auth/email_link/DDL.sql | 6 +- auth/email_link/contract.go | 16 +- auth/email_link/email_modify.go | 2 +- auth/email_link/emaillink.go | 35 +-- auth/email_link/link_start_auth.go | 152 +++++----- auth/email_link/queue.go | 267 ------------------ auth/email_link/users.go | 2 +- cmd/eskimo-hut/api/docs.go | 8 - cmd/eskimo-hut/api/swagger.json | 8 - cmd/eskimo-hut/api/swagger.yaml | 6 - cmd/eskimo-hut/auth.go | 5 +- cmd/eskimo-hut/contract.go | 4 +- cmd/eskimo-hut/eskimo_hut.go | 7 +- cmd/eskimo-hut/users.go | 2 +- ...rebase_phone_login_with_ice_email_login.go | 2 +- go.mod | 2 - go.sum | 6 - 19 files changed, 102 insertions(+), 442 deletions(-) delete mode 100644 auth/email_link/.testdata/docker-compose.yaml delete mode 100644 auth/email_link/queue.go diff --git a/application.yaml b/application.yaml index 56b5d560..eee98567 100644 --- a/application.yaml +++ b/application.yaml @@ -85,8 +85,6 @@ kyc/quiz: auth/email-link: extraLoadBalancersCount: 2 wintr/connectors/storage/v2: *db - wintr/connectors/storage/v3: - url: redis://default:@localhost:6379/ fromEmailAddress: no-reply@ice.io fromEmailName: ice emailValidation: diff --git a/auth/email_link/.testdata/docker-compose.yaml b/auth/email_link/.testdata/docker-compose.yaml deleted file mode 100644 index e9525650..00000000 --- a/auth/email_link/.testdata/docker-compose.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-License-Identifier: ice License 1.0 - -version: '3.7' -services: - eskimo-dfly: - container_name: eskimo-dfly - image: 'docker.dragonflydb.io/dragonflydb/dragonfly' - ulimits: - memlock: -1 - ports: - - 6379:6379 - command: "dragonfly --dbnum=1" \ No newline at end of file diff --git a/auth/email_link/DDL.sql b/auth/email_link/DDL.sql index f2638e4d..099b70d1 100644 --- a/auth/email_link/DDL.sql +++ b/auth/email_link/DDL.sql @@ -9,7 +9,6 @@ CREATE TABLE IF NOT EXISTS email_link_sign_ins ( previously_issued_token_seq BIGINT DEFAULT 0 NOT NULL, confirmation_code_wrong_attempts_count BIGINT DEFAULT 0 NOT NULL, email TEXT NOT NULL, - language TEXT NOT NULL, confirmation_code TEXT, user_id TEXT, phone_number_to_email_migration_user_id TEXT, @@ -34,7 +33,4 @@ CREATE TABLE IF NOT EXISTS sign_ins_per_ip ( login_attempts BIGINT DEFAULT 0 NOT NULL CONSTRAINT sign_ins_per_ip_login_attempts_count CHECK (login_attempts <= 10), ip TEXT NOT NULL, PRIMARY KEY (login_session_number, ip) -); - -ALTER TABLE email_link_sign_ins - ADD COLUMN IF NOT EXISTS language TEXT NOT NULL DEFAULT 'en'; \ No newline at end of file +); \ No newline at end of file diff --git a/auth/email_link/contract.go b/auth/email_link/contract.go index 892e019c..141f3bd6 100644 --- a/auth/email_link/contract.go +++ b/auth/email_link/contract.go @@ -7,7 +7,6 @@ import ( "embed" "io" "mime/multipart" - "sync" "text/template" stdlibtime "time" @@ -17,7 +16,6 @@ import ( "github.com/ice-blockchain/eskimo/users" "github.com/ice-blockchain/wintr/auth" "github.com/ice-blockchain/wintr/connectors/storage/v2" - storagev3 "github.com/ice-blockchain/wintr/connectors/storage/v3" "github.com/ice-blockchain/wintr/email" "github.com/ice-blockchain/wintr/time" ) @@ -30,11 +28,10 @@ type ( } Client interface { IceUserIDClient - SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (queuePos int64, rateLimit, loginSession string, err error) + SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (loginSession string, err error) SignIn(ctx context.Context, loginFlowToken, confirmationCode string) (tokens *Tokens, emailConfirmed bool, err error) RegenerateTokens(ctx context.Context, prevToken string) (tokens *Tokens, err error) UpdateMetadata(ctx context.Context, userID string, metadata *users.JSON) (*users.JSON, error) - CheckHealth(ctx context.Context) error } IceUserIDClient interface { io.Closer @@ -89,24 +86,18 @@ const ( sameIPCheckRate = 24 * stdlibtime.Hour duplicatedSignInRequestsInLessThan = 2 * stdlibtime.Second - loginQueueKey = "login_queue" - loginRateLimitKey = "login_rate_limit" - initEmailRateLimit = "1000:1m" ) type ( languageCode = string client struct { - queueDB storagev3.DB - authClient auth.Client - userModifier UserModifier db *storage.DB cfg *config shutdown func() error - cancel context.CancelFunc + authClient auth.Client + userModifier UserModifier emailClients []email.Client fromRecipients []fromRecipient - queueWg sync.WaitGroup emailClientLBIndex uint64 } config struct { @@ -189,5 +180,4 @@ var ( modifyEmailType, notifyEmailChangedType, } - errAlreadyEnqueued = errors.New("already enqueued") ) diff --git a/auth/email_link/email_modify.go b/auth/email_link/email_modify.go index 44d64aa9..412ed315 100644 --- a/auth/email_link/email_modify.go +++ b/auth/email_link/email_modify.go @@ -44,7 +44,7 @@ func (c *client) handleEmailModification(ctx context.Context, els *emailLinkSign if notifyEmail != "" { now := time.Now() resetConfirmationCode := generateConfirmationCode() - uErr := c.upsertEmailLinkSignIn(ctx, oldEmail, els.DeviceUniqueID, resetConfirmationCode, els.Language, now) + uErr := c.upsertEmailLinkSignIn(ctx, oldEmail, els.DeviceUniqueID, resetConfirmationCode, now) if uErr != nil { return multierror.Append( //nolint:wrapcheck // . errors.Wrapf(c.resetEmailModification(ctx, usr.ID, oldEmail), "[reset] resetEmailModification failed for email:%v", oldEmail), diff --git a/auth/email_link/emaillink.go b/auth/email_link/emaillink.go index 56a9ddf8..706852fe 100644 --- a/auth/email_link/emaillink.go +++ b/auth/email_link/emaillink.go @@ -19,7 +19,6 @@ import ( "github.com/ice-blockchain/wintr/auth" appcfg "github.com/ice-blockchain/wintr/config" "github.com/ice-blockchain/wintr/connectors/storage/v2" - storagev3 "github.com/ice-blockchain/wintr/connectors/storage/v3" "github.com/ice-blockchain/wintr/email" "github.com/ice-blockchain/wintr/log" "github.com/ice-blockchain/wintr/time" @@ -30,20 +29,15 @@ func init() { loadEmailMagicLinkTranslationTemplates() } -//nolint:funlen // . -func NewClient(ctx context.Context, cancel context.CancelFunc, userModifier UserModifier, authClient auth.Client) Client { +func NewClient(ctx context.Context, userModifier UserModifier, authClient auth.Client) Client { cfg := loadConfiguration() cfg.validate() db := storage.MustConnect(ctx, ddl, applicationYamlKey) - //nolint:contextcheck // Used in queue processing. - queueDB := storagev3.MustConnect(context.Background(), applicationYamlKey) - log.Panic(errors.Wrapf(queueDB.SetNX(ctx, loginRateLimitKey, initEmailRateLimit, 0).Err(), "failed to init email sending rate limit")) //nolint:revive // . + cl := &client{ cfg: cfg, shutdown: db.Close, db: db, - cancel: cancel, - queueDB: queueDB, authClient: authClient, userModifier: userModifier, emailClients: make([]email.Client, 0, cfg.ExtraLoadBalancersCount+1), @@ -58,7 +52,6 @@ func NewClient(ctx context.Context, cancel context.CancelFunc, userModifier User cl.emailClients = append(cl.emailClients, email.New(fmt.Sprintf("%v/%v", applicationYamlKey, i+1))) cl.fromRecipients = append(cl.fromRecipients, fromRecipient{nestedCfg.FromEmailName, nestedCfg.FromEmailAddress}) } - go cl.processEmailQueue(ctx) } go cl.startOldLoginAttemptsCleaner(ctx) @@ -75,33 +68,9 @@ func NewROClient(ctx context.Context) IceUserIDClient { } func (c *client) Close() error { - if c.cancel != nil { - c.cancel() - } - c.queueWg.Wait() - return errors.Wrap(c.shutdown(), "closing auth/emaillink repository failed") } -func (c *client) CheckHealth(ctx context.Context) error { - return errors.Wrapf(c.checkQueueDBHealth(ctx), "[health-check] failed to ping queueDB/dfly for email client") -} - -func (c *client) checkQueueDBHealth(ctx context.Context) error { - if resp := c.queueDB.Ping(ctx); resp.Err() != nil || resp.Val() != "PONG" { - if resp.Err() == nil { - resp.SetErr(errors.Errorf("response `%v` is not `PONG`", resp.Val())) - } - - return errors.Wrap(resp.Err(), "[health-check] failed to ping DB") - } - if !c.queueDB.IsRW(ctx) { - return errors.New("db is not writeable") - } - - return nil -} - func loadConfiguration() *config { var cfg config appcfg.MustLoadFromKey(applicationYamlKey, &cfg) diff --git a/auth/email_link/link_start_auth.go b/auth/email_link/link_start_auth.go index fb42d51b..99c0c057 100644 --- a/auth/email_link/link_start_auth.go +++ b/auth/email_link/link_start_auth.go @@ -22,88 +22,75 @@ import ( "github.com/ice-blockchain/wintr/time" ) -//nolint:funlen,gocognit,revive,gocritic,lll //. -func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (posInQueue int64, rateLimit, loginSession string, err error) { +//nolint:funlen,gocognit,revive //. +func (c *client) SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (loginSession string, err error) { if ctx.Err() != nil { - return 0, "", "", errors.Wrap(ctx.Err(), "send sign in link to email failed because context failed") + return "", errors.Wrap(ctx.Err(), "send sign in link to email failed because context failed") } - now := time.Now() id := loginID{emailValue, deviceUniqueID} + now := time.Now() loginSessionNumber := now.Time.Unix() / int64(sameIPCheckRate.Seconds()) - oldEmail := users.ConfirmedEmail(ctx) - if oldEmail == "" { - posInQueue, rateLimit, err = c.enqueueLoginAttempt(ctx, now, emailValue) - if err != nil { - if errors.Is(err, errAlreadyEnqueued) { - loginSession, err = c.getExistingLoginSession(ctx, &id, loginSessionNumber, clientIP) - - return posInQueue, rateLimit, loginSession, errors.Wrapf(err, "failed to fetch existing login session for email %v", id.Email) - } - - return 0, "", "", errors.Wrapf(err, "failed to enqueue email %v", emailValue) - } - } - if vErr := c.validateEmailSignIn(ctx, &id); vErr != nil { - return 0, "", "", errors.Wrapf(vErr, "can't validate email sign in for:%#v", id) + return "", errors.Wrapf(vErr, "can't validate email sign in for:%#v", id) } + oldEmail := users.ConfirmedEmail(ctx) if oldEmail != "" { loginSessionNumber = 0 clientIP = "" //nolint:revive // . oldID := loginID{oldEmail, deviceUniqueID} if vErr := c.validateEmailModification(ctx, emailValue, &oldID); vErr != nil { - return 0, "", "", errors.Wrapf(vErr, "can't validate modification email for:%#v", oldID) + return "", errors.Wrapf(vErr, "can't validate modification email for:%#v", oldID) } } confirmationCode := generateConfirmationCode() loginSession, err = c.generateLoginSession(&id, clientIP, oldEmail, loginSessionNumber) if err != nil { - return 0, "", "", errors.Wrap(err, "can't call generateLoginSession") + return "", errors.Wrap(err, "can't call generateLoginSession") } - if uErr := c.upsertEmailLinkSignIn(ctx, id.Email, id.DeviceUniqueID, confirmationCode, language, now); uErr != nil { + if loginSessionNumber > 0 && clientIP != "" && userIDForPhoneNumberToEmailMigration(ctx) == "" { + if ipErr := c.upsertIPLoginAttempt(ctx, &id, clientIP, loginSessionNumber); ipErr != nil { + return "", errors.Wrapf(ipErr, "failed increment login attempts for IP:%v (session num %v)", clientIP, loginSessionNumber) + } + } + if uErr := c.upsertEmailLinkSignIn(ctx, id.Email, id.DeviceUniqueID, confirmationCode, now); uErr != nil { if errors.Is(uErr, ErrUserDuplicate) { - oldLoginSession, oErr := c.restoreOldLoginSession(&id, clientIP, oldEmail, loginSessionNumber) + oldLoginSession, oErr := c.restoreOldLoginSession(ctx, &id, clientIP, oldEmail, loginSessionNumber) if oErr != nil { - return 0, "", "", multierror.Append( //nolint:wrapcheck // . + return "", multierror.Append( //nolint:wrapcheck // . errors.Wrapf(oErr, "failed to calculate oldLoginSession"), errors.Wrapf(uErr, "failed to store/update email link sign ins for id:%#v", id), ).ErrorOrNil() } - return posInQueue, rateLimit, oldLoginSession, nil + return oldLoginSession, nil } - return 0, "", "", errors.Wrapf(uErr, "failed to store/update email link sign ins for id:%#v", id) - } - if oldEmail != "" { - if sendModEmailErr := c.sendEmailWithType(ctx, modifyEmailType, language, []string{id.Email}, []string{confirmationCode}); sendModEmailErr != nil { - return 0, "", loginSession, errors.Wrapf(sendModEmailErr, "failed to send validation email for id:%#v", id) - } + return "", multierror.Append( //nolint:wrapcheck // . + errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"), + errors.Wrapf(uErr, "failed to store/update email link sign ins for id:%#v", id), + ).ErrorOrNil() } - - return posInQueue, rateLimit, loginSession, nil -} - -func (c *client) getExistingLoginSession(ctx context.Context, id *loginID, loginSessionNumber int64, clientIP string) (loginSession string, err error) { - _, sErr := c.getEmailLinkSignInByPk(ctx, id, "") - if sErr != nil { - return "", errors.Wrapf(sErr, "failed to get user info by email:%v", id.Email) - } - loginSession, err = c.generateLoginSession(id, clientIP, "", loginSessionNumber) - if err != nil { - return "", errors.Wrap(err, "can't call generateLoginSession") + if sErr := c.sendConfirmationCode(ctx, &id, oldEmail, confirmationCode, language); sErr != nil { + return "", multierror.Append( //nolint:wrapcheck // . + errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"), + errors.Wrapf(sErr, "can't send magic link for id:%#v", id), + ).ErrorOrNil() } return loginSession, nil } -func (c *client) restoreOldLoginSession(id *loginID, clientIP, oldEmail string, loginSessionNumber int64) (string, error) { +func (c *client) restoreOldLoginSession(ctx context.Context, id *loginID, clientIP, oldEmail string, loginSessionNumber int64) (string, error) { oldLoginSession, dErr := c.generateLoginSession(id, clientIP, oldEmail, loginSessionNumber) if dErr != nil { - return "", errors.Wrap(dErr, "can't generate loginSession") + return "", multierror.Append( //nolint:wrapcheck // . + errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), "[rollback] failed to rollback login attempts for ip"), + errors.Wrap(dErr, "can't generate loginSession"), + ).ErrorOrNil() } - return oldLoginSession, nil + return oldLoginSession, errors.Wrapf(c.decrementIPLoginAttempts(ctx, clientIP, loginSessionNumber), + "failed to rollback login attempts for ip due to reuse of loginSession") } func (c *client) validateEmailSignIn(ctx context.Context, id *loginID) error { @@ -125,6 +112,19 @@ func (c *client) validateEmailSignIn(ctx context.Context, id *loginID) error { return nil } +func (c *client) decrementIPLoginAttempts(ctx context.Context, ip string, loginSessionNumber int64) error { + if ip != "" && loginSessionNumber > 0 && userIDForPhoneNumberToEmailMigration(ctx) == "" { + sql := `UPDATE sign_ins_per_ip SET + login_attempts = GREATEST(sign_ins_per_ip.login_attempts - 1, 0) + WHERE ip = $1 AND login_session_number = $2` + _, err := storage.Exec(ctx, c.db, sql, ip, loginSessionNumber) + + return errors.Wrapf(err, "failed to decrease login attempts for ip %v lsn %v", ip, loginSessionNumber) + } + + return nil +} + func (c *client) validateEmailModification(ctx context.Context, newEmail string, oldID *loginID) error { if iErr := c.isUserExist(ctx, newEmail); !storage.IsErr(iErr, storage.ErrNotFound) { if iErr != nil { @@ -149,8 +149,19 @@ func (c *client) validateEmailModification(ctx context.Context, newEmail string, return nil } +func (c *client) sendConfirmationCode(ctx context.Context, id *loginID, oldEmail, confirmationCode, language string) error { + var emailType string + if oldEmail != "" { + emailType = modifyEmailType + } else { + emailType = signInEmailType + } + + return errors.Wrapf(c.sendEmailWithType(ctx, emailType, id.Email, language, confirmationCode), "failed to send validation email for id:%#v", id) +} + //nolint:funlen // . -func (c *client) sendEmailWithType(ctx context.Context, emailType, language string, toEmails, confirmationCodes []string) error { +func (c *client) sendEmailWithType(ctx context.Context, emailType, toEmail, language, confirmationCode string) error { var tmpl *emailTemplate tmpl, ok := allEmailLinkTemplates[emailType][language] if !ok { @@ -163,26 +174,17 @@ func (c *client) sendEmailWithType(ctx context.Context, emailType, language stri AppName string TeamName string }{ + Email: toEmail, + ConfirmationCode: confirmationCode, PetName: c.cfg.PetName, AppName: c.cfg.AppName, TeamName: c.cfg.TeamName, - Email: "{{.Email}}", - ConfirmationCode: "{{.ConfirmationCode}}", } dataSubject := struct { AppName string }{ AppName: c.cfg.AppName, } - participants := make([]email.Participant, 0, len(toEmails)) - for i := range toEmails { - participants = append(participants, email.Participant{ - Name: "", - Email: toEmails[i], - SubstitutionFields: map[string]string{"{{.ConfirmationCode}}": confirmationCodes[i], "{{.Email}}": toEmails[i]}, - }) - } - lbIdx := atomic.AddUint64(&c.emailClientLBIndex, 1) % uint64(c.cfg.ExtraLoadBalancersCount+1) return errors.Wrapf(c.emailClients[lbIdx].Send(ctx, &email.Parcel{ @@ -195,26 +197,27 @@ func (c *client) sendEmailWithType(ctx context.Context, emailType, language stri Name: c.fromRecipients[lbIdx].FromEmailName, Email: c.fromRecipients[lbIdx].FromEmailAddress, }, - }, participants...), "failed to send email with type:%v for user with emails:%v", emailType, toEmails) + }, email.Participant{ + Name: "", + Email: toEmail, + }), "failed to send email with type:%v for user with email:%v", emailType, toEmail) } -//nolint:lll,revive // . -func (c *client) upsertEmailLinkSignIn(ctx context.Context, toEmail, deviceUniqueID, code, language string, now *time.Time) error { +//nolint:lll // . +func (c *client) upsertEmailLinkSignIn(ctx context.Context, toEmail, deviceUniqueID, code string, now *time.Time) error { confirmationCodeWrongAttempts := 0 - params := []any{now.Time, toEmail, deviceUniqueID, code, language, confirmationCodeWrongAttempts, userIDForPhoneNumberToEmailMigration(ctx)} + params := []any{now.Time, toEmail, deviceUniqueID, code, confirmationCodeWrongAttempts, userIDForPhoneNumberToEmailMigration(ctx)} sql := fmt.Sprintf(`INSERT INTO email_link_sign_ins ( created_at, email, device_unique_id, confirmation_code, - language, confirmation_code_wrong_attempts_count, phone_number_to_email_migration_user_id) - VALUES ($1, $2, $3, $4,$5, $6, NULLIF($7,'')) + VALUES ($1, $2, $3, $4, $5, NULLIF($6,'')) ON CONFLICT (email, device_unique_id) DO UPDATE SET created_at = EXCLUDED.created_at, confirmation_code = EXCLUDED.confirmation_code, - language = EXCLUDED.language, confirmation_code_wrong_attempts_count = EXCLUDED.confirmation_code_wrong_attempts_count, phone_number_to_email_migration_user_id = COALESCE(NULLIF(EXCLUDED.phone_number_to_email_migration_user_id,''),email_link_sign_ins.phone_number_to_email_migration_user_id), email_confirmed_at = null, @@ -231,6 +234,25 @@ func (c *client) upsertEmailLinkSignIn(ctx context.Context, toEmail, deviceUniqu return errors.Wrapf(err, "failed to insert/update email link sign ins record for email:%v", toEmail) } +func (c *client) upsertIPLoginAttempt(ctx context.Context, id *loginID, clientIP string, loginSessionNumber int64) error { + sql := `INSERT INTO sign_ins_per_ip (ip, login_session_number, login_attempts) + VALUES ($1, $2, 1) + ON CONFLICT (login_session_number, ip) DO UPDATE + SET login_attempts = sign_ins_per_ip.login_attempts + 1` + _, err := storage.Exec(ctx, c.db, sql, clientIP, loginSessionNumber) + if err != nil { + if storage.IsErr(err, storage.ErrCheckFailed) { + err = errors.Wrapf(ErrTooManyAttempts, "user %#v is blocked due to a lot of requests from IP %v", id, clientIP) + + return terror.New(err, map[string]any{"ip": clientIP}) + } + + return errors.Wrapf(err, "failed to increment login attempts from IP %v", clientIP) + } + + return nil +} + func (c *client) generateMagicLinkPayload(id *loginID, oldEmail string, now *time.Time) (string, error) { token := jwt.NewWithClaims(jwt.SigningMethodHS256, loginFlowToken{ RegisteredClaims: &jwt.RegisteredClaims{ diff --git a/auth/email_link/queue.go b/auth/email_link/queue.go deleted file mode 100644 index 70fa9215..00000000 --- a/auth/email_link/queue.go +++ /dev/null @@ -1,267 +0,0 @@ -// SPDX-License-Identifier: ice License 1.0 - -package emaillinkiceauth - -import ( - "context" - "fmt" - "math" - "math/rand/v2" - "strconv" - "strings" - stdlibtime "time" - - "github.com/hashicorp/go-multierror" - "github.com/pkg/errors" - "github.com/redis/go-redis/v9" - - "github.com/ice-blockchain/wintr/connectors/storage/v2" - "github.com/ice-blockchain/wintr/email" - "github.com/ice-blockchain/wintr/log" - "github.com/ice-blockchain/wintr/time" -) - -//nolint:funlen // . -func (c *client) enqueueLoginAttempt(ctx context.Context, now *time.Time, userEmail string) (queuePosition int64, rateLimit string, err error) { - var result []redis.Cmder - result, err = c.queueDB.TxPipelined(ctx, func(pipeliner redis.Pipeliner) error { - if zErr := pipeliner.ZAddNX(ctx, loginQueueKey, redis.Z{ - Score: float64(now.Nanosecond()), - Member: userEmail, - }).Err(); zErr != nil { - return zErr //nolint:wrapcheck // . - } - if zRankErr := pipeliner.ZRank(ctx, loginQueueKey, userEmail).Err(); zRankErr != nil { - return zRankErr //nolint:wrapcheck // . - } - - return pipeliner.Get(ctx, loginRateLimitKey).Err() - }) - if err != nil { - return 0, "", errors.Wrapf(err, "failed to enqueue email") - } - errs := make([]error, 0, len(result)) - for idx := 2; idx >= 0; idx-- { - cmdRes := result[idx] - if cmdRes.Err() != nil { - errs = append(errs, errors.Wrapf(cmdRes.Err(), "failed to enqueue email because of failed %v", cmdRes.String())) - - continue - } - switch idx { - case 2: //nolint:gomnd // Index in pipeline. - strCmd := cmdRes.(*redis.StringCmd) //nolint:errcheck,forcetypeassert // . - rateLimit = strCmd.Val() - case 1: - intCmd := cmdRes.(*redis.IntCmd) //nolint:errcheck,forcetypeassert // . - queuePosition = intCmd.Val() + 1 - case 0: - intCmd := cmdRes.(*redis.IntCmd) //nolint:errcheck,forcetypeassert // . - if intCmd.Val() == 0 { - return queuePosition, rateLimit, errAlreadyEnqueued - } - } - } - if cmdErr := multierror.Append(nil, errs...).ErrorOrNil(); cmdErr != nil { - return queuePosition, rateLimit, errors.Wrapf(cmdErr, "failed to enqueue email %v", userEmail) - } - - return queuePosition, rateLimit, nil -} - -//nolint:funlen,gocognit,revive,contextcheck // Keep processing in signle place. -func (c *client) processEmailQueue(rootCtx context.Context) { - lastProcessed := time.Now() - - emailQueueLock := storage.NewMutex(c.db, loginQueueKey) - lockCtx, lockCancel := context.WithTimeout(context.Background(), 30*stdlibtime.Second) //nolint:gomnd // . - if err := emailQueueLock.Lock(lockCtx); err != nil { - if !errors.Is(err, storage.ErrMutexNotLocked) { - log.Panic(errors.Wrapf(err, "failed to obtain emailQueueLock for email queue")) - } - } - lockCancel() - c.queueWg.Add(1) - defer func() { - log.Error(errors.Wrapf(c.queueDB.Close(), "failed to close email queue db")) - c.queueWg.Done() - }() - for rootCtx.Err() == nil { - now := time.Now() - reqCtx, reqCancel := context.WithTimeout(context.Background(), 30*stdlibtime.Second) //nolint:gomnd // . - if lockErr := emailQueueLock.EnsureLocked(reqCtx); lockErr != nil { - reqCancel() - if errors.Is(lockErr, storage.ErrTxAborted) { - return - } - if errors.Is(lockErr, storage.ErrMutexNotLocked) { - _ = wait(rootCtx, stdlibtime.Duration(1+rand.IntN(4))*stdlibtime.Second) //nolint:errcheck,gosec,gomnd // Nothing to rollback. - - continue - } - } - reqCancel() - reqCtx, reqCancel = context.WithTimeout(context.Background(), 30*stdlibtime.Second) //nolint:gomnd // . - emails, scores, rateLimit, err := c.dequeueNextEmails(reqCtx) //nolint:contextcheck // Background context. - if err != nil { - log.Error(errors.Wrapf(err, "failed to fetch next %v emails in queue", email.MaxBatchSize)) - reqCancel() - _ = wait(rootCtx, 1*stdlibtime.Second) //nolint:errcheck // Noting to rollback. - - continue - } - reqCancel() - - if len(emails) == 0 { - log.Info("No emails in queue for sending") - _ = wait(rootCtx, 10*stdlibtime.Second) //nolint:errcheck,gomnd // Nothing to rollback. - - continue - } - - rlCount, rlDuration, rlErr := parseRateLimit(rateLimit) - if rlErr != nil { - log.Error(errors.Wrapf(c.rollbackEmailsBackToQueue(emails, scores), "failed to rollback emails %#v back to queue", emails)) - log.Panic(errors.Wrapf(rlErr, "failed to parse rate limit for email queue %v", rateLimit)) //nolint:revive // . - } - limit := int(math.Min(float64(rlCount), float64(len(emails)))) - if rlCount < len(emails) { - log.Error(errors.Wrapf(c.rollbackEmailsBackToQueue(emails[rlCount:], scores), "failed to rollback emails %#v back to queue cuz rate limit %v is less than batch %v", emails[rlCount:], rlCount, email.MaxBatchSize)) //nolint:lll // . - emails = emails[:rlCount] - } - - reqCtx, reqCancel = context.WithTimeout(context.Background(), 30*stdlibtime.Second) //nolint:gomnd // . - loginInformation, err := c.fetchLoginInformationForEmailBatch(reqCtx, now, emails, limit) - if err != nil { - log.Error(errors.Wrapf(err, "failed to fetch login information for emails: %v", emails)) - reqCancel() - log.Error(errors.Wrapf(c.rollbackEmailsBackToQueue(emails, scores), "failed to rollback emails %#v back to queue", emails)) - _ = wait(rootCtx, 1*stdlibtime.Second) //nolint:errcheck // Already rolled back. - - continue - } - reqCancel() - lastTimeBatchProcessingDuration := time.Now().Sub(*lastProcessed.Time) - rateLimitEstimationDuration := lastTimeBatchProcessingDuration * stdlibtime.Duration(int64(rlCount)/int64(len(emails))) - if rateLimitEstimationDuration < rlDuration { - oneBatchProcessingTimeToRespectRateLimit := stdlibtime.Duration(int64(len(emails))/int64(rlCount)) * rlDuration - if wait(rootCtx, oneBatchProcessingTimeToRespectRateLimit) != nil { - log.Error(errors.Wrapf(c.rollbackEmailsBackToQueue(emails, scores), "failed to rollback fetched emails %#v back to queue", emails)) - - continue - } - } - reqCtx, reqCancel = context.WithTimeout(context.Background(), 30*stdlibtime.Second) //nolint:gomnd // . - if failed, sErr := c.sendEmails(reqCtx, loginInformation); sErr != nil { - reqCancel() - log.Error(errors.Wrapf(sErr, "failed to send email batch for emails %#v", failed)) - log.Error(errors.Wrapf(c.rollbackEmailsBackToQueue(failed, scores), "failed to rollback failed emails %#v back to queue", failed)) - stdlibtime.Sleep(1 * stdlibtime.Second) - - continue - } - reqCancel() - lastProcessed = time.Now() - } -} - -func (c *client) rollbackEmailsBackToQueue(failed []string, scores map[string]int64) error { - rollbackCtx, rollbackCancel := context.WithTimeout(context.Background(), 30*stdlibtime.Second) //nolint:gomnd // . - defer rollbackCancel() - failedZ := make([]redis.Z, 0, len(failed)) - for _, failedEmail := range failed { - failedZ = append(failedZ, redis.Z{ - Score: float64(scores[failedEmail]), - Member: failedEmail, - }) - } - - return errors.Wrapf(c.queueDB.ZAddNX(rollbackCtx, loginQueueKey, failedZ...).Err(), "failed to rollback unsent emails %#v", failed) -} - -//nolint:gocritic,revive // We need all the results from the pipeline -func (c *client) dequeueNextEmails(ctx context.Context) (emailsBatch []string, scores map[string]int64, rateLimit string, err error) { - var pipeRes []redis.Cmder - pipeRes, err = c.queueDB.TxPipelined(ctx, func(pipeliner redis.Pipeliner) error { - if zpopErr := pipeliner.ZPopMin(ctx, loginQueueKey, email.MaxBatchSize).Err(); zpopErr != nil { - return zpopErr //nolint:wrapcheck // . - } - - return pipeliner.Get(ctx, loginRateLimitKey).Err() - }) - if err != nil { - return nil, nil, "", errors.Wrapf(err, "failed to fetch email queue batch") - } - if zpopErr := pipeRes[0].Err(); zpopErr != nil { - return nil, nil, "", errors.Wrapf(zpopErr, "failed to fetch %v email queue batch", pipeRes[0].String()) - } - if len(pipeRes) > 1 { - if rateErr := pipeRes[1].Err(); rateErr != nil { - return nil, nil, "", errors.Wrapf(rateErr, "failed to fetch %v email sending rate", pipeRes[1].String()) - } - } - batch := pipeRes[0].(*redis.ZSliceCmd).Val() //nolint:forcetypeassert // . - emailsBatch = make([]string, 0, len(batch)) - scores = make(map[string]int64, 0) - for _, itemInBatch := range batch { - emailsBatch = append(emailsBatch, itemInBatch.Member.(string)) //nolint:forcetypeassert // . - scores[emailsBatch[len(emailsBatch)-1]] = int64(itemInBatch.Score) - } - rate := pipeRes[1].(*redis.StringCmd).Val() //nolint:forcetypeassert // . - - return emailsBatch, scores, rate, nil -} - -func (c *client) fetchLoginInformationForEmailBatch(ctx context.Context, now *time.Time, emails []string, limit int) ([]*emailLinkSignIn, error) { - sql := fmt.Sprintf(` - SELECT * FROM public.email_link_sign_ins - WHERE email = ANY($1) AND created_at > ($2::TIMESTAMP - (%[2]v * interval '1 second')) - ORDER BY created_at DESC - LIMIT %[1]v;`, limit, c.cfg.EmailValidation.ExpirationTime.Seconds()) - res, err := storage.Select[emailLinkSignIn](ctx, c.db, sql, emails, now.Time) - - return res, err -} - -func parseRateLimit(rateLimit string) (int, stdlibtime.Duration, error) { - spl := strings.Split(rateLimit, ":") - rateLimitCount, rlErr := strconv.Atoi(spl[0]) - if rlErr != nil { - return 0, stdlibtime.Duration(0), rlErr - } - rateLimitDuration, rlErr := stdlibtime.ParseDuration(spl[1]) - if rlErr != nil { - return 0, stdlibtime.Duration(0), rlErr - } - - return rateLimitCount, rateLimitDuration, nil -} - -func (c *client) sendEmails(ctx context.Context, emails []*emailLinkSignIn) (failed []string, err error) { - emailsByLanguage := make(map[string][]string) - confCodesByLanguage := make(map[string][]string) - for _, userEmail := range emails { - emailsByLanguage[userEmail.Language] = append(emailsByLanguage[userEmail.Language], userEmail.Email) - confCodesByLanguage[userEmail.Language] = append(confCodesByLanguage[userEmail.Language], userEmail.ConfirmationCode) - } - var mErr *multierror.Error - for language := range emailsByLanguage { - if sErr := c.sendEmailWithType(ctx, signInEmailType, language, emailsByLanguage[language], confCodesByLanguage[language]); sErr != nil { - mErr = multierror.Append(mErr, errors.Wrapf(sErr, "failed to send emails for language %v: %#v", language, emailsByLanguage[language])) - failed = append(failed, emailsByLanguage[language]...) - } - } - - return failed, mErr.ErrorOrNil() //nolint:wrapcheck // . -} - -func wait(ctx context.Context, d stdlibtime.Duration) error { - select { - case <-stdlibtime.After(d): - return nil - case <-ctx.Done(): - log.Info("cancelled") - - return context.Canceled - } -} diff --git a/auth/email_link/users.go b/auth/email_link/users.go index c9b1253f..4ace4dc4 100644 --- a/auth/email_link/users.go +++ b/auth/email_link/users.go @@ -113,7 +113,7 @@ func (c *client) getUserByIDOrPk(ctx context.Context, userID string, id *loginID phone_number_to_email_migration_user_id, email, $3 AS device_unique_id, - language, + 'en' AS language, COALESCE((account_metadata.metadata -> 'hash_code')::BIGINT,0) AS hash_code, account_metadata.metadata, 2 AS idx diff --git a/cmd/eskimo-hut/api/docs.go b/cmd/eskimo-hut/api/docs.go index c1d3d605..86a04345 100644 --- a/cmd/eskimo-hut/api/docs.go +++ b/cmd/eskimo-hut/api/docs.go @@ -2149,14 +2149,6 @@ const docTemplate = `{ "loginSession": { "type": "string", "example": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM" - }, - "positionInQueue": { - "type": "integer", - "example": 675 - }, - "rateLimit": { - "type": "string", - "example": "1000:24h" } } }, diff --git a/cmd/eskimo-hut/api/swagger.json b/cmd/eskimo-hut/api/swagger.json index b262d4d3..aa58d723 100644 --- a/cmd/eskimo-hut/api/swagger.json +++ b/cmd/eskimo-hut/api/swagger.json @@ -2142,14 +2142,6 @@ "loginSession": { "type": "string", "example": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM" - }, - "positionInQueue": { - "type": "integer", - "example": 675 - }, - "rateLimit": { - "type": "string", - "example": "1000:24h" } } }, diff --git a/cmd/eskimo-hut/api/swagger.yaml b/cmd/eskimo-hut/api/swagger.yaml index f4fb3ef2..e174e5c4 100644 --- a/cmd/eskimo-hut/api/swagger.yaml +++ b/cmd/eskimo-hut/api/swagger.yaml @@ -6,12 +6,6 @@ definitions: loginSession: example: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM type: string - positionInQueue: - example: 675 - type: integer - rateLimit: - example: 1000:24h - type: string type: object main.CreateUserRequestBody: properties: diff --git a/cmd/eskimo-hut/auth.go b/cmd/eskimo-hut/auth.go index 28a29491..b2e61bfe 100644 --- a/cmd/eskimo-hut/auth.go +++ b/cmd/eskimo-hut/auth.go @@ -61,8 +61,7 @@ func (s *service) SendSignInLinkToEmail( //nolint:gocritic,funlen // . return nil, server.BadRequest(err, invalidEmail) } ctx = emaillink.ContextWithPhoneNumberToEmailMigration(ctx, req.Data.UserID) //nolint:revive // Not a problem. - posInQueue, rateLimit, loginSession, err := s.authEmailLinkClient.SendSignInLinkToEmail(ctx, email, req.Data.DeviceUniqueID, - req.Data.Language, req.ClientIP.String()) + loginSession, err := s.authEmailLinkClient.SendSignInLinkToEmail(ctx, email, req.Data.DeviceUniqueID, req.Data.Language, req.ClientIP.String()) if err != nil { switch { case errors.Is(err, emaillink.ErrUserBlocked): @@ -82,7 +81,7 @@ func (s *service) SendSignInLinkToEmail( //nolint:gocritic,funlen // . } } - return server.OK[Auth](&Auth{LoginSession: loginSession, PositionInQueue: posInQueue, RateLimit: rateLimit}), nil + return server.OK[Auth](&Auth{LoginSession: loginSession}), nil } // SignIn godoc diff --git a/cmd/eskimo-hut/contract.go b/cmd/eskimo-hut/contract.go index 72581d92..aa21ff9c 100644 --- a/cmd/eskimo-hut/contract.go +++ b/cmd/eskimo-hut/contract.go @@ -138,9 +138,7 @@ type ( KycFaceAvailable bool `json:"kycFaceAvailable,omitempty" example:"true"` } Auth struct { - RateLimit string `json:"rateLimit" example:"1000:24h"` - LoginSession string `json:"loginSession" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . - PositionInQueue int64 `json:"positionInQueue" example:"675"` + LoginSession string `json:"loginSession" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // . } RefreshedToken struct { *emaillink.Tokens diff --git a/cmd/eskimo-hut/eskimo_hut.go b/cmd/eskimo-hut/eskimo_hut.go index 2d69cdfd..f13ff88b 100644 --- a/cmd/eskimo-hut/eskimo_hut.go +++ b/cmd/eskimo-hut/eskimo_hut.go @@ -51,7 +51,7 @@ func (s *service) RegisterRoutes(router *server.Router) { func (s *service) Init(ctx context.Context, cancel context.CancelFunc) { s.usersProcessor = users.StartProcessor(ctx, cancel) - s.authEmailLinkClient = emaillink.NewClient(ctx, cancel, s.usersProcessor, server.Auth(ctx)) + s.authEmailLinkClient = emaillink.NewClient(ctx, s.usersProcessor, server.Auth(ctx)) s.socialRepository = social.New(ctx, s.usersProcessor) s.quizRepository = kycquiz.NewRepository(ctx, s.usersProcessor) s.faceKycClient = facekyc.New(ctx, s.usersProcessor) @@ -74,8 +74,5 @@ func (s *service) Close(ctx context.Context) error { func (s *service) CheckHealth(ctx context.Context) error { log.Debug("checking health...", "package", "users") - return multierror.Append( //nolint:wrapcheck // Not needed. - errors.Wrapf(s.usersProcessor.CheckHealth(ctx), "processor health check failed"), - errors.Wrapf(s.authEmailLinkClient.CheckHealth(ctx), "email client health check failed"), - ).ErrorOrNil() + return errors.Wrapf(s.usersProcessor.CheckHealth(ctx), "processor health check failed") } diff --git a/cmd/eskimo-hut/users.go b/cmd/eskimo-hut/users.go index b992c4c8..f243c3f1 100644 --- a/cmd/eskimo-hut/users.go +++ b/cmd/eskimo-hut/users.go @@ -231,7 +231,7 @@ func (s *service) emailUpdateRequested( language = oldUser.Language } - if _, _, loginSession, err = s.authEmailLinkClient.SendSignInLinkToEmail( + if loginSession, err = s.authEmailLinkClient.SendSignInLinkToEmail( users.ConfirmedEmailContext(ctx, loggedInUser.Email), newEmail, deviceID, language, "", ); err != nil { diff --git a/cmd/scripts/merge_firebase_phone_login_with_ice_email_login/merge_firebase_phone_login_with_ice_email_login.go b/cmd/scripts/merge_firebase_phone_login_with_ice_email_login/merge_firebase_phone_login_with_ice_email_login.go index 7916222b..9c021905 100644 --- a/cmd/scripts/merge_firebase_phone_login_with_ice_email_login/merge_firebase_phone_login_with_ice_email_login.go +++ b/cmd/scripts/merge_firebase_phone_login_with_ice_email_login/merge_firebase_phone_login_with_ice_email_login.go @@ -47,7 +47,7 @@ type ( func main() { usersProcessor := users.StartProcessor(context.Background(), func() {}) authClient := auth.New(context.Background(), applicationYamlAuthKey) - authEmailLinkClient := emaillink.NewClient(context.Background(), nil, usersProcessor, authClient) + authEmailLinkClient := emaillink.NewClient(context.Background(), usersProcessor, authClient) db := storage.MustConnect(context.Background(), ddl, applicationYamlEskimoKey) defer db.Close() defer usersProcessor.Close() diff --git a/go.mod b/go.mod index 613469ae..47db0cf2 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/common v0.55.0 github.com/prometheus/prometheus v0.53.0 - github.com/redis/go-redis/v9 v9.5.3 github.com/stretchr/testify v1.9.0 github.com/swaggo/swag v1.16.3 github.com/testcontainers/testcontainers-go v0.31.0 @@ -56,7 +55,6 @@ require ( github.com/containerd/errdefs v0.1.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dennwc/varint v1.0.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v27.0.3+incompatible // indirect diff --git a/go.sum b/go.sum index c9946b54..1c45632d 100644 --- a/go.sum +++ b/go.sum @@ -52,10 +52,6 @@ github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= -github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= -github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.11.9 h1:LFHENlIY/SLzDWverzdOvgMztTxcfcF+cqNsz9pK5zg= github.com/bytedance/sonic v1.11.9/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -334,8 +330,6 @@ github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjCA= github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= -github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= -github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=