perf(clients): take one email snapshot per client fan-out, not one per inbound (#6091)

Create and Attach called the exported AddInboundClient once per target
inbound, and that wrapper passes a nil email→subId map, so every iteration
re-ran getAllEmailSubIDs -- a JSON_EACH expansion over the settings blob of
every inbound in the panel. Adding one client to 24 inbounds on a panel with
~300 users meant 24 full expansions of ~7k rows to answer the same question.

Hoist the snapshot above the loop and call the unexported addInboundClient
with it, exactly as BulkAttach (client_bulk.go:63) and BulkCreate
(client_bulk.go:1151) already do. The snapshot goes stale from the second
inbound onward, but the identity being added is the same on every iteration,
so its own entry can only ever match itself -- checkEmailsExistForClients
accepts an email whose stored subId equals the incoming one, and an absent
entry is accepted too.

This is the database half of #6091. The dominant cost there is the other
half -- one synchronous 10s-capped node round-trip per remote inbound,
which multiplies again on chained nodes -- and that needs the push batched
per node rather than per inbound; left for a separate change.
This commit is contained in:
Sanaei
2026-07-24 22:24:30 +02:00
parent aa60d54ea5
commit c3967e57dc
2 changed files with 98 additions and 4 deletions
@@ -0,0 +1,84 @@
package service
import (
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func TestCreateAcrossManyInboundsUsesOneEmailSnapshot(t *testing.T) {
setupBulkDB(t)
svc := &ClientService{}
inboundSvc := &InboundService{}
const uuid = "bbbbbbbb-1111-2222-3333-555555555555"
ids := make([]int, 0, 6)
for i := range 6 {
ib := mkInbound(t, 23001+i, model.VLESS, `{"clients":[]}`)
ids = append(ids, ib.Id)
}
if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
Client: model.Client{Email: "fan@x", ID: uuid, SubID: "sub-fan", Enable: true},
InboundIds: ids,
}); err != nil {
t.Fatalf("Create across %d inbounds: %v", len(ids), err)
}
if n := countClientRecords(t); n != 1 {
t.Fatalf("client records = %d, want 1", n)
}
rec := lookupClientRecord(t, "fan@x")
if rec.UUID != uuid || rec.SubID != "sub-fan" {
t.Fatalf("record = {uuid:%q sub:%q}, want {%q sub-fan}", rec.UUID, rec.SubID, uuid)
}
for _, id := range ids {
if !settingsHoldUUID(t, inboundSvc, id, uuid) {
t.Fatalf("inbound %d settings missing the client", id)
}
}
linked, err := svc.GetInboundIdsForRecord(rec.Id)
if err != nil {
t.Fatalf("GetInboundIdsForRecord: %v", err)
}
if len(linked) != len(ids) {
t.Fatalf("linked inbounds = %d, want %d", len(linked), len(ids))
}
}
func TestAttachAcrossManyInboundsUsesOneEmailSnapshot(t *testing.T) {
setupBulkDB(t)
svc := &ClientService{}
inboundSvc := &InboundService{}
first := mkInbound(t, 23101, model.VLESS, `{"clients":[]}`)
if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
Client: model.Client{Email: "att@x", ID: "cccccccc-1111-2222-3333-666666666666", SubID: "sub-att", Enable: true},
InboundIds: []int{first.Id},
}); err != nil {
t.Fatalf("seed Create: %v", err)
}
rec := lookupClientRecord(t, "att@x")
ids := []int{first.Id}
for i := range 4 {
ib := mkInbound(t, 23102+i, model.VLESS, `{"clients":[]}`)
ids = append(ids, ib.Id)
}
if _, err := svc.Attach(inboundSvc, rec.Id, ids); err != nil {
t.Fatalf("Attach across %d inbounds: %v", len(ids), err)
}
if n := countClientRecords(t); n != 1 {
t.Fatalf("client records after attach = %d, want 1", n)
}
linked, err := svc.GetInboundIdsForRecord(rec.Id)
if err != nil {
t.Fatalf("GetInboundIdsForRecord: %v", err)
}
if len(linked) != len(ids) {
t.Fatalf("linked inbounds = %d, want %d", len(linked), len(ids))
}
}
+14 -4
View File
@@ -110,6 +110,11 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
}
}
emailSubIDs, sidErr := inboundSvc.getAllEmailSubIDs()
if sidErr != nil {
return false, sidErr
}
needRestart := false
for _, ibId := range payload.InboundIds {
inbound, getErr := inboundSvc.GetInbound(ibId)
@@ -123,10 +128,10 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
if mErr != nil {
return needRestart, mErr
}
nr, addErr := s.AddInboundClient(inboundSvc, &model.Inbound{
nr, addErr := s.addInboundClient(inboundSvc, &model.Inbound{
Id: ibId,
Settings: string(settingsPayload),
})
}, emailSubIDs)
if addErr != nil {
return needRestart, addErr
}
@@ -615,6 +620,11 @@ func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []
clientWire.Flow = flow
clientWire.UpdatedAt = time.Now().UnixMilli()
emailSubIDs, sidErr := inboundSvc.getAllEmailSubIDs()
if sidErr != nil {
return false, sidErr
}
needRestart := false
for _, ibId := range inboundIds {
if _, attached := have[ibId]; attached {
@@ -632,10 +642,10 @@ func (s *ClientService) Attach(inboundSvc *InboundService, id int, inboundIds []
if mErr != nil {
return needRestart, mErr
}
nr, addErr := s.AddInboundClient(inboundSvc, &model.Inbound{
nr, addErr := s.addInboundClient(inboundSvc, &model.Inbound{
Id: ibId,
Settings: string(settingsPayload),
})
}, emailSubIDs)
if addErr != nil {
return needRestart, addErr
}