mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-10 05:10:58 +00:00
perf(scale): speed up traffic, auto-renew, and node bulk ops at 50k-100k clients
Local hot paths: - autoRenewClients: replace the O(clients x expired) inner scan with an email->traffic map lookup (quadratic at scale). - node traffic sync: scope the client_traffics email-membership query to the snapshot's emails instead of plucking the whole table every poll. - add a (expiry_time, reset) index for the per-tick auto-renew filter. - SQLite: add cache_size/mmap_size/temp_store pragmas (env-tunable); keep the single-file DELETE journal and synchronous=FULL defaults. - scale benchmarks now run on SQLite too via XUI_SCALE_TEST=1 (shared setupScaleDB/resetScaleTables helpers), not just Postgres. Node paths: - bulk add/delete/adjust on a node-attached inbound folded one HTTP RPC per client; above nodeBulkPushThreshold (32) mark the node dirty and let one ReconcileNode push converge it instead of O(M) sequential round-trips. Small ops keep the live per-client path. Also hoist nodePushPlan out of the per-email delete loop. - ReconcileNode skips inbounds whose wire payload is unchanged (per-tag fingerprint on Remote), guarded by node-side tag presence so a restarted node is still re-seeded. Tests: auto-renew multi-inbound correctness, node-path dispatch (large ops fold to dirty, small ops push live) via a manager runtime override seam, and reconcile delta-skip.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// TestReconcileInbound_SkipsUnchanged proves the delta-skip: a second reconcile
|
||||
// of an unchanged inbound that the node still reports sends no push, while a
|
||||
// content change or an absent-on-node inbound forces a fresh push.
|
||||
func TestReconcileInbound_SkipsUnchanged(t *testing.T) {
|
||||
var pushes atomic.Int32
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/panel/api/inbounds/update/") {
|
||||
pushes.Add(1)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"success":true}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
r := NewRemote(nodeForPlainServer(t, srv, "verify", "tok"), nil)
|
||||
ib := &model.Inbound{Tag: "in-1", Protocol: model.VLESS, Port: 443, Settings: `{"clients":[]}`}
|
||||
// Pre-seed the tag→id cache so resolveRemoteID needs no network round-trip.
|
||||
r.cacheSet(ib.Tag, 7)
|
||||
|
||||
// First reconcile: node doesn't report it yet → must push and record the fp.
|
||||
if pushed, err := r.ReconcileInbound(context.Background(), ib, false); err != nil || !pushed {
|
||||
t.Fatalf("first reconcile: pushed=%v err=%v, want push", pushed, err)
|
||||
}
|
||||
if got := pushes.Load(); got != 1 {
|
||||
t.Fatalf("after first reconcile pushes=%d, want 1", got)
|
||||
}
|
||||
|
||||
// Second reconcile: unchanged and present on node → skip.
|
||||
if pushed, err := r.ReconcileInbound(context.Background(), ib, true); err != nil || pushed {
|
||||
t.Fatalf("second reconcile: pushed=%v err=%v, want skip", pushed, err)
|
||||
}
|
||||
if got := pushes.Load(); got != 1 {
|
||||
t.Fatalf("unchanged reconcile pushed again: pushes=%d, want 1", got)
|
||||
}
|
||||
|
||||
// Content change → push again even though it's present on node.
|
||||
ib.Settings = `{"clients":[{"email":"a@x"}]}`
|
||||
if pushed, err := r.ReconcileInbound(context.Background(), ib, true); err != nil || !pushed {
|
||||
t.Fatalf("changed reconcile: pushed=%v err=%v, want push", pushed, err)
|
||||
}
|
||||
if got := pushes.Load(); got != 2 {
|
||||
t.Fatalf("changed reconcile pushes=%d, want 2", got)
|
||||
}
|
||||
|
||||
// Absent on node (e.g. node restarted/lost it) → re-push even if fp matches.
|
||||
if pushed, err := r.ReconcileInbound(context.Background(), ib, false); err != nil || !pushed {
|
||||
t.Fatalf("absent-on-node reconcile: pushed=%v err=%v, want push", pushed, err)
|
||||
}
|
||||
if got := pushes.Load(); got != 3 {
|
||||
t.Fatalf("absent-on-node reconcile pushes=%d, want 3", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user