Files
3x-ui/internal/xray/log_writer_race_test.go
T
n0ctal 2bb29468d8 fix(xray): guard log-writer race and bound handler gRPC deadlines (#5442)
* perf(xray): compile log/traffic regexps once at package scope

GetTraffic recompiled two stats regexps on every traffic tick, and LogWriter.Write
recompiled two more on every log line. Hoist all four to package-level vars so they
compile once at load instead of per call on hot paths.

* fix(xray): guard LogWriter.lastLine against the GetResult reader race

Write is driven by the Xray process goroutine while Process.GetResult
reads lastLine from the caller's goroutine, so the unsynchronized field
is a data race under `go test -race`. Add an RWMutex and route every
write through setLastLine; GetResult reads via LastLine().

* fix(xray): bound handler gRPC calls with a deadline

AddInbound, DelInbound and the AddUser AlterInbound call used
context.Background(), so a hung core connection could block the caller
indefinitely (for example while the process restart lock is held). Give
them a 10s deadline (handlerRPCTimeout) and a nil-client guard, matching
the other handler operations.
2026-06-20 18:10:18 +02:00

37 lines
859 B
Go

package xray
import (
"sync"
"testing"
)
// TestLogWriterLastLineConcurrent exercises the LogWriter from multiple
// goroutines: Xray drives Write while another goroutine (Process.GetResult)
// reads the last line. Run under `go test -race` this fails on an unguarded
// lastLine field and passes once the access is serialized.
func TestLogWriterLastLineConcurrent(t *testing.T) {
lw := NewLogWriter()
const writers, readers, iterations = 4, 4, 500
var wg sync.WaitGroup
wg.Add(writers + readers)
for i := 0; i < writers; i++ {
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
_, _ = lw.Write([]byte("2024/01/01 00:00:00.000000 [Info] connection accepted"))
}
}()
}
for i := 0; i < readers; i++ {
go func() {
defer wg.Done()
for j := 0; j < iterations; j++ {
_ = lw.LastLine()
}
}()
}
wg.Wait()
}