From 7b28c489b8a3c3ddcc35639aa61c35478be65fab Mon Sep 17 00:00:00 2001 From: Anirudh Oppiliappan Date: Tue, 3 Mar 2026 22:21:18 +0200 Subject: [PATCH] appview/oauth: store and reuse the same app password session We seem to be hammering bsky's PDS, which explains the 429s. Perhaps re-using the session alleviates the pressure. It's hard to test this locally since their rate limits vanish (different host/IP). Signed-off-by: Anirudh Oppiliappan --- appview/oauth/handler.go | 41 +++++++++++++++++++++++++++++++++++++--- appview/oauth/oauth.go | 4 ++++ appview/state/state.go | 2 +- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/appview/oauth/handler.go b/appview/oauth/handler.go index 8d64cf24..66ca626a 100644 --- a/appview/oauth/handler.go +++ b/appview/oauth/handler.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "log/slog" "net/http" "slices" "time" @@ -130,7 +131,7 @@ func (o *OAuth) addToDefaultSpindle(did string) { } l.Debug("adding to default spindle") - session, err := CreateAppPasswordSession(o.IdResolver, o.Config.Core.AppPassword, consts.TangledDid, o.Config.Core.RateLimitBypass) + session, err := o.getAppPasswordSession() if err != nil { l.Error("failed to create session", "err", err) return @@ -144,6 +145,7 @@ func (o *OAuth) addToDefaultSpindle(did string) { } if err := session.putRecord(record, tangled.SpindleMemberNSID); err != nil { + o.invalidateAppPasswordSession() l.Error("failed to add to default spindle", "err", err) return } @@ -169,7 +171,7 @@ func (o *OAuth) addToDefaultKnot(did string) { } l.Debug("adding to default knot") - session, err := CreateAppPasswordSession(o.IdResolver, o.Config.Core.AppPassword, consts.TangledDid, o.Config.Core.RateLimitBypass) + session, err := o.getAppPasswordSession() if err != nil { l.Error("failed to create session", "err", err) return @@ -183,6 +185,7 @@ func (o *OAuth) addToDefaultKnot(did string) { } if err := session.putRecord(record, tangled.KnotMemberNSID); err != nil { + o.invalidateAppPasswordSession() l.Error("failed to add to default knot", "err", err) return } @@ -248,9 +251,10 @@ type AppPasswordSession struct { PdsEndpoint string Did string RateLimitBypass string + Logger *slog.Logger } -func CreateAppPasswordSession(res *idresolver.Resolver, appPassword, did, rateLimitBypass string) (*AppPasswordSession, error) { +func CreateAppPasswordSession(res *idresolver.Resolver, appPassword, did, rateLimitBypass string, logger *slog.Logger) (*AppPasswordSession, error) { if appPassword == "" { return nil, fmt.Errorf("no app password configured") } @@ -284,6 +288,8 @@ func CreateAppPasswordSession(res *idresolver.Resolver, appPassword, did, rateLi sessionReq.Header.Set("x-ratelimit-bypass", rateLimitBypass) } + logger.Debug("creating app password session", "url", sessionURL, "headers", sessionReq.Header) + client := &http.Client{Timeout: 30 * time.Second} sessionResp, err := client.Do(sessionReq) if err != nil { @@ -303,6 +309,7 @@ func CreateAppPasswordSession(res *idresolver.Resolver, appPassword, did, rateLi session.PdsEndpoint = pdsEndpoint session.Did = did session.RateLimitBypass = rateLimitBypass + session.Logger = logger return &session, nil } @@ -337,6 +344,8 @@ func (s *AppPasswordSession) putRecord(record any, collection string) error { req.Header.Set("x-ratelimit-bypass", s.RateLimitBypass) } + s.Logger.Debug("putting record", "url", url, "collection", collection, "headers", req.Header) + client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { @@ -350,3 +359,29 @@ func (s *AppPasswordSession) putRecord(record any, collection string) error { return nil } + +// getAppPasswordSession returns a cached AppPasswordSession, creating one if needed. +func (o *OAuth) getAppPasswordSession() (*AppPasswordSession, error) { + o.appPasswordSessionMu.Lock() + defer o.appPasswordSessionMu.Unlock() + + if o.appPasswordSession != nil { + return o.appPasswordSession, nil + } + + session, err := CreateAppPasswordSession(o.IdResolver, o.Config.Core.AppPassword, consts.TangledDid, o.Config.Core.RateLimitBypass, o.Logger) + if err != nil { + return nil, err + } + + o.appPasswordSession = session + return session, nil +} + +// invalidateAppPasswordSession clears the cached session so the next call to +// getAppPasswordSession will create a fresh one. +func (o *OAuth) invalidateAppPasswordSession() { + o.appPasswordSessionMu.Lock() + defer o.appPasswordSessionMu.Unlock() + o.appPasswordSession = nil +} diff --git a/appview/oauth/oauth.go b/appview/oauth/oauth.go index 6c4c105a..e56afc53 100644 --- a/appview/oauth/oauth.go +++ b/appview/oauth/oauth.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "net/http" + "sync" "time" comatproto "github.com/bluesky-social/indigo/api/atproto" @@ -33,6 +34,9 @@ type OAuth struct { Enforcer *rbac.Enforcer IdResolver *idresolver.Resolver Logger *slog.Logger + + appPasswordSession *AppPasswordSession + appPasswordSessionMu sync.Mutex } func New(config *config.Config, ph posthog.Client, db *db.DB, enforcer *rbac.Enforcer, res *idresolver.Resolver, logger *slog.Logger) (*OAuth, error) { diff --git a/appview/state/state.go b/appview/state/state.go index 271bdc62..85f99c73 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -622,7 +622,7 @@ func fetchBskyPosts(ctx context.Context, res *idresolver.Resolver, config *confi return } - session, err := oauth.CreateAppPasswordSession(res, config.Core.AppPassword, consts.TangledDid, config.Core.RateLimitBypass) + session, err := oauth.CreateAppPasswordSession(res, config.Core.AppPassword, consts.TangledDid, config.Core.RateLimitBypass, logger) if err != nil { logger.Error("failed to create appassword session... skipping fetch", "err", err) return -- 2.51.2