mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-10 12:12:37 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00bb0dc737 | ||
|
|
761d8032db | ||
|
|
4296be5d05 | ||
|
|
4bcd0c6405 | ||
|
|
8fe26a91d5 | ||
|
|
1c8bf4067b |
@@ -82,7 +82,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v4.8.0
|
||||
uses: docker/login-action@v4.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
@@ -212,7 +212,7 @@ jobs:
|
||||
# go build -o build_assets/wxray.exe -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-H windowsgui -X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid=" -v ./main
|
||||
elif [[ ${GOOS} == 'android' ]]; then
|
||||
echo 'Building Xray for Android...'
|
||||
go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid= -checklinkname=0 -extldflags=-Wl,--pack-dyn-relocs=android" -v ./main
|
||||
go build -o build_assets/xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=${COMMID} -s -w -buildid= -checklinkname=0" -v ./main
|
||||
else
|
||||
echo 'Building Xray...'
|
||||
if [[ ${GOARCH} == 'mips' || ${GOARCH} == 'mipsle' ]]; then
|
||||
|
||||
@@ -234,8 +234,8 @@ CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ld
|
||||
For Android:
|
||||
|
||||
```bash
|
||||
GOOS=android GOARCH=arm64 CGO_ENABLED=1 CC=/path/to/aarch64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0 -extldflags=-Wl,--pack-dyn-relocs=android" -v ./main
|
||||
GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC=/path/to/x86_64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0 -extldflags=-Wl,--pack-dyn-relocs=android" -v ./main
|
||||
GOOS=android GOARCH=arm64 CGO_ENABLED=1 CC=/path/to/aarch64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
|
||||
GOOS=android GOARCH=amd64 CGO_ENABLED=1 CC=/path/to/x86_64-linux-android24-clang go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags="-X github.com/xtls/xray-core/core.build=REPLACE -s -w -buildid= -checklinkname=0" -v ./main
|
||||
```
|
||||
|
||||
If you are compiling a 32-bit MIPS/MIPSLE target, use this command instead:
|
||||
|
||||
@@ -188,7 +188,6 @@ var (
|
||||
|
||||
// NewClientWorker creates a new mux.Client.
|
||||
func NewClientWorker(stream transport.Link, s ClientStrategy) (*ClientWorker, error) {
|
||||
stream.Writer = &MutexWriter{Writer: stream.Writer}
|
||||
c := &ClientWorker{
|
||||
sessionManager: NewSessionManager(),
|
||||
link: stream,
|
||||
|
||||
@@ -95,7 +95,7 @@ type ServerWorker struct {
|
||||
func NewServerWorker(ctx context.Context, d routing.Dispatcher, link *transport.Link) (*ServerWorker, error) {
|
||||
worker := &ServerWorker{
|
||||
dispatcher: d,
|
||||
link: &transport.Link{Reader: link.Reader, Writer: &MutexWriter{Writer: link.Writer}},
|
||||
link: link,
|
||||
sessionManager: NewSessionManager(),
|
||||
done: done.New(),
|
||||
timer: time.NewTicker(60 * time.Second),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package mux
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/xtls/xray-core/common"
|
||||
"github.com/xtls/xray-core/common/buf"
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
@@ -11,17 +9,6 @@ import (
|
||||
"github.com/xtls/xray-core/common/session"
|
||||
)
|
||||
|
||||
type MutexWriter struct {
|
||||
sync.Mutex
|
||||
buf.Writer
|
||||
}
|
||||
|
||||
func (w *MutexWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||
w.Lock()
|
||||
defer w.Unlock()
|
||||
return w.Writer.WriteMultiBuffer(mb)
|
||||
}
|
||||
|
||||
type Writer struct {
|
||||
dest net.Destination
|
||||
writer buf.Writer
|
||||
|
||||
+2
-2
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
var (
|
||||
Version_x byte = 26
|
||||
Version_y byte = 11
|
||||
Version_z byte = 9
|
||||
Version_y byte = 10
|
||||
Version_z byte = 31
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -16,11 +16,11 @@ require (
|
||||
github.com/pelletier/go-toml v1.9.5
|
||||
github.com/pion/stun/v3 v3.1.7
|
||||
github.com/pires/go-proxyproto v0.16.0
|
||||
github.com/refraction-networking/utls v1.8.3-0.20261108164504-a13c59ffe98b
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/stretchr/testify v1.12.1
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
github.com/xtls/reality v0.0.0-20261108101329-4bb46b2afd8b
|
||||
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8
|
||||
github.com/yuin/gopher-lua v1.1.2
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.58.0
|
||||
@@ -41,14 +41,14 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.3 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/huin/goupnp v1.2.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
github.com/klauspost/compress v1.20.0 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/molecule-man/go-brrr v1.2.0 // indirect
|
||||
github.com/pion/dtls/v3 v3.1.5 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.1.0 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
|
||||
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e h1:5mgtR5gwIgBKMiGI1QdXldZZ+SNor06Nbu1wCBulQBg=
|
||||
github.com/apernet/quic-go v0.61.1-0.20260806010916-184d081eef3e/go.mod h1:x7qxEvX6MCVtDuBKHj3E+88+BtrbEMuAL5qGUKItjW8=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
@@ -31,8 +33,8 @@ github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7Bd
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
|
||||
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||
github.com/klauspost/cpuid/v2 v2.4.2 h1:xRw7IZEsAgVCp6trS/DgWbTAqpixhmywgVgBjYGg1bQ=
|
||||
github.com/klauspost/cpuid/v2 v2.4.2/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||
@@ -47,8 +49,6 @@ github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9t
|
||||
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
|
||||
github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE=
|
||||
github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ=
|
||||
github.com/molecule-man/go-brrr v1.2.0 h1:dOJU45BC3Gc2WXoxAW0rgCAgVuu2gruAOGncfieA0Jw=
|
||||
github.com/molecule-man/go-brrr v1.2.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
|
||||
@@ -67,8 +67,8 @@ github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4
|
||||
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20261108164504-a13c59ffe98b h1:Mi/DuaTxHeNIQ9g1ylZicAqa6Cb9wkHRiNtQH9uZ7Ts=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20261108164504-a13c59ffe98b/go.mod h1:rc/ctWeKlh9LSk5k90KlSPcwF7P6D31mn/9GI2NAfBI=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
|
||||
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g=
|
||||
@@ -81,8 +81,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
|
||||
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||
github.com/xtls/reality v0.0.0-20261108101329-4bb46b2afd8b h1:fAn11r21QiUY2oAGwENWb5ummjhWwMcs2WzkbjDMh+k=
|
||||
github.com/xtls/reality v0.0.0-20261108101329-4bb46b2afd8b/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
|
||||
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8 h1:RjLnVvlzK4q6zWaSBPNRGsrgx2fbJDeUWffjw574uzs=
|
||||
github.com/xtls/reality v0.0.0-20261014101318-83f621788ca8/go.mod h1:ptlSkJ8k/7IDauNH0SA96JPOo+nYGLTv2Y3UMpwvAsY=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||
|
||||
@@ -275,7 +275,6 @@ var ModernFingerprints = map[string]*utls.ClientHelloID{
|
||||
"hellochrome_120": &utls.HelloChrome_120,
|
||||
"hellochrome_131": &utls.HelloChrome_131,
|
||||
"hellochrome_133": &utls.HelloChrome_133,
|
||||
"hellochrome_155": &utls.HelloChrome_155,
|
||||
"helloios_13": &utls.HelloIOS_13,
|
||||
"helloios_14": &utls.HelloIOS_14,
|
||||
"helloedge_106": &utls.HelloEdge_106,
|
||||
|
||||
@@ -433,10 +433,6 @@ func (b *doneBody) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// a connection that has not answered yet is given this long before its
|
||||
// timeouts retire it, so that a dial or handshake is not cut short again
|
||||
var stalledGrace = 12 * time.Second
|
||||
|
||||
// sessionTransport sends the requests of one session over a connection of
|
||||
// XMUX, and over another once that one has no requests or time left
|
||||
type sessionTransport struct {
|
||||
@@ -447,8 +443,6 @@ type sessionTransport struct {
|
||||
closed bool
|
||||
slowAt, retiredAt time.Time
|
||||
movedAt time.Time
|
||||
takenAt time.Time
|
||||
served bool
|
||||
}
|
||||
|
||||
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
@@ -469,7 +463,6 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
t.movedAt = time.Now()
|
||||
}
|
||||
client = t.client
|
||||
t.takenAt, t.served = time.Now(), false
|
||||
}
|
||||
// counted under mu, or another request of the session that moves on could
|
||||
// close the connection before this one is sent
|
||||
@@ -478,15 +471,7 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
conn.inflight++
|
||||
conn.mu.Unlock()
|
||||
t.mu.Unlock()
|
||||
resp, err := conn.roundTrip(r)
|
||||
if err == nil {
|
||||
t.mu.Lock()
|
||||
if t.client == client {
|
||||
t.served = true
|
||||
}
|
||||
t.mu.Unlock()
|
||||
}
|
||||
return resp, err
|
||||
return conn.roundTrip(r)
|
||||
}
|
||||
|
||||
func (t *sessionTransport) stalled(began time.Time) {
|
||||
@@ -496,9 +481,6 @@ func (t *sessionTransport) stalled(began time.Time) {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
if !t.served && now.Sub(t.takenAt) < stalledGrace {
|
||||
return
|
||||
}
|
||||
slow := now.Sub(t.slowAt) < 3*time.Second
|
||||
t.slowAt = now
|
||||
if !slow || t.client == nil || now.Sub(t.retiredAt) < 2*time.Second {
|
||||
|
||||
@@ -1006,9 +1006,6 @@ func (okConn) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
}
|
||||
|
||||
func TestSessionRetiresStalledConn(t *testing.T) {
|
||||
oldGrace := stalledGrace
|
||||
stalledGrace = 0
|
||||
t.Cleanup(func() { stalledGrace = oldGrace })
|
||||
made := atomic.Int32{}
|
||||
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
|
||||
if made.Add(1) == 1 {
|
||||
@@ -1090,54 +1087,3 @@ func TestSessionIgnoresStallOfRetiredConn(t *testing.T) {
|
||||
t.Fatalf("a stall of the current connection made %d connections, want it retired", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionSparesFreshConn(t *testing.T) {
|
||||
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
|
||||
return &http.Client{Transport: okConn{}}
|
||||
})}
|
||||
send := func() {
|
||||
req, err := http.NewRequest(http.MethodGet, "http://drive.invalid/files", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err := transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
current := func() *poolConn {
|
||||
transport.mu.Lock()
|
||||
defer transport.mu.Unlock()
|
||||
return transport.client.XmuxConn.(*poolConn)
|
||||
}
|
||||
send()
|
||||
conn := current()
|
||||
transport.mu.Lock()
|
||||
transport.takenAt, transport.served = time.Now(), false
|
||||
transport.mu.Unlock()
|
||||
transport.stalled(time.Now())
|
||||
transport.stalled(time.Now())
|
||||
if conn.IsClosed() {
|
||||
t.Fatal("a fresh connection was retired before its grace period ended")
|
||||
}
|
||||
transport.mu.Lock()
|
||||
transport.takenAt = time.Now().Add(-stalledGrace - time.Second)
|
||||
transport.mu.Unlock()
|
||||
transport.stalled(time.Now())
|
||||
transport.stalled(time.Now())
|
||||
if !conn.IsClosed() {
|
||||
t.Fatal("a fresh connection older than its grace period was not retired")
|
||||
}
|
||||
send()
|
||||
conn = current()
|
||||
transport.mu.Lock()
|
||||
transport.takenAt, transport.served = time.Now(), true
|
||||
transport.slowAt, transport.retiredAt = time.Time{}, time.Time{}
|
||||
transport.mu.Unlock()
|
||||
transport.stalled(time.Now())
|
||||
transport.stalled(time.Now())
|
||||
if !conn.IsClosed() {
|
||||
t.Fatal("a connection that had answered was not retired")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,12 +47,12 @@ func newConn(ctx context.Context, storage Storage, writePrefix, readPrefix strin
|
||||
}
|
||||
}
|
||||
|
||||
func newSlotConn(ctx context.Context, store slotStore, write, read slotTable, p params, first []byte, onClose func()) *Conn {
|
||||
func newSlotConn(ctx context.Context, store slotStore, write, read slotTable, p params, onClose func()) *Conn {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Conn{
|
||||
cancel: cancel,
|
||||
writer: newSlotWriter(ctx, store, write, p),
|
||||
reader: newSlotReader(ctx, store, read, p, first),
|
||||
reader: newSlotReader(ctx, store, read, p),
|
||||
onClose: onClose,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/dice"
|
||||
@@ -19,9 +18,6 @@ import (
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
// testDriveIO is a nil-by-default test hook observing every Drive request
|
||||
var testDriveIO func(method, target string, body []byte)
|
||||
|
||||
const (
|
||||
flatSeparator = "~"
|
||||
|
||||
@@ -44,24 +40,8 @@ var (
|
||||
driveCooldown = 2 * time.Second
|
||||
driveSlotRead = 3 * time.Second
|
||||
driveSlotWrite = 5 * time.Second
|
||||
tokenRefreshBackoff = 30 * time.Second
|
||||
maxTokenBackoff = 5 * time.Minute
|
||||
driveMaxResponse = 32 << 20
|
||||
driveMaxAnnounce = 1 << 20
|
||||
)
|
||||
|
||||
// maxBodyKey lets a caller tighten the response-body cap (announcements).
|
||||
type maxBodyKey struct{}
|
||||
|
||||
// tokenFlight is one shared foreground token fetch; waiters read its result
|
||||
// after done closes instead of starting their own fetch.
|
||||
type tokenFlight struct {
|
||||
done chan struct{}
|
||||
token string
|
||||
lifetime time.Duration
|
||||
err error
|
||||
}
|
||||
|
||||
type laneKey struct{}
|
||||
|
||||
type attemptKey struct{}
|
||||
@@ -88,23 +68,15 @@ type driveShared struct {
|
||||
uploadURL string
|
||||
backoff time.Duration
|
||||
|
||||
tokenMu sync.Mutex
|
||||
token string
|
||||
tokenExpiry time.Time
|
||||
tokenLifetime time.Duration
|
||||
refreshing bool
|
||||
authErr error
|
||||
flight *tokenFlight
|
||||
tokenRetryAt time.Time
|
||||
tokenBackoff time.Duration
|
||||
lastTokenErr error
|
||||
tokenMu sync.Mutex
|
||||
token string
|
||||
tokenExpiry time.Time
|
||||
|
||||
idMu sync.Mutex
|
||||
ids map[string]string
|
||||
|
||||
coolMu sync.Mutex
|
||||
coolUntil time.Time
|
||||
throttle atomic.Int64
|
||||
}
|
||||
|
||||
func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*driveStorage, error) {
|
||||
@@ -165,130 +137,14 @@ func quoteDriveValue(value string) string {
|
||||
return strings.NewReplacer(`\`, `\\`, `'`, `\'`).Replace(value)
|
||||
}
|
||||
|
||||
type authError struct{ code string }
|
||||
|
||||
func (e authError) Error() string {
|
||||
return "Google rejected the OAuth credentials (" + e.code + "); create a new refresh token, see the XDRIVE setup guide"
|
||||
}
|
||||
|
||||
func isAuthError(err error) bool {
|
||||
_, ok := err.(authError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (s *driveStorage) accessToken(ctx context.Context) (string, error) {
|
||||
s.tokenMu.Lock()
|
||||
if s.authErr != nil {
|
||||
err := s.authErr
|
||||
s.tokenMu.Unlock()
|
||||
return "", err
|
||||
}
|
||||
if s.token != "" && time.Now().Before(s.tokenExpiry) {
|
||||
if s.tokenLifetime > 0 && time.Until(s.tokenExpiry) < s.tokenLifetime/5 && !s.refreshing && time.Now().After(s.tokenRetryAt) {
|
||||
s.refreshing = true
|
||||
go s.refreshTokenBackground()
|
||||
}
|
||||
token := s.token
|
||||
s.tokenMu.Unlock()
|
||||
return token, nil
|
||||
}
|
||||
if f := s.flight; f != nil {
|
||||
s.tokenMu.Unlock()
|
||||
select {
|
||||
case <-f.done:
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
}
|
||||
if f.err != nil {
|
||||
return "", f.err
|
||||
}
|
||||
return f.token, nil
|
||||
}
|
||||
if time.Now().Before(s.tokenRetryAt) && s.lastTokenErr != nil {
|
||||
err := s.lastTokenErr
|
||||
s.tokenMu.Unlock()
|
||||
return "", err
|
||||
}
|
||||
f := &tokenFlight{done: make(chan struct{})}
|
||||
s.flight = f
|
||||
s.tokenMu.Unlock()
|
||||
|
||||
// The shared fetch must outlive any single waiter's cancellation.
|
||||
fetchCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
|
||||
token, lifetime, err := s.fetchToken(fetchCtx)
|
||||
cancel()
|
||||
|
||||
s.tokenMu.Lock()
|
||||
s.flight = nil
|
||||
f.token, f.lifetime, f.err = token, lifetime, err
|
||||
if err != nil {
|
||||
s.lastTokenErr = err
|
||||
if isAuthError(err) {
|
||||
s.authErr = err
|
||||
errors.LogError(ctx, err)
|
||||
}
|
||||
s.noteTokenFailure()
|
||||
} else {
|
||||
s.storeToken(token, lifetime)
|
||||
s.tokenBackoff = 0
|
||||
s.tokenRetryAt = time.Time{}
|
||||
s.lastTokenErr = nil
|
||||
}
|
||||
close(f.done)
|
||||
s.tokenMu.Unlock()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// noteTokenFailure backs off further token fetches so a failing endpoint is
|
||||
// not hit once per API call; called with tokenMu held.
|
||||
func (s *driveStorage) noteTokenFailure() {
|
||||
if s.tokenBackoff == 0 {
|
||||
s.tokenBackoff = tokenRefreshBackoff
|
||||
} else {
|
||||
s.tokenBackoff = min(2*s.tokenBackoff, maxTokenBackoff)
|
||||
}
|
||||
s.tokenRetryAt = time.Now().Add(s.tokenBackoff)
|
||||
}
|
||||
|
||||
func (s *driveStorage) storeToken(token string, lifetime time.Duration) {
|
||||
s.token = token
|
||||
s.tokenLifetime = lifetime
|
||||
expiry := lifetime
|
||||
if expiry > time.Minute {
|
||||
expiry -= time.Minute
|
||||
}
|
||||
s.tokenExpiry = time.Now().Add(expiry)
|
||||
}
|
||||
|
||||
func (s *driveStorage) refreshTokenBackground() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
token, lifetime, err := s.fetchToken(ctx)
|
||||
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
s.refreshing = false
|
||||
if err != nil {
|
||||
if isAuthError(err) && s.authErr == nil {
|
||||
s.authErr = err
|
||||
errors.LogError(ctx, err)
|
||||
}
|
||||
s.lastTokenErr = err
|
||||
s.noteTokenFailure()
|
||||
return
|
||||
}
|
||||
if s.authErr == nil {
|
||||
s.storeToken(token, lifetime)
|
||||
s.tokenBackoff = 0
|
||||
s.tokenRetryAt = time.Time{}
|
||||
s.lastTokenErr = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *driveStorage) fetchToken(ctx context.Context) (string, time.Duration, error) {
|
||||
if s.token != "" && time.Now().Before(s.tokenExpiry) {
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
form := url.Values{
|
||||
"client_id": {s.clientID},
|
||||
"client_secret": {s.clientSecret},
|
||||
@@ -297,31 +153,22 @@ func (s *driveStorage) fetchToken(ctx context.Context) (string, time.Duration, e
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.tokenURL, strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", 0, errors.New("failed to build the token request").Base(err)
|
||||
return "", errors.New("failed to build the token request").Base(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := s.tokenClient.Do(req)
|
||||
if err != nil {
|
||||
return "", 0, errors.New("failed to refresh the access token").Base(err)
|
||||
return "", errors.New("failed to refresh the access token").Base(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return "", 0, errors.New("failed to read the token response").Base(err)
|
||||
return "", errors.New("failed to read the token response").Base(err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
var rejected struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
if json.Unmarshal(body, &rejected) == nil {
|
||||
switch rejected.Error {
|
||||
case "invalid_grant", "invalid_client", "unauthorized_client":
|
||||
return "", 0, authError{rejected.Error}
|
||||
}
|
||||
}
|
||||
return "", 0, errors.New("the token endpoint answered ", resp.StatusCode, ": ", string(body))
|
||||
return "", errors.New("the token endpoint answered ", resp.StatusCode, ": ", string(body))
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
@@ -329,12 +176,19 @@ func (s *driveStorage) fetchToken(ctx context.Context) (string, time.Duration, e
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return "", 0, errors.New("failed to parse the token response").Base(err)
|
||||
return "", errors.New("failed to parse the token response").Base(err)
|
||||
}
|
||||
if parsed.AccessToken == "" {
|
||||
return "", 0, errors.New("the token endpoint returned no access token")
|
||||
return "", errors.New("the token endpoint returned no access token")
|
||||
}
|
||||
return parsed.AccessToken, time.Duration(parsed.ExpiresIn) * time.Second, nil
|
||||
|
||||
lifetime := parsed.ExpiresIn
|
||||
if lifetime > 60 {
|
||||
lifetime -= 60
|
||||
}
|
||||
s.token = parsed.AccessToken
|
||||
s.tokenExpiry = time.Now().Add(time.Duration(lifetime) * time.Second)
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
func jitter(backoff time.Duration) time.Duration {
|
||||
@@ -379,6 +233,17 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
||||
backoff := s.backoff
|
||||
var lastErr error
|
||||
|
||||
s.coolMu.Lock()
|
||||
until := s.coolUntil
|
||||
s.coolMu.Unlock()
|
||||
if d := time.Until(until); d > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, nil, ctx.Err()
|
||||
case <-time.After(d):
|
||||
}
|
||||
}
|
||||
|
||||
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
|
||||
if attempt > 0 {
|
||||
select {
|
||||
@@ -391,18 +256,9 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
||||
backoff = driveMaxBackoff
|
||||
}
|
||||
}
|
||||
if err := s.waitCool(ctx); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if testDriveIO != nil {
|
||||
testDriveIO(method, target, body)
|
||||
}
|
||||
|
||||
token, err := s.accessToken(ctx)
|
||||
if err != nil {
|
||||
if isAuthError(err) {
|
||||
return 0, nil, err
|
||||
}
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
@@ -443,7 +299,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
||||
timedOut := attemptCtx.Err() == context.DeadlineExceeded && ctx.Err() == nil
|
||||
cancel()
|
||||
if timedOut {
|
||||
s.throttle.Add(1)
|
||||
if t, ok := s.client.Transport.(*sessionTransport); ok {
|
||||
t.stalled(began)
|
||||
}
|
||||
@@ -456,11 +311,7 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
||||
attempt+1, " of ", driveMaxAttempts)
|
||||
continue
|
||||
}
|
||||
limit := int64(driveMaxResponse)
|
||||
if v, ok := ctx.Value(maxBodyKey{}).(int64); ok && v > 0 {
|
||||
limit = v
|
||||
}
|
||||
payload, err := io.ReadAll(io.LimitReader(resp.Body, limit+1))
|
||||
payload, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
cancel()
|
||||
if err != nil {
|
||||
@@ -470,12 +321,9 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
||||
lastErr = errors.New("failed to read the Drive response").Base(err)
|
||||
continue
|
||||
}
|
||||
if int64(len(payload)) > limit {
|
||||
return 0, nil, errors.New("Drive response exceeds the ", limit, " byte cap")
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
s.invalidateToken(token)
|
||||
s.invalidateToken()
|
||||
lastErr = errors.New("Drive rejected the access token")
|
||||
continue
|
||||
}
|
||||
@@ -503,36 +351,15 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
||||
return 0, nil, lastErr
|
||||
}
|
||||
|
||||
func (s *driveStorage) waitCool(ctx context.Context) error {
|
||||
s.coolMu.Lock()
|
||||
until := s.coolUntil
|
||||
s.coolMu.Unlock()
|
||||
if d := time.Until(until); d > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(d):
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *driveStorage) cool() {
|
||||
s.throttle.Add(1)
|
||||
s.coolMu.Lock()
|
||||
s.coolUntil = time.Now().Add(jitter(driveCooldown))
|
||||
s.coolMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *driveStorage) throttled() int64 {
|
||||
return s.throttle.Load()
|
||||
}
|
||||
|
||||
func (s *driveStorage) invalidateToken(used string) {
|
||||
func (s *driveStorage) invalidateToken() {
|
||||
s.tokenMu.Lock()
|
||||
if s.token == used {
|
||||
s.token = ""
|
||||
}
|
||||
s.token = ""
|
||||
s.tokenMu.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDecodeRingCaps(t *testing.T) {
|
||||
good := ring{
|
||||
Up: slotTable{Slots: []string{"a", "b"}, Watermark: "w"},
|
||||
Down: slotTable{Slots: []string{"c", "d"}, Watermark: "w"},
|
||||
}
|
||||
b, _ := json.Marshal(good)
|
||||
if _, ok := decodeRing(b); !ok {
|
||||
t.Fatal("a normal ring was rejected")
|
||||
}
|
||||
|
||||
tooMany := good
|
||||
tooMany.Up.Slots = make([]string, maxConcurrency+1)
|
||||
for i := range tooMany.Up.Slots {
|
||||
tooMany.Up.Slots[i] = "s"
|
||||
}
|
||||
b, _ = json.Marshal(tooMany)
|
||||
if _, ok := decodeRing(b); ok {
|
||||
t.Fatalf("a ring with %d slots per table was accepted", maxConcurrency+1)
|
||||
}
|
||||
|
||||
longID := good
|
||||
longID.Up.Slots[0] = strings.Repeat("x", maxSlotIDLen+1)
|
||||
b, _ = json.Marshal(longID)
|
||||
if _, ok := decodeRing(b); ok {
|
||||
t.Fatal("a ring with an over-long slot id was accepted")
|
||||
}
|
||||
|
||||
longWM := good
|
||||
longWM.Down.Watermark = strings.Repeat("w", maxSlotIDLen+1)
|
||||
b, _ = json.Marshal(longWM)
|
||||
if _, ok := decodeRing(b); ok {
|
||||
t.Fatal("a ring with an over-long watermark id was accepted")
|
||||
}
|
||||
|
||||
emptyID := good
|
||||
emptyID.Down.Slots[1] = ""
|
||||
b, _ = json.Marshal(emptyID)
|
||||
if _, ok := decodeRing(b); ok {
|
||||
t.Fatal("a ring with an empty slot id was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func capTestStorage(t *testing.T, body []byte) (*driveStorage, string) {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(body)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return &driveStorage{
|
||||
driveShared: &driveShared{token: "t", tokenExpiry: time.Now().Add(time.Hour)},
|
||||
client: srv.Client(),
|
||||
inflight: make(chan struct{}, 1),
|
||||
reserved: make(chan struct{}, 1),
|
||||
}, srv.URL
|
||||
}
|
||||
|
||||
func TestDriveResponseBodyCap(t *testing.T) {
|
||||
old := driveMaxResponse
|
||||
driveMaxResponse = 16
|
||||
t.Cleanup(func() { driveMaxResponse = old })
|
||||
s, url := capTestStorage(t, make([]byte, 32))
|
||||
_, _, err := s.do(context.Background(), http.MethodGet, url, "", nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "exceeds") {
|
||||
t.Fatalf("an over-cap body returned %v, want a cap error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveAnnouncementBodyCap(t *testing.T) {
|
||||
s, url := capTestStorage(t, make([]byte, 32))
|
||||
ctx := context.WithValue(context.Background(), maxBodyKey{}, int64(8))
|
||||
_, _, err := s.do(ctx, http.MethodGet, url, "", nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "exceeds") {
|
||||
t.Fatalf("an over-cap announcement returned %v, want a cap error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveNormal16MiBSegment(t *testing.T) {
|
||||
body := make([]byte, 16<<20)
|
||||
s, url := capTestStorage(t, body)
|
||||
_, payload, err := s.do(context.Background(), http.MethodGet, url, "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a normal 16 MiB segment failed: %v", err)
|
||||
}
|
||||
if len(payload) != len(body) {
|
||||
t.Fatalf("read %d bytes, want %d", len(payload), len(body))
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func livePercentileReport(t *testing.T, label string, samples []time.Duration) {
|
||||
t.Helper()
|
||||
if len(samples) == 0 {
|
||||
t.Logf("%-36s no samples", label)
|
||||
return
|
||||
}
|
||||
sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] })
|
||||
q := func(p float64) time.Duration {
|
||||
return samples[min(int(p*float64(len(samples))), len(samples)-1)]
|
||||
}
|
||||
t.Logf("%-36s n=%d p50=%v p95=%v p99=%v max=%v",
|
||||
label, len(samples), q(0.50), q(0.95), q(0.99), samples[len(samples)-1])
|
||||
}
|
||||
|
||||
func TestLiveDrivePercentiles(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
root, err := newDriveStorage(&internet.MemoryStreamConfig{}, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
store := root.session().(*driveStorage)
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
const n = 60
|
||||
inlineDesc := strings.Repeat("a", 10<<10)
|
||||
body := make([]byte, 256<<10)
|
||||
if _, err := rand.Read(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inlineID, err := store.makeSlot(ctx, "percentiles/inline")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.deleteID(ctx, flatten("percentiles/inline"), inlineID)
|
||||
mediaID, err := store.makeSlot(ctx, "percentiles/media")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.deleteID(ctx, flatten("percentiles/media"), mediaID)
|
||||
if err := store.setSlot(ctx, mediaID, "media", body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
measure := func(label string, parallelism int, op func() error) {
|
||||
var mu sync.Mutex
|
||||
samples := make([]time.Duration, 0, n)
|
||||
var wg sync.WaitGroup
|
||||
per := n / parallelism
|
||||
for w := 0; w < parallelism; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < per; i++ {
|
||||
start := time.Now()
|
||||
err := op()
|
||||
elapsed := time.Since(start)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", label, err)
|
||||
continue
|
||||
}
|
||||
mu.Lock()
|
||||
samples = append(samples, elapsed)
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
livePercentileReport(t, fmt.Sprintf("%s (p=%d)", label, parallelism), samples)
|
||||
}
|
||||
|
||||
for _, p := range []int{1, 8} {
|
||||
measure("inline setSlot 10KiB", p, func() error {
|
||||
return store.setSlot(ctx, inlineID, inlineDesc, nil)
|
||||
})
|
||||
measure("media setSlot 256KiB", p, func() error {
|
||||
return store.setSlot(ctx, mediaID, "media", body)
|
||||
})
|
||||
measure("readDescription", p, func() error {
|
||||
_, err := store.readDescription(ctx, mediaID)
|
||||
return err
|
||||
})
|
||||
measure("readBody 256KiB", p, func() error {
|
||||
_, err := store.readBody(ctx, mediaID)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveDriveLatencyUnderLoad(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
config.Concurrency = 64
|
||||
config.FlushIntervalMs = 10
|
||||
config.MaxPollIntervalMs = 200
|
||||
settings := &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: config,
|
||||
}
|
||||
|
||||
run := func(withBulk bool) []time.Duration {
|
||||
client, server, cleanup := pairWith(t, settings)
|
||||
defer cleanup()
|
||||
if err := server.SetReadDeadline(time.Now().Add(10 * time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var writeMu sync.Mutex
|
||||
writeFrame := func(kind byte, payload []byte) error {
|
||||
writeMu.Lock()
|
||||
defer writeMu.Unlock()
|
||||
var header [5]byte
|
||||
header[0] = kind
|
||||
binary.BigEndian.PutUint32(header[1:], uint32(len(payload)))
|
||||
if _, err := client.Write(header[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := client.Write(payload)
|
||||
return err
|
||||
}
|
||||
readFrame := func() (byte, []byte, error) {
|
||||
var header [5]byte
|
||||
if _, err := io.ReadFull(server, header[:]); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
payload := make([]byte, binary.BigEndian.Uint32(header[1:]))
|
||||
if _, err := io.ReadFull(server, payload); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return header[0], payload, nil
|
||||
}
|
||||
|
||||
markers := make(chan time.Duration, 5)
|
||||
bulkTime := make(chan time.Duration, 1)
|
||||
go func() {
|
||||
var bulkRecv int
|
||||
var bulkStart time.Time
|
||||
for {
|
||||
kind, payload, err := readFrame()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if kind == 1 {
|
||||
if bulkRecv == 0 {
|
||||
bulkStart = time.Now()
|
||||
}
|
||||
bulkRecv += len(payload)
|
||||
if bulkRecv == 8<<20 {
|
||||
bulkTime <- time.Since(bulkStart)
|
||||
}
|
||||
}
|
||||
if kind == 2 && len(payload) == 16 {
|
||||
sent := int64(binary.BigEndian.Uint64(payload[:8]))
|
||||
markers <- time.Since(time.Unix(0, sent))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
bulkDone := make(chan struct{})
|
||||
if withBulk {
|
||||
go func() {
|
||||
defer close(bulkDone)
|
||||
chunk := make([]byte, 64<<10)
|
||||
for i := 0; i < 128; i++ {
|
||||
if err := writeFrame(1, chunk); err != nil {
|
||||
t.Errorf("bulk frame: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
var payload [16]byte
|
||||
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().UnixNano()))
|
||||
if err := writeFrame(2, payload[:]); err != nil {
|
||||
t.Fatalf("marker frame: %v", err)
|
||||
}
|
||||
if i < 4 {
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
if withBulk {
|
||||
<-bulkDone
|
||||
}
|
||||
out := make([]time.Duration, 0, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
select {
|
||||
case d := <-markers:
|
||||
out = append(out, d)
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Fatal("marker did not arrive")
|
||||
}
|
||||
}
|
||||
if withBulk {
|
||||
select {
|
||||
case d := <-bulkTime:
|
||||
t.Logf("bulk throughput: %.2f MiB/s", 8/d.Seconds())
|
||||
case <-time.After(60 * time.Second):
|
||||
t.Log("bulk throughput: bulk did not complete")
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
t.Log("marker one-way latency with no bulk:")
|
||||
for i, d := range run(false) {
|
||||
t.Logf(" marker %d: %v", i+1, d.Round(time.Millisecond))
|
||||
}
|
||||
t.Log("marker one-way latency under 8MiB bulk:")
|
||||
for i, d := range run(true) {
|
||||
t.Logf(" marker %d: %v", i+1, d.Round(time.Millisecond))
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
func TestLiveDriveWriteSweep(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
root, err := newDriveStorage(&internet.MemoryStreamConfig{}, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer root.Close()
|
||||
store := root.session().(*driveStorage)
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
inlineID, err := store.makeSlot(ctx, "sweep/inline")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.deleteID(ctx, flatten("sweep/inline"), inlineID)
|
||||
mediaID, err := store.makeSlot(ctx, "sweep/media")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store.deleteID(ctx, flatten("sweep/media"), mediaID)
|
||||
|
||||
desc := strings.Repeat("a", 10<<10)
|
||||
body := make([]byte, 256<<10)
|
||||
if _, err := rand.Read(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kinds := []struct {
|
||||
name string
|
||||
op func(context.Context) error
|
||||
}{
|
||||
{"inline 10KiB", func(ctx context.Context) error { return store.setSlot(ctx, inlineID, desc, nil) }},
|
||||
{"media 256KiB", func(ctx context.Context) error { return store.setSlot(ctx, mediaID, "media", body) }},
|
||||
}
|
||||
|
||||
for _, p := range []int{1, 2, 3, 4, 6, 8, 12} {
|
||||
for _, kind := range kinds {
|
||||
levelCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
thr0 := store.throttled()
|
||||
var mu sync.Mutex
|
||||
var samples []time.Duration
|
||||
success := 0
|
||||
var wg sync.WaitGroup
|
||||
per := 40 / p
|
||||
levelStart := time.Now()
|
||||
for w := 0; w < p; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < per; i++ {
|
||||
if levelCtx.Err() != nil {
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
if err := kind.op(levelCtx); err != nil {
|
||||
continue
|
||||
}
|
||||
mu.Lock()
|
||||
samples = append(samples, time.Since(start))
|
||||
success++
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
wall := time.Since(levelStart)
|
||||
cancel()
|
||||
if len(samples) == 0 {
|
||||
t.Logf("sweep p=%-2d %-14s writes=0 wall=%v no samples", p, kind.name, wall.Round(time.Millisecond))
|
||||
continue
|
||||
}
|
||||
sort.Slice(samples, func(i, j int) bool { return samples[i] < samples[j] })
|
||||
q := func(f float64) time.Duration {
|
||||
return samples[min(int(f*float64(len(samples))), len(samples)-1)]
|
||||
}
|
||||
t.Logf("sweep p=%-2d %-14s writes=%d wall=%v writes/s=%.2f p50=%v p95=%v p99=%v max=%v throttled=+%d",
|
||||
p, kind.name, success, wall.Round(time.Millisecond), float64(success)/wall.Seconds(),
|
||||
q(0.50), q(0.95), q(0.99), samples[len(samples)-1], store.throttled()-thr0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLiveDriveBrowsingExchanges(t *testing.T) {
|
||||
config := liveDriveConfig(t)
|
||||
config.Concurrency = 64
|
||||
config.FlushIntervalMs = 10
|
||||
config.MaxPollIntervalMs = 200
|
||||
settings := &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: config,
|
||||
}
|
||||
|
||||
var slotWrites, ackWrites, announceWrites, reads atomic.Int64
|
||||
testDriveIO = func(method, target string, body []byte) {
|
||||
switch method {
|
||||
case http.MethodGet:
|
||||
reads.Add(1)
|
||||
case http.MethodPatch:
|
||||
var doc struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if json.Unmarshal(body, &doc) != nil {
|
||||
return
|
||||
}
|
||||
if _, ok := decodeSlot(doc.Description); ok {
|
||||
slotWrites.Add(1)
|
||||
return
|
||||
}
|
||||
if _, err := strconv.ParseInt(doc.Description, 10, 64); err == nil && doc.Description != "" {
|
||||
ackWrites.Add(1)
|
||||
return
|
||||
}
|
||||
slotWrites.Add(1)
|
||||
case http.MethodPost, http.MethodPut:
|
||||
if method == http.MethodPut || strings.Contains(target, "/upload") {
|
||||
slotWrites.Add(1)
|
||||
return
|
||||
}
|
||||
var doc struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if json.Unmarshal(body, &doc) == nil && strings.HasPrefix(doc.Name, sessionsDir+flatSeparator) {
|
||||
announceWrites.Add(1)
|
||||
return
|
||||
}
|
||||
slotWrites.Add(1)
|
||||
}
|
||||
}
|
||||
defer func() { testDriveIO = nil }()
|
||||
|
||||
client, server, cleanup := pairWith(t, settings)
|
||||
if err := server.SetReadDeadline(time.Now().Add(10 * time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := client.SetReadDeadline(time.Now().Add(10 * time.Minute)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
answer := randomPayload(30 << 10)
|
||||
latencies := make([]time.Duration, 0, 40)
|
||||
for i := 0; i < 40; i++ {
|
||||
start := time.Now()
|
||||
request := randomPayload(500)
|
||||
if _, err := client.Write(request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotRequest := make([]byte, len(request))
|
||||
if _, err := io.ReadFull(server, gotRequest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(gotRequest, request) {
|
||||
t.Fatal("the request came back changed")
|
||||
}
|
||||
if _, err := server.Write(answer); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := make([]byte, len(answer))
|
||||
if _, err := io.ReadFull(client, got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, answer) {
|
||||
t.Fatal("the answer came back changed")
|
||||
}
|
||||
latencies = append(latencies, time.Since(start))
|
||||
if i < 39 {
|
||||
time.Sleep(400 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
cleanup()
|
||||
|
||||
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
||||
q := func(f float64) time.Duration {
|
||||
return latencies[min(int(f*float64(len(latencies))), len(latencies)-1)]
|
||||
}
|
||||
t.Logf("browsing: exchanges=%d p50=%v p95=%v max=%v",
|
||||
len(latencies), q(0.50), q(0.95), latencies[len(latencies)-1])
|
||||
total := slotWrites.Load() + ackWrites.Load() + announceWrites.Load()
|
||||
share := float64(0)
|
||||
if total > 0 {
|
||||
share = 100 * float64(ackWrites.Load()) / float64(total)
|
||||
}
|
||||
t.Logf("browsing writes: slot-data=%d watermark-ack=%d announcement=%d total=%d ack-share=%.1f%% reads=%d",
|
||||
slotWrites.Load(), ackWrites.Load(), announceWrites.Load(), total, share, reads.Load())
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
|
||||
type fakeFile struct {
|
||||
@@ -30,17 +29,14 @@ type fakeFile struct {
|
||||
type fakeDrive struct {
|
||||
server *httptest.Server
|
||||
|
||||
mu sync.Mutex
|
||||
files map[string]*fakeFile
|
||||
nextID int
|
||||
failOnce map[string]bool
|
||||
failStatus map[string]int
|
||||
failBody map[string]string
|
||||
tokens int
|
||||
tokenStatus int
|
||||
tokenBody string
|
||||
uploadAt []time.Time
|
||||
hosts map[string]bool
|
||||
mu sync.Mutex
|
||||
files map[string]*fakeFile
|
||||
nextID int
|
||||
failOnce map[string]bool
|
||||
failStatus map[string]int
|
||||
failBody map[string]string
|
||||
tokens int
|
||||
hosts map[string]bool
|
||||
}
|
||||
|
||||
func newFakeDrive(t *testing.T) *fakeDrive {
|
||||
@@ -93,13 +89,7 @@ func newFakeDrive(t *testing.T) *fakeDrive {
|
||||
func (d *fakeDrive) handleToken(w http.ResponseWriter, r *http.Request) {
|
||||
d.mu.Lock()
|
||||
d.tokens++
|
||||
status, body := d.tokenStatus, d.tokenBody
|
||||
d.mu.Unlock()
|
||||
if status != 0 {
|
||||
w.WriteHeader(status)
|
||||
w.Write([]byte(body))
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": "fake-token",
|
||||
"expires_in": 3600,
|
||||
@@ -107,9 +97,6 @@ func (d *fakeDrive) handleToken(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
d.mu.Lock()
|
||||
d.uploadAt = append(d.uploadAt, time.Now())
|
||||
d.mu.Unlock()
|
||||
if d.shouldFail("upload") {
|
||||
if status, body := d.failure("upload"); status != 0 {
|
||||
w.WriteHeader(status)
|
||||
@@ -180,9 +167,6 @@ func (d *fakeDrive) handleFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (d *fakeDrive) handleCreate(w http.ResponseWriter, r *http.Request) {
|
||||
d.mu.Lock()
|
||||
d.uploadAt = append(d.uploadAt, time.Now())
|
||||
d.mu.Unlock()
|
||||
if d.shouldFail("upload") {
|
||||
if status, body := d.failure("upload"); status != 0 {
|
||||
w.WriteHeader(status)
|
||||
@@ -757,365 +741,3 @@ func TestDriveFronting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveLargeAnnouncement(t *testing.T) {
|
||||
listenerCtx, cancelListener := context.WithCancel(context.Background())
|
||||
defer cancelListener()
|
||||
newFakeDrive(t)
|
||||
storage := newDriveBackend(t)
|
||||
ctx := context.Background()
|
||||
p := paramsFromConfig(driveSettings().ProtocolSettings.(*Config))
|
||||
|
||||
session := "large-announcement"
|
||||
r := ring{
|
||||
Up: slotTable{Watermark: "up-watermark"},
|
||||
Down: slotTable{Watermark: "down-watermark"},
|
||||
}
|
||||
for i := range 200 {
|
||||
id := fmt.Sprintf("%s-%03d", strings.Repeat("s", 40), i)
|
||||
r.Up.Slots = append(r.Up.Slots, id)
|
||||
r.Down.Slots = append(r.Down.Slots, id)
|
||||
}
|
||||
announce, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(announce) <= driveInlineLimit {
|
||||
t.Fatalf("announcement is %d bytes, want more than %d", len(announce), driveInlineLimit)
|
||||
}
|
||||
if err := storage.Put(ctx, announceName(session, time.Now()), announce); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries, err := storage.List(ctx, sessionsDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(entries) != 1 || entries[0].Inline != nil {
|
||||
t.Fatalf("large announcement was inlined: %v", entries)
|
||||
}
|
||||
|
||||
conns := make(chan *Conn, 2)
|
||||
l := &Listener{
|
||||
ctx: listenerCtx,
|
||||
storage: storage,
|
||||
addConn: func(c stat.Connection) { conns <- c.(*Conn) },
|
||||
params: p,
|
||||
active: make(map[string]bool),
|
||||
handled: make(map[string]time.Time),
|
||||
idleSince: make(map[string]time.Time),
|
||||
announceWarned: make(map[string]bool),
|
||||
}
|
||||
accepted, err := l.acceptPending(ctx)
|
||||
if err != nil || !accepted {
|
||||
t.Fatalf("acceptPending: %v, accepted %v", err, accepted)
|
||||
}
|
||||
conn := <-conns
|
||||
reader, ok := conn.reader.(*slotReader)
|
||||
if !ok || len(reader.table.Slots) != 200 {
|
||||
t.Fatalf("large announcement did not become a 200-slot connection: %T", conn.reader)
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
empty := "empty-announcement"
|
||||
if err := storage.Put(ctx, announceName(empty, time.Now()), nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
accepted, err = l.acceptPending(ctx)
|
||||
if err != nil || !accepted {
|
||||
t.Fatalf("acceptPending empty: %v, accepted %v", err, accepted)
|
||||
}
|
||||
conn = <-conns
|
||||
if _, ok := conn.reader.(*walReader); !ok {
|
||||
t.Fatalf("empty announcement did not become a segment connection: %T", conn.reader)
|
||||
}
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func TestDriveDeadCredentials(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
drive.mu.Lock()
|
||||
drive.tokenStatus = http.StatusBadRequest
|
||||
drive.tokenBody = `{"error":"invalid_grant"}`
|
||||
drive.mu.Unlock()
|
||||
storage := newDriveBackend(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
_, _, err := storage.do(ctx, http.MethodGet, storage.filesURL, "", nil)
|
||||
if !isAuthError(err) {
|
||||
t.Fatalf("do returned %v, want the sticky OAuth error", err)
|
||||
}
|
||||
if time.Since(start) > time.Second {
|
||||
t.Fatalf("do took %v, want it to fail at once", time.Since(start))
|
||||
}
|
||||
for range 3 {
|
||||
if _, err := storage.readDescription(ctx, "id"); !isAuthError(err) {
|
||||
t.Fatalf("readDescription returned %v, want the sticky OAuth error", err)
|
||||
}
|
||||
}
|
||||
drive.mu.Lock()
|
||||
tokens := drive.tokens
|
||||
drive.mu.Unlock()
|
||||
if tokens != 1 {
|
||||
t.Fatalf("the fake saw %d token requests, want exactly one", tokens)
|
||||
}
|
||||
|
||||
reader := newSlotReader(ctx, storage, slotTable{Slots: []string{"id"}, Watermark: "w"}, params{concurrency: 1}, nil)
|
||||
select {
|
||||
case <-reader.chunks():
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("the slot reader kept polling after the credentials were rejected")
|
||||
}
|
||||
if !isAuthError(reader.Err()) {
|
||||
t.Fatalf("the slot reader ended with %v, want the sticky OAuth error", reader.Err())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveRetryWaitsCooldown(t *testing.T) {
|
||||
drive := newFakeDrive(t)
|
||||
oldCooldown := driveCooldown
|
||||
driveCooldown = 200 * time.Millisecond
|
||||
t.Cleanup(func() { driveCooldown = oldCooldown })
|
||||
storage := newDriveBackend(t)
|
||||
|
||||
drive.failOnceWith("upload", http.StatusForbidden,
|
||||
`{"error":{"code":403,"errors":[{"reason":"userRateLimitExceeded"}]}}`)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
if err := storage.Put(ctx, "sessions/abc", nil); err != nil {
|
||||
t.Fatalf("Put did not survive a 403: %v", err)
|
||||
}
|
||||
drive.mu.Lock()
|
||||
times := append([]time.Time(nil), drive.uploadAt...)
|
||||
drive.mu.Unlock()
|
||||
if len(times) < 2 {
|
||||
t.Fatalf("the fake saw %d writes, want a retry", len(times))
|
||||
}
|
||||
if gap := times[1].Sub(times[0]); gap < driveCooldown/2 {
|
||||
t.Fatalf("the retry arrived after %v, less than the jitter floor %v", gap, driveCooldown/2)
|
||||
}
|
||||
storage.coolMu.Lock()
|
||||
until := storage.coolUntil
|
||||
storage.coolMu.Unlock()
|
||||
if times[1].Before(until) {
|
||||
t.Fatalf("the retry reached the fake at %v, before the cooldown ended at %v", times[1], until)
|
||||
}
|
||||
if took := time.Since(start); took < until.Sub(times[0]) {
|
||||
t.Fatalf("the request took %v, less than the cooldown %v", took, until.Sub(times[0]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveBackgroundTokenRefresh(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
calls := 0
|
||||
mode := "ok"
|
||||
delay := 100 * time.Millisecond
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
calls++
|
||||
n := calls
|
||||
current := mode
|
||||
pause := delay
|
||||
mu.Unlock()
|
||||
time.Sleep(pause)
|
||||
switch current {
|
||||
case "transient":
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"error":"temporarily_unavailable"}`))
|
||||
case "auth":
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(`{"error":"invalid_grant"}`))
|
||||
default:
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": fmt.Sprintf("token-%d", n),
|
||||
"expires_in": 100,
|
||||
})
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
newStorage := func() *driveStorage {
|
||||
return &driveStorage{driveShared: &driveShared{
|
||||
tokenClient: server.Client(),
|
||||
tokenURL: server.URL,
|
||||
}}
|
||||
}
|
||||
seed := func(s *driveStorage) {
|
||||
s.tokenMu.Lock()
|
||||
s.token = "old"
|
||||
s.tokenLifetime = 100 * time.Second
|
||||
s.tokenExpiry = time.Now().Add(10 * time.Second)
|
||||
s.tokenMu.Unlock()
|
||||
}
|
||||
setMode := func(m string) {
|
||||
mu.Lock()
|
||||
mode = m
|
||||
mu.Unlock()
|
||||
}
|
||||
callCount := func() int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return calls
|
||||
}
|
||||
|
||||
t.Run("returns at once inside the window", func(t *testing.T) {
|
||||
s := newStorage()
|
||||
seed(s)
|
||||
start := time.Now()
|
||||
token, err := s.accessToken(context.Background())
|
||||
if err != nil || token != "old" {
|
||||
t.Fatalf("accessToken gave %q, %v", token, err)
|
||||
}
|
||||
if time.Since(start) > 50*time.Millisecond {
|
||||
t.Fatalf("accessToken waited %v for the background refresh", time.Since(start))
|
||||
}
|
||||
waitFor(t, "the background refresh", func() bool {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
return s.token != "old" && !s.refreshing
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("concurrent calls start one refresh", func(t *testing.T) {
|
||||
mu.Lock()
|
||||
calls = 0
|
||||
mu.Unlock()
|
||||
s := newStorage()
|
||||
seed(s)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if token, err := s.accessToken(context.Background()); err != nil || token != "old" {
|
||||
t.Errorf("accessToken gave %q, %v", token, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
waitFor(t, "one background refresh", func() bool {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
return !s.refreshing && s.token != "old"
|
||||
})
|
||||
if n := callCount(); n != 1 {
|
||||
t.Fatalf("concurrent calls started %d refreshes, want one", n)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("transient failure keeps the old token", func(t *testing.T) {
|
||||
mu.Lock()
|
||||
calls = 0
|
||||
mu.Unlock()
|
||||
setMode("transient")
|
||||
defer setMode("ok")
|
||||
s := newStorage()
|
||||
seed(s)
|
||||
if token, err := s.accessToken(context.Background()); err != nil || token != "old" {
|
||||
t.Fatalf("accessToken gave %q, %v", token, err)
|
||||
}
|
||||
waitFor(t, "the failed refresh to clear", func() bool {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
return !s.refreshing
|
||||
})
|
||||
s.tokenMu.Lock()
|
||||
token := s.token
|
||||
s.tokenMu.Unlock()
|
||||
if token != "old" {
|
||||
t.Fatalf("a transient failure replaced the token with %q", token)
|
||||
}
|
||||
// Simulate the backoff having elapsed so the next call may retry.
|
||||
s.tokenMu.Lock()
|
||||
s.tokenRetryAt = time.Time{}
|
||||
s.tokenBackoff = 0
|
||||
s.tokenMu.Unlock()
|
||||
setMode("ok")
|
||||
if _, err := s.accessToken(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor(t, "the later refresh", func() bool {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
return s.token != "old" && !s.refreshing
|
||||
})
|
||||
if n := callCount(); n != 2 {
|
||||
t.Fatalf("the retry made %d token requests, want two", n)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("auth failure turns sticky", func(t *testing.T) {
|
||||
mu.Lock()
|
||||
calls = 0
|
||||
mu.Unlock()
|
||||
setMode("auth")
|
||||
defer setMode("ok")
|
||||
s := newStorage()
|
||||
seed(s)
|
||||
if _, err := s.accessToken(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitFor(t, "the sticky auth error", func() bool {
|
||||
s.tokenMu.Lock()
|
||||
defer s.tokenMu.Unlock()
|
||||
return s.authErr != nil && !s.refreshing
|
||||
})
|
||||
before := callCount()
|
||||
if _, err := s.accessToken(context.Background()); !isAuthError(err) {
|
||||
t.Fatalf("accessToken gave %v, want the sticky auth error", err)
|
||||
}
|
||||
if n := callCount(); n != before {
|
||||
t.Fatalf("the sticky error made %d more token requests", n-before)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSlotPathDefaults(t *testing.T) {
|
||||
p := paramsForStorage(&Config{}, &driveStorage{})
|
||||
if p.concurrency != 64 || p.flushInterval != 10*time.Millisecond || p.maxPollInterval != 200*time.Millisecond {
|
||||
t.Fatalf("slot defaults gave concurrency %d, flush %v, max poll %v", p.concurrency, p.flushInterval, p.maxPollInterval)
|
||||
}
|
||||
if wal, base := paramsForStorage(&Config{}, &localStorage{}), paramsFromConfig(&Config{}); wal != base {
|
||||
t.Fatalf("wal defaults changed: %+v, want %+v", wal, base)
|
||||
}
|
||||
explicit := paramsForStorage(&Config{Concurrency: 4, FlushIntervalMs: 30, MaxPollIntervalMs: 100}, &driveStorage{})
|
||||
if explicit.concurrency != 4 || explicit.flushInterval != 30*time.Millisecond || explicit.maxPollInterval != 100*time.Millisecond {
|
||||
t.Fatalf("explicit values did not win: %+v", explicit)
|
||||
}
|
||||
if p := paramsForStorage(&Config{PollIntervalMs: 500}, &driveStorage{}); p.maxPollInterval < p.minPollInterval {
|
||||
t.Fatalf("max poll %v is below min poll %v", p.maxPollInterval, p.minPollInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialDefaultRingDepth(t *testing.T) {
|
||||
newFakeDrive(t)
|
||||
config := &Config{
|
||||
RemoteFolder: "folder-id",
|
||||
Service: "google-drive",
|
||||
Secrets: []string{"client", "secret", "refresh"},
|
||||
}
|
||||
settings := &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: config,
|
||||
}
|
||||
conn, err := Dial(context.Background(), net.Destination{}, settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
writer, ok := conn.(*Conn).writer.(*slotWriter)
|
||||
if !ok {
|
||||
t.Fatalf("Dial with unset concurrency built %T, want a slot writer", conn.(*Conn).writer)
|
||||
}
|
||||
if len(writer.table.Slots) != 64 {
|
||||
t.Fatalf("Dial with unset concurrency built %d slots, want 64", len(writer.table.Slots))
|
||||
}
|
||||
box := spareBoxFor(config)
|
||||
box.stop.Store(true)
|
||||
for box.busy.Load() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type tokenServer struct {
|
||||
mu sync.Mutex
|
||||
count int
|
||||
mode int
|
||||
delay time.Duration
|
||||
srv *httptest.Server
|
||||
}
|
||||
|
||||
func newTokenServer(mode int, delay time.Duration) *tokenServer {
|
||||
ts := &tokenServer{mode: mode, delay: delay}
|
||||
ts.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ts.mu.Lock()
|
||||
ts.count++
|
||||
mode, delay := ts.mode, ts.delay
|
||||
ts.mu.Unlock()
|
||||
time.Sleep(delay)
|
||||
if mode != http.StatusOK {
|
||||
w.WriteHeader(mode)
|
||||
w.Write([]byte(`{"error":"temporarily_unavailable"}`))
|
||||
return
|
||||
}
|
||||
w.Write([]byte(`{"access_token":"new-token","expires_in":3600}`))
|
||||
}))
|
||||
return ts
|
||||
}
|
||||
|
||||
func (ts *tokenServer) requests() int {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
return ts.count
|
||||
}
|
||||
|
||||
func (ts *tokenServer) storage() *driveStorage {
|
||||
return &driveStorage{driveShared: &driveShared{
|
||||
tokenClient: ts.srv.Client(),
|
||||
tokenURL: ts.srv.URL + "/token",
|
||||
}}
|
||||
}
|
||||
|
||||
func TestTokenSingleFlightDuringOutage(t *testing.T) {
|
||||
ts := newTokenServer(http.StatusInternalServerError, 200*time.Millisecond)
|
||||
defer ts.srv.Close()
|
||||
s := ts.storage()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errs := make(chan error, 9)
|
||||
start := make(chan struct{})
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); <-start; _, err := s.accessToken(context.Background()); errs <- err }()
|
||||
close(start)
|
||||
// Wait until the single flight is in progress, then pile on waiters.
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for ts.requests() == 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
for i := 0; i < 7; i++ {
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); _, err := s.accessToken(context.Background()); errs <- err }()
|
||||
}
|
||||
cancelledCtx, cancel := context.WithCancel(context.Background())
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
cancel()
|
||||
_, err := s.accessToken(cancelledCtx)
|
||||
errs <- err
|
||||
}()
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
var cancelledSeen bool
|
||||
for err := range errs {
|
||||
if err == nil {
|
||||
t.Fatal("an outage caller succeeded")
|
||||
}
|
||||
if err == context.Canceled {
|
||||
cancelledSeen = true
|
||||
}
|
||||
}
|
||||
if !cancelledSeen {
|
||||
t.Fatal("the cancelled waiter did not return ctx.Err")
|
||||
}
|
||||
if n := ts.requests(); n != 1 {
|
||||
t.Fatalf("N concurrent callers caused %d token fetches, want 1", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenBackgroundFailureBacksOff(t *testing.T) {
|
||||
ts := newTokenServer(http.StatusInternalServerError, 0)
|
||||
defer ts.srv.Close()
|
||||
s := ts.storage()
|
||||
s.token = "cached"
|
||||
s.tokenLifetime = 10 * time.Second
|
||||
s.tokenExpiry = time.Now().Add(time.Second) // inside the refresh window
|
||||
|
||||
if tok, err := s.accessToken(context.Background()); err != nil || tok != "cached" {
|
||||
t.Fatalf("accessToken=%q err=%v", tok, err)
|
||||
}
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for ts.requests() == 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if n := ts.requests(); n != 1 {
|
||||
t.Fatalf("background refresh made %d requests, want 1", n)
|
||||
}
|
||||
// A second call inside the backoff must not hit the endpoint again.
|
||||
if _, err := s.accessToken(context.Background()); err != nil {
|
||||
t.Fatalf("cached token should still be returned: %v", err)
|
||||
}
|
||||
if n := ts.requests(); n != 1 {
|
||||
t.Fatalf("a failing endpoint was hit %d times, want 1", n)
|
||||
}
|
||||
s.tokenMu.Lock()
|
||||
backoff := s.tokenBackoff
|
||||
retryAt := s.tokenRetryAt
|
||||
s.tokenMu.Unlock()
|
||||
if backoff < tokenRefreshBackoff {
|
||||
t.Fatalf("backoff %v below the default %v", backoff, tokenRefreshBackoff)
|
||||
}
|
||||
if !retryAt.After(time.Now()) {
|
||||
t.Fatal("tokenRetryAt not set into the future after a background failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToken401RefetchOnce(t *testing.T) {
|
||||
ts := newTokenServer(http.StatusOK, 0)
|
||||
defer ts.srv.Close()
|
||||
s := ts.storage()
|
||||
s.token = "old"
|
||||
s.tokenLifetime = time.Hour
|
||||
s.tokenExpiry = time.Now().Add(time.Hour)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
s.invalidateToken("old")
|
||||
if _, err := s.accessToken(context.Background()); err != nil {
|
||||
t.Errorf("accessToken: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if n := ts.requests(); n != 1 {
|
||||
t.Fatalf("many 401 handlers caused %d refetches, want 1", n)
|
||||
}
|
||||
s.tokenMu.Lock()
|
||||
tok := s.token
|
||||
s.tokenMu.Unlock()
|
||||
if tok != "new-token" {
|
||||
t.Fatalf("token=%q, want new-token", tok)
|
||||
}
|
||||
// Invalidating with a stale used token must not clear the fresh one.
|
||||
s.invalidateToken("old")
|
||||
s.tokenMu.Lock()
|
||||
tok = s.token
|
||||
s.tokenMu.Unlock()
|
||||
if tok != "new-token" {
|
||||
t.Fatalf("stale invalidation cleared the fresh token: %q", tok)
|
||||
}
|
||||
}
|
||||
@@ -67,23 +67,3 @@ func paramsFromConfig(c *Config) params {
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// paramsForStorage applies the Drive slot-path defaults only where the storage
|
||||
// is a slotStore and only to fields the config leaves at 0; wal-based backends
|
||||
// keep the defaults above because their concurrency and flush mean other things
|
||||
func paramsForStorage(c *Config, storage Storage) params {
|
||||
if _, ok := storage.(slotStore); ok {
|
||||
cc := *c
|
||||
if cc.Concurrency == 0 {
|
||||
cc.Concurrency = 64
|
||||
}
|
||||
if cc.FlushIntervalMs == 0 {
|
||||
cc.FlushIntervalMs = 10
|
||||
}
|
||||
if cc.MaxPollIntervalMs == 0 {
|
||||
cc.MaxPollIntervalMs = 200
|
||||
}
|
||||
c = &cc
|
||||
}
|
||||
return paramsFromConfig(c)
|
||||
}
|
||||
|
||||
@@ -36,9 +36,6 @@ const (
|
||||
slotMedia = 2
|
||||
slotEnd = 3
|
||||
slotErr = 4
|
||||
|
||||
// maxSlotIDLen bounds any slot or watermark id accepted from a peer.
|
||||
maxSlotIDLen = 128
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -74,12 +71,6 @@ type slotStore interface {
|
||||
readBody(ctx context.Context, id string) ([]byte, error)
|
||||
}
|
||||
|
||||
// throttleStore is the optional part of a slotStore that reports how often
|
||||
// Drive throttled or timed out its requests; only driveStorage implements it
|
||||
type throttleStore interface {
|
||||
throttled() int64
|
||||
}
|
||||
|
||||
// slotFrame is what the description of a slot says: which segment it holds,
|
||||
// and either the segment itself or the size and checksum of the body that does
|
||||
type slotFrame struct {
|
||||
@@ -144,9 +135,8 @@ type slotTable struct {
|
||||
// ring is what a client announces a session with, the tables are named from
|
||||
// its side
|
||||
type ring struct {
|
||||
Up slotTable `json:"u"`
|
||||
Down slotTable `json:"d"`
|
||||
First []byte `json:"first,omitempty"`
|
||||
Up slotTable `json:"u"`
|
||||
Down slotTable `json:"d"`
|
||||
}
|
||||
|
||||
func decodeRing(b []byte) (ring, bool) {
|
||||
@@ -155,19 +145,6 @@ func decodeRing(b []byte) (ring, bool) {
|
||||
r.Up.Watermark == "" || r.Down.Watermark == "" {
|
||||
return ring{}, false
|
||||
}
|
||||
if len(r.Up.Slots) > maxConcurrency || len(r.Down.Slots) > maxConcurrency {
|
||||
return ring{}, false
|
||||
}
|
||||
for _, t := range []slotTable{r.Up, r.Down} {
|
||||
if len(t.Watermark) > maxSlotIDLen {
|
||||
return ring{}, false
|
||||
}
|
||||
for _, id := range t.Slots {
|
||||
if len(id) == 0 || len(id) > maxSlotIDLen {
|
||||
return ring{}, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return r, true
|
||||
}
|
||||
|
||||
@@ -232,7 +209,6 @@ type slotWriter struct {
|
||||
held int
|
||||
closed bool
|
||||
err error
|
||||
first chan []byte
|
||||
|
||||
// ackMu is held while the watermark is read, which other uploads wait for
|
||||
ackMu sync.Mutex
|
||||
@@ -355,12 +331,6 @@ func (w *slotWriter) flushLocked() error {
|
||||
w.buf = append(w.buf[:0], w.buf[n:]...)
|
||||
seq := w.seq
|
||||
w.seq++
|
||||
if seq == 0 && w.first != nil {
|
||||
select {
|
||||
case w.first <- chunk:
|
||||
default:
|
||||
}
|
||||
}
|
||||
w.lastSize = len(w.buf)
|
||||
w.held = 0
|
||||
|
||||
@@ -524,51 +494,32 @@ type slotReader struct {
|
||||
// hitAt is when a segment last came and gap the time between them, activeAt
|
||||
// when a segment came, was seen coming or Wake. Nothing for the eager window
|
||||
// is idle, and active is closed when that ends
|
||||
hitAt time.Time
|
||||
gap time.Duration
|
||||
activeAt time.Time
|
||||
active chan struct{}
|
||||
frontier int64
|
||||
err error
|
||||
stop context.CancelFunc
|
||||
reads atomic.Int64
|
||||
tailMin time.Duration
|
||||
tailMax time.Duration
|
||||
stallTimeout time.Duration
|
||||
bodyRate float64
|
||||
hitAt time.Time
|
||||
gap time.Duration
|
||||
activeAt time.Time
|
||||
active chan struct{}
|
||||
frontier int64
|
||||
err error
|
||||
reads atomic.Int64
|
||||
tailMin time.Duration
|
||||
tailMax time.Duration
|
||||
}
|
||||
|
||||
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params, first []byte) *slotReader {
|
||||
head := int64(0)
|
||||
if len(first) > 0 {
|
||||
head = 1
|
||||
}
|
||||
runCtx, stop := context.WithCancel(ctx)
|
||||
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params) *slotReader {
|
||||
r := &slotReader{
|
||||
ctx: runCtx,
|
||||
stop: stop,
|
||||
store: store,
|
||||
table: table,
|
||||
params: p,
|
||||
head: head,
|
||||
started: len(first) > 0,
|
||||
ch: make(chan []byte, max(p.concurrency, 1)),
|
||||
wake: make(chan struct{}, 1),
|
||||
acks: make(chan struct{}, 1),
|
||||
grow: make(chan struct{}, 1),
|
||||
hitAt: time.Now(),
|
||||
activeAt: time.Now(),
|
||||
active: make(chan struct{}),
|
||||
tailMin: slotTailMin,
|
||||
tailMax: slotTailMax,
|
||||
stallTimeout: slotStallTimeout,
|
||||
}
|
||||
if len(first) > 0 {
|
||||
r.ch <- first
|
||||
select {
|
||||
case r.acks <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
ctx: ctx,
|
||||
store: store,
|
||||
table: table,
|
||||
params: p,
|
||||
ch: make(chan []byte, p.concurrency),
|
||||
wake: make(chan struct{}, 1),
|
||||
acks: make(chan struct{}, 1),
|
||||
grow: make(chan struct{}, 1),
|
||||
hitAt: time.Now(),
|
||||
activeAt: time.Now(),
|
||||
active: make(chan struct{}),
|
||||
tailMin: slotTailMin,
|
||||
tailMax: slotTailMax,
|
||||
}
|
||||
go r.run()
|
||||
go r.ackLoop()
|
||||
@@ -724,7 +675,7 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
||||
|
||||
id := r.slotID(seq)
|
||||
backoff := slotHuntFloor
|
||||
var goneSince, warnedAt, visibleSince time.Time
|
||||
var goneSince, warnedAt time.Time
|
||||
for {
|
||||
var description string
|
||||
var err error
|
||||
@@ -738,10 +689,6 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
||||
description, err = r.store.readDescription(ctx, id)
|
||||
}
|
||||
frame, ok := decodeSlot(description)
|
||||
if err == errNotFound || (err == nil && description == "") {
|
||||
// An absent or empty slot is idle, not a failure.
|
||||
visibleSince = time.Time{}
|
||||
}
|
||||
switch {
|
||||
case err == nil && ok && frame.seq == seq:
|
||||
// what follows it is asked for while its body comes
|
||||
@@ -761,37 +708,11 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
||||
r.hit()
|
||||
return
|
||||
case slotMedia:
|
||||
body, bodyErr := r.readBodyHedged(ctx, id, frame.size)
|
||||
if bodyErr == nil && frame.holds(body) {
|
||||
if body, err := r.readBodyHedged(ctx, id); err == nil && frame.holds(body) {
|
||||
f.body = body
|
||||
r.hit()
|
||||
return
|
||||
}
|
||||
if ctx.Err() == nil && time.Since(warnedAt) > 10*time.Second {
|
||||
warnedAt = time.Now()
|
||||
if bodyErr != nil {
|
||||
errors.LogWarningInner(ctx, bodyErr, "failed to read body of slot ", id)
|
||||
} else {
|
||||
errors.LogWarning(ctx, "body of slot ", id, " does not match segment ", seq)
|
||||
}
|
||||
}
|
||||
if visibleSince.IsZero() {
|
||||
visibleSince = time.Now()
|
||||
}
|
||||
if time.Since(visibleSince) > r.stallTimeout {
|
||||
f.err = errors.New("segment ", seq, " has no usable body after ", r.stallTimeout).Base(bodyErr)
|
||||
return
|
||||
}
|
||||
}
|
||||
case err == nil && !ok && description != "":
|
||||
// A present but undecodable description for this slot is not idle;
|
||||
// fail once it has persisted beyond the stall timeout.
|
||||
if visibleSince.IsZero() {
|
||||
visibleSince = time.Now()
|
||||
}
|
||||
if time.Since(visibleSince) > r.stallTimeout {
|
||||
f.err = errors.New("slot ", id, " of segment ", seq, " holds an invalid frame")
|
||||
return
|
||||
}
|
||||
case err == errNotFound:
|
||||
if goneSince.IsZero() {
|
||||
@@ -801,10 +722,6 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
||||
return
|
||||
}
|
||||
case err != nil:
|
||||
if isAuthError(err) {
|
||||
f.err = err
|
||||
return
|
||||
}
|
||||
goneSince = time.Time{}
|
||||
if ctx.Err() == nil && time.Since(warnedAt) > 10*time.Second {
|
||||
warnedAt = time.Now()
|
||||
@@ -898,22 +815,12 @@ func (r *slotReader) readHead(ctx context.Context, id string, seq int64) (string
|
||||
}
|
||||
}
|
||||
|
||||
func (r *slotReader) readBodyHedged(ctx context.Context, id string, size int) ([]byte, error) {
|
||||
func (r *slotReader) readBodyHedged(ctx context.Context, id string) ([]byte, error) {
|
||||
if slotBodyHedge <= 0 {
|
||||
return r.store.readBody(ctx, id)
|
||||
}
|
||||
delay := slotBodyHedge
|
||||
r.mu.Lock()
|
||||
rate := r.bodyRate
|
||||
r.mu.Unlock()
|
||||
if rate > 0 && size > 0 {
|
||||
if scaled := time.Duration(float64(2*size) / rate * float64(time.Second)); scaled > delay {
|
||||
delay = scaled
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
|
||||
type result struct {
|
||||
body []byte
|
||||
@@ -928,7 +835,7 @@ func (r *slotReader) readBodyHedged(ctx context.Context, id string, size int) ([
|
||||
}
|
||||
launch()
|
||||
outstanding := 1
|
||||
timer := time.NewTimer(delay)
|
||||
timer := time.NewTimer(slotBodyHedge)
|
||||
defer timer.Stop()
|
||||
var last result
|
||||
for {
|
||||
@@ -936,16 +843,6 @@ func (r *slotReader) readBodyHedged(ctx context.Context, id string, size int) ([
|
||||
case res := <-results:
|
||||
outstanding--
|
||||
if res.err == nil {
|
||||
if elapsed := time.Since(start); elapsed > 0 && len(res.body) > 0 {
|
||||
instant := float64(len(res.body)) / elapsed.Seconds()
|
||||
r.mu.Lock()
|
||||
if r.bodyRate == 0 {
|
||||
r.bodyRate = instant
|
||||
} else {
|
||||
r.bodyRate = (r.bodyRate + instant) / 2
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
return res.body, nil
|
||||
}
|
||||
last = res
|
||||
@@ -1043,15 +940,10 @@ func (r *slotReader) ackLoop() {
|
||||
|
||||
func (r *slotReader) setErr(err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.err == nil {
|
||||
r.err = err
|
||||
}
|
||||
r.mu.Unlock()
|
||||
// A stored error means the connection is dead: stop the run loop so the
|
||||
// chunk channel closes and Read returns the error instead of blocking.
|
||||
if r.stop != nil {
|
||||
r.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *slotReader) Err() error {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// badBodySlots serves a media frame whose body never matches its checksum.
|
||||
type badBodySlots struct{}
|
||||
|
||||
func (badBodySlots) makeSlot(ctx context.Context, name string) (string, error) { return name, nil }
|
||||
func (badBodySlots) setSlot(ctx context.Context, id, description string, body []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (badBodySlots) readDescription(ctx context.Context, id string) (string, error) {
|
||||
return newSlotFrame(0, slotMedia, make([]byte, 64)).encode(), nil
|
||||
}
|
||||
|
||||
func (badBodySlots) readBody(ctx context.Context, id string) ([]byte, error) {
|
||||
return bytes.Repeat([]byte{1}, 64), nil
|
||||
}
|
||||
|
||||
// emptySlots serves an empty description: an idle slot, not a failure.
|
||||
type emptySlots struct{}
|
||||
|
||||
func (emptySlots) makeSlot(ctx context.Context, name string) (string, error) { return name, nil }
|
||||
func (emptySlots) setSlot(ctx context.Context, id, description string, body []byte) error {
|
||||
return nil
|
||||
}
|
||||
func (emptySlots) readDescription(ctx context.Context, id string) (string, error) { return "", nil }
|
||||
func (emptySlots) readBody(ctx context.Context, id string) ([]byte, error) { return nil, nil }
|
||||
|
||||
func shrinkStallClocks(t *testing.T) {
|
||||
t.Helper()
|
||||
// Only the stall timeout is shrunk; readers snapshot it at construction,
|
||||
// so lingering goroutines from other tests never read a mutated global.
|
||||
oldStall := slotStallTimeout
|
||||
slotStallTimeout = 100 * time.Millisecond
|
||||
t.Cleanup(func() { slotStallTimeout = oldStall })
|
||||
}
|
||||
|
||||
func TestHeadBodyChecksumMismatchFails(t *testing.T) {
|
||||
shrinkStallClocks(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
r := newSlotReader(ctx, badBodySlots{}, slotTable{Slots: []string{"id"}, Watermark: "w"},
|
||||
params{concurrency: 1, eagerWindow: 50 * time.Millisecond, maxPollInterval: 5 * time.Millisecond}, nil)
|
||||
select {
|
||||
case _, ok := <-r.chunks():
|
||||
if ok {
|
||||
t.Fatal("a mismatched body was delivered")
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("a persistent checksum mismatch retried forever")
|
||||
}
|
||||
if err := r.Err(); err == nil || !strings.Contains(err.Error(), "no usable body") {
|
||||
t.Fatalf("reader ended with %v, want a clear body error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptySlotStaysIdle(t *testing.T) {
|
||||
shrinkStallClocks(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
r := newSlotReader(ctx, emptySlots{}, slotTable{Slots: []string{"id"}, Watermark: "w"},
|
||||
params{concurrency: 1, eagerWindow: 50 * time.Millisecond, maxPollInterval: 5 * time.Millisecond}, nil)
|
||||
select {
|
||||
case <-r.chunks():
|
||||
t.Fatal("an empty slot failed the reader instead of staying idle")
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"math/rand"
|
||||
@@ -88,8 +87,8 @@ func slotPair(t *testing.T, store slotStore, concurrency, segmentBytes uint32) (
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := newSlotConn(context.Background(), store, r.Up, r.Down, p, nil, nil)
|
||||
server := newSlotConn(context.Background(), store, r.Down, r.Up, p, nil, nil)
|
||||
client := newSlotConn(context.Background(), store, r.Up, r.Down, p, nil)
|
||||
server := newSlotConn(context.Background(), store, r.Down, r.Up, p, nil)
|
||||
t.Cleanup(func() {
|
||||
client.Close()
|
||||
server.Close()
|
||||
@@ -366,84 +365,6 @@ func TestSlotBodyHedge(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type timedBodySlots struct {
|
||||
*memSlots
|
||||
calls atomic.Int32
|
||||
delay time.Duration
|
||||
stuck atomic.Bool
|
||||
}
|
||||
|
||||
func (s *timedBodySlots) readBody(ctx context.Context, id string) ([]byte, error) {
|
||||
n := s.calls.Add(1)
|
||||
if s.stuck.Load() && n == 1 {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(s.delay):
|
||||
}
|
||||
return make([]byte, 1000), nil
|
||||
}
|
||||
|
||||
func TestSlotBodyHedgeRate(t *testing.T) {
|
||||
old := slotBodyHedge
|
||||
slotBodyHedge = 20 * time.Millisecond
|
||||
t.Cleanup(func() { slotBodyHedge = old })
|
||||
|
||||
store := &timedBodySlots{memSlots: newMemSlots(), delay: 60 * time.Millisecond}
|
||||
r := &slotReader{store: store}
|
||||
ctx := context.Background()
|
||||
if _, err := r.readBodyHedged(ctx, "id", 1000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store.calls.Store(0)
|
||||
start := time.Now()
|
||||
if _, err := r.readBodyHedged(ctx, "id", 1000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed < 60*time.Millisecond {
|
||||
t.Fatalf("the progressing read returned in %v, before its body was ready", elapsed)
|
||||
}
|
||||
if n := store.calls.Load(); n != 1 {
|
||||
t.Fatalf("a progressing read after a learned slow rate hedged, %d reads", n)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
rate := r.bodyRate
|
||||
r.mu.Unlock()
|
||||
if rate <= 0 {
|
||||
t.Fatal("no body read rate was learned")
|
||||
}
|
||||
expected := max(slotBodyHedge, time.Duration(float64(2*1000)/rate*float64(time.Second)))
|
||||
store.stuck.Store(true)
|
||||
store.calls.Store(0)
|
||||
start = time.Now()
|
||||
if _, err := r.readBodyHedged(ctx, "id", 1000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
if n := store.calls.Load(); n < 2 {
|
||||
t.Fatalf("a stuck read after a learned rate was not hedged, %d reads", n)
|
||||
}
|
||||
if elapsed < expected/2 {
|
||||
t.Fatalf("the stuck read hedged after %v, want about %v", elapsed, expected)
|
||||
}
|
||||
|
||||
freshStore := &timedBodySlots{memSlots: newMemSlots()}
|
||||
freshStore.stuck.Store(true)
|
||||
fresh := &slotReader{store: freshStore}
|
||||
store.calls.Store(0)
|
||||
start = time.Now()
|
||||
if _, err := fresh.readBodyHedged(ctx, "id", 1000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed < slotBodyHedge {
|
||||
t.Fatalf("with no learned rate the hedge came after %v, want at least %v", elapsed, slotBodyHedge)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTakeSpareSkipsStale(t *testing.T) {
|
||||
cfg := &Config{}
|
||||
p := params{sessionTTL: 5 * time.Minute}
|
||||
@@ -639,7 +560,7 @@ func TestSlotReaderReachesPastWindow(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
reader := newSlotReader(ctx, store, ring.Down, p, nil)
|
||||
reader := newSlotReader(ctx, store, ring.Down, p)
|
||||
next := func() []byte {
|
||||
select {
|
||||
case chunk := <-reader.chunks():
|
||||
@@ -754,173 +675,3 @@ func TestSlotBurstInlineWaves(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type holdSlots struct {
|
||||
*memSlots
|
||||
target string
|
||||
hold chan struct{}
|
||||
}
|
||||
|
||||
func (s *holdSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
|
||||
if id == s.target {
|
||||
select {
|
||||
case <-s.hold:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
return s.memSlots.setSlot(ctx, id, description, body)
|
||||
}
|
||||
|
||||
func TestSlotFirstAnnouncement(t *testing.T) {
|
||||
p := params{
|
||||
concurrency: 2,
|
||||
segmentBytes: 4096,
|
||||
flushInterval: time.Millisecond,
|
||||
minPollInterval: time.Millisecond,
|
||||
maxPollInterval: 5 * time.Millisecond,
|
||||
sessionTTL: time.Minute,
|
||||
}
|
||||
store := &holdSlots{memSlots: newMemSlots(), hold: make(chan struct{})}
|
||||
ctx := context.Background()
|
||||
r, err := makeRing(ctx, store, "first", p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store.target = r.Up.Slots[0]
|
||||
client := newSlotConn(ctx, store, r.Up, r.Down, p, nil, nil)
|
||||
defer client.Close()
|
||||
writer := client.writer.(*slotWriter)
|
||||
writer.first = make(chan []byte, 1)
|
||||
|
||||
payload := randomPayload(10000)
|
||||
errs := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := client.Write(payload)
|
||||
if err == nil {
|
||||
err = client.Close()
|
||||
}
|
||||
errs <- err
|
||||
}()
|
||||
var first []byte
|
||||
select {
|
||||
case first = <-writer.first:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the writer never flushed seq 0")
|
||||
}
|
||||
announced := r
|
||||
announced.First = first
|
||||
encoded, err := json.Marshal(announced)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(encoded) > driveInlineLimit {
|
||||
t.Fatalf("the test announcement is %d bytes, want the inline path", len(encoded))
|
||||
}
|
||||
decoded, ok := decodeRing(encoded)
|
||||
if !ok {
|
||||
t.Fatal("the announcement with First did not decode")
|
||||
}
|
||||
server := newSlotConn(ctx, store, decoded.Down, decoded.Up, p, decoded.First, nil)
|
||||
defer server.Close()
|
||||
if err := server.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotFirst := make([]byte, len(first))
|
||||
if _, err := io.ReadFull(server, gotFirst); err != nil {
|
||||
t.Fatalf("the first bytes did not arrive before the ring write: %v", err)
|
||||
}
|
||||
if !bytes.Equal(gotFirst, first) {
|
||||
t.Fatal("the primed first bytes changed")
|
||||
}
|
||||
close(store.hold)
|
||||
rest := make([]byte, len(payload)-len(first))
|
||||
if _, err := io.ReadFull(server, rest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(append(gotFirst, rest...), payload) {
|
||||
t.Fatal("the stream was duplicated or changed")
|
||||
}
|
||||
if err := <-errs; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlotFirstAnnouncementTooLarge(t *testing.T) {
|
||||
p := params{
|
||||
concurrency: 2,
|
||||
segmentBytes: 256 << 10,
|
||||
flushInterval: time.Millisecond,
|
||||
sessionTTL: time.Minute,
|
||||
}
|
||||
store := newMemSlots()
|
||||
ctx := context.Background()
|
||||
r, err := makeRing(ctx, store, "large-first", p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := newSlotConn(ctx, store, r.Up, r.Down, p, nil, nil)
|
||||
defer client.Close()
|
||||
writer := client.writer.(*slotWriter)
|
||||
writer.first = make(chan []byte, 1)
|
||||
go client.Write(randomPayload(300 << 10))
|
||||
var first []byte
|
||||
select {
|
||||
case first = <-writer.first:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the writer never flushed seq 0")
|
||||
}
|
||||
if len(first) <= slotInlineLimit {
|
||||
t.Fatalf("the first segment is %d bytes, want a media segment", len(first))
|
||||
}
|
||||
withFirst := r
|
||||
withFirst.First = first
|
||||
encoded, err := json.Marshal(withFirst)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(encoded) <= driveInlineLimit {
|
||||
t.Fatalf("a %d byte first segment fit the inline announcement", len(first))
|
||||
}
|
||||
plain, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decoded, ok := decodeRing(plain)
|
||||
if !ok || decoded.First != nil {
|
||||
t.Fatal("the announcement without First did not decode cleanly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlotRingFirstCompatibility(t *testing.T) {
|
||||
p := params{concurrency: 2, sessionTTL: time.Minute}
|
||||
store := newMemSlots()
|
||||
r, err := makeRing(context.Background(), store, "compat", p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.First = []byte("hello")
|
||||
encoded, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var oldServer struct {
|
||||
Up slotTable `json:"u"`
|
||||
Down slotTable `json:"d"`
|
||||
}
|
||||
if err := json.Unmarshal(encoded, &oldServer); err != nil {
|
||||
t.Fatalf("an old server could not ignore First: %v", err)
|
||||
}
|
||||
if len(oldServer.Up.Slots) != len(r.Up.Slots) || len(oldServer.Down.Slots) != len(r.Down.Slots) {
|
||||
t.Fatal("an old server lost the ring tables")
|
||||
}
|
||||
r.First = nil
|
||||
plain, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
decoded, ok := decodeRing(plain)
|
||||
if !ok || decoded.First != nil {
|
||||
t.Fatal("an old client announcement did not decode")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/xtls/xray-core/common/errors"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
@@ -27,18 +26,11 @@ type Storage interface {
|
||||
Close() error
|
||||
}
|
||||
|
||||
// testStorageFactory lets tests substitute a backend for newStorage; nil in
|
||||
// production so behaviour is unchanged.
|
||||
var testStorageFactory atomic.Pointer[func(config *Config) (Storage, error)]
|
||||
|
||||
func newStorage(streamSettings *internet.MemoryStreamConfig) (Storage, error) {
|
||||
config, err := streamConfig(streamSettings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f := testStorageFactory.Load(); f != nil {
|
||||
return (*f)(config)
|
||||
}
|
||||
|
||||
switch config.Service {
|
||||
case "local":
|
||||
|
||||
@@ -224,51 +224,30 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
||||
}
|
||||
}
|
||||
|
||||
p = paramsForStorage(config, storage)
|
||||
|
||||
var announce []byte
|
||||
slots, ok := storage.(slotStore)
|
||||
if ok && !warm {
|
||||
if r, err = makeRing(ctx, slots, session, p); err != nil {
|
||||
if ok {
|
||||
if warm {
|
||||
announce, _ = json.Marshal(r)
|
||||
} else if r, err = makeRing(ctx, slots, session, p); err == nil {
|
||||
announce, _ = json.Marshal(r)
|
||||
} else {
|
||||
ok = false
|
||||
errors.LogWarningInner(ctx, err, "failed to make the slots of session ", session, ", it goes in segments")
|
||||
}
|
||||
}
|
||||
|
||||
if err := storage.Put(ctx, announceName(session, time.Now()), announce); err != nil {
|
||||
storage.Close()
|
||||
return nil, errors.New("failed to announce session ", session).Base(err)
|
||||
}
|
||||
|
||||
if ok {
|
||||
warmSpares(streamSettings, config, p)
|
||||
errors.LogInfo(ctx, "opened session ", session, " in ", len(r.Up.Slots), " slots")
|
||||
conn := newSlotConn(context.Background(), slots, r.Up, r.Down, p, nil, func() {
|
||||
return newSlotConn(context.Background(), slots, r.Up, r.Down, p, func() {
|
||||
storage.Close()
|
||||
})
|
||||
writer := conn.writer.(*slotWriter)
|
||||
writer.first = make(chan []byte, 1)
|
||||
go func() {
|
||||
var first []byte
|
||||
select {
|
||||
case first = <-writer.first:
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
}
|
||||
announce, _ := json.Marshal(r)
|
||||
if len(first) > 0 && len(first) <= slotInlineLimit {
|
||||
withFirst := r
|
||||
withFirst.First = first
|
||||
if encoded, err := json.Marshal(withFirst); err == nil && len(encoded) <= driveInlineLimit {
|
||||
announce = encoded
|
||||
}
|
||||
}
|
||||
// An announce failure surfaces as a connection error on the next Write/Read.
|
||||
if err := storage.Put(context.Background(), announceName(session, time.Now()), announce); err != nil {
|
||||
err = errors.New("failed to announce session ", session).Base(err)
|
||||
writer.fail(err)
|
||||
conn.reader.(*slotReader).setErr(err)
|
||||
}
|
||||
}()
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
if err := storage.Put(ctx, announceName(session, time.Now()), nil); err != nil {
|
||||
storage.Close()
|
||||
return nil, errors.New("failed to announce session ", session).Base(err)
|
||||
}), nil
|
||||
}
|
||||
errors.LogInfo(ctx, "opened session ", session)
|
||||
return newConn(context.Background(), storage,
|
||||
@@ -284,12 +263,10 @@ type Listener struct {
|
||||
addConn internet.ConnHandler
|
||||
params
|
||||
|
||||
mu sync.Mutex
|
||||
active map[string]bool
|
||||
handled map[string]time.Time
|
||||
idleSince map[string]time.Time
|
||||
announceWarned map[string]bool
|
||||
firstSeen map[string]time.Time
|
||||
mu sync.Mutex
|
||||
active map[string]bool
|
||||
handled map[string]time.Time
|
||||
idleSince map[string]time.Time
|
||||
}
|
||||
|
||||
func Serve(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, addConn internet.ConnHandler) (internet.Listener, error) {
|
||||
@@ -305,16 +282,14 @@ func Serve(ctx context.Context, address net.Address, port net.Port, streamSettin
|
||||
|
||||
listenerCtx, cancel := context.WithCancel(context.Background())
|
||||
listener := &Listener{
|
||||
ctx: listenerCtx,
|
||||
cancel: cancel,
|
||||
storage: storage,
|
||||
addConn: addConn,
|
||||
params: paramsForStorage(config, storage),
|
||||
active: make(map[string]bool),
|
||||
handled: make(map[string]time.Time),
|
||||
idleSince: make(map[string]time.Time),
|
||||
announceWarned: make(map[string]bool),
|
||||
firstSeen: make(map[string]time.Time),
|
||||
ctx: listenerCtx,
|
||||
cancel: cancel,
|
||||
storage: storage,
|
||||
addConn: addConn,
|
||||
params: paramsFromConfig(config),
|
||||
active: make(map[string]bool),
|
||||
handled: make(map[string]time.Time),
|
||||
idleSince: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
go listener.acceptLoop(ctx)
|
||||
@@ -328,7 +303,7 @@ func (l *Listener) acceptLoop(logCtx context.Context) {
|
||||
active := time.Now()
|
||||
for {
|
||||
accepted, err := l.acceptPending(logCtx)
|
||||
if err != nil && !isAuthError(err) {
|
||||
if err != nil {
|
||||
errors.LogWarningInner(logCtx, err, "failed to list sessions")
|
||||
}
|
||||
|
||||
@@ -360,72 +335,27 @@ func (l *Listener) acceptPending(logCtx context.Context) (bool, error) {
|
||||
}
|
||||
|
||||
accepted := false
|
||||
l.mu.Lock()
|
||||
if l.firstSeen == nil {
|
||||
l.firstSeen = make(map[string]time.Time)
|
||||
}
|
||||
now := time.Now()
|
||||
listedEntries := make(map[string]bool, len(sessions))
|
||||
for _, listed := range sessions {
|
||||
listedEntries[listed.Name] = true
|
||||
if _, seen := l.firstSeen[listed.Name]; !seen {
|
||||
l.firstSeen[listed.Name] = now
|
||||
}
|
||||
}
|
||||
for entry := range l.firstSeen {
|
||||
if !listedEntries[entry] {
|
||||
delete(l.firstSeen, entry)
|
||||
}
|
||||
}
|
||||
l.mu.Unlock()
|
||||
|
||||
for _, listed := range sessions {
|
||||
entry := listed.Name
|
||||
full := sessionsDir + "/" + entry
|
||||
|
||||
session, _, ok := parseAnnounce(entry)
|
||||
session, at, ok := parseAnnounce(entry)
|
||||
if !ok {
|
||||
go l.drop(full)
|
||||
continue
|
||||
}
|
||||
l.mu.Lock()
|
||||
age := now.Sub(l.firstSeen[entry])
|
||||
l.mu.Unlock()
|
||||
if age > l.sessionTTL {
|
||||
if time.Since(at) > l.sessionTTL {
|
||||
errors.LogInfo(logCtx, "dropping the stale announcement of session ", session)
|
||||
go l.drop(full)
|
||||
l.mu.Lock()
|
||||
protected := l.active[session] || !l.handled[session].IsZero()
|
||||
l.mu.Unlock()
|
||||
if !protected {
|
||||
go l.drop(sessionPrefix(session))
|
||||
}
|
||||
go l.drop(sessionPrefix(session))
|
||||
continue
|
||||
}
|
||||
if !l.claim(session) {
|
||||
continue
|
||||
}
|
||||
announce := listed.Inline
|
||||
if len(announce) == 0 {
|
||||
if _, ok := sessionStorage(l.storage).(slotStore); ok {
|
||||
announce, err = l.storage.Get(context.WithValue(l.ctx, maxBodyKey{}, int64(driveMaxAnnounce)), full)
|
||||
if err != nil {
|
||||
l.mu.Lock()
|
||||
delete(l.active, session)
|
||||
delete(l.handled, session)
|
||||
warn := !l.announceWarned[session]
|
||||
l.announceWarned[session] = true
|
||||
l.mu.Unlock()
|
||||
if warn {
|
||||
errors.LogWarningInner(logCtx, err, "failed to read the announcement of session ", session)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
go l.drop(full)
|
||||
accepted = true
|
||||
l.addConn(l.newSessionConn(logCtx, session, announce))
|
||||
l.addConn(l.newSessionConn(logCtx, session, listed.Inline))
|
||||
}
|
||||
return accepted, nil
|
||||
}
|
||||
@@ -458,7 +388,7 @@ func (l *Listener) newSessionConn(logCtx context.Context, session string, announ
|
||||
if slots, ok := storage.(slotStore); ok {
|
||||
if r, ok := decodeRing(announce); ok {
|
||||
errors.LogInfo(logCtx, "accepted session ", session, " in ", len(r.Down.Slots), " slots")
|
||||
return newSlotConn(l.ctx, slots, r.Down, r.Up, l.params, r.First, onClose)
|
||||
return newSlotConn(l.ctx, slots, r.Down, r.Up, l.params, onClose)
|
||||
}
|
||||
}
|
||||
errors.LogInfo(logCtx, "accepted session ", session)
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/common/net"
|
||||
"github.com/xtls/xray-core/transport/internet"
|
||||
)
|
||||
|
||||
// failPutStore is a slotStore whose Put always fails, to exercise the announce
|
||||
// failure path without any network.
|
||||
type failPutStore struct {
|
||||
*memSlots
|
||||
}
|
||||
|
||||
func (s *failPutStore) Put(ctx context.Context, name string, data []byte) error {
|
||||
return errors.New("put refused")
|
||||
}
|
||||
|
||||
func (s *failPutStore) Close() error { return nil }
|
||||
|
||||
func (s *failPutStore) Get(ctx context.Context, name string) ([]byte, error) {
|
||||
return nil, errNotFound
|
||||
}
|
||||
|
||||
func (s *failPutStore) List(ctx context.Context, prefix string) ([]Entry, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *failPutStore) Delete(ctx context.Context, name string) error { return nil }
|
||||
|
||||
func TestDialAnnounceFailureFailsRead(t *testing.T) {
|
||||
store := &failPutStore{memSlots: newMemSlots()}
|
||||
factory := func(*Config) (Storage, error) { return store, nil }
|
||||
testStorageFactory.Store(&factory)
|
||||
t.Cleanup(func() { testStorageFactory.Store(nil) })
|
||||
|
||||
settings := &internet.MemoryStreamConfig{
|
||||
ProtocolName: protocolName,
|
||||
ProtocolSettings: &Config{
|
||||
Service: "sim",
|
||||
RemoteFolder: "x",
|
||||
Secrets: []string{"a", "b", "c"},
|
||||
Concurrency: 4,
|
||||
FlushIntervalMs: 1,
|
||||
PollIntervalMs: 1,
|
||||
MaxPollIntervalMs: 5,
|
||||
SessionTtlSeconds: 60,
|
||||
},
|
||||
}
|
||||
conn, err := Dial(context.Background(), net.Destination{}, settings)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
go conn.Write([]byte("hello"))
|
||||
|
||||
type result struct {
|
||||
n int
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func() {
|
||||
n, err := conn.Read(make([]byte, 16))
|
||||
done <- result{n, err}
|
||||
}()
|
||||
select {
|
||||
case r := <-done:
|
||||
if r.err == nil || !strings.Contains(r.err.Error(), "failed to announce") {
|
||||
t.Fatalf("Read returned %v, want the announce error", r.err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("Read blocked after the announcement failed")
|
||||
}
|
||||
|
||||
// The reader loop must have exited: it no longer polls the store.
|
||||
store.mu.Lock()
|
||||
before := store.reads
|
||||
store.mu.Unlock()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
store.mu.Lock()
|
||||
after := store.reads
|
||||
store.mu.Unlock()
|
||||
if after != before {
|
||||
t.Fatalf("reader kept polling after the announce failure: %d -> %d", before, after)
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package xdrive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/stat"
|
||||
)
|
||||
|
||||
func announceEntry(name string) string {
|
||||
return strings.TrimPrefix(name, sessionsDir+"/")
|
||||
}
|
||||
|
||||
func TestAcceptPendingClientClockBehind(t *testing.T) {
|
||||
l := newTestListener(t, t.TempDir())
|
||||
var conns int
|
||||
l.addConn = func(c stat.Connection) { conns++; c.Close() }
|
||||
ctx := context.Background()
|
||||
|
||||
session := "clockbehind"
|
||||
ring := sessionPrefix(session) + "/" + uplinkDir + "/s0"
|
||||
if err := l.storage.Put(ctx, ring, []byte("x")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The client's clock is ten minutes behind the server's.
|
||||
name := announceName(session, time.Now().Add(-10*time.Minute))
|
||||
if err := l.storage.Put(ctx, name, []byte("{}")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
accepted, err := l.acceptPending(ctx)
|
||||
if err != nil || !accepted {
|
||||
t.Fatalf("a slow client clock must not drop the session: accepted=%v err=%v", accepted, err)
|
||||
}
|
||||
if conns != 1 {
|
||||
t.Fatalf("addConn called %d times, want 1", conns)
|
||||
}
|
||||
if _, err := l.storage.Get(ctx, ring); err != nil {
|
||||
t.Fatalf("the ring of an accepted session was dropped: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptPendingStaleLeftoverProtectsActiveRing(t *testing.T) {
|
||||
l := newTestListener(t, t.TempDir())
|
||||
l.params.sessionTTL = 50 * time.Millisecond
|
||||
var conns int
|
||||
l.addConn = func(c stat.Connection) { conns++; c.Close() }
|
||||
ctx := context.Background()
|
||||
|
||||
session := "active"
|
||||
l.active[session] = true
|
||||
l.handled[session] = time.Now()
|
||||
ring := sessionPrefix(session) + "/" + uplinkDir + "/s0"
|
||||
if err := l.storage.Put(ctx, ring, []byte("x")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := announceName(session, time.Now())
|
||||
if err := l.storage.Put(ctx, name, []byte("{}")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The listener first saw this leftover entry long ago on its own clock.
|
||||
l.firstSeen = map[string]time.Time{announceEntry(name): time.Now().Add(-time.Hour)}
|
||||
|
||||
accepted, err := l.acceptPending(ctx)
|
||||
if err != nil || accepted {
|
||||
t.Fatalf("a stale leftover must not be accepted: accepted=%v err=%v", accepted, err)
|
||||
}
|
||||
if conns != 0 {
|
||||
t.Fatalf("addConn called %d times, want 0", conns)
|
||||
}
|
||||
waitFor(t, "the leftover announcement file to drop", func() bool {
|
||||
_, err := l.storage.Get(ctx, name)
|
||||
return err == errNotFound
|
||||
})
|
||||
if _, err := l.storage.Get(ctx, ring); err != nil {
|
||||
t.Fatalf("the ring of an active session was dropped: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcceptPendingTrulyLeftoverDropped(t *testing.T) {
|
||||
l := newTestListener(t, t.TempDir())
|
||||
l.params.sessionTTL = 50 * time.Millisecond
|
||||
var conns int
|
||||
l.addConn = func(c stat.Connection) { conns++; c.Close() }
|
||||
ctx := context.Background()
|
||||
|
||||
session := "leftover"
|
||||
ring := sessionPrefix(session) + "/" + uplinkDir + "/s0"
|
||||
if err := l.storage.Put(ctx, ring, []byte("x")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
name := announceName(session, time.Now())
|
||||
if err := l.storage.Put(ctx, name, []byte("{}")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
l.firstSeen = map[string]time.Time{announceEntry(name): time.Now().Add(-time.Hour)}
|
||||
|
||||
accepted, err := l.acceptPending(ctx)
|
||||
if err != nil || accepted {
|
||||
t.Fatalf("accepted=%v err=%v", accepted, err)
|
||||
}
|
||||
waitFor(t, "the leftover announcement and ring to drop", func() bool {
|
||||
_, e1 := l.storage.Get(ctx, name)
|
||||
_, e2 := l.storage.Get(ctx, ring)
|
||||
return e1 == errNotFound && e2 == errNotFound
|
||||
})
|
||||
}
|
||||
|
||||
func TestAcceptPendingPrunesFirstSeen(t *testing.T) {
|
||||
l := newTestListener(t, t.TempDir())
|
||||
l.addConn = func(c stat.Connection) { c.Close() }
|
||||
ctx := context.Background()
|
||||
l.firstSeen = map[string]time.Time{"gone-entry": time.Now()}
|
||||
if _, err := l.acceptPending(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, still := l.firstSeen["gone-entry"]; still {
|
||||
t.Fatal("firstSeen kept an entry that is no longer listed")
|
||||
}
|
||||
}
|
||||
@@ -375,10 +375,6 @@ func TestStaleAnnounce(t *testing.T) {
|
||||
if err := listener.storage.Put(ctx, uplinkPrefix("ghost")+"/000000000.seg", []byte("x")); err != nil {
|
||||
t.Fatalf("Put: %v", err)
|
||||
}
|
||||
// The listener first saw this entry an hour ago on its own clock.
|
||||
listener.firstSeen = map[string]time.Time{
|
||||
strings.TrimPrefix(stale, sessionsDir+"/"): time.Now().Add(-time.Hour),
|
||||
}
|
||||
|
||||
accepted, err := listener.acceptPending(ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user