mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
73fad424ed
Every subscription fetch read and scanned each inbound's whole settings blob, about 25 MB at 100k clients, although subscription bodies resolve their clients from the clients table and never use its clients array. Each inbound's settings minus the clients array are now cached, keyed by inbound and shared by all its subscribers; the inbound row, clients, stats and hosts are still read per fetch, and WireGuard and AmneziaWG keep full settings for their peers. Panel edits purge the cache through an invalidator registry the subscription server releases on stop, with a 10 s TTL as the backstop. At 100k clients a hit takes 2.1 ms on SQLite (72.5 ms before) and 20.7 ms on PostgreSQL (332 ms).
104 lines
2.8 KiB
Go
104 lines
2.8 KiB
Go
package sub
|
|
|
|
import (
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/goccy/go-json"
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
// Subscription apps poll on multi-minute intervals, so inbound settings at most this old read the
|
|
// same as fresh ones; panel edits purge them at once.
|
|
const settingsCacheTTL = 10 * time.Second
|
|
|
|
// inboundSettingsCache keeps each inbound's settings without its clients array for subscription
|
|
// bodies, so every subscriber of a large inbound shares one small copy instead of the whole blob.
|
|
type inboundSettingsCache struct {
|
|
ttl time.Duration
|
|
now func() time.Time
|
|
group singleflight.Group
|
|
|
|
mu sync.Mutex
|
|
generation uint64
|
|
entries map[int]settingsEntry
|
|
}
|
|
|
|
type settingsEntry struct {
|
|
settings string
|
|
expiresAt time.Time
|
|
}
|
|
|
|
func newInboundSettingsCache(ttl time.Duration) *inboundSettingsCache {
|
|
return &inboundSettingsCache{ttl: ttl, now: time.Now, entries: map[int]settingsEntry{}}
|
|
}
|
|
|
|
// settingsFor returns the inbound's client-less settings, loading them at most once per TTL window
|
|
// even when concurrent requests miss together.
|
|
func (c *inboundSettingsCache) settingsFor(inboundId int, load func() (string, error)) (string, error) {
|
|
if settings, ok := c.lookup(inboundId); ok {
|
|
return settings, nil
|
|
}
|
|
v, err, _ := c.group.Do(strconv.Itoa(inboundId), func() (any, error) {
|
|
if settings, ok := c.lookup(inboundId); ok {
|
|
return settings, nil
|
|
}
|
|
c.mu.Lock()
|
|
generation := c.generation
|
|
c.mu.Unlock()
|
|
raw, err := load()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
settings := withoutClients(raw)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
// A purge during the load means the row may have changed under it; serve it, never keep it.
|
|
if generation == c.generation {
|
|
c.entries[inboundId] = settingsEntry{settings: settings, expiresAt: c.now().Add(c.ttl)}
|
|
}
|
|
return settings, nil
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return v.(string), nil
|
|
}
|
|
|
|
func (c *inboundSettingsCache) lookup(inboundId int) (string, bool) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
entry, ok := c.entries[inboundId]
|
|
if !ok || !c.now().Before(entry.expiresAt) {
|
|
return "", false
|
|
}
|
|
return entry.settings, true
|
|
}
|
|
|
|
// purgeAll drops every entry after a panel mutation, so an edit reaches the next fetch.
|
|
func (c *inboundSettingsCache) purgeAll() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.generation++
|
|
clear(c.entries)
|
|
}
|
|
|
|
// withoutClients drops the clients array, nearly all of a large inbound's settings, which
|
|
// subscription bodies resolve from the clients table instead; malformed settings stay as they are.
|
|
func withoutClients(settings string) string {
|
|
var shallow map[string]json.RawMessage
|
|
if err := json.Unmarshal([]byte(settings), &shallow); err != nil {
|
|
return settings
|
|
}
|
|
if _, ok := shallow["clients"]; !ok {
|
|
return settings
|
|
}
|
|
delete(shallow, "clients")
|
|
out, err := json.Marshal(shallow)
|
|
if err != nil {
|
|
return settings
|
|
}
|
|
return string(out)
|
|
}
|