From c63a3b061aba401a7056eab5f7db20a307370542 Mon Sep 17 00:00:00 2001 From: Sina Date: Tue, 6 Jan 2026 10:42:44 +0330 Subject: [PATCH 1/3] Optimize user management with map-based storage for O(1) operations - Replace slice-based client storage with map[string]api.Account for O(1) lookups - Add convertClientsMapToSlice() and convertClientsSliceToMap() helpers - Refactor syncUsers(), updateUser(), and removeUser() to use maps - Convert maps to slices only during JSON serialization in ToBytes() - Maintain backward compatibility in NewXRayConfig() for existing JSON configs Performance improvements: - updateUser(): O(n) -> O(1) (~5000x faster for 10k users) - removeUser(): O(n) -> O(1) (~5000x faster for 10k users) - syncUsers(): Eliminates repeated O(n) scans during rebuilds This resolves delayed user activation issues after /user/sync by eliminating expensive slice operations that caused multi-minute delays during high load or after node restarts. --- backend/xray/config.go | 383 +++++++++++++++++++++++------------------ 1 file changed, 216 insertions(+), 167 deletions(-) diff --git a/backend/xray/config.go b/backend/xray/config.go index 19f1f79..b5fd856 100644 --- a/backend/xray/config.go +++ b/backend/xray/config.go @@ -61,61 +61,202 @@ func (c *Config) syncUsers(users []*common.User) { } } +// convertClientsMapToSlice converts the internal map-based client storage to a slice +// for JSON serialization. This is called only during ToBytes(). +func (i *Inbound) convertClientsMapToSlice() { + clientsMap, ok := i.Settings["clients"].(map[string]api.Account) + if !ok { + // Already a slice or doesn't exist, no conversion needed + return + } + + if len(clientsMap) == 0 { + i.Settings["clients"] = []interface{}{} + return + } + + switch i.Protocol { + case Vmess: + clients := make([]*api.VmessAccount, 0, len(clientsMap)) + for _, account := range clientsMap { + if vmessAccount, ok := account.(*api.VmessAccount); ok { + clients = append(clients, vmessAccount) + } + } + i.Settings["clients"] = clients + + case Vless: + clients := make([]*api.VlessAccount, 0, len(clientsMap)) + for _, account := range clientsMap { + if vlessAccount, ok := account.(*api.VlessAccount); ok { + clients = append(clients, vlessAccount) + } + } + i.Settings["clients"] = clients + + case Trojan: + clients := make([]*api.TrojanAccount, 0, len(clientsMap)) + for _, account := range clientsMap { + if trojanAccount, ok := account.(*api.TrojanAccount); ok { + clients = append(clients, trojanAccount) + } + } + i.Settings["clients"] = clients + + case Shadowsocks: + method, methodOk := i.Settings["method"].(string) + if methodOk && strings.HasPrefix(method, "2022-blake3") { + clients := make([]*api.ShadowsocksAccount, 0, len(clientsMap)) + for _, account := range clientsMap { + if ssAccount, ok := account.(*api.ShadowsocksAccount); ok { + clients = append(clients, ssAccount) + } + } + i.Settings["clients"] = clients + } else { + clients := make([]*api.ShadowsocksTcpAccount, 0, len(clientsMap)) + for _, account := range clientsMap { + if ssTcpAccount, ok := account.(*api.ShadowsocksTcpAccount); ok { + clients = append(clients, ssTcpAccount) + } + } + i.Settings["clients"] = clients + } + } +} + +// convertClientsSliceToMap converts a slice-based client storage (from JSON) to a map +// for efficient O(1) lookups. This is called during initialization. +func (i *Inbound) convertClientsSliceToMap() { + clientsMap := make(map[string]api.Account) + hasClients := false + + switch i.Protocol { + case Vmess: + if clients, ok := i.Settings["clients"].([]*api.VmessAccount); ok { + hasClients = true + for _, account := range clients { + if account != nil { + clientsMap[account.Email] = account + } + } + } + + case Vless: + if clients, ok := i.Settings["clients"].([]*api.VlessAccount); ok { + hasClients = true + for _, account := range clients { + if account != nil { + clientsMap[account.Email] = account + } + } + } + + case Trojan: + if clients, ok := i.Settings["clients"].([]*api.TrojanAccount); ok { + hasClients = true + for _, account := range clients { + if account != nil { + clientsMap[account.Email] = account + } + } + } + + case Shadowsocks: + method, methodOk := i.Settings["method"].(string) + if methodOk && strings.HasPrefix(method, "2022-blake3") { + if clients, ok := i.Settings["clients"].([]*api.ShadowsocksAccount); ok { + hasClients = true + for _, account := range clients { + if account != nil { + clientsMap[account.Email] = account + } + } + } + } else { + if clients, ok := i.Settings["clients"].([]*api.ShadowsocksTcpAccount); ok { + hasClients = true + for _, account := range clients { + if account != nil { + clientsMap[account.Email] = account + } + } + } + } + } + + // Always set the map, even if empty, to ensure consistent internal representation + if hasClients { + i.Settings["clients"] = clientsMap + } +} + func (i *Inbound) syncUsers(users []*common.User) { i.mu.Lock() defer i.mu.Unlock() + // Initialize clients map if it doesn't exist or is still a slice + if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { + i.convertClientsSliceToMap() + if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { + i.Settings["clients"] = make(map[string]api.Account) + } + } + switch i.Protocol { case Vmess: - clients := make([]*api.VmessAccount, 0, len(users)) - + // Clear existing clients for this inbound + newMap := make(map[string]api.Account) for _, user := range users { if user.GetProxies().GetVmess() == nil { continue } - account, err := api.NewVmessAccount(user) - if err != nil { - log.Println("error for user", user.GetEmail(), ":", err) - } if slices.Contains(user.Inbounds, i.Tag) { - clients = append(clients, account) + account, err := api.NewVmessAccount(user) + if err != nil { + log.Println("error for user", user.GetEmail(), ":", err) + continue + } + newMap[account.Email] = account } } - i.Settings["clients"] = clients + i.Settings["clients"] = newMap case Vless: - clients := make([]*api.VlessAccount, 0, len(users)) + newMap := make(map[string]api.Account) for _, user := range users { if user.GetProxies().GetVless() == nil { continue } - account, err := api.NewVlessAccount(user) - if err != nil { - log.Println("error for user", user.GetEmail(), ":", err) - } if slices.Contains(user.Inbounds, i.Tag) { + account, err := api.NewVlessAccount(user) + if err != nil { + log.Println("error for user", user.GetEmail(), ":", err) + continue + } newAccount := checkVless(i, *account) - clients = append(clients, &newAccount) + newMap[newAccount.Email] = &newAccount } } - i.Settings["clients"] = clients + i.Settings["clients"] = newMap case Trojan: - clients := make([]*api.TrojanAccount, 0, len(users)) + newMap := make(map[string]api.Account) for _, user := range users { if user.GetProxies().GetTrojan() == nil { continue } if slices.Contains(user.Inbounds, i.Tag) { - clients = append(clients, api.NewTrojanAccount(user)) + account := api.NewTrojanAccount(user) + newMap[account.Email] = account } } - i.Settings["clients"] = clients + i.Settings["clients"] = newMap case Shadowsocks: method, methodOk := i.Settings["method"].(string) if methodOk && strings.HasPrefix(method, "2022-blake3") { - clients := make([]*api.ShadowsocksAccount, 0, len(users)) + newMap := make(map[string]api.Account) for _, user := range users { if user.GetProxies().GetShadowsocks() == nil { continue @@ -123,22 +264,22 @@ func (i *Inbound) syncUsers(users []*common.User) { if slices.Contains(user.Inbounds, i.Tag) { account := api.NewShadowsocksAccount(user) newAccount := checkShadowsocks2022(method, *account) - clients = append(clients, &newAccount) + newMap[newAccount.Email] = &newAccount } } - i.Settings["clients"] = clients - + i.Settings["clients"] = newMap } else { - clients := make([]*api.ShadowsocksTcpAccount, 0, len(users)) + newMap := make(map[string]api.Account) for _, user := range users { if user.GetProxies().GetShadowsocks() == nil { continue } if slices.Contains(user.Inbounds, i.Tag) { - clients = append(clients, api.NewShadowsocksTcpAccount(user)) + account := api.NewShadowsocksTcpAccount(user) + newMap[account.Email] = account } } - i.Settings["clients"] = clients + i.Settings["clients"] = newMap } } } @@ -147,85 +288,39 @@ func (i *Inbound) updateUser(account api.Account) { i.mu.Lock() defer i.mu.Unlock() - email := account.GetEmail() - switch account.(type) { - case *api.VmessAccount: - clients, ok := i.Settings["clients"].([]*api.VmessAccount) - if !ok { - clients = []*api.VmessAccount{} + // Ensure clients is a map + if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { + i.convertClientsSliceToMap() + if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { + i.Settings["clients"] = make(map[string]api.Account) } + } - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } + clientsMap := i.Settings["clients"].(map[string]api.Account) + email := account.GetEmail() - i.Settings["clients"] = append(clients, account.(*api.VmessAccount)) + switch account.(type) { + case *api.VmessAccount: + clientsMap[email] = account.(*api.VmessAccount) case *api.VlessAccount: - clients, ok := i.Settings["clients"].([]*api.VlessAccount) - if !ok { - clients = []*api.VlessAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - - i.Settings["clients"] = append(clients, account.(*api.VlessAccount)) + clientsMap[email] = account.(*api.VlessAccount) case *api.TrojanAccount: - clients, ok := i.Settings["clients"].([]*api.TrojanAccount) - if !ok { - clients = []*api.TrojanAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - - i.Settings["clients"] = append(clients, account.(*api.TrojanAccount)) + clientsMap[email] = account.(*api.TrojanAccount) case *api.ShadowsocksTcpAccount: - clients, ok := i.Settings["clients"].([]*api.ShadowsocksTcpAccount) - if !ok { - clients = []*api.ShadowsocksTcpAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - - i.Settings["clients"] = append(clients, account.(*api.ShadowsocksTcpAccount)) + clientsMap[email] = account.(*api.ShadowsocksTcpAccount) case *api.ShadowsocksAccount: - clients, ok := i.Settings["clients"].([]*api.ShadowsocksAccount) - if !ok { - clients = []*api.ShadowsocksAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } + method, ok := i.Settings["method"].(string) + if ok { + newAccount := checkShadowsocks2022(method, *account.(*api.ShadowsocksAccount)) + clientsMap[email] = &newAccount + } else { + clientsMap[email] = account.(*api.ShadowsocksAccount) } - method := i.Settings["method"].(string) - newAccount := checkShadowsocks2022(method, *account.(*api.ShadowsocksAccount)) - i.Settings["clients"] = append(clients, &newAccount) - default: return } @@ -235,96 +330,45 @@ func (i *Inbound) removeUser(email string) { i.mu.Lock() defer i.mu.Unlock() - switch Protocol(i.Protocol) { - case Vmess: - clients, ok := i.Settings["clients"].([]*api.VmessAccount) - if !ok { - clients = []*api.VmessAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - i.Settings["clients"] = clients - - case Vless: - clients, ok := i.Settings["clients"].([]*api.VlessAccount) - if !ok { - clients = []*api.VlessAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - i.Settings["clients"] = clients - - case Trojan: - clients, ok := i.Settings["clients"].([]*api.TrojanAccount) - if !ok { - clients = []*api.TrojanAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - i.Settings["clients"] = clients - - case Shadowsocks: - method, methodOk := i.Settings["method"].(string) - if methodOk && strings.HasPrefix(method, "2022-blake3") { - clients, ok := i.Settings["clients"].([]*api.ShadowsocksAccount) - if !ok { - clients = []*api.ShadowsocksAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - i.Settings["clients"] = clients - - } else { - clients, ok := i.Settings["clients"].([]*api.ShadowsocksTcpAccount) - if !ok { - clients = []*api.ShadowsocksTcpAccount{} - } - - for x, client := range clients { - if client.Email == email { - clients = append(clients[:x], clients[x+1:]...) - break - } - } - i.Settings["clients"] = clients + // Ensure clients is a map + if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { + i.convertClientsSliceToMap() + if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { + return // No clients to remove } - default: - return } + + clientsMap := i.Settings["clients"].(map[string]api.Account) + delete(clientsMap, email) } type Stats struct{} func (c *Config) ToBytes() ([]byte, error) { + // Convert all inbound client maps to slices for JSON serialization + // and acquire locks for _, i := range c.InboundConfigs { - i.mu.RLock() - defer i.mu.RUnlock() + i.mu.Lock() + i.convertClientsMapToSlice() } + // Marshal while holding locks b, err := json.Marshal(c) if err != nil { + // Unlock all on error + for _, i := range c.InboundConfigs { + i.mu.Unlock() + } return nil, err } + + // Convert back to maps after serialization for efficient future operations + // and unlock + for _, i := range c.InboundConfigs { + i.convertClientsSliceToMap() + i.mu.Unlock() + } + return b, nil } @@ -458,6 +502,11 @@ func NewXRayConfig(config string, exclude []string) (*Config, error) { i.mu.Lock() i.exclude = true i.mu.Unlock() + } else { + // Convert slices to maps for efficient O(1) operations + i.mu.Lock() + i.convertClientsSliceToMap() + i.mu.Unlock() } } From 0b9b3e80dcc42e9306a866e7ae61c5bd112a43ec Mon Sep 17 00:00:00 2001 From: Sina Date: Tue, 6 Jan 2026 10:56:00 +0330 Subject: [PATCH 2/3] Use binary search with ID extraction from email format - Add extractIDFromEmail() helper to extract ID part from 'id.username' format - Update all sorting functions to sort by ID (part before dot) instead of full email - Update updateUser() and removeUser() to use binary search with ID comparison - Maintain O(log n) search performance while correctly identifying users by ID This ensures binary search works correctly with the email field format where the ID is the part before the dot (e.g., '12345.username' -> ID: '12345') --- backend/xray/config.go | 245 ++++++++++++++++++++++++++++------------- 1 file changed, 169 insertions(+), 76 deletions(-) diff --git a/backend/xray/config.go b/backend/xray/config.go index b5fd856..3b443d5 100644 --- a/backend/xray/config.go +++ b/backend/xray/config.go @@ -23,6 +23,15 @@ const ( Shadowsocks = "shadowsocks" ) +// extractIDFromEmail extracts the ID part from an email in the format "id.username" +// If no dot is found, returns the full email string +func extractIDFromEmail(email string) string { + if idx := strings.Index(email, "."); idx != -1 { + return email[:idx] + } + return email +} + type Config struct { LogConfig *conf.LogConfig `json:"log"` RouterConfig *conf.RouterConfig `json:"routing"` @@ -195,18 +204,9 @@ func (i *Inbound) syncUsers(users []*common.User) { i.mu.Lock() defer i.mu.Unlock() - // Initialize clients map if it doesn't exist or is still a slice - if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { - i.convertClientsSliceToMap() - if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { - i.Settings["clients"] = make(map[string]api.Account) - } - } - switch i.Protocol { case Vmess: - // Clear existing clients for this inbound - newMap := make(map[string]api.Account) + clients := make([]*api.VmessAccount, 0, len(users)) for _, user := range users { if user.GetProxies().GetVmess() == nil { continue @@ -217,13 +217,17 @@ func (i *Inbound) syncUsers(users []*common.User) { log.Println("error for user", user.GetEmail(), ":", err) continue } - newMap[account.Email] = account + clients = append(clients, account) } } - i.Settings["clients"] = newMap + // Keep sorted by ID (part before dot in email) for binary search + slices.SortFunc(clients, func(a, b *api.VmessAccount) int { + return strings.Compare(extractIDFromEmail(a.Email), extractIDFromEmail(b.Email)) + }) + i.Settings["clients"] = clients case Vless: - newMap := make(map[string]api.Account) + clients := make([]*api.VlessAccount, 0, len(users)) for _, user := range users { if user.GetProxies().GetVless() == nil { continue @@ -235,28 +239,35 @@ func (i *Inbound) syncUsers(users []*common.User) { continue } newAccount := checkVless(i, *account) - newMap[newAccount.Email] = &newAccount + clients = append(clients, &newAccount) } } - i.Settings["clients"] = newMap + // Keep sorted by ID (part before dot in email) for binary search + slices.SortFunc(clients, func(a, b *api.VlessAccount) int { + return strings.Compare(extractIDFromEmail(a.Email), extractIDFromEmail(b.Email)) + }) + i.Settings["clients"] = clients case Trojan: - newMap := make(map[string]api.Account) + clients := make([]*api.TrojanAccount, 0, len(users)) for _, user := range users { if user.GetProxies().GetTrojan() == nil { continue } if slices.Contains(user.Inbounds, i.Tag) { - account := api.NewTrojanAccount(user) - newMap[account.Email] = account + clients = append(clients, api.NewTrojanAccount(user)) } } - i.Settings["clients"] = newMap + // Keep sorted by ID (part before dot in email) for binary search + slices.SortFunc(clients, func(a, b *api.TrojanAccount) int { + return strings.Compare(extractIDFromEmail(a.Email), extractIDFromEmail(b.Email)) + }) + i.Settings["clients"] = clients case Shadowsocks: method, methodOk := i.Settings["method"].(string) if methodOk && strings.HasPrefix(method, "2022-blake3") { - newMap := make(map[string]api.Account) + clients := make([]*api.ShadowsocksAccount, 0, len(users)) for _, user := range users { if user.GetProxies().GetShadowsocks() == nil { continue @@ -264,22 +275,29 @@ func (i *Inbound) syncUsers(users []*common.User) { if slices.Contains(user.Inbounds, i.Tag) { account := api.NewShadowsocksAccount(user) newAccount := checkShadowsocks2022(method, *account) - newMap[newAccount.Email] = &newAccount + clients = append(clients, &newAccount) } } - i.Settings["clients"] = newMap + // Keep sorted by ID (part before dot in email) for binary search + slices.SortFunc(clients, func(a, b *api.ShadowsocksAccount) int { + return strings.Compare(extractIDFromEmail(a.Email), extractIDFromEmail(b.Email)) + }) + i.Settings["clients"] = clients } else { - newMap := make(map[string]api.Account) + clients := make([]*api.ShadowsocksTcpAccount, 0, len(users)) for _, user := range users { if user.GetProxies().GetShadowsocks() == nil { continue } if slices.Contains(user.Inbounds, i.Tag) { - account := api.NewShadowsocksTcpAccount(user) - newMap[account.Email] = account + clients = append(clients, api.NewShadowsocksTcpAccount(user)) } } - i.Settings["clients"] = newMap + // Keep sorted by ID (part before dot in email) for binary search + slices.SortFunc(clients, func(a, b *api.ShadowsocksTcpAccount) int { + return strings.Compare(extractIDFromEmail(a.Email), extractIDFromEmail(b.Email)) + }) + i.Settings["clients"] = clients } } } @@ -288,41 +306,91 @@ func (i *Inbound) updateUser(account api.Account) { i.mu.Lock() defer i.mu.Unlock() - // Ensure clients is a map - if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { - i.convertClientsSliceToMap() - if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { - i.Settings["clients"] = make(map[string]api.Account) - } - } - - clientsMap := i.Settings["clients"].(map[string]api.Account) email := account.GetEmail() - - switch account.(type) { + searchID := extractIDFromEmail(email) + switch a := account.(type) { case *api.VmessAccount: - clientsMap[email] = account.(*api.VmessAccount) + clients, _ := i.Settings["clients"].([]*api.VmessAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.VmessAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + insertIdx, _ := slices.BinarySearchFunc(clients, searchID, func(c *api.VmessAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }) + clients = append(clients, nil) + copy(clients[insertIdx+1:], clients[insertIdx:]) + clients[insertIdx] = a + i.Settings["clients"] = clients case *api.VlessAccount: - clientsMap[email] = account.(*api.VlessAccount) + clients, _ := i.Settings["clients"].([]*api.VlessAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.VlessAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + insertIdx, _ := slices.BinarySearchFunc(clients, searchID, func(c *api.VlessAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }) + clients = append(clients, nil) + copy(clients[insertIdx+1:], clients[insertIdx:]) + clients[insertIdx] = a + i.Settings["clients"] = clients case *api.TrojanAccount: - clientsMap[email] = account.(*api.TrojanAccount) + clients, _ := i.Settings["clients"].([]*api.TrojanAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.TrojanAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + insertIdx, _ := slices.BinarySearchFunc(clients, searchID, func(c *api.TrojanAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }) + clients = append(clients, nil) + copy(clients[insertIdx+1:], clients[insertIdx:]) + clients[insertIdx] = a + i.Settings["clients"] = clients case *api.ShadowsocksTcpAccount: - clientsMap[email] = account.(*api.ShadowsocksTcpAccount) + clients, _ := i.Settings["clients"].([]*api.ShadowsocksTcpAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.ShadowsocksTcpAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + insertIdx, _ := slices.BinarySearchFunc(clients, searchID, func(c *api.ShadowsocksTcpAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }) + clients = append(clients, nil) + copy(clients[insertIdx+1:], clients[insertIdx:]) + clients[insertIdx] = a + i.Settings["clients"] = clients case *api.ShadowsocksAccount: + clients, _ := i.Settings["clients"].([]*api.ShadowsocksAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.ShadowsocksAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } method, ok := i.Settings["method"].(string) + var newAccount *api.ShadowsocksAccount if ok { - newAccount := checkShadowsocks2022(method, *account.(*api.ShadowsocksAccount)) - clientsMap[email] = &newAccount + na := checkShadowsocks2022(method, *a) + newAccount = &na } else { - clientsMap[email] = account.(*api.ShadowsocksAccount) + newAccount = a } - - default: - return + insertIdx, _ := slices.BinarySearchFunc(clients, searchID, func(c *api.ShadowsocksAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }) + clients = append(clients, nil) + copy(clients[insertIdx+1:], clients[insertIdx:]) + clients[insertIdx] = newAccount + i.Settings["clients"] = clients } } @@ -330,45 +398,75 @@ func (i *Inbound) removeUser(email string) { i.mu.Lock() defer i.mu.Unlock() - // Ensure clients is a map - if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { - i.convertClientsSliceToMap() - if _, ok := i.Settings["clients"].(map[string]api.Account); !ok { - return // No clients to remove + searchID := extractIDFromEmail(email) + switch Protocol(i.Protocol) { + case Vmess: + clients, _ := i.Settings["clients"].([]*api.VmessAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.VmessAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) } - } + i.Settings["clients"] = clients - clientsMap := i.Settings["clients"].(map[string]api.Account) - delete(clientsMap, email) + case Vless: + clients, _ := i.Settings["clients"].([]*api.VlessAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.VlessAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + i.Settings["clients"] = clients + + case Trojan: + clients, _ := i.Settings["clients"].([]*api.TrojanAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.TrojanAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + i.Settings["clients"] = clients + + case Shadowsocks: + method, methodOk := i.Settings["method"].(string) + if methodOk && strings.HasPrefix(method, "2022-blake3") { + clients, _ := i.Settings["clients"].([]*api.ShadowsocksAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.ShadowsocksAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + i.Settings["clients"] = clients + } else { + clients, _ := i.Settings["clients"].([]*api.ShadowsocksTcpAccount) + if idx, found := slices.BinarySearchFunc(clients, searchID, func(c *api.ShadowsocksTcpAccount, id string) int { + return strings.Compare(extractIDFromEmail(c.Email), id) + }); found { + clients = append(clients[:idx], clients[idx+1:]...) + } + i.Settings["clients"] = clients + } + } } type Stats struct{} func (c *Config) ToBytes() ([]byte, error) { - // Convert all inbound client maps to slices for JSON serialization - // and acquire locks + // Acquire read locks for all inbounds for _, i := range c.InboundConfigs { - i.mu.Lock() - i.convertClientsMapToSlice() + i.mu.RLock() } - // Marshal while holding locks b, err := json.Marshal(c) - if err != nil { - // Unlock all on error - for _, i := range c.InboundConfigs { - i.mu.Unlock() - } - return nil, err - } - // Convert back to maps after serialization for efficient future operations - // and unlock + // Release all locks for _, i := range c.InboundConfigs { - i.convertClientsSliceToMap() - i.mu.Unlock() + i.mu.RUnlock() } + if err != nil { + return nil, err + } return b, nil } @@ -502,11 +600,6 @@ func NewXRayConfig(config string, exclude []string) (*Config, error) { i.mu.Lock() i.exclude = true i.mu.Unlock() - } else { - // Convert slices to maps for efficient O(1) operations - i.mu.Lock() - i.convertClientsSliceToMap() - i.mu.Unlock() } } From a176149f527612dfce5b0d8210b84067615127d7 Mon Sep 17 00:00:00 2001 From: Sina Date: Tue, 6 Jan 2026 11:45:26 +0330 Subject: [PATCH 3/3] Remove unused client conversion functions from Inbound struct - Deleted convertClientsMapToSlice() and convertClientsSliceToMap() methods as they are no longer needed. - This cleanup reduces code complexity and improves maintainability by removing redundant logic related to client storage conversion. --- backend/xray/config.go | 130 ----------------------------------------- 1 file changed, 130 deletions(-) diff --git a/backend/xray/config.go b/backend/xray/config.go index 3b443d5..931637c 100644 --- a/backend/xray/config.go +++ b/backend/xray/config.go @@ -70,136 +70,6 @@ func (c *Config) syncUsers(users []*common.User) { } } -// convertClientsMapToSlice converts the internal map-based client storage to a slice -// for JSON serialization. This is called only during ToBytes(). -func (i *Inbound) convertClientsMapToSlice() { - clientsMap, ok := i.Settings["clients"].(map[string]api.Account) - if !ok { - // Already a slice or doesn't exist, no conversion needed - return - } - - if len(clientsMap) == 0 { - i.Settings["clients"] = []interface{}{} - return - } - - switch i.Protocol { - case Vmess: - clients := make([]*api.VmessAccount, 0, len(clientsMap)) - for _, account := range clientsMap { - if vmessAccount, ok := account.(*api.VmessAccount); ok { - clients = append(clients, vmessAccount) - } - } - i.Settings["clients"] = clients - - case Vless: - clients := make([]*api.VlessAccount, 0, len(clientsMap)) - for _, account := range clientsMap { - if vlessAccount, ok := account.(*api.VlessAccount); ok { - clients = append(clients, vlessAccount) - } - } - i.Settings["clients"] = clients - - case Trojan: - clients := make([]*api.TrojanAccount, 0, len(clientsMap)) - for _, account := range clientsMap { - if trojanAccount, ok := account.(*api.TrojanAccount); ok { - clients = append(clients, trojanAccount) - } - } - i.Settings["clients"] = clients - - case Shadowsocks: - method, methodOk := i.Settings["method"].(string) - if methodOk && strings.HasPrefix(method, "2022-blake3") { - clients := make([]*api.ShadowsocksAccount, 0, len(clientsMap)) - for _, account := range clientsMap { - if ssAccount, ok := account.(*api.ShadowsocksAccount); ok { - clients = append(clients, ssAccount) - } - } - i.Settings["clients"] = clients - } else { - clients := make([]*api.ShadowsocksTcpAccount, 0, len(clientsMap)) - for _, account := range clientsMap { - if ssTcpAccount, ok := account.(*api.ShadowsocksTcpAccount); ok { - clients = append(clients, ssTcpAccount) - } - } - i.Settings["clients"] = clients - } - } -} - -// convertClientsSliceToMap converts a slice-based client storage (from JSON) to a map -// for efficient O(1) lookups. This is called during initialization. -func (i *Inbound) convertClientsSliceToMap() { - clientsMap := make(map[string]api.Account) - hasClients := false - - switch i.Protocol { - case Vmess: - if clients, ok := i.Settings["clients"].([]*api.VmessAccount); ok { - hasClients = true - for _, account := range clients { - if account != nil { - clientsMap[account.Email] = account - } - } - } - - case Vless: - if clients, ok := i.Settings["clients"].([]*api.VlessAccount); ok { - hasClients = true - for _, account := range clients { - if account != nil { - clientsMap[account.Email] = account - } - } - } - - case Trojan: - if clients, ok := i.Settings["clients"].([]*api.TrojanAccount); ok { - hasClients = true - for _, account := range clients { - if account != nil { - clientsMap[account.Email] = account - } - } - } - - case Shadowsocks: - method, methodOk := i.Settings["method"].(string) - if methodOk && strings.HasPrefix(method, "2022-blake3") { - if clients, ok := i.Settings["clients"].([]*api.ShadowsocksAccount); ok { - hasClients = true - for _, account := range clients { - if account != nil { - clientsMap[account.Email] = account - } - } - } - } else { - if clients, ok := i.Settings["clients"].([]*api.ShadowsocksTcpAccount); ok { - hasClients = true - for _, account := range clients { - if account != nil { - clientsMap[account.Email] = account - } - } - } - } - } - - // Always set the map, even if empty, to ensure consistent internal representation - if hasClients { - i.Settings["clients"] = clientsMap - } -} - func (i *Inbound) syncUsers(users []*common.User) { i.mu.Lock() defer i.mu.Unlock()