mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
perf(sub): cache each inbound's settings without clients for subscription bodies (#6758)
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).
This commit is contained in:
@@ -1047,6 +1047,7 @@ func (s *ClientService) bulkDelInboundClients(
|
||||
) bulkInboundDeleteResult {
|
||||
res := bulkInboundDeleteResult{perEmailSkipped: map[string]string{}}
|
||||
|
||||
defer InvalidateSubData()
|
||||
defer lockInbound(inboundId).Unlock()
|
||||
|
||||
oldInbound, err := inboundSvc.GetInbound(inboundId)
|
||||
|
||||
@@ -59,6 +59,7 @@ func advancePushedInbound(rt runtime.Runtime, prevSettings, pushedSettings strin
|
||||
// semantics of DelInboundClientByEmail for each removed client. needRestart is
|
||||
// the OR across all removals.
|
||||
func (s *ClientService) delInboundClients(inboundSvc *InboundService, inboundId int, recs []*model.ClientRecord, keepTraffic bool) (bool, error) {
|
||||
defer InvalidateSubData()
|
||||
if len(recs) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
@@ -325,6 +326,7 @@ func (s *ClientService) checkEmailsExistForClients(inboundSvc *InboundService, c
|
||||
}
|
||||
|
||||
func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model.Inbound) (bool, error) {
|
||||
defer InvalidateSubData()
|
||||
defer lockInbound(data.Id).Unlock()
|
||||
|
||||
clients, err := inboundSvc.GetClients(data)
|
||||
@@ -667,6 +669,7 @@ func (s *ClientService) AddInboundClient(inboundSvc *InboundService, data *model
|
||||
}
|
||||
|
||||
func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *model.Inbound, oldEmail string) (bool, error) {
|
||||
defer InvalidateSubData()
|
||||
defer lockInbound(data.Id).Unlock()
|
||||
|
||||
clients, err := inboundSvc.GetClients(data)
|
||||
@@ -1112,6 +1115,7 @@ func (s *ClientService) UpdateInboundClient(inboundSvc *InboundService, data *mo
|
||||
}
|
||||
|
||||
func (s *ClientService) DelInboundClientByEmail(inboundSvc *InboundService, inboundId int, email string, keepTraffic bool, fullDelete bool) (bool, error) {
|
||||
defer InvalidateSubData()
|
||||
defer lockInbound(inboundId).Unlock()
|
||||
|
||||
oldInbound, err := inboundSvc.GetInbound(inboundId)
|
||||
|
||||
@@ -1276,6 +1276,7 @@ func (s *InboundService) normalizeMtprotoXrayPort(inbound *model.Inbound, oldSet
|
||||
// then saves the inbound to the database and optionally adds it to the running Xray instance.
|
||||
// Returns the created inbound, whether Xray needs restart, and any error.
|
||||
func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
defer InvalidateSubData()
|
||||
inbound.Id = 0
|
||||
if err := normalizeTuicSettings(inbound); err != nil {
|
||||
return inbound, false, err
|
||||
@@ -1602,6 +1603,7 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
}
|
||||
|
||||
func (s *InboundService) DelInbound(id int) (bool, error) {
|
||||
defer InvalidateSubData()
|
||||
needRestart, nodePush, err := s.delInbound(id)
|
||||
if nodePush != nil {
|
||||
nodePush()
|
||||
@@ -1734,6 +1736,7 @@ type BulkDelInboundReport struct {
|
||||
// the rest; the aggregated needRestart is returned so the caller restarts
|
||||
// xray at most once.
|
||||
func (s *InboundService) DelInbounds(ids []int) (BulkDelInboundResult, bool, error) {
|
||||
defer InvalidateSubData()
|
||||
result := BulkDelInboundResult{}
|
||||
needRestart := false
|
||||
var pushIDs []int
|
||||
@@ -1786,6 +1789,7 @@ func (s *InboundService) GetInboundDetail(id int) (*model.Inbound, error) {
|
||||
// SetInboundSubSortIndex changes only the subscription sort order, so a
|
||||
// reorder cannot carry a stale settings/client payload over another edit.
|
||||
func (s *InboundService) SetInboundSubSortIndex(id int, index int) error {
|
||||
defer InvalidateSubData()
|
||||
index = normalizeSubSortIndex(index)
|
||||
inbound, err := s.GetInbound(id)
|
||||
if err != nil {
|
||||
@@ -1832,6 +1836,7 @@ func (s *InboundService) SetInboundSubSortIndex(id int, index int) error {
|
||||
}
|
||||
|
||||
func (s *InboundService) SetInboundEnable(id int, enable bool) (bool, error) {
|
||||
defer InvalidateSubData()
|
||||
inbound, err := s.GetInbound(id)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -1963,6 +1968,7 @@ func (s *InboundService) validateUpdatedInboundClients(inbound, existing *model.
|
||||
}
|
||||
|
||||
func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound, bool, error) {
|
||||
defer InvalidateSubData()
|
||||
legacyShareAddr := legacyMtprotoShareAddr(inbound)
|
||||
inbound.TrafficResetDay = normalizeTrafficResetDay(inbound.TrafficResetDay)
|
||||
// Normalize streamSettings based on protocol
|
||||
|
||||
@@ -232,6 +232,8 @@ func (s *InboundService) markClientsDisabledInSettings(tx *gorm.DB, inboundID in
|
||||
Update("settings", ib.Settings).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// A just-disabled client must leave subscriptions within this tick.
|
||||
InvalidateSubData()
|
||||
return &snapshot, &ib, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1266,6 +1266,9 @@ func (s *InboundService) setRemoteTrafficLocked(nodeID int, snap *runtime.Traffi
|
||||
return false, err
|
||||
}
|
||||
structuralChange = true
|
||||
// Adopted wire settings feed subscription bodies; drop snapshots
|
||||
// so the adopted rows are served this tick, not after the TTL.
|
||||
InvalidateSubData()
|
||||
}
|
||||
// The fingerprint stamps the un-lifted wire blob on purpose: a lift must
|
||||
// leave reconcile a mismatch to re-push against.
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
// commitInboundClientSettings writes a client op's edit (base → ib.Settings) onto
|
||||
// the settings committed now: a traffic tick after the op's read must survive.
|
||||
func commitInboundClientSettings(tx *gorm.DB, ib *model.Inbound, base string) error {
|
||||
defer InvalidateSubData()
|
||||
var current []string
|
||||
if err := tx.Model(&model.Inbound{}).Where("id = ?", ib.Id).Pluck("settings", ¤t).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package service
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
subDataInvalidatorsMu sync.Mutex
|
||||
subDataInvalidators = map[int]func(){}
|
||||
nextSubDataInvalidator int
|
||||
)
|
||||
|
||||
// RegisterSubDataInvalidator registers a callback run after any mutation that can change what a
|
||||
// subscription serves; release unregisters it, so a stopped subscription server's cache goes.
|
||||
func RegisterSubDataInvalidator(f func()) (release func()) {
|
||||
subDataInvalidatorsMu.Lock()
|
||||
defer subDataInvalidatorsMu.Unlock()
|
||||
id := nextSubDataInvalidator
|
||||
nextSubDataInvalidator++
|
||||
subDataInvalidators[id] = f
|
||||
return func() {
|
||||
subDataInvalidatorsMu.Lock()
|
||||
defer subDataInvalidatorsMu.Unlock()
|
||||
delete(subDataInvalidators, id)
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidateSubData drops every registered subscription snapshot. Purging is
|
||||
// clearing a small map, so callers never need to compute affected subIds.
|
||||
func InvalidateSubData() {
|
||||
subDataInvalidatorsMu.Lock()
|
||||
defer subDataInvalidatorsMu.Unlock()
|
||||
for _, invalidate := range subDataInvalidators {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user