ccm,ocm: merge fallback into balancer strategy, use hyphenated constant names

Merge the fallback credential type into balancer as a strategy
(C.BalancerStrategyFallback). Replace raw string literals with
C.BalancerStrategyXxx constants and switch to hyphens (least-used,
round-robin) per project convention.
This commit is contained in:
世界
2026-03-14 19:08:32 +08:00
parent 4d8baf7175
commit 51d564c9ff
7 changed files with 88 additions and 185 deletions

View File

@@ -38,6 +38,13 @@ const (
TypeURLTest = "urltest"
)
const (
BalancerStrategyLeastUsed = "least-used"
BalancerStrategyRoundRobin = "round-robin"
BalancerStrategyRandom = "random"
BalancerStrategyFallback = "fallback"
)
func ProxyDisplayName(proxyType string) string {
switch proxyType {
case TypeTun:

View File

@@ -32,7 +32,6 @@ type _CCMCredential struct {
DefaultOptions CCMDefaultCredentialOptions `json:"-"`
ExternalOptions CCMExternalCredentialOptions `json:"-"`
BalancerOptions CCMBalancerCredentialOptions `json:"-"`
FallbackOptions CCMFallbackCredentialOptions `json:"-"`
}
type CCMCredential _CCMCredential
@@ -47,8 +46,6 @@ func (c CCMCredential) MarshalJSON() ([]byte, error) {
v = c.ExternalOptions
case "balancer":
v = c.BalancerOptions
case "fallback":
v = c.FallbackOptions
default:
return nil, E.New("unknown credential type: ", c.Type)
}
@@ -72,8 +69,6 @@ func (c *CCMCredential) UnmarshalJSON(bytes []byte) error {
v = &c.ExternalOptions
case "balancer":
v = &c.BalancerOptions
case "fallback":
v = &c.FallbackOptions
default:
return E.New("unknown credential type: ", c.Type)
}
@@ -106,8 +101,3 @@ type CCMExternalCredentialOptions struct {
UsagesPath string `json:"usages_path,omitempty"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}
type CCMFallbackCredentialOptions struct {
Credentials badoption.Listable[string] `json:"credentials"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}

View File

@@ -32,7 +32,6 @@ type _OCMCredential struct {
DefaultOptions OCMDefaultCredentialOptions `json:"-"`
ExternalOptions OCMExternalCredentialOptions `json:"-"`
BalancerOptions OCMBalancerCredentialOptions `json:"-"`
FallbackOptions OCMFallbackCredentialOptions `json:"-"`
}
type OCMCredential _OCMCredential
@@ -47,8 +46,6 @@ func (c OCMCredential) MarshalJSON() ([]byte, error) {
v = c.ExternalOptions
case "balancer":
v = c.BalancerOptions
case "fallback":
v = c.FallbackOptions
default:
return nil, E.New("unknown credential type: ", c.Type)
}
@@ -72,8 +69,6 @@ func (c *OCMCredential) UnmarshalJSON(bytes []byte) error {
v = &c.ExternalOptions
case "balancer":
v = &c.BalancerOptions
case "fallback":
v = &c.FallbackOptions
default:
return E.New("unknown credential type: ", c.Type)
}
@@ -106,8 +101,3 @@ type OCMExternalCredentialOptions struct {
UsagesPath string `json:"usages_path,omitempty"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}
type OCMFallbackCredentialOptions struct {
Credentials badoption.Listable[string] `json:"credentials"`
PollInterval badoption.Duration `json:"poll_interval,omitempty"`
}

View File

@@ -47,9 +47,9 @@ type externalCredential struct {
requestAccess sync.Mutex
// Reverse proxy fields
reverse bool
reverseHttpClient *http.Client
reverseSession *yamux.Session
reverse bool
reverseHttpClient *http.Client
reverseSession *yamux.Session
reverseAccess sync.RWMutex
closed bool
reverseContext context.Context

View File

@@ -19,6 +19,7 @@ import (
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
@@ -1015,6 +1016,14 @@ func newBalancerProvider(credentials []credential, strategy string, pollInterval
}
func (p *balancerProvider) selectCredential(sessionID string, selection credentialSelection) (credential, bool, error) {
if p.strategy == C.BalancerStrategyFallback {
best := p.pickCredential(selection.filter)
if best == nil {
return nil, false, allCredentialsUnavailableError(p.credentials)
}
return best, false, nil
}
selectionScope := selection.scopeOrDefault()
if sessionID != "" {
p.sessionMutex.RLock()
@@ -1024,7 +1033,7 @@ func (p *balancerProvider) selectCredential(sessionID string, selection credenti
if entry.selectionScope == selectionScope {
for _, cred := range p.credentials {
if cred.tagName() == entry.tag && selection.allows(cred) && cred.isUsable() {
if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == "least_used") {
if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == C.BalancerStrategyLeastUsed) {
better := p.pickLeastUsed(selection.filter)
if better != nil && better.tagName() != cred.tagName() {
effectiveThreshold := p.rebalanceThreshold / cred.planWeight()
@@ -1086,6 +1095,9 @@ func (p *balancerProvider) rebalanceCredential(tag string, selectionScope creden
}
func (p *balancerProvider) linkProviderInterrupt(cred credential, selection credentialSelection, onInterrupt func()) func() bool {
if p.strategy == C.BalancerStrategyFallback {
return func() bool { return false }
}
key := credentialInterruptKey{
tag: cred.tagName(),
selectionScope: selection.scopeOrDefault(),
@@ -1103,6 +1115,9 @@ func (p *balancerProvider) linkProviderInterrupt(cred credential, selection cred
func (p *balancerProvider) onRateLimited(sessionID string, cred credential, resetAt time.Time, selection credentialSelection) credential {
cred.markRateLimited(resetAt)
if p.strategy == C.BalancerStrategyFallback {
return p.pickCredential(selection.filter)
}
if sessionID != "" {
p.sessionMutex.Lock()
delete(p.sessions, sessionID)
@@ -1124,15 +1139,29 @@ func (p *balancerProvider) onRateLimited(sessionID string, cred credential, rese
func (p *balancerProvider) pickCredential(filter func(credential) bool) credential {
switch p.strategy {
case "round_robin":
case C.BalancerStrategyRoundRobin:
return p.pickRoundRobin(filter)
case "random":
case C.BalancerStrategyRandom:
return p.pickRandom(filter)
case C.BalancerStrategyFallback:
return p.pickFallback(filter)
default:
return p.pickLeastUsed(filter)
}
}
func (p *balancerProvider) pickFallback(filter func(credential) bool) credential {
for _, cred := range p.credentials {
if filter != nil && !filter(cred) {
continue
}
if cred.isUsable() {
return cred
}
}
return nil
}
func (p *balancerProvider) pickLeastUsed(filter func(credential) bool) credential {
var best credential
bestScore := float64(-1)
@@ -1239,69 +1268,6 @@ func (p *balancerProvider) allCredentials() []credential {
func (p *balancerProvider) close() {}
// fallbackProvider tries credentials in order.
type fallbackProvider struct {
credentials []credential
pollInterval time.Duration
logger log.ContextLogger
}
func newFallbackProvider(credentials []credential, pollInterval time.Duration, logger log.ContextLogger) *fallbackProvider {
if pollInterval <= 0 {
pollInterval = defaultPollInterval
}
return &fallbackProvider{
credentials: credentials,
pollInterval: pollInterval,
logger: logger,
}
}
func (p *fallbackProvider) selectCredential(_ string, selection credentialSelection) (credential, bool, error) {
for _, cred := range p.credentials {
if !selection.allows(cred) {
continue
}
if cred.isUsable() {
return cred, false, nil
}
}
return nil, false, allCredentialsUnavailableError(p.credentials)
}
func (p *fallbackProvider) onRateLimited(_ string, cred credential, resetAt time.Time, selection credentialSelection) credential {
cred.markRateLimited(resetAt)
for _, candidate := range p.credentials {
if !selection.allows(candidate) {
continue
}
if candidate.isUsable() {
return candidate
}
}
return nil
}
func (p *fallbackProvider) pollIfStale(ctx context.Context) {
for _, cred := range p.credentials {
if time.Since(cred.lastUpdatedTime()) > cred.pollBackoff(p.pollInterval) {
cred.pollUsage(ctx)
}
}
}
func (p *fallbackProvider) allCredentials() []credential {
return p.credentials
}
func (p *fallbackProvider) linkProviderInterrupt(_ credential, _ credentialSelection, _ func()) func() bool {
return func() bool {
return false
}
}
func (p *fallbackProvider) close() {}
func allCredentialsUnavailableError(credentials []credential) error {
var hasUnavailable bool
var earliest time.Time
@@ -1373,21 +1339,14 @@ func buildCredentialProviders(
}
}
// Pass 2: create balancer and fallback providers
// Pass 2: create balancer providers
for _, credOpt := range options.Credentials {
switch credOpt.Type {
case "balancer":
if credOpt.Type == "balancer" {
subCredentials, err := resolveCredentialTags(credOpt.BalancerOptions.Credentials, allCredentialMap, credOpt.Tag)
if err != nil {
return nil, nil, err
}
providers[credOpt.Tag] = newBalancerProvider(subCredentials, credOpt.BalancerOptions.Strategy, time.Duration(credOpt.BalancerOptions.PollInterval), credOpt.BalancerOptions.RebalanceThreshold, logger)
case "fallback":
subCredentials, err := resolveCredentialTags(credOpt.FallbackOptions.Credentials, allCredentialMap, credOpt.Tag)
if err != nil {
return nil, nil, err
}
providers[credOpt.Tag] = newFallbackProvider(subCredentials, time.Duration(credOpt.FallbackOptions.PollInterval), logger)
}
}
@@ -1476,7 +1435,7 @@ func validateCCMOptions(options option.CCMServiceOptions) error {
}
if cred.Type == "balancer" {
switch cred.BalancerOptions.Strategy {
case "", "least_used", "round_robin", "random":
case "", C.BalancerStrategyLeastUsed, C.BalancerStrategyRoundRobin, C.BalancerStrategyRandom, C.BalancerStrategyFallback:
default:
return E.New("credential ", cred.Tag, ": unknown balancer strategy: ", cred.BalancerOptions.Strategy)
}

View File

@@ -49,10 +49,10 @@ type externalCredential struct {
requestAccess sync.Mutex
// Reverse proxy fields
reverse bool
reverseHttpClient *http.Client
reverseCredDialer N.Dialer
reverseSession *yamux.Session
reverse bool
reverseHttpClient *http.Client
reverseCredDialer N.Dialer
reverseSession *yamux.Session
reverseAccess sync.RWMutex
closed bool
reverseContext context.Context

View File

@@ -18,6 +18,7 @@ import (
"github.com/sagernet/fswatch"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
@@ -1014,6 +1015,14 @@ func newBalancerProvider(credentials []credential, strategy string, pollInterval
}
func (p *balancerProvider) selectCredential(sessionID string, selection credentialSelection) (credential, bool, error) {
if p.strategy == C.BalancerStrategyFallback {
best := p.pickCredential(selection.filter)
if best == nil {
return nil, false, allRateLimitedError(p.credentials)
}
return best, false, nil
}
selectionScope := selection.scopeOrDefault()
if sessionID != "" {
p.sessionMutex.RLock()
@@ -1023,7 +1032,7 @@ func (p *balancerProvider) selectCredential(sessionID string, selection credenti
if entry.selectionScope == selectionScope {
for _, cred := range p.credentials {
if cred.tagName() == entry.tag && compositeCredentialSelectable(cred) && selection.allows(cred) && cred.isUsable() {
if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == "least_used") {
if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == C.BalancerStrategyLeastUsed) {
better := p.pickLeastUsed(selection.filter)
if better != nil && better.tagName() != cred.tagName() {
effectiveThreshold := p.rebalanceThreshold / cred.planWeight()
@@ -1085,6 +1094,9 @@ func (p *balancerProvider) rebalanceCredential(tag string, selectionScope creden
}
func (p *balancerProvider) linkProviderInterrupt(cred credential, selection credentialSelection, onInterrupt func()) func() bool {
if p.strategy == C.BalancerStrategyFallback {
return func() bool { return false }
}
key := credentialInterruptKey{
tag: cred.tagName(),
selectionScope: selection.scopeOrDefault(),
@@ -1102,6 +1114,9 @@ func (p *balancerProvider) linkProviderInterrupt(cred credential, selection cred
func (p *balancerProvider) onRateLimited(sessionID string, cred credential, resetAt time.Time, selection credentialSelection) credential {
cred.markRateLimited(resetAt)
if p.strategy == C.BalancerStrategyFallback {
return p.pickCredential(selection.filter)
}
if sessionID != "" {
p.sessionMutex.Lock()
delete(p.sessions, sessionID)
@@ -1123,15 +1138,32 @@ func (p *balancerProvider) onRateLimited(sessionID string, cred credential, rese
func (p *balancerProvider) pickCredential(filter func(credential) bool) credential {
switch p.strategy {
case "round_robin":
case C.BalancerStrategyRoundRobin:
return p.pickRoundRobin(filter)
case "random":
case C.BalancerStrategyRandom:
return p.pickRandom(filter)
case C.BalancerStrategyFallback:
return p.pickFallback(filter)
default:
return p.pickLeastUsed(filter)
}
}
func (p *balancerProvider) pickFallback(filter func(credential) bool) credential {
for _, cred := range p.credentials {
if filter != nil && !filter(cred) {
continue
}
if !compositeCredentialSelectable(cred) {
continue
}
if cred.isUsable() {
return cred
}
}
return nil
}
func (p *balancerProvider) pickLeastUsed(filter func(credential) bool) credential {
var best credential
bestScore := float64(-1)
@@ -1237,74 +1269,6 @@ func (p *balancerProvider) allCredentials() []credential {
func (p *balancerProvider) close() {}
type fallbackProvider struct {
credentials []credential
pollInterval time.Duration
logger log.ContextLogger
}
func newFallbackProvider(credentials []credential, pollInterval time.Duration, logger log.ContextLogger) *fallbackProvider {
if pollInterval <= 0 {
pollInterval = defaultPollInterval
}
return &fallbackProvider{
credentials: credentials,
pollInterval: pollInterval,
logger: logger,
}
}
func (p *fallbackProvider) selectCredential(_ string, selection credentialSelection) (credential, bool, error) {
for _, cred := range p.credentials {
if !selection.allows(cred) {
continue
}
if !compositeCredentialSelectable(cred) {
continue
}
if cred.isUsable() {
return cred, false, nil
}
}
return nil, false, allRateLimitedError(p.credentials)
}
func (p *fallbackProvider) onRateLimited(_ string, cred credential, resetAt time.Time, selection credentialSelection) credential {
cred.markRateLimited(resetAt)
for _, candidate := range p.credentials {
if !selection.allows(candidate) {
continue
}
if !compositeCredentialSelectable(candidate) {
continue
}
if candidate.isUsable() {
return candidate
}
}
return nil
}
func (p *fallbackProvider) pollIfStale(ctx context.Context) {
for _, cred := range p.credentials {
if time.Since(cred.lastUpdatedTime()) > cred.pollBackoff(p.pollInterval) {
cred.pollUsage(ctx)
}
}
}
func (p *fallbackProvider) allCredentials() []credential {
return p.credentials
}
func (p *fallbackProvider) linkProviderInterrupt(_ credential, _ credentialSelection, _ func()) func() bool {
return func() bool {
return false
}
}
func (p *fallbackProvider) close() {}
func allRateLimitedError(credentials []credential) error {
var hasUnavailable bool
var earliest time.Time
@@ -1358,21 +1322,14 @@ func buildOCMCredentialProviders(
}
}
// Pass 2: create balancer and fallback providers
// Pass 2: create balancer providers
for _, credOpt := range options.Credentials {
switch credOpt.Type {
case "balancer":
if credOpt.Type == "balancer" {
subCredentials, err := resolveCredentialTags(credOpt.BalancerOptions.Credentials, allCredentialMap, credOpt.Tag)
if err != nil {
return nil, nil, err
}
providers[credOpt.Tag] = newBalancerProvider(subCredentials, credOpt.BalancerOptions.Strategy, time.Duration(credOpt.BalancerOptions.PollInterval), credOpt.BalancerOptions.RebalanceThreshold, logger)
case "fallback":
subCredentials, err := resolveCredentialTags(credOpt.FallbackOptions.Credentials, allCredentialMap, credOpt.Tag)
if err != nil {
return nil, nil, err
}
providers[credOpt.Tag] = newFallbackProvider(subCredentials, time.Duration(credOpt.FallbackOptions.PollInterval), logger)
}
}
@@ -1469,7 +1426,7 @@ func validateOCMOptions(options option.OCMServiceOptions) error {
}
if cred.Type == "balancer" {
switch cred.BalancerOptions.Strategy {
case "", "least_used", "round_robin", "random":
case "", C.BalancerStrategyLeastUsed, C.BalancerStrategyRoundRobin, C.BalancerStrategyRandom, C.BalancerStrategyFallback:
default:
return E.New("credential ", cred.Tag, ": unknown balancer strategy: ", cred.BalancerOptions.Strategy)
}
@@ -1505,7 +1462,7 @@ func validateOCMCompositeCredentialModes(
providers map[string]credentialProvider,
) error {
for _, credOpt := range options.Credentials {
if credOpt.Type != "balancer" && credOpt.Type != "fallback" {
if credOpt.Type != "balancer" {
continue
}