| @@ -7,6 +7,7 @@ import ( |
| 7 | "net/http" | 7 | "net/http" |
| 8 | "os" | 8 | "os" |
| 9 | "sync" | 9 | "sync" |
| | 10 | + "sync/atomic" |
| 10 | "time" | 11 | "time" |
| 11 | | 12 | |
| 12 | "github.com/bluesky-social/indigo/atproto/identity" | 13 | "github.com/bluesky-social/indigo/atproto/identity" |
| @@ -128,12 +129,37 @@ var profileCache sync.Map |
| 128 | | 129 | |
| 129 | const profileCacheTTL = 4 * time.Hour | 130 | const profileCacheTTL = 4 * time.Hour |
| 130 | | 131 | |
| | 132 | +// maxProfileCacheSize caps the in-memory profile cache to prevent unbounded |
| | 133 | +// growth. When exceeded, the oldest entries are evicted. |
| | 134 | +const maxProfileCacheSize = 50_000 |
| | 135 | + |
| | 136 | +var profileCacheSize atomic.Int64 |
| | 137 | + |
| | 138 | +func pruneProfileCache() { |
| | 139 | + if profileCacheSize.Load() < maxProfileCacheSize { |
| | 140 | + return |
| | 141 | + } |
| | 142 | + var toDelete []string |
| | 143 | + count := 0 |
| | 144 | + profileCache.Range(func(key, value any) bool { |
| | 145 | + toDelete = append(toDelete, key.(string)) |
| | 146 | + count++ |
| | 147 | + return count < maxProfileCacheSize/5 |
| | 148 | + }) |
| | 149 | + for _, k := range toDelete { |
| | 150 | + profileCache.Delete(k) |
| | 151 | + } |
| | 152 | + profileCacheSize.Store(int64(len(toDelete))) |
| | 153 | +} |
| | 154 | + |
| 131 | func ResolveProfile(ctx context.Context, did string) Profile { | 155 | func ResolveProfile(ctx context.Context, did string) Profile { |
| 132 | if cached, ok := profileCache.Load(did); ok { | 156 | if cached, ok := profileCache.Load(did); ok { |
| 133 | entry := cached.(*profileEntry) | 157 | entry := cached.(*profileEntry) |
| 134 | if time.Since(entry.fetched) < profileCacheTTL { | 158 | if time.Since(entry.fetched) < profileCacheTTL { |
| 135 | return entry.profile | 159 | return entry.profile |
| 136 | } | 160 | } |
| | 161 | + profileCache.Delete(did) |
| | 162 | + profileCacheSize.Add(-1) |
| 137 | } | 163 | } |
| 138 | | 164 | |
| 139 | ident, err := ResolveIdentity(ctx, did) | 165 | ident, err := ResolveIdentity(ctx, did) |
| @@ -157,6 +183,10 @@ func ResolveProfile(ctx context.Context, did string) Profile { |
| 157 | p.DisplayName = actor.DisplayName | 183 | p.DisplayName = actor.DisplayName |
| 158 | p.AvatarURL = actor.Avatar | 184 | p.AvatarURL = actor.Avatar |
| 159 | | 185 | |
| | 186 | + if profileCacheSize.Load() >= maxProfileCacheSize { |
| | 187 | + pruneProfileCache() |
| | 188 | + } |
| 160 | profileCache.Store(did, &profileEntry{profile: p, fetched: time.Now()}) | 189 | profileCache.Store(did, &profileEntry{profile: p, fetched: time.Now()}) |
| | 190 | + profileCacheSize.Add(1) |
| 161 | return p | 191 | return p |
| 162 | } | 192 | } |