Files
3x-ui/internal/web/runtime/local_api.go
T
25f2002654 f105132866 perf(runtime): pool the local xray gRPC connection (#6916)
A client per runtime operation made a bulk delete pay a TCP and HTTP/2
handshake for every client. The local runtime now keeps one gRPC client
per API port and replaces it when a core restart moves the port; the
traffic poll keeps its own client, since StatsLastValues is unlocked.

A pooled channel keeps grpc-go's reconnect backoff after the core
restarts, so a call that fails with Unavailable drops the client and
dials again, as a client per call did.
2026-10-24 19:30:09 +02:00

53 lines
1.2 KiB
Go

package runtime
import (
"sync"
"github.com/mhsanaei/3x-ui/v3/internal/xray"
)
// One pooled gRPC client serves local runtime operations; never use it for traffic stats, whose
// unlocked StatsLastValues a second reader would corrupt, so the traffic poll keeps its own client.
var (
localAPIMu sync.Mutex
localAPIPort int
localAPIClient *xray.XrayAPI
)
// localAPIForPort returns the pooled client for the API port, replacing (and closing) it when a core
// restart moved the port; callers must not close it.
func localAPIForPort(port int) (*xray.XrayAPI, error) {
localAPIMu.Lock()
defer localAPIMu.Unlock()
if localAPIClient != nil {
if localAPIPort == port {
return localAPIClient, nil
}
localAPIClient.Close()
localAPIClient = nil
}
api := &xray.XrayAPI{}
if err := api.Init(port); err != nil {
return nil, err
}
localAPIClient = api
localAPIPort = port
return api, nil
}
// evictLocalAPIClient closes the pooled client so the next call dials a fresh connection, unless
// another call already replaced the one that failed.
func evictLocalAPIClient(api *xray.XrayAPI) {
localAPIMu.Lock()
defer localAPIMu.Unlock()
if localAPIClient != api {
return
}
localAPIClient.Close()
localAPIClient = nil
localAPIPort = 0
}