Compare commits

..
1 Commits
Author SHA1 Message Date
Fangliding c8c24a2e60 Remove dice package 2026-11-05 18:19:47 +08:00
33 changed files with 105 additions and 1261 deletions
+2 -2
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory # Download geodat into a staging directory
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
+2 -2
View File
@@ -9,8 +9,8 @@ ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
# Download geodat into a staging directory # Download geodat into a staging directory
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
+2 -2
View File
@@ -3,12 +3,12 @@ package burst
import ( import (
"context" "context"
"fmt" "fmt"
"math/rand/v2"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/features/routing" "github.com/xtls/xray-core/features/routing"
) )
@@ -181,7 +181,7 @@ func (h *HealthPing) doCheck(ctx context.Context, tags []string, duration time.D
for i := 0; i < rounds; i++ { for i := 0; i < rounds; i++ {
delay := time.Duration(0) delay := time.Duration(0)
if duration > 0 { if duration > 0 {
delay = time.Duration(dice.RollInt63n(int64(duration))) delay = rand.N(duration)
} }
timers = append(timers, time.AfterFunc(delay, func() { timers = append(timers, time.AfterFunc(delay, func() {
errors.LogDebug(h.ctx, "checking ", handler) errors.LogDebug(h.ctx, "checking ", handler)
+6 -6
View File
@@ -7,7 +7,7 @@ import (
"io" "io"
"math/big" "math/big"
"github.com/xtls/xray-core/common/dice" mrand "math/rand/v2"
"github.com/xtls/xray-core/app/proxyman" "github.com/xtls/xray-core/app/proxyman"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@@ -122,8 +122,8 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
if config.Concurrency < 0 { if config.Concurrency < 0 {
h.mux = &mux.ClientManager{Enabled: false} h.mux = &mux.ClientManager{Enabled: false}
} }
if config.Concurrency >= 0 && config.Concurrency <= 128 { if config.Concurrency == 0 {
config.Concurrency = 128 config.Concurrency = 8 // same as before
} }
if config.Concurrency > 0 { if config.Concurrency > 0 {
h.mux = &mux.ClientManager{ h.mux = &mux.ClientManager{
@@ -134,7 +134,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h, Dialer: h,
Strategy: mux.ClientStrategy{ Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.Concurrency), MaxConcurrency: uint32(config.Concurrency),
MaxConnection: 1024, MaxConnection: 128,
}, },
}, },
}, },
@@ -155,7 +155,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
Dialer: h, Dialer: h,
Strategy: mux.ClientStrategy{ Strategy: mux.ClientStrategy{
MaxConcurrency: uint32(config.XudpConcurrency), MaxConcurrency: uint32(config.XudpConcurrency),
MaxConnection: 1024, MaxConnection: 128,
}, },
}, },
}, },
@@ -196,7 +196,7 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
} }
} else { } else {
unchangedDomain := ob.Target.Address.Domain() unchangedDomain := ob.Target.Address.Domain()
ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))]) ob.Target.Address = net.IPAddress(ips[mrand.N(len(ips))])
errors.LogInfo(ctx, "target: ", unchangedDomain, " resolved to: ", ob.Target.Address.String()) errors.LogInfo(ctx, "target: ", unchangedDomain, " resolved to: ", ob.Target.Address.String())
} }
} }
+2 -3
View File
@@ -3,12 +3,11 @@ package reverse
import ( import (
"crypto/rand" "crypto/rand"
"io" "io"
mrand "math/rand/v2"
"github.com/xtls/xray-core/common/dice"
) )
func (c *Control) FillInRandom() { func (c *Control) FillInRandom() {
randomLength := dice.Roll(64) randomLength := mrand.N(64)
randomLength++ randomLength++
c.Random = make([]byte, randomLength) c.Random = make([]byte, randomLength)
io.ReadFull(rand.Reader, c.Random) io.ReadFull(rand.Reader, c.Random)
+2 -2
View File
@@ -3,13 +3,13 @@ package router
import ( import (
"context" "context"
"math" "math"
"math/rand/v2"
"slices" "slices"
"sort" "sort"
"time" "time"
"github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/app/observatory"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/extension" "github.com/xtls/xray-core/features/extension"
@@ -74,7 +74,7 @@ func (s *LeastLoadStrategy) PickOutbound(candidates []string) string {
// goes to fallbackTag // goes to fallbackTag
return "" return ""
} }
return selects[dice.Roll(count)].Tag return selects[rand.N(count)].Tag
} }
func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node { func (s *LeastLoadStrategy) pickOutbounds(candidates []string) []*node {
+2 -2
View File
@@ -2,10 +2,10 @@ package router
import ( import (
"context" "context"
"math/rand/v2"
"github.com/xtls/xray-core/app/observatory" "github.com/xtls/xray-core/app/observatory"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/core" "github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/extension" "github.com/xtls/xray-core/features/extension"
) )
@@ -63,5 +63,5 @@ func (s *RandomStrategy) PickOutbound(candidates []string) string {
// goes to fallbackTag // goes to fallbackTag
return "" return ""
} }
return candidates[dice.Roll(count)] return candidates[rand.N(count)]
} }
-55
View File
@@ -1,55 +0,0 @@
// Package dice contains common functions to generate random number.
// It also initialize math/rand with the time in seconds at launch time.
package dice // import "github.com/xtls/xray-core/common/dice"
import (
"math/rand"
)
// Roll returns a non-negative number between 0 (inclusive) and n (exclusive).
func Roll(n int) int {
if n == 1 {
return 0
}
return rand.Intn(n)
}
// RollInt63n returns a non-negative number between 0 (inclusive) and n (exclusive).
func RollInt63n(n int64) int64 {
if n == 1 {
return 0
}
return rand.Int63n(n)
}
// Roll returns a non-negative number between 0 (inclusive) and n (exclusive).
func RollDeterministic(n int, seed int64) int {
if n == 1 {
return 0
}
return rand.New(rand.NewSource(seed)).Intn(n)
}
// RollUint16 returns a random uint16 value.
func RollUint16() uint16 {
return uint16(rand.Int63() >> 47)
}
func RollUint64() uint64 {
return rand.Uint64()
}
func NewDeterministicDice(seed int64) *DeterministicDice {
return &DeterministicDice{rand.New(rand.NewSource(seed))}
}
type DeterministicDice struct {
*rand.Rand
}
func (dd *DeterministicDice) Roll(n int) int {
if n == 1 {
return 0
}
return dd.Intn(n)
}
-50
View File
@@ -1,50 +0,0 @@
package dice_test
import (
"math/rand"
"testing"
. "github.com/xtls/xray-core/common/dice"
)
func BenchmarkRoll1(b *testing.B) {
for i := 0; i < b.N; i++ {
Roll(1)
}
}
func BenchmarkRoll20(b *testing.B) {
for i := 0; i < b.N; i++ {
Roll(20)
}
}
func BenchmarkIntn1(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Intn(1)
}
}
func BenchmarkIntn20(b *testing.B) {
for i := 0; i < b.N; i++ {
rand.Intn(20)
}
}
func BenchmarkInt63(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = uint16(rand.Int63() >> 47)
}
}
func BenchmarkInt31(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = uint16(rand.Int31() >> 15)
}
}
func BenchmarkIntn(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = uint16(rand.Intn(65536))
}
}
+5 -5
View File
@@ -2,8 +2,8 @@ package drain
import ( import (
"io" "io"
"math/rand/v2"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
) )
@@ -12,10 +12,10 @@ type BehaviorSeedLimitedDrainer struct {
} }
func NewBehaviorSeedLimitedDrainer(behaviorSeed int64, drainFoundation, maxBaseDrainSize, maxRandDrain int) (Drainer, error) { func NewBehaviorSeedLimitedDrainer(behaviorSeed int64, drainFoundation, maxBaseDrainSize, maxRandDrain int) (Drainer, error) {
behaviorRand := dice.NewDeterministicDice(behaviorSeed) behaviorRand := rand.New(rand.NewPCG(uint64(behaviorSeed), 0))
BaseDrainSize := behaviorRand.Roll(maxBaseDrainSize) BaseDrainSize := behaviorRand.N(maxBaseDrainSize)
RandDrainMax := behaviorRand.Roll(maxRandDrain) + 1 RandDrainMax := behaviorRand.N(maxRandDrain) + 1
RandDrainRolled := dice.Roll(RandDrainMax) RandDrainRolled := rand.N(RandDrainMax)
DrainSize := drainFoundation + BaseDrainSize + RandDrainRolled DrainSize := drainFoundation + BaseDrainSize + RandDrainRolled
return &BehaviorSeedLimitedDrainer{DrainSize: DrainSize}, nil return &BehaviorSeedLimitedDrainer{DrainSize: DrainSize}, nil
} }
+2 -3
View File
@@ -1,9 +1,8 @@
package protocol package protocol
import ( import (
"math/rand/v2"
"time" "time"
"github.com/xtls/xray-core/common/dice"
) )
type Timestamp int64 type Timestamp int64
@@ -16,7 +15,7 @@ func NowTime() Timestamp {
func NewTimestampGenerator(base Timestamp, delta int) TimestampGenerator { func NewTimestampGenerator(base Timestamp, delta int) TimestampGenerator {
return func() Timestamp { return func() Timestamp {
rangeInDelta := dice.Roll(delta*2) - delta rangeInDelta := rand.N(delta*2) - delta
return base + Timestamp(rangeInDelta) return base + Timestamp(rangeInDelta)
} }
} }
+1 -11
View File
@@ -2,7 +2,6 @@ package conf
import ( import (
"net/url" "net/url"
"strings"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/xtls/xray-core/app/geodata" "github.com/xtls/xray-core/app/geodata"
@@ -20,20 +19,11 @@ func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err) return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
} }
return &geodata.Asset{ return &geodata.Asset{
Url: migrateGeodataURL(c.URL), Url: c.URL,
File: c.File, File: c.File,
}, nil }, nil
} }
func migrateGeodataURL(assetURL string) string {
if strings.HasSuffix(strings.ToLower(assetURL), ".dat") && (strings.HasPrefix(strings.ToLower(assetURL), "https://github.com/xtls/xray-rules-dat/") ||
strings.HasPrefix(strings.ToLower(assetURL), "https://raw.githubusercontent.com/xtls/xray-rules-dat/")) {
errors.PrintDeprecatedFeatureWarning(assetURL, assetURL+".gz")
return assetURL + ".gz"
}
return assetURL
}
func validateHTTPS(s string) error { func validateHTTPS(s string) error {
u, err := url.ParseRequestURI(s) u, err := url.ParseRequestURI(s)
if err != nil { if err != nil {
+2 -2
View File
@@ -4,12 +4,12 @@ package blackhole
import ( import (
"bytes" "bytes"
"context" "context"
"math/rand/v2"
"net/http" "net/http"
"time" "time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/session" "github.com/xtls/xray-core/common/session"
@@ -74,7 +74,7 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, func() { timer := signal.CancelAfterInactivity(ctx, func() {
cancel() cancel()
}, time.Duration(30+dice.Roll(61))*time.Second) }, time.Duration(30+rand.N(61))*time.Second)
go buf.Copy(link.Reader, buf.Discard, buf.UpdateActivity(timer)) go buf.Copy(link.Reader, buf.Discard, buf.UpdateActivity(timer))
<-ctx.Done() <-ctx.Done()
} }
+3 -3
View File
@@ -12,7 +12,6 @@ import (
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/geodata" "github.com/xtls/xray-core/common/geodata"
"github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/log"
@@ -30,6 +29,7 @@ import (
"github.com/xtls/xray-core/transport" "github.com/xtls/xray-core/transport"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat" "github.com/xtls/xray-core/transport/internet/stat"
mrand "math/rand/v2"
) )
var ( var (
@@ -228,7 +228,7 @@ func (r *FinalRule) blockDuration() time.Duration {
if max < min { if max < min {
span = min - max span = min - max
} }
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second return time.Duration(min+mrand.N(span+1)) * time.Second
} }
func (r *FinalRule) log(ctx context.Context, msg ...interface{}) { func (r *FinalRule) log(ctx context.Context, msg ...interface{}) {
@@ -634,7 +634,7 @@ func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
continue continue
} }
} else { } else {
ip = net.IPAddress(ips[dice.Roll(len(ips))]) ip = net.IPAddress(ips[mrand.N(len(ips))])
shouldUseSystemResolver = false shouldUseSystemResolver = false
} }
} }
+2 -2
View File
@@ -5,10 +5,10 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"hash/crc64" "hash/crc64"
"math/rand/v2"
"strings" "strings"
"sync" "sync"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
) )
@@ -158,7 +158,7 @@ func (v *Validator) GetBehaviorSeed() uint64 {
v.behaviorFused = true v.behaviorFused = true
if v.behaviorSeed == 0 { if v.behaviorSeed == 0 {
v.behaviorSeed = dice.RollUint64() v.behaviorSeed = rand.Uint64()
} }
return v.behaviorSeed return v.behaviorSeed
} }
+1 -1
View File
@@ -223,7 +223,7 @@ To make it start, wintun.dll specific for your Windows/arch must be present next
After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. A Wintun adapter that already has that name is reused. After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. A Wintun adapter that already has that name is reused.
With IPv6 disabled system-wide (the `DisabledComponents` registry value), the adapter has no IPv6, and the TUN starts with IPv4 only, unless `gateway`, `dns` or `autoSystemRoutingTable` has IPv6 entries, which then keep it from starting. With IPv6 disabled system-wide (the `DisabledComponents` registry value), the adapter has no IPv6, and the TUN starts with IPv4 only, unless `gateway` or `autoSystemRoutingTable` has IPv6 entries, which then keep it from starting.
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops. Xray's own lookups through the system's DNS servers, like those of the `localhost` DNS server with `autoOutboundsInterface`, skip them, unless another interface uses them as well, as they would lead back into the TUN. When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops. Xray's own lookups through the system's DNS servers, like those of the `localhost` DNS server with `autoOutboundsInterface`, skip them, unless another interface uses them as well, as they would lead back into the TUN.
+2 -3
View File
@@ -115,7 +115,6 @@ func (t *WindowsTun) Start() (err error) {
for _, ip := range t.options.DNS { for _, ip := range t.options.DNS {
dns = append(dns, netip.MustParseAddr(ip)) dns = append(dns, netip.MustParseAddr(ip))
} }
dns6 := slices.ContainsFunc(dns, netip.Addr.Is6)
var route4, route6 bool var route4, route6 bool
routesMap := make(map[winipcfg.RouteData]struct{}) routesMap := make(map[winipcfg.RouteData]struct{})
@@ -176,7 +175,7 @@ startOver:
if err != nil { if err != nil {
// With IPv6 disabled system-wide (DisabledComponents), the adapter has no // With IPv6 disabled system-wide (DisabledComponents), the adapter has no
// IPv6 interface at all. Skip the family unless the config asks for it. // IPv6 interface at all. Skip the family unless the config asks for it.
if err == windows.ERROR_NOT_FOUND && family == windows.AF_INET6 && !address6 && !route6 && !dns6 { if err == windows.ERROR_NOT_FOUND && family == windows.AF_INET6 && !address6 && !route6 {
continue continue
} }
return err return err
@@ -205,7 +204,7 @@ startOver:
// Skip IPv6 the config does not ask for also when setting its DNS // Skip IPv6 the config does not ask for also when setting its DNS
// servers fails, as reported on Windows 11 24H2 with IPv6 disabled // servers fails, as reported on Windows 11 24H2 with IPv6 disabled
// (Access is denied). // (Access is denied).
if family == windows.AF_INET6 && !address6 && !route6 && !dns6 { if family == windows.AF_INET6 && !address6 && !route6 {
continue continue
} }
firstErr = errors.New("unable to set DNS").Base(err) firstErr = errors.New("unable to set DNS").Base(err)
+2 -2
View File
@@ -13,13 +13,13 @@ import (
"github.com/xtls/xray-core/common/bitmask" "github.com/xtls/xray-core/common/bitmask"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/crypto" "github.com/xtls/xray-core/common/crypto"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/drain" "github.com/xtls/xray-core/common/drain"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/proxy/vmess" "github.com/xtls/xray-core/proxy/vmess"
vmessaead "github.com/xtls/xray-core/proxy/vmess/aead" vmessaead "github.com/xtls/xray-core/proxy/vmess/aead"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
mrand "math/rand/v2"
) )
// ClientSession stores connection session info for VMess client. // ClientSession stores connection session info for VMess client.
@@ -72,7 +72,7 @@ func (c *ClientSession) EncodeRequestHeader(header *protocol.RequestHeader, writ
common.Must(buffer.WriteByte(c.responseHeader)) common.Must(buffer.WriteByte(c.responseHeader))
common.Must(buffer.WriteByte(byte(header.Option))) common.Must(buffer.WriteByte(byte(header.Option)))
paddingLen := dice.Roll(16) paddingLen := mrand.N(16)
security := byte(paddingLen<<4) | byte(header.Security) security := byte(paddingLen<<4) | byte(header.Security)
common.Must2(buffer.Write([]byte{security, byte(0), byte(header.Command)})) common.Must2(buffer.Write([]byte{security, byte(0), byte(header.Command)}))
+2 -2
View File
@@ -4,10 +4,10 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha256" "crypto/sha256"
"hash/crc64" "hash/crc64"
"math/rand/v2"
"strings" "strings"
"sync" "sync"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/proxy/vmess/aead" "github.com/xtls/xray-core/proxy/vmess/aead"
@@ -117,7 +117,7 @@ func (v *TimedUserValidator) GetBehaviorSeed() uint64 {
v.behaviorFused = true v.behaviorFused = true
if v.behaviorSeed == 0 { if v.behaviorSeed == 0 {
v.behaviorSeed = dice.RollUint64() v.behaviorSeed = rand.Uint64()
} }
return v.behaviorSeed return v.behaviorSeed
} }
+2 -2
View File
@@ -3,6 +3,7 @@ package wireguard
import ( import (
"context" "context"
"fmt" "fmt"
"math/rand/v2"
"net/netip" "net/netip"
"reflect" "reflect"
"strings" "strings"
@@ -14,7 +15,6 @@ import (
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log" "github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
@@ -364,7 +364,7 @@ func MakeResolveLocal(d dns.Client, streamSettings *internet.MemoryStreamConfig)
return nil, dns.ErrEmptyResponse return nil, dns.ErrEmptyResponse
} }
} }
return got[dice.Roll(len(got))], nil return got[rand.N(len(got))], nil
} }
} }
+2 -2
View File
@@ -3,10 +3,10 @@ package internet
import ( import (
"context" "context"
"fmt" "fmt"
"math/rand/v2"
"strings" "strings"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/net/cnc" "github.com/xtls/xray-core/common/net/cnc"
@@ -260,7 +260,7 @@ func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig
return nil, err return nil, err
} }
} else if sockopt.HappyEyeballs == nil || sockopt.HappyEyeballs.TryDelayMs == 0 || sockopt.HappyEyeballs.MaxConcurrentTry == 0 || len(ips) < 2 || len(sockopt.DialerProxy) > 0 || dest.Network != net.Network_TCP { } else if sockopt.HappyEyeballs == nil || sockopt.HappyEyeballs.TryDelayMs == 0 || sockopt.HappyEyeballs.MaxConcurrentTry == 0 || len(ips) < 2 || len(sockopt.DialerProxy) > 0 || dest.Network != net.Network_TCP {
dest.Address = net.IPAddress(ips[dice.Roll(len(ips))]) dest.Address = net.IPAddress(ips[rand.N(len(ips))])
errors.LogInfo(ctx, "replace destination with "+dest.String()) errors.LogInfo(ctx, "replace destination with "+dest.String())
} else { } else {
return TcpRaceDial(ctx, src, ips, dest.Port, sockopt, dest.Address.String()) return TcpRaceDial(ctx, src, ips, dest.Port, sockopt, dest.Address.String())
@@ -3,8 +3,7 @@ package header
import ( import (
"encoding/binary" "encoding/binary"
"errors" "errors"
"math/rand/v2"
"github.com/xtls/xray-core/common/dice"
) )
type dns struct { type dns struct {
@@ -17,7 +16,7 @@ func (h *dns) Size() int {
func (h *dns) Serialize(b []byte) { func (h *dns) Serialize(b []byte) {
copy(b, h.header) copy(b, h.header)
binary.BigEndian.PutUint16(b[0:], dice.RollUint16()) binary.BigEndian.PutUint16(b[0:], uint16(rand.Uint32()))
} }
func NewHeaderDNS(domain string) (*dns, error) { func NewHeaderDNS(domain string) (*dns, error) {
+2 -3
View File
@@ -1,9 +1,8 @@
package http package http
import ( import (
"math/rand/v2"
"strings" "strings"
"github.com/xtls/xray-core/common/dice"
) )
func pickString(arr []string) string { func pickString(arr []string) string {
@@ -14,7 +13,7 @@ func pickString(arr []string) string {
case 1: case 1:
return arr[0] return arr[0]
default: default:
return arr[dice.Roll(n)] return arr[rand.N(n)]
} }
} }
+2 -2
View File
@@ -3,11 +3,11 @@ package kcp
import ( import (
"context" "context"
"io" "io"
"math/rand/v2"
"sync/atomic" "sync/atomic"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
@@ -15,7 +15,7 @@ import (
"github.com/xtls/xray-core/transport/internet/tls" "github.com/xtls/xray-core/transport/internet/tls"
) )
var globalConv = uint32(dice.RollUint16()) var globalConv = uint32(uint16(rand.Uint32()))
func fetchInput(_ context.Context, input io.Reader, reader PacketReader, conn *Connection) { func fetchInput(_ context.Context, input io.Reader, reader PacketReader, conn *Connection) {
cache := make(chan *buf.Buffer, 1024) cache := make(chan *buf.Buffer, 1024)
+4 -23
View File
@@ -438,11 +438,9 @@ func (b *doneBody) Close() error {
type sessionTransport struct { type sessionTransport struct {
pool *connPool pool *connPool
mu sync.Mutex mu sync.Mutex
client *splithttp.XmuxClient client *splithttp.XmuxClient
closed bool closed bool
slowAt, retiredAt time.Time
movedAt time.Time
} }
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) { func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
@@ -452,7 +450,7 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return nil, errSessionClosed return nil, errSessionClosed
} }
client := t.client client := t.client
if client == nil || client.XmuxConn.IsClosed() || client.LeftRequests.Add(-1) < 0 || !client.UnreusableAt.IsZero() && time.Now().After(client.UnreusableAt) { if client == nil || client.LeftRequests.Add(-1) < 0 || !client.UnreusableAt.IsZero() && time.Now().After(client.UnreusableAt) {
t.pool.mu.Lock() t.pool.mu.Lock()
t.client = t.pool.xmux.GetXmuxClient(r.Context()) t.client = t.pool.xmux.GetXmuxClient(r.Context())
t.pool.mu.Unlock() t.pool.mu.Unlock()
@@ -460,7 +458,6 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
t.client.LeftRequests.Add(-1) t.client.LeftRequests.Add(-1)
if client != nil { if client != nil {
client.DoneRunning() client.DoneRunning()
t.movedAt = time.Now()
} }
client = t.client client = t.client
} }
@@ -474,22 +471,6 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return conn.roundTrip(r) return conn.roundTrip(r)
} }
func (t *sessionTransport) stalled(began time.Time) {
t.mu.Lock()
defer t.mu.Unlock()
if began.Before(t.movedAt) {
return
}
now := time.Now()
slow := now.Sub(t.slowAt) < 3*time.Second
t.slowAt = now
if !slow || t.client == nil || now.Sub(t.retiredAt) < 2*time.Second {
return
}
t.retiredAt = now
t.client.XmuxConn.(*poolConn).Close()
}
func (t *sessionTransport) Close() { func (t *sessionTransport) Close() {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
-100
View File
@@ -987,103 +987,3 @@ func TestDriveXmuxOverH3(t *testing.T) {
} }
waitFor(t, "the first connection of XMUX to close", func() bool { return server.closed.Load() == 1 }) waitFor(t, "the first connection of XMUX to close", func() bool { return server.closed.Load() == 1 })
} }
type stallConn struct{}
func (stallConn) RoundTrip(r *http.Request) (*http.Response, error) {
<-r.Context().Done()
return nil, r.Context().Err()
}
type okConn struct{}
func (okConn) RoundTrip(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(`{"id":"file-1"}`)),
}, nil
}
func TestSessionRetiresStalledConn(t *testing.T) {
made := atomic.Int32{}
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
if made.Add(1) == 1 {
return &http.Client{Transport: stallConn{}}
}
return &http.Client{Transport: okConn{}}
})}
storage := &driveStorage{
driveShared: &driveShared{
token: "token",
tokenExpiry: time.Now().Add(time.Hour),
backoff: time.Millisecond,
},
client: &http.Client{Transport: transport},
inflight: make(chan struct{}, 1),
reserved: make(chan struct{}, 1),
}
ctx := context.WithValue(context.Background(), attemptKey{}, 100*time.Millisecond)
code, body, err := storage.do(ctx, http.MethodPost, "http://drive.invalid/files", "", nil)
if err != nil {
t.Fatalf("the first request over a stalled connection: %v", err)
}
if code != http.StatusOK || string(body) != `{"id":"file-1"}` {
t.Fatalf("the stalled connection gave %d %q", code, body)
}
if n := made.Load(); n != 2 {
t.Fatalf("a stalled connection took %d connections, want it retired and one made after it", n)
}
if _, _, err := storage.do(ctx, http.MethodPost, "http://drive.invalid/files", "", nil); err != nil {
t.Fatalf("the request after the retirement: %v", err)
}
if n := made.Load(); n != 2 {
t.Fatalf("%d connections were made, want the healthy one kept", n)
}
}
func TestSessionIgnoresStallOfRetiredConn(t *testing.T) {
var made atomic.Int32
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
made.Add(1)
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()
}
send()
old := time.Now()
time.Sleep(2 * time.Millisecond)
transport.stalled(old)
transport.stalled(old)
send()
if n := made.Load(); n != 2 {
t.Fatalf("a stalled connection took %d connections, want it retired and one made after it", n)
}
transport.mu.Lock()
transport.slowAt, transport.retiredAt = time.Time{}, time.Time{}
transport.mu.Unlock()
transport.stalled(old)
transport.stalled(old)
send()
if n := made.Load(); n != 2 {
t.Fatalf("the stall of a retired connection took the healthy one too, %d connections", n)
}
time.Sleep(2 * time.Millisecond)
fresh := time.Now()
transport.stalled(fresh)
transport.stalled(fresh)
send()
if n := made.Load(); n != 3 {
t.Fatalf("a stall of the current connection made %d connections, want it retired", n)
}
}
+15 -90
View File
@@ -7,13 +7,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math/rand/v2"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/transport/internet" "github.com/xtls/xray-core/transport/internet"
) )
@@ -21,12 +21,11 @@ import (
const ( const (
flatSeparator = "~" flatSeparator = "~"
driveBoundary = "xdrive-boundary" driveBoundary = "xdrive-boundary"
drivePageSize = 1000 drivePageSize = 1000
driveMaxAttempts = 8 driveMaxAttempts = 8
driveMaxInflight = 64 driveMaxInflight = 32
driveReservedInflight = 8 driveInlineLimit = 12000
driveInlineLimit = 12000
// driveTimeout ends a request that makes no progress for that long // driveTimeout ends a request that makes no progress for that long
driveTimeout = 60 * time.Second driveTimeout = 60 * time.Second
) )
@@ -37,21 +36,13 @@ var (
driveFilesURL = "https://www.googleapis.com/drive/v3/files" driveFilesURL = "https://www.googleapis.com/drive/v3/files"
driveUploadURL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name" driveUploadURL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name"
driveInitialBackoff = 200 * time.Millisecond driveInitialBackoff = 200 * time.Millisecond
driveCooldown = 2 * time.Second
driveSlotRead = 3 * time.Second
driveSlotWrite = 5 * time.Second
) )
type laneKey struct{}
type attemptKey struct{}
type driveStorage struct { type driveStorage struct {
*driveShared *driveShared
// a session has a client and inflight of its own, see session // a session has a client and inflight of its own, see session
client *http.Client client *http.Client
inflight chan struct{} inflight chan struct{}
reserved chan struct{}
release func() release func()
} }
@@ -74,9 +65,6 @@ type driveShared struct {
idMu sync.Mutex idMu sync.Mutex
ids map[string]string ids map[string]string
coolMu sync.Mutex
coolUntil time.Time
} }
func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*driveStorage, error) { func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*driveStorage, error) {
@@ -112,7 +100,6 @@ func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config
}, },
client: client, client: client,
inflight: make(chan struct{}, driveMaxInflight), inflight: make(chan struct{}, driveMaxInflight),
reserved: make(chan struct{}, driveReservedInflight),
}, nil }, nil
} }
@@ -124,7 +111,6 @@ func (s *driveStorage) session() Storage {
driveShared: s.driveShared, driveShared: s.driveShared,
client: &http.Client{Transport: transport}, client: &http.Client{Transport: transport},
inflight: make(chan struct{}, driveMaxInflight), inflight: make(chan struct{}, driveMaxInflight),
reserved: make(chan struct{}, driveReservedInflight),
release: transport.Close, release: transport.Close,
} }
} }
@@ -196,7 +182,7 @@ func jitter(backoff time.Duration) time.Duration {
if half <= 0 { if half <= 0 {
return backoff return backoff
} }
return half + time.Duration(dice.RollInt63n(int64(half))) return half + rand.N(half)
} }
func rateLimited(payload []byte) bool { func rateLimited(payload []byte) bool {
@@ -233,17 +219,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
backoff := s.backoff backoff := s.backoff
var lastErr error 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++ { for attempt := 0; attempt < driveMaxAttempts; attempt++ {
if attempt > 0 { if attempt > 0 {
select { select {
@@ -263,12 +238,8 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
continue continue
} }
pool := s.inflight
if ctx.Value(laneKey{}) != nil {
pool = s.reserved
}
select { select {
case pool <- struct{}{}: case s.inflight <- struct{}{}:
case <-ctx.Done(): case <-ctx.Done():
return 0, nil, ctx.Err() return 0, nil, ctx.Err()
} }
@@ -277,14 +248,8 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
if body != nil { if body != nil {
reader = bytes.NewReader(body) reader = bytes.NewReader(body)
} }
attemptCtx, cancel := ctx, context.CancelFunc(func() {}) req, err := http.NewRequestWithContext(ctx, method, target, reader)
if d, ok := ctx.Value(attemptKey{}).(time.Duration); ok {
attemptCtx, cancel = context.WithTimeout(ctx, d)
}
req, err := http.NewRequestWithContext(attemptCtx, method, target, reader)
if err != nil { if err != nil {
cancel()
<-pool
return 0, nil, errors.New("failed to build a Drive request").Base(err) return 0, nil, errors.New("failed to build a Drive request").Base(err)
} }
req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Authorization", "Bearer "+token)
@@ -292,17 +257,9 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
req.Header.Set("Content-Type", contentType) req.Header.Set("Content-Type", contentType)
} }
began := time.Now()
resp, err := s.client.Do(req) resp, err := s.client.Do(req)
<-pool <-s.inflight
if err != nil { if err != nil {
timedOut := attemptCtx.Err() == context.DeadlineExceeded && ctx.Err() == nil
cancel()
if timedOut {
if t, ok := s.client.Transport.(*sessionTransport); ok {
t.stalled(began)
}
}
if ctx.Err() != nil { if ctx.Err() != nil {
return 0, nil, ctx.Err() return 0, nil, ctx.Err()
} }
@@ -313,7 +270,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
} }
payload, err := io.ReadAll(resp.Body) payload, err := io.ReadAll(resp.Body)
resp.Body.Close() resp.Body.Close()
cancel()
if err != nil { if err != nil {
if ctx.Err() != nil { if ctx.Err() != nil {
return 0, nil, ctx.Err() return 0, nil, ctx.Err()
@@ -330,19 +286,13 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
if resp.StatusCode == http.StatusForbidden && rateLimited(payload) { if resp.StatusCode == http.StatusForbidden && rateLimited(payload) {
lastErr = errors.New("Drive is rate limiting: ", string(payload)) lastErr = errors.New("Drive is rate limiting: ", string(payload))
errors.LogWarning(ctx, "rate limited by Drive, attempt ", errors.LogWarning(ctx, "rate limited by Drive, attempt ",
attempt+1, " of ", driveMaxAttempts, ", ", method, " ", len(body), attempt+1, " of ", driveMaxAttempts)
" bytes, body read ", strings.Contains(target, "alt=media"),
", lane ", ctx.Value(laneKey{}) != nil, ": ", string(payload[:min(len(payload), 300)]))
s.cool()
continue continue
} }
if retryableStatus(resp.StatusCode) { if retryableStatus(resp.StatusCode) {
lastErr = errors.New("Drive answered ", resp.StatusCode, ": ", string(payload)) lastErr = errors.New("Drive answered ", resp.StatusCode, ": ", string(payload))
errors.LogWarning(ctx, "retrying after Drive answered ", resp.StatusCode, errors.LogWarning(ctx, "retrying after Drive answered ", resp.StatusCode,
", attempt ", attempt+1, " of ", driveMaxAttempts) ", attempt ", attempt+1, " of ", driveMaxAttempts)
if resp.StatusCode == http.StatusTooManyRequests {
s.cool()
}
continue continue
} }
return resp.StatusCode, payload, nil return resp.StatusCode, payload, nil
@@ -351,12 +301,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
return 0, nil, lastErr return 0, nil, lastErr
} }
func (s *driveStorage) cool() {
s.coolMu.Lock()
s.coolUntil = time.Now().Add(jitter(driveCooldown))
s.coolMu.Unlock()
}
func (s *driveStorage) invalidateToken() { func (s *driveStorage) invalidateToken() {
s.tokenMu.Lock() s.tokenMu.Lock()
s.token = "" s.token = ""
@@ -598,27 +542,12 @@ func (s *driveStorage) Delete(ctx context.Context, name string) error {
if err != nil { if err != nil {
return err return err
} }
sem := make(chan struct{}, 8)
var wg sync.WaitGroup
var mu sync.Mutex
var first error
for childName, file := range children { for childName, file := range children {
wg.Add(1) if err := s.deleteID(ctx, childName, file.id); err != nil {
sem <- struct{}{} return err
go func(name, id string) { }
defer wg.Done()
defer func() { <-sem }()
if err := s.deleteID(ctx, name, id); err != nil {
mu.Lock()
if first == nil {
first = err
}
mu.Unlock()
}
}(childName, file.id)
} }
wg.Wait() return nil
return first
} }
func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error) { func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error) {
@@ -675,9 +604,6 @@ func (s *driveStorage) makeSlot(ctx context.Context, name string) (string, error
// setSlot implements slotStore, a description and a body go in one request // setSlot implements slotStore, a description and a body go in one request
func (s *driveStorage) setSlot(ctx context.Context, id, description string, body []byte) error { func (s *driveStorage) setSlot(ctx context.Context, id, description string, body []byte) error {
if body == nil {
ctx = context.WithValue(ctx, attemptKey{}, driveSlotWrite)
}
metadata, err := json.Marshal(map[string]string{"description": description}) metadata, err := json.Marshal(map[string]string{"description": description})
if err != nil { if err != nil {
return errors.New("failed to build the slot metadata").Base(err) return errors.New("failed to build the slot metadata").Base(err)
@@ -705,7 +631,6 @@ func (s *driveStorage) setSlot(ctx context.Context, id, description string, body
// readDescription implements slotStore // readDescription implements slotStore
func (s *driveStorage) readDescription(ctx context.Context, id string) (string, error) { func (s *driveStorage) readDescription(ctx context.Context, id string) (string, error) {
ctx = context.WithValue(ctx, attemptKey{}, driveSlotRead)
status, payload, err := s.do(ctx, http.MethodGet, status, payload, err := s.do(ctx, http.MethodGet,
s.filesURL+"/"+url.PathEscape(id)+"?fields=description&supportsAllDrives=true", "", nil) s.filesURL+"/"+url.PathEscape(id)+"?fields=description&supportsAllDrives=true", "", nil)
switch { switch {
@@ -7,13 +7,8 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http"
"net/http/httptrace"
"net/url"
"os" "os"
"sort"
"strconv" "strconv"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -348,130 +343,3 @@ func TestLiveDriveListLag(t *testing.T) {
} }
t.Logf("worst listing lag: %v", worst.Round(time.Millisecond)) t.Logf("worst listing lag: %v", worst.Round(time.Millisecond))
} }
func TestLiveDriveLatencyFloor(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 probe = "latency-floor/probe"
id, err := store.makeSlot(ctx, probe)
if err != nil {
t.Fatal(err)
}
defer store.deleteID(ctx, flatten(probe), id)
small := newSlotFrame(1, slotInline, randomPayload(700)).encode()
large := newSlotFrame(1, slotInline, randomPayload(slotInlineLimit)).encode()
file := store.filesURL + "/" + url.PathEscape(id)
type sample struct {
total, ttfb time.Duration
reused bool
}
measure := func(label string, op func(context.Context) error) []sample {
var out []sample
for i := 0; i < 28; i++ {
var wrote, first time.Time
var reused bool
trace := &httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) { reused = info.Reused },
WroteRequest: func(httptrace.WroteRequestInfo) { wrote = time.Now() },
GotFirstResponseByte: func() { first = time.Now() },
}
start := time.Now()
err := op(httptrace.WithClientTrace(ctx, trace))
total := time.Since(start)
if err != nil {
t.Logf("latency-floor: %s sample failed: %v", label, err)
} else if i >= 3 {
var ttfb time.Duration
if !wrote.IsZero() && !first.IsZero() {
ttfb = first.Sub(wrote)
}
out = append(out, sample{total, ttfb, reused})
}
time.Sleep(250 * time.Millisecond)
}
return out
}
report := func(label string, s []sample) time.Duration {
if len(s) == 0 {
t.Logf("latency-floor: %-30s no samples", label)
return 0
}
totals := make([]time.Duration, len(s))
ttfbs := make([]time.Duration, len(s))
reused := 0
for i, x := range s {
totals[i], ttfbs[i] = x.total, x.ttfb
if x.reused {
reused++
}
}
sort.Slice(totals, func(a, b int) bool { return totals[a] < totals[b] })
sort.Slice(ttfbs, func(a, b int) bool { return ttfbs[a] < ttfbs[b] })
q := func(v []time.Duration, p float64) time.Duration { return v[min(int(p*float64(len(v))), len(v)-1)] }
t.Logf("latency-floor: %-30s n=%d min=%v p50=%v p95=%v ttfb_p50=%v reused=%d/%d",
label, len(s), totals[0], q(totals, 0.5), q(totals, 0.95), q(ttfbs, 0.5), reused, len(s))
return q(ttfbs, 0.5)
}
patch := func(target string, doc map[string]any) func(context.Context) error {
body, _ := json.Marshal(doc)
return func(ctx context.Context) error {
status, reply, err := store.do(ctx, http.MethodPatch, target, "application/json; charset=UTF-8", body)
if err == nil && status != http.StatusOK {
err = fmt.Errorf("status %d: %s", status, reply)
}
return err
}
}
get := func(target string) func(context.Context) error {
return func(ctx context.Context) error {
status, reply, err := store.do(ctx, http.MethodGet, target, "", nil)
if err == nil && status != http.StatusOK {
err = fmt.Errorf("status %d: %s", status, reply)
}
return err
}
}
proto := ""
netFloor := report("net floor (401, no auth)", measure("net", func(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/drive/v3/about?fields=user", nil)
if err != nil {
return err
}
resp, err := store.client.Do(req)
if err != nil {
return err
}
proto = resp.Proto
io.Copy(io.Discard, resp.Body)
return resp.Body.Close()
}))
t.Logf("latency-floor: negotiated protocol %s", proto)
readDesc := report("GET description (prod)", measure("get", func(ctx context.Context) error {
_, err := store.readDescription(ctx, id)
return err
}))
report("GET fields=id", measure("getid", get(file+"?fields=id&supportsAllDrives=true")))
report("GET description, no allDrives", measure("getnoall", get(file+"?fields=description")))
write1k := report("PATCH inline 1 KB (prod)", measure("p1k", func(ctx context.Context) error {
return store.setSlot(ctx, id, small, nil)
}))
write14k := report("PATCH inline 14 KB (prod)", measure("p14k", func(ctx context.Context) error {
return store.setSlot(ctx, id, large, nil)
}))
report("PATCH 1 KB, no allDrives", measure("pnoall", patch(file+"?fields=id", map[string]any{"description": small})))
report("PATCH name 200 B", measure("pname", patch(file+"?fields=id&supportsAllDrives=true", map[string]any{"name": strings.Repeat("n", 200)})))
report("PATCH appProperties 100 B", measure("pprop", patch(file+"?fields=id&supportsAllDrives=true", map[string]any{"appProperties": map[string]string{"k": strings.Repeat("a", 100)}})))
t.Logf("latency-floor: derived: write backend ~ %v, read backend ~ %v, 14 KB payload cost ~ %v",
write1k-netFloor, readDesc-netFloor, write14k-write1k)
}
+1 -1
View File
@@ -13,7 +13,7 @@ const (
defaultConcurrency = 8 defaultConcurrency = 8
maxSegmentBytes = 16 * 1024 * 1024 maxSegmentBytes = 16 * 1024 * 1024
maxConcurrency = 256 maxConcurrency = 64
) )
type params struct { type params struct {
+20 -225
View File
@@ -10,7 +10,6 @@ import (
"io" "io"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -28,6 +27,7 @@ const (
// slotInlineLimit is how large a segment is at most to go in the description // slotInlineLimit is how large a segment is at most to go in the description
// of its slot, which takes one request less than a body // of its slot, which takes one request less than a body
slotInlineLimit = 14 << 10 slotInlineLimit = 14 << 10
slotDepthLimit = 8
// inlineRampSegments segments of one size in a row double it, from // inlineRampSegments segments of one size in a row double it, from
// slotInlineLimit up to segmentBytes // slotInlineLimit up to segmentBytes
inlineRampSegments = 8 inlineRampSegments = 8
@@ -41,26 +41,19 @@ const (
var ( var (
// the head is asked for between slotHuntFloor and slotHuntCeiling, a segment // the head is asked for between slotHuntFloor and slotHuntCeiling, a segment
// behind it at about the pace that segments come // behind it at about the pace that segments come
slotHuntFloor = 10 * time.Millisecond slotHuntFloor = 50 * time.Millisecond
slotHuntCeiling = 25 * time.Millisecond slotHuntCeiling = 200 * time.Millisecond
slotTailMin = 150 * time.Millisecond slotTailMin = 150 * time.Millisecond
slotTailMax = 800 * time.Millisecond slotTailMax = 800 * time.Millisecond
slotFetchAhead = 12 slotFetchAhead = 6
slotAckRetry = 30 * time.Millisecond slotAckRetry = 100 * time.Millisecond
slotWatermarkTTL = 100 * time.Millisecond slotWatermarkTTL = 250 * time.Millisecond
slotStallTimeout = 60 * time.Second slotStallTimeout = 60 * time.Second
slotCloseGrace = 10 * time.Second slotCloseGrace = 10 * time.Second
slotGoneTimeout = 60 * time.Second slotGoneTimeout = 60 * time.Second
slotBodyHedge = 2 * time.Second
slotReadAhead int64 = 3
slotReadAheadPoll = 40 * time.Millisecond
slotReadAheadBurst = 500 * time.Millisecond
) )
var slotWriteInflight = 12
// slotStore is a storage of objects that are written and read again by id, // slotStore is a storage of objects that are written and read again by id,
// each with a description and a body // each with a description and a body
type slotStore interface { type slotStore interface {
@@ -150,7 +143,7 @@ func decodeRing(b []byte) (ring, bool) {
// makeRing makes the slots and watermarks of a session, all at once // makeRing makes the slots and watermarks of a session, all at once
func makeRing(ctx context.Context, store slotStore, session string, p params) (ring, error) { func makeRing(ctx context.Context, store slotStore, session string, p params) (ring, error) {
depth := max(p.concurrency, 2) depth := min(max(p.concurrency, 2), slotDepthLimit)
r := ring{ r := ring{
Up: slotTable{Slots: make([]string, depth)}, Up: slotTable{Slots: make([]string, depth)},
Down: slotTable{Slots: make([]string, depth)}, Down: slotTable{Slots: make([]string, depth)},
@@ -184,7 +177,7 @@ func makeRing(ctx context.Context, store slotStore, session string, p params) (r
} }
func readWatermark(ctx context.Context, store slotStore, id string) (int64, error) { func readWatermark(ctx context.Context, store slotStore, id string) (int64, error) {
d, err := store.readDescription(context.WithValue(ctx, laneKey{}, true), id) d, err := store.readDescription(ctx, id)
if err != nil || d == "" { if err != nil || d == "" {
return -1, err return -1, err
} }
@@ -223,7 +216,7 @@ func newSlotWriter(ctx context.Context, store slotStore, table slotTable, p para
store: store, store: store,
table: table, table: table,
params: p, params: p,
slots: make(chan struct{}, min(len(table.Slots), slotWriteInflight)), slots: make(chan struct{}, len(table.Slots)),
size: min(slotInlineLimit, p.segmentBytes), size: min(slotInlineLimit, p.segmentBytes),
acked: -1, acked: -1,
progress: time.Now(), progress: time.Now(),
@@ -278,10 +271,6 @@ func (w *slotWriter) readyToFlush() bool {
grew := len(w.buf) > w.lastSize grew := len(w.buf) > w.lastSize
w.lastSize = len(w.buf) w.lastSize = len(w.buf)
if len(w.slots) > 0 && w.held < maxCoalescedTicks && len(w.buf) < w.size/2 {
w.held++
return false
}
if grew && w.held < maxCoalescedTicks { if grew && w.held < maxCoalescedTicks {
w.held++ w.held++
return false return false
@@ -307,19 +296,11 @@ func (w *slotWriter) flushLocked() error {
return w.err return w.err
} }
if w.size < w.segmentBytes && len(w.buf) >= w.size && len(w.slots)*4 > cap(w.slots)*3 {
w.size = w.segmentBytes
w.full = 0
}
n := min(len(w.buf), w.size) n := min(len(w.buf), w.size)
if len(w.buf) > 4*slotInlineLimit { if len(w.buf) > 4*slotInlineLimit {
// a burst goes at the full size at once // a burst goes at the full size at once
n = min(len(w.buf), w.segmentBytes) n = min(len(w.buf), w.segmentBytes)
} }
if n > slotInlineLimit && len(w.buf) <= 2*cap(w.slots)*slotInlineLimit {
n = slotInlineLimit
}
if n == w.size && w.size < w.segmentBytes { if n == w.size && w.size < w.segmentBytes {
if w.full++; w.full == inlineRampSegments { if w.full++; w.full == inlineRampSegments {
w.full = 0 w.full = 0
@@ -351,21 +332,15 @@ func (w *slotWriter) upload(seq int64, chunk []byte) {
if len(chunk) <= slotInlineLimit { if len(chunk) <= slotInlineLimit {
mode = slotInline mode = slotInline
} }
began := time.Now()
if err := w.await(seq); err != nil { if err := w.await(seq); err != nil {
w.fail(errors.New("failed to store segment ", seq).Base(err)) w.fail(errors.New("failed to store segment ", seq).Base(err))
return return
} }
awaited := time.Since(began)
putAt := time.Now()
if err := w.write(seq, mode, chunk); err != nil { if err := w.write(seq, mode, chunk); err != nil {
w.fail(errors.New("failed to store segment ", seq).Base(err)) w.fail(errors.New("failed to store segment ", seq).Base(err))
// the slot is free, the reader learns there what happened // the slot is free, the reader learns there what happened
w.write(seq, slotErr, nil) w.write(seq, slotErr, nil)
return
} }
errors.LogDebug(w.ctx, "xdrive write ", slotTag(w.table.Watermark), " seq=", seq, " bytes=", len(chunk),
" mode=", mode, " await=", awaited, " put=", time.Since(putAt))
} }
func (w *slotWriter) write(seq int64, mode byte, data []byte) error { func (w *slotWriter) write(seq int64, mode byte, data []byte) error {
@@ -418,7 +393,7 @@ func (w *slotWriter) await(seq int64) error {
w.ackMu.Lock() w.ackMu.Lock()
still := time.Since(w.progress) still := time.Since(w.progress)
w.ackMu.Unlock() w.ackMu.Unlock()
if min(time.Since(start), still) > slotStallTimeout || closed && still > slotCloseGrace { if time.Since(start) > slotStallTimeout || closed && still > slotCloseGrace {
return errors.New("the reader took nothing from slot ", w.slotID(seq), " for ", still).Base(err) return errors.New("the reader took nothing from slot ", w.slotID(seq), " for ", still).Base(err)
} }
@@ -486,7 +461,6 @@ type slotReader struct {
ch chan []byte ch chan []byte
wake chan struct{} wake chan struct{}
acks chan struct{} acks chan struct{}
grow chan struct{}
mu sync.Mutex mu sync.Mutex
head int64 head int64
@@ -498,11 +472,7 @@ type slotReader struct {
gap time.Duration gap time.Duration
activeAt time.Time activeAt time.Time
active chan struct{} active chan struct{}
frontier int64
err error err error
reads atomic.Int64
tailMin time.Duration
tailMax time.Duration
} }
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params) *slotReader { func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params) *slotReader {
@@ -514,12 +484,9 @@ func newSlotReader(ctx context.Context, store slotStore, table slotTable, p para
ch: make(chan []byte, p.concurrency), ch: make(chan []byte, p.concurrency),
wake: make(chan struct{}, 1), wake: make(chan struct{}, 1),
acks: make(chan struct{}, 1), acks: make(chan struct{}, 1),
grow: make(chan struct{}, 1),
hitAt: time.Now(), hitAt: time.Now(),
activeAt: time.Now(), activeAt: time.Now(),
active: make(chan struct{}), active: make(chan struct{}),
tailMin: slotTailMin,
tailMax: slotTailMax,
} }
go r.run() go r.run()
go r.ackLoop() go r.ackLoop()
@@ -549,20 +516,9 @@ type slotFetch struct {
ready chan struct{} ready chan struct{}
// promoted is closed when the segment becomes the head // promoted is closed when the segment becomes the head
promoted chan struct{} promoted chan struct{}
near chan struct{}
body []byte body []byte
end bool end bool
err error err error
born time.Time
readyAt time.Time
}
func (f *slotFetch) enter() {
select {
case <-f.near:
default:
close(f.near)
}
} }
func (r *slotReader) run() { func (r *slotReader) run() {
@@ -573,20 +529,17 @@ func (r *slotReader) run() {
fetches := make(map[int64]*slotFetch) fetches := make(map[int64]*slotFetch)
for { for {
r.mu.Lock() r.mu.Lock()
head, started, frontier := r.head, r.started, r.frontier head, started := r.head, r.started
r.mu.Unlock() r.mu.Unlock()
window := 1 window := 1
if started { if started {
window = min(max(int(frontier-head)+slotFetchAhead, slotFetchAhead), len(r.table.Slots)) window = min(slotFetchAhead, len(r.table.Slots))
} }
for i := range window { for i := range window {
seq := head + int64(i) seq := head + int64(i)
if fetches[seq] == nil { if fetches[seq] == nil {
fetches[seq] = &slotFetch{ready: make(chan struct{}), promoted: make(chan struct{}), near: make(chan struct{}), born: time.Now()} fetches[seq] = &slotFetch{ready: make(chan struct{}), promoted: make(chan struct{})}
if int64(i) <= slotReadAhead {
fetches[seq].enter()
}
go r.fetch(ctx, seq, i, fetches[seq]) go r.fetch(ctx, seq, i, fetches[seq])
} }
} }
@@ -594,8 +547,6 @@ func (r *slotReader) run() {
f := fetches[head] f := fetches[head]
select { select {
case <-f.ready: case <-f.ready:
case <-r.grow:
continue
case <-ctx.Done(): case <-ctx.Done():
return return
} }
@@ -613,14 +564,8 @@ func (r *slotReader) run() {
r.head++ r.head++
r.started = true r.started = true
r.mu.Unlock() r.mu.Unlock()
errors.LogDebug(r.ctx, "xdrive read ", slotTag(r.table.Watermark), " seq=", head, " fetch=",
f.readyAt.Sub(f.born), " blocked=", time.Since(f.readyAt), " bytes=", len(f.body),
" reads=", r.reads.Load())
if next := fetches[head+1]; next != nil { if next := fetches[head+1]; next != nil {
close(next.promoted) close(next.promoted)
if nf := fetches[head+1+slotReadAhead]; nf != nil {
nf.enter()
}
} }
select { select {
case r.acks <- struct{}{}: case r.acks <- struct{}{}:
@@ -641,35 +586,11 @@ func (r *slotReader) isHead(seq int64) bool {
return r.head == seq return r.head == seq
} }
func (r *slotReader) bursting() bool {
r.mu.Lock()
defer r.mu.Unlock()
return time.Since(r.hitAt) <= slotReadAheadBurst
}
func (r *slotReader) reach(seq int64) {
r.mu.Lock()
grown := seq > r.frontier
if grown {
r.frontier = seq
}
r.mu.Unlock()
if grown {
select {
case r.grow <- struct{}{}:
default:
}
}
}
// fetch asks for the slot of seq until it holds seq: the head often, a segment // fetch asks for the slot of seq until it holds seq: the head often, a segment
// behind it soon after it was asked for, then at the pace that segments come // behind it at the pace that segments come, as it would wait for the head anyway
func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFetch) { func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFetch) {
defer func() { defer close(f.ready)
f.readyAt = time.Now() if offset > 0 && !r.hold(ctx, time.Duration(offset)*r.pace(), f.promoted, nil) {
close(f.ready)
}()
if offset > 0 && !r.hold(ctx, r.tailMin, f.promoted, f.near) {
return return
} }
@@ -677,17 +598,7 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
backoff := slotHuntFloor backoff := slotHuntFloor
var goneSince, warnedAt time.Time var goneSince, warnedAt time.Time
for { for {
var description string description, err := r.store.readDescription(ctx, id)
var err error
r.mu.Lock()
hot := time.Since(r.activeAt) <= r.eagerWindow
r.mu.Unlock()
if hot && r.isHead(seq) {
description, err = r.readHead(ctx, id, seq)
} else {
r.reads.Add(1)
description, err = r.store.readDescription(ctx, id)
}
frame, ok := decodeSlot(description) frame, ok := decodeSlot(description)
switch { switch {
case err == nil && ok && frame.seq == seq: case err == nil && ok && frame.seq == seq:
@@ -695,7 +606,6 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
r.mu.Lock() r.mu.Lock()
r.touch() r.touch()
r.mu.Unlock() r.mu.Unlock()
r.reach(seq)
switch frame.mode { switch frame.mode {
case slotEnd: case slotEnd:
f.end = true f.end = true
@@ -708,7 +618,7 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
r.hit() r.hit()
return return
case slotMedia: case slotMedia:
if body, err := r.readBodyHedged(ctx, id); err == nil && frame.holds(body) { if body, err := r.store.readBody(ctx, id); err == nil && frame.holds(body) {
f.body = body f.body = body
r.hit() r.hit()
return return
@@ -744,15 +654,7 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
// only the head is asked for until something comes // only the head is asked for until something comes
wait, a, b, backoff = 0, f.promoted, active, slotHuntFloor wait, a, b, backoff = 0, f.promoted, active, slotHuntFloor
default: default:
wait, a, b, backoff = min(max(r.pace(), r.tailMin), r.tailMax), f.promoted, f.near, slotHuntFloor wait, a, backoff = min(max(r.pace(), slotTailMin), slotTailMax), f.promoted, slotHuntFloor
select {
case <-f.near:
b = nil
if r.bursting() {
wait = slotReadAheadPoll
}
default:
}
} }
if !r.hold(ctx, wait, a, b) { if !r.hold(ctx, wait, a, b) {
return return
@@ -760,106 +662,6 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
} }
} }
func (r *slotReader) readHead(ctx context.Context, id string, seq int64) (string, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = context.WithValue(ctx, laneKey{}, true)
type result struct {
description string
err error
}
results := make(chan result, 4)
outstanding := 0
launch := func() {
outstanding++
go func() {
r.reads.Add(1)
description, err := r.store.readDescription(ctx, id)
results <- result{description, err}
}()
}
launch()
ticker := time.NewTicker(slotHuntCeiling)
defer ticker.Stop()
var last result
seen := false
for {
select {
case res := <-results:
outstanding--
if res.err != nil {
return res.description, res.err
}
if frame, ok := decodeSlot(res.description); ok && frame.seq == seq {
return res.description, nil
}
last, seen = res, true
case <-ticker.C:
r.mu.Lock()
idle := time.Since(r.activeAt) > r.eagerWindow
r.mu.Unlock()
switch {
case idle && seen:
return last.description, nil
case !idle && outstanding < 3:
launch()
}
case <-r.wake:
if outstanding < 3 {
launch()
}
case <-ctx.Done():
return "", ctx.Err()
}
}
}
func (r *slotReader) readBodyHedged(ctx context.Context, id string) ([]byte, error) {
if slotBodyHedge <= 0 {
return r.store.readBody(ctx, id)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
type result struct {
body []byte
err error
}
results := make(chan result, 2)
launch := func() {
go func() {
body, err := r.store.readBody(ctx, id)
results <- result{body, err}
}()
}
launch()
outstanding := 1
timer := time.NewTimer(slotBodyHedge)
defer timer.Stop()
var last result
for {
select {
case res := <-results:
outstanding--
if res.err == nil {
return res.body, nil
}
last = res
if outstanding == 0 {
return nil, last.err
}
case <-timer.C:
if outstanding == 1 {
launch()
outstanding++
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// pace is about how long segments take to come one after the other // pace is about how long segments take to come one after the other
func (r *slotReader) pace() time.Duration { func (r *slotReader) pace() time.Duration {
r.mu.Lock() r.mu.Lock()
@@ -924,7 +726,7 @@ func (r *slotReader) ackLoop() {
if want <= acked { if want <= acked {
break break
} }
if err := r.store.setSlot(context.WithValue(r.ctx, laneKey{}, true), r.table.Watermark, strconv.FormatInt(want, 10), nil); err == nil { if err := r.store.setSlot(r.ctx, r.table.Watermark, strconv.FormatInt(want, 10), nil); err == nil {
acked, wait = want, slotAckRetry acked, wait = want, slotAckRetry
continue continue
} }
@@ -954,10 +756,3 @@ func (r *slotReader) Err() error {
} }
return io.EOF return io.EOF
} }
func slotTag(id string) string {
if len(id) > 6 {
return id[len(id)-6:]
}
return id
}
-368
View File
@@ -10,7 +10,6 @@ import (
"os" "os"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
) )
@@ -305,373 +304,6 @@ func TestSlotStaleBody(t *testing.T) {
client, server := slotPair(t, &staleSlots{memSlots: newMemSlots(), served: make(map[string]bool)}, 2, 64<<10) client, server := slotPair(t, &staleSlots{memSlots: newMemSlots(), served: make(map[string]bool)}, 2, 64<<10)
payload := randomPayload(200 << 10) payload := randomPayload(200 << 10)
errs := make(chan error, 1) errs := make(chan error, 1)
go func() {
_, err := client.Write(payload)
if err == nil {
err = client.Close()
}
errs <- err
}()
readAll(t, server, payload)
}
func TestSlotBurstSplitsInline(t *testing.T) {
store := newMemSlots()
client, server := slotPair(t, store, 8, 512<<10)
payload := randomPayload(90 << 10)
errs := make(chan error, 1)
writeAll(client, payload, true, errs) writeAll(client, payload, true, errs)
readAll(t, server, payload) readAll(t, server, payload)
store.mu.Lock()
bodies := len(store.bodies)
store.mu.Unlock()
if bodies != 0 {
t.Fatalf("a burst of 90 KiB went in %d bodies, want none", bodies)
}
}
type stuckBodySlots struct {
*memSlots
calls atomic.Int32
}
func (s *stuckBodySlots) readBody(ctx context.Context, id string) ([]byte, error) {
if s.calls.Add(1) == 1 {
<-ctx.Done()
return nil, ctx.Err()
}
return s.memSlots.readBody(ctx, id)
}
func TestSlotBodyHedge(t *testing.T) {
old := slotBodyHedge
slotBodyHedge = 50 * time.Millisecond
defer func() { slotBodyHedge = old }()
store := &stuckBodySlots{memSlots: newMemSlots()}
client, server := slotPair(t, store, 2, 64<<10)
payload := randomPayload(200 << 10)
errs := make(chan error, 1)
go func() {
_, err := client.Write(payload)
if err == nil {
err = client.Close()
}
errs <- err
}()
readAll(t, server, payload)
if store.calls.Load() < 2 {
t.Fatalf("the stuck body read was not hedged, %d reads", store.calls.Load())
}
}
func TestTakeSpareSkipsStale(t *testing.T) {
cfg := &Config{}
p := params{sessionTTL: 5 * time.Minute}
st1, err := newLocalStorage(t.TempDir())
if err != nil {
t.Fatal(err)
}
st2, err := newLocalStorage(t.TempDir())
if err != nil {
t.Fatal(err)
}
box := spareBoxFor(cfg)
box.ch <- spareRing{storage: st1, session: "old", at: time.Now().Add(-time.Hour)}
box.ch <- spareRing{storage: st2, session: "new", at: time.Now()}
got, ok := takeSpare(cfg, p)
if !ok || got.session != "new" {
t.Fatalf("takeSpare gave %q, %v, want the fresh one", got.session, ok)
}
if _, ok := takeSpare(cfg, p); ok {
t.Fatal("takeSpare returned a second spare")
}
}
func TestDropSpareDeletesSession(t *testing.T) {
st, err := newLocalStorage(t.TempDir())
if err != nil {
t.Fatal(err)
}
name := sessionPrefix("gone") + "/" + uplinkDir + "/s0"
if err := st.Put(context.Background(), name, []byte("x")); err != nil {
t.Fatal(err)
}
dropSpare(spareRing{storage: st, session: "gone"})
if got, _ := st.List(context.Background(), sessionPrefix("gone")); len(got) != 0 {
t.Fatalf("%d entries left of a dropped spare", len(got))
}
}
type stuckHeadSlots struct {
*memSlots
calls atomic.Int32
}
func (s *stuckHeadSlots) readDescription(ctx context.Context, id string) (string, error) {
if s.calls.Add(1) == 1 {
<-ctx.Done()
return "", ctx.Err()
}
return s.memSlots.readDescription(ctx, id)
}
func TestSlotHeadReadOverlaps(t *testing.T) {
client, server := slotPair(t, &stuckHeadSlots{memSlots: newMemSlots()}, 4, 64<<10)
for _, c := range [][2]*Conn{{client, server}, {server, client}} {
c[0].Write([]byte("ping"))
if _, err := io.ReadFull(c[1], make([]byte, 4)); err != nil {
t.Fatal(err)
}
}
}
func TestSlotBurstReadsTogether(t *testing.T) {
client, server := slotPair(t, newMemSlots(), 16, 512<<10)
payload := randomPayload(8 * slotInlineLimit)
go client.Write(payload)
start := time.Now()
got := make([]byte, len(payload))
if _, err := io.ReadFull(server, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatal("the burst came back changed")
}
if took := time.Since(start); took > time.Second {
t.Fatalf("a burst of 8 segments took %v to read", took)
}
}
type lagSlots struct{ *memSlots }
func (s lagSlots) readDescription(ctx context.Context, id string) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(200 * time.Millisecond):
}
return s.memSlots.readDescription(ctx, id)
}
func (s lagSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
if _, ok := decodeSlot(description); ok {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(600 * time.Millisecond):
}
}
return s.memSlots.setSlot(ctx, id, description, body)
}
func TestSlotReadAheadBurst(t *testing.T) {
oldMin, oldMax := slotTailMin, slotTailMax
slotTailMin, slotTailMax = 150*time.Millisecond, 800*time.Millisecond
t.Cleanup(func() { slotTailMin, slotTailMax = oldMin, oldMax })
client, server := slotPair(t, lagSlots{newMemSlots()}, 16, 512<<10)
payload := randomPayload(8 * slotInlineLimit)
go client.Write(payload)
start := time.Now()
got := make([]byte, len(payload))
if _, err := io.ReadFull(server, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatal("the burst came back changed")
}
if took := time.Since(start); took > 1700*time.Millisecond {
t.Fatalf("a burst of 8 segments took %v to read", took)
}
client.Close()
server.Close()
}
func TestSlotLargeBurstInline(t *testing.T) {
store := newMemSlots()
client, server := slotPair(t, store, 16, 512<<10)
payload := randomPayload(10 * slotInlineLimit)
go client.Write(payload)
got := make([]byte, len(payload))
if _, err := io.ReadFull(server, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatal("the burst came back changed")
}
store.mu.Lock()
bodies := len(store.bodies)
store.mu.Unlock()
if bodies != 0 {
t.Fatalf("a burst of 140 KiB went in %d bodies, want none", bodies)
}
}
func TestSlotDepthFollowsConcurrency(t *testing.T) {
for _, c := range []struct{ set, want int }{{100, 100}, {maxConcurrency, maxConcurrency}, {1 << 20, maxConcurrency}} {
p := paramsFromConfig(&Config{Concurrency: uint32(c.set)})
r, err := makeRing(context.Background(), newMemSlots(), "session", p)
if err != nil {
t.Fatal(err)
}
if len(r.Up.Slots) != c.want || len(r.Down.Slots) != c.want {
t.Fatalf("concurrency %d made %d and %d slots, want %d", c.set, len(r.Up.Slots), len(r.Down.Slots), c.want)
}
}
}
func TestSlotDeepRingRoundTrip(t *testing.T) {
client, server := slotPair(t, newMemSlots(), 64, 64<<10)
payload := randomPayload(2 << 20)
errs := make(chan error, 1)
writeAll(client, payload, true, errs)
readAll(t, server, payload)
if err := <-errs; err != nil {
t.Fatal(err)
}
}
type pollSlots struct {
*memSlots
pmu sync.Mutex
polled map[string]bool
}
func (s *pollSlots) readDescription(ctx context.Context, id string) (string, error) {
s.pmu.Lock()
s.polled[id] = true
s.pmu.Unlock()
return s.memSlots.readDescription(ctx, id)
}
func TestSlotReaderReachesPastWindow(t *testing.T) {
store := &pollSlots{memSlots: newMemSlots(), polled: make(map[string]bool)}
p := paramsFromConfig(&Config{FlushIntervalMs: 1, PollIntervalMs: 1, MaxPollIntervalMs: 20, EagerWindowMs: 50, Concurrency: 64})
ring, err := makeRing(context.Background(), store, "session", p)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
put := func(seq int64) {
slot := ring.Down.Slots[seq%int64(len(ring.Down.Slots))]
if err := store.setSlot(ctx, slot, newSlotFrame(seq, slotInline, []byte{byte(seq)}).encode(), nil); err != nil {
t.Fatal(err)
}
}
reader := newSlotReader(ctx, store, ring.Down, p)
next := func() []byte {
select {
case chunk := <-reader.chunks():
return chunk
case <-time.After(10 * time.Second):
t.Fatal("no segment came")
return nil
}
}
put(0)
if got := next(); !bytes.Equal(got, []byte{0}) {
t.Fatalf("segment 0 came as %v", got)
}
for seq := int64(2); seq <= 40; seq++ {
put(seq)
}
last := ring.Down.Slots[40]
deadline := time.Now().Add(10 * time.Second)
for {
reader.Wake()
store.pmu.Lock()
polled := store.polled[last]
store.pmu.Unlock()
if polled {
break
}
if time.Now().After(deadline) {
t.Fatal("segment 40 was never asked for while segment 1 was missing")
}
time.Sleep(10 * time.Millisecond)
}
put(1)
for seq := int64(1); seq <= 40; seq++ {
if got := next(); !bytes.Equal(got, []byte{byte(seq)}) {
t.Fatalf("segment %d came as %v", seq, got)
}
}
}
type countSlots struct {
*memSlots
cur atomic.Int32
peak atomic.Int32
}
func (s *countSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
if _, ok := decodeSlot(description); !ok {
return s.memSlots.setSlot(ctx, id, description, body)
}
n := s.cur.Add(1)
for {
p := s.peak.Load()
if n <= p || s.peak.CompareAndSwap(p, n) {
break
}
}
time.Sleep(20 * time.Millisecond)
s.cur.Add(-1)
return s.memSlots.setSlot(ctx, id, description, body)
}
func TestSlotWriteInflightCap(t *testing.T) {
store := &countSlots{memSlots: newMemSlots()}
client, server := slotPair(t, store, 48, 512<<10)
payload := randomPayload(60 * slotInlineLimit)
errs := make(chan error, 1)
go func() {
for off := 0; off < len(payload); off += slotInlineLimit {
if _, err := client.Write(payload[off : off+slotInlineLimit]); err != nil {
errs <- err
return
}
}
errs <- nil
}()
got := make([]byte, len(payload))
if _, err := io.ReadFull(server, got); err != nil {
t.Fatal(err)
}
if err := <-errs; err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatal("the payload came back changed")
}
if p := int(store.peak.Load()); p > slotWriteInflight {
t.Fatalf("%d segments were written at once, want at most %d", p, slotWriteInflight)
}
}
func TestSlotBurstInlineWaves(t *testing.T) {
for _, c := range []struct {
pieces int
media bool
}{{20, false}, {30, true}} {
store := newMemSlots()
client, server := slotPair(t, store, 16, 512<<10)
payload := randomPayload(c.pieces * slotInlineLimit)
go client.Write(payload)
got := make([]byte, len(payload))
if _, err := io.ReadFull(server, got); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, payload) {
t.Fatal("the burst came back changed")
}
store.mu.Lock()
bodies := len(store.bodies)
store.mu.Unlock()
if (bodies > 0) != c.media {
t.Fatalf("a burst of %d pieces went in %d bodies, media wanted %v", c.pieces, bodies, c.media)
}
}
} }
+15 -145
View File
@@ -9,7 +9,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
@@ -27,11 +26,6 @@ const (
downlinkDir = "s2c" downlinkDir = "s2c"
) )
const (
spareCount = 2
spareIdleStop = 10 * time.Minute
)
func init() { func init() {
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
return new(Config) return new(Config)
@@ -84,152 +78,30 @@ func streamConfig(streamSettings *internet.MemoryStreamConfig) (*Config, error)
return config, nil return config, nil
} }
type spareRing struct {
storage Storage
session string
ring ring
at time.Time
}
type spareBox struct {
ch chan spareRing
busy atomic.Bool
last atomic.Int64
stop atomic.Bool
}
var spareBoxes sync.Map
func spareBoxFor(config *Config) *spareBox {
if b, ok := spareBoxes.Load(config); ok {
return b.(*spareBox)
}
b, _ := spareBoxes.LoadOrStore(config, &spareBox{ch: make(chan spareRing, spareCount)})
return b.(*spareBox)
}
func dropSpare(s spareRing) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.storage.Delete(ctx, sessionPrefix(s.session))
s.storage.Close()
}
func takeSpare(config *Config, p params) (spareRing, bool) {
box := spareBoxFor(config)
for {
select {
case s := <-box.ch:
if time.Since(s.at) < p.sessionTTL*2/3 {
return s, true
}
go dropSpare(s)
default:
return spareRing{}, false
}
}
}
func warmSpares(streamSettings *internet.MemoryStreamConfig, config *Config, p params) {
spareBoxFor(config).last.Store(time.Now().UnixNano())
fillSpares(streamSettings, config, p)
}
func fillSpares(streamSettings *internet.MemoryStreamConfig, config *Config, p params) {
box := spareBoxFor(config)
if !box.busy.CompareAndSwap(false, true) {
return
}
go func() {
defer box.busy.Store(false)
for n := len(box.ch); n > 0; n-- {
select {
case s := <-box.ch:
if time.Since(s.at) < p.sessionTTL/3 {
box.ch <- s
} else {
dropSpare(s)
}
default:
}
}
for len(box.ch) < cap(box.ch) {
for range 40 {
if box.stop.Load() {
return
}
time.Sleep(50 * time.Millisecond)
}
if box.stop.Load() {
return
}
storage, err := newStorage(streamSettings)
if err != nil {
return
}
storage = sessionStorage(storage)
slots, ok := storage.(slotStore)
if !ok {
storage.Close()
return
}
session, err := newSessionID()
if err != nil {
storage.Close()
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
r, err := makeRing(ctx, slots, session, p)
cancel()
if err != nil {
dropSpare(spareRing{storage: storage, session: session})
return
}
select {
case box.ch <- spareRing{storage: storage, session: session, ring: r, at: time.Now()}:
default:
storage.Close()
return
}
}
time.AfterFunc(p.sessionTTL/4, func() {
if !box.stop.Load() && time.Since(time.Unix(0, box.last.Load())) < spareIdleStop {
fillSpares(streamSettings, config, p)
}
})
}()
}
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) { func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
config, err := streamConfig(streamSettings) config, err := streamConfig(streamSettings)
if err != nil { if err != nil {
return nil, err return nil, err
} }
p := paramsFromConfig(config) storage, err := newStorage(streamSettings)
if err != nil {
return nil, err
}
storage = sessionStorage(storage)
spare, warm := takeSpare(config, p) session, err := newSessionID()
storage, session, r := spare.storage, spare.session, spare.ring if err != nil {
if !warm { storage.Close()
storage, err = newStorage(streamSettings) return nil, err
if err != nil {
return nil, err
}
storage = sessionStorage(storage)
session, err = newSessionID()
if err != nil {
storage.Close()
return nil, err
}
} }
p := paramsFromConfig(config)
var announce []byte var announce []byte
var r ring
slots, ok := storage.(slotStore) slots, ok := storage.(slotStore)
if ok { if ok {
if warm { if r, err = makeRing(ctx, slots, session, p); err == nil {
announce, _ = json.Marshal(r)
} else if r, err = makeRing(ctx, slots, session, p); err == nil {
announce, _ = json.Marshal(r) announce, _ = json.Marshal(r)
} else { } else {
ok = false ok = false
@@ -243,7 +115,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
} }
if ok { if ok {
warmSpares(streamSettings, config, p)
errors.LogInfo(ctx, "opened session ", session, " in ", len(r.Up.Slots), " slots") errors.LogInfo(ctx, "opened session ", session, " in ", len(r.Up.Slots), " slots")
return newSlotConn(context.Background(), slots, r.Up, r.Down, p, func() { return newSlotConn(context.Background(), slots, r.Up, r.Down, p, func() {
storage.Close() storage.Close()
@@ -449,13 +320,12 @@ func (l *Listener) collect() error {
} }
l.mu.Unlock() l.mu.Unlock()
var first error
for _, session := range expired { for _, session := range expired {
if err := l.storage.Delete(l.ctx, sessionPrefix(session)); err != nil && first == nil { if err := l.storage.Delete(l.ctx, sessionPrefix(session)); err != nil {
first = err return err
} }
} }
return first return nil
} }
func (l *Listener) Addr() net.Addr { func (l *Listener) Addr() net.Addr {
-7
View File
@@ -69,13 +69,6 @@ func pairWith(t *testing.T, streamSettings *internet.MemoryStreamConfig) (client
} }
return client, server, func() { return client, server, func() {
if config, err := streamConfig(streamSettings); err == nil {
box := spareBoxFor(config)
box.stop.Store(true)
for box.busy.Load() {
time.Sleep(10 * time.Millisecond)
}
}
client.Close() client.Close()
server.Close() server.Close()
listener.Close() listener.Close()