mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-22 05:45:00 +03:00
fix(node-sync): write only changed client rows in the node traffic merge
Every 5s node merge rewrote one client_traffics row and one node_client_traffics baseline per (inbound, client) pair the node reported, even when nothing had moved: a client is reported once per inbound it is on, and idle clients every tick. On top of that each inbound's SyncInbound wrote its own per-inbound values onto the shared clients record, so a client on a flow and a non-flow inbound (or on a WireGuard tunnel plus any other inbound) had its record flipped back and forth twice per merge. With 7 nodes and ~20k client links that is tens of thousands of statements per tick, all inside transactions on the single serialized writer, holding row locks for seconds. A renew of a client on 38 inbounds takes one writer turn per inbound, queued behind those merges each time, and outlived the 30s HTTP write timeout even though it committed; last_sub_fetch updates waited on the same locks. The merge now computes the row its UPDATE would leave, mirroring the SQL merge expressions, and skips the write when nothing changes; the baseline upsert is skipped when the node counter has not moved. The record's per-inbound fields get one owner each, so repeated syncs agree: flow is settled to the effective flow (lowest-id link carrying one) for clients whose links changed, and WireGuard/AmneziaWG peer fields come from the client's lowest-id tunnel. A repeated unchanged snapshot now writes no client row; one active client writes two. Verified beyond make verify: the CI Postgres lane, the merge and flow tests run against PostgreSQL 16, and make node-e2e. Closes #6815
This commit is contained in:
@@ -324,3 +324,35 @@ func TestBulkAttach_PreservesVisionFlowWhenCanonicalColumnZeroed(t *testing.T) {
|
||||
t.Errorf("bulk-attached non-flow inbound must not receive Vision flow, got %#v", wsList)
|
||||
}
|
||||
}
|
||||
|
||||
// A client detached from its last inbound has no effective flow left to settle
|
||||
// its record to, so the record keeps the flow a later attach falls back on.
|
||||
func TestRecordFlow_DetachedFromLastInboundKeepsFlow(t *testing.T) {
|
||||
dbDir := t.TempDir()
|
||||
t.Setenv("XUI_DB_FOLDER", dbDir)
|
||||
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
||||
|
||||
reality := &model.Inbound{Tag: "vless-reality", Enable: true, Port: 43001, Protocol: model.VLESS, StreamSettings: `{"network":"tcp","security":"reality"}`}
|
||||
if err := database.GetDB().Create(reality).Error; err != nil {
|
||||
t.Fatalf("create reality inbound: %v", err)
|
||||
}
|
||||
|
||||
svc := ClientService{}
|
||||
const email = "detached@example.com"
|
||||
const vision = "xtls-rprx-vision"
|
||||
client := model.Client{Email: email, ID: "ce8d33df-3a64-4f10-8f9b-91c3a8e0c333", Enable: true, Flow: vision}
|
||||
if err := svc.SyncInbound(nil, reality.Id, []model.Client{client}); err != nil {
|
||||
t.Fatalf("SyncInbound(attach): %v", err)
|
||||
}
|
||||
if err := svc.SyncInbound(nil, reality.Id, nil); err != nil {
|
||||
t.Fatalf("SyncInbound(detach): %v", err)
|
||||
}
|
||||
|
||||
rec, err := svc.GetRecordByEmail(nil, email)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRecordByEmail: %v", err)
|
||||
}
|
||||
if rec.Flow != vision {
|
||||
t.Errorf("record flow after detaching its last inbound = %q, want %q", rec.Flow, vision)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user