perf(mem): replace Scheme and ExcludedReason string with uint8 type to reduce mem usage

This commit is contained in:
yusing
2025-10-15 14:35:44 +08:00
parent feafdf05f2
commit 290af4e311
6 changed files with 160 additions and 61 deletions

View File

@@ -13,6 +13,7 @@ import (
. "github.com/yusing/godoxy/internal/entrypoint" . "github.com/yusing/godoxy/internal/entrypoint"
"github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/route"
"github.com/yusing/godoxy/internal/route/routes" "github.com/yusing/godoxy/internal/route/routes"
routeTypes "github.com/yusing/godoxy/internal/route/types"
"github.com/yusing/godoxy/internal/types" "github.com/yusing/godoxy/internal/types"
"github.com/yusing/goutils/task" "github.com/yusing/goutils/task"
) )
@@ -78,7 +79,7 @@ func BenchmarkEntrypointReal(b *testing.B) {
r := &route.Route{ r := &route.Route{
Alias: "test", Alias: "test",
Scheme: "http", Scheme: routeTypes.SchemeHTTP,
Host: host, Host: host,
Port: route.Port{Proxy: portInt}, Port: route.Port{Proxy: portInt},
HealthCheck: &types.HealthCheckConfig{Disable: true}, HealthCheck: &types.HealthCheckConfig{Disable: true},
@@ -119,7 +120,7 @@ func BenchmarkEntrypoint(b *testing.B) {
r := &route.Route{ r := &route.Route{
Alias: "test", Alias: "test",
Scheme: "http", Scheme: routeTypes.SchemeHTTP,
Host: "localhost", Host: "localhost",
Port: route.Port{ Port: route.Port{
Proxy: 8080, Proxy: 8080,

View File

@@ -1,5 +1,7 @@
package route package route
import route "github.com/yusing/godoxy/internal/route/types"
var ( var (
ImageNamePortMapTCP = map[string]int{ ImageNamePortMapTCP = map[string]int{
"mssql": 1433, "mssql": 1433,
@@ -57,25 +59,25 @@ var (
} }
) )
func getSchemePortByImageName(imageName string) (scheme string, port int, ok bool) { func getSchemePortByImageName(imageName string) (scheme route.Scheme, port int, ok bool) {
if port, ok := ImageNamePortMapHTTP[imageName]; ok { if port, ok := ImageNamePortMapHTTP[imageName]; ok {
return "http", port, true return route.SchemeHTTP, port, true
} }
if port, ok := ImageNamePortMapHTTPS[imageName]; ok { if port, ok := ImageNamePortMapHTTPS[imageName]; ok {
return "https", port, true return route.SchemeHTTPS, port, true
} }
if port, ok := ImageNamePortMapTCP[imageName]; ok { if port, ok := ImageNamePortMapTCP[imageName]; ok {
return "tcp", port, true return route.SchemeTCP, port, true
} }
return scheme, port, ok return scheme, port, ok
} }
func getSchemePortByAlias(alias string) (scheme string, port int, ok bool) { func getSchemePortByAlias(alias string) (scheme route.Scheme, port int, ok bool) {
if port, ok := AliasPortMapHTTP[alias]; ok { if port, ok := AliasPortMapHTTP[alias]; ok {
return "http", port, true return route.SchemeHTTP, port, true
} }
if port, ok := AliasPortMapHTTPS[alias]; ok { if port, ok := AliasPortMapHTTPS[alias]; ok {
return "https", port, true return route.SchemeHTTPS, port, true
} }
return scheme, port, ok return scheme, port, ok
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
D "github.com/yusing/godoxy/internal/docker" D "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/route" "github.com/yusing/godoxy/internal/route"
T "github.com/yusing/godoxy/internal/route/types" routeTypes "github.com/yusing/godoxy/internal/route/types"
expect "github.com/yusing/goutils/testing" expect "github.com/yusing/goutils/testing"
) )
@@ -91,8 +91,8 @@ func TestApplyLabel(t *testing.T) {
b, ok := entries["b"] b, ok := entries["b"]
expect.True(t, ok) expect.True(t, ok)
expect.Equal(t, a.Scheme, "https") expect.Equal(t, a.Scheme, routeTypes.SchemeHTTPS)
expect.Equal(t, b.Scheme, "https") expect.Equal(t, b.Scheme, routeTypes.SchemeHTTPS)
expect.Equal(t, a.Host, "app") expect.Equal(t, a.Host, "app")
expect.Equal(t, b.Host, "app") expect.Equal(t, b.Host, "app")
@@ -152,12 +152,12 @@ func TestApplyLabelWithAlias(t *testing.T) {
c, ok := entries["c"] c, ok := entries["c"]
expect.True(t, ok) expect.True(t, ok)
expect.Equal(t, a.Scheme, "http") expect.Equal(t, a.Scheme, routeTypes.SchemeHTTP)
expect.Equal(t, a.Port.Proxy, 3333) expect.Equal(t, a.Port.Proxy, 3333)
expect.Equal(t, a.NoTLSVerify, true) expect.Equal(t, a.NoTLSVerify, true)
expect.Equal(t, b.Scheme, "http") expect.Equal(t, b.Scheme, routeTypes.SchemeHTTP)
expect.Equal(t, b.Port.Proxy, 1234) expect.Equal(t, b.Port.Proxy, 1234)
expect.Equal(t, c.Scheme, "https") expect.Equal(t, c.Scheme, routeTypes.SchemeHTTPS)
} }
func TestApplyLabelWithRef(t *testing.T) { func TestApplyLabelWithRef(t *testing.T) {
@@ -180,11 +180,11 @@ func TestApplyLabelWithRef(t *testing.T) {
c, ok := entries["c"] c, ok := entries["c"]
expect.True(t, ok) expect.True(t, ok)
expect.Equal(t, a.Scheme, "http") expect.Equal(t, a.Scheme, routeTypes.SchemeHTTP)
expect.Equal(t, a.Host, "localhost") expect.Equal(t, a.Host, "localhost")
expect.Equal(t, a.Port.Proxy, 4444) expect.Equal(t, a.Port.Proxy, 4444)
expect.Equal(t, b.Port.Proxy, 9999) expect.Equal(t, b.Port.Proxy, 9999)
expect.Equal(t, c.Scheme, "https") expect.Equal(t, c.Scheme, routeTypes.SchemeHTTPS)
expect.Equal(t, c.Port.Proxy, 1111) expect.Equal(t, c.Port.Proxy, 1111)
} }
@@ -229,12 +229,12 @@ func TestDynamicAliases(t *testing.T) {
r, ok := entries["app1"] r, ok := entries["app1"]
expect.True(t, ok) expect.True(t, ok)
expect.Equal(t, r.Scheme, "http") expect.Equal(t, r.Scheme, routeTypes.SchemeHTTP)
expect.Equal(t, r.Port.Proxy, 1234) expect.Equal(t, r.Port.Proxy, 1234)
r, ok = entries["app1_backend"] r, ok = entries["app1_backend"]
expect.True(t, ok) expect.True(t, ok)
expect.Equal(t, r.Scheme, "http") expect.Equal(t, r.Scheme, routeTypes.SchemeHTTP)
expect.Equal(t, r.Port.Proxy, 5678) expect.Equal(t, r.Port.Proxy, 5678)
} }
@@ -327,7 +327,7 @@ func TestStreamDefaultValues(t *testing.T) {
r, ok := makeRoutes(cont)["a"] r, ok := makeRoutes(cont)["a"]
expect.True(t, ok) expect.True(t, ok)
expect.NoError(t, r.Validate()) expect.NoError(t, r.Validate())
expect.Equal(t, r.Scheme, T.Scheme("udp")) expect.Equal(t, r.Scheme, routeTypes.SchemeUDP)
expect.Equal(t, r.TargetURL().Hostname(), privIP) expect.Equal(t, r.TargetURL().Hostname(), privIP)
expect.Equal(t, r.Port.Listening, 0) expect.Equal(t, r.Port.Listening, 0)
expect.Equal(t, r.Port.Proxy, int(privPort)) expect.Equal(t, r.Port.Proxy, int(privPort))
@@ -337,7 +337,7 @@ func TestStreamDefaultValues(t *testing.T) {
r, ok := makeRoutes(cont, testIP)["a"] r, ok := makeRoutes(cont, testIP)["a"]
expect.True(t, ok) expect.True(t, ok)
expect.NoError(t, r.Validate()) expect.NoError(t, r.Validate())
expect.Equal(t, r.Scheme, T.Scheme("udp")) expect.Equal(t, r.Scheme, routeTypes.SchemeUDP)
expect.Equal(t, r.TargetURL().Hostname(), testIP) expect.Equal(t, r.TargetURL().Hostname(), testIP)
expect.Equal(t, r.Port.Listening, 0) expect.Equal(t, r.Port.Listening, 0)
expect.Equal(t, r.Port.Proxy, int(pubPort)) expect.Equal(t, r.Port.Proxy, int(pubPort))

View File

@@ -8,6 +8,7 @@ import (
"os" "os"
"reflect" "reflect"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -40,7 +41,7 @@ type (
_ utils.NoCopy _ utils.NoCopy
Alias string `json:"alias"` Alias string `json:"alias"`
Scheme route.Scheme `json:"scheme,omitempty"` Scheme route.Scheme `json:"scheme,omitempty" swaggertype:"string"`
Host string `json:"host,omitempty"` Host string `json:"host,omitempty"`
Port route.Port `json:"port"` Port route.Port `json:"port"`
Root string `json:"root,omitempty"` Root string `json:"root,omitempty"`
@@ -71,8 +72,8 @@ type (
LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"` LisURL *nettypes.URL `json:"lurl,omitempty" swaggertype:"string" extensions:"x-nullable"`
ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"` ProxyURL *nettypes.URL `json:"purl,omitempty" swaggertype:"string"`
Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"` Excluded bool `json:"excluded,omitempty" extensions:"x-nullable"`
ExcludedReason string `json:"excluded_reason,omitempty" extensions:"x-nullable"` ExcludedReason ExcludedReason `json:"excluded_reason,omitempty" extensions:"x-nullable"`
HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"` HealthMon types.HealthMonitor `json:"health,omitempty" swaggerignore:"true"`
// for swagger // for swagger
@@ -272,7 +273,7 @@ func (r *Route) Validate() gperr.Error {
r.impl = impl r.impl = impl
r.Excluded = r.ShouldExclude() r.Excluded = r.ShouldExclude()
if r.Excluded { if r.Excluded {
r.ExcludedReason = r.GetExcludedReason() r.ExcludedReason = r.findExcludedReason()
} }
return nil return nil
} }
@@ -518,31 +519,73 @@ func (r *Route) ShouldExclude() bool {
return false return false
} }
func (r *Route) GetExcludedReason() string { type ExcludedReason uint8
if r.lastError != nil {
return string(gperr.Plain(r.lastError)) const (
ExcludedReasonNone ExcludedReason = iota
ExcludedReasonError
ExcludedReasonManual
ExcludedReasonNoPortContainer
ExcludedReasonNoPortSpecified
ExcludedReasonBlacklisted
ExcludedReasonBuildx
ExcludedReasonOld
)
func (re ExcludedReason) String() string {
switch re {
case ExcludedReasonNone:
return ""
case ExcludedReasonError:
return "Error"
case ExcludedReasonManual:
return "Manual exclusion"
case ExcludedReasonNoPortContainer:
return "No port exposed in container"
case ExcludedReasonNoPortSpecified:
return "No port specified"
case ExcludedReasonBlacklisted:
return "Blacklisted (backend service or database)"
case ExcludedReasonBuildx:
return "Buildx"
case ExcludedReasonOld:
return "Container renaming intermediate state"
default:
return "Unknown"
} }
if r.ExcludedReason != "" { }
func (re ExcludedReason) MarshalJSON() ([]byte, error) {
return strconv.AppendQuote(nil, re.String()), nil
}
// no need to unmarshal json because we don't store this
func (r *Route) findExcludedReason() ExcludedReason {
if r.lastError != nil {
return ExcludedReasonError
}
if r.ExcludedReason != ExcludedReasonNone {
return r.ExcludedReason return r.ExcludedReason
} }
if r.Container != nil { if r.Container != nil {
switch { switch {
case r.Container.IsExcluded: case r.Container.IsExcluded:
return "Manual exclusion" return ExcludedReasonManual
case r.IsZeroPort() && !r.UseIdleWatcher(): case r.IsZeroPort() && !r.UseIdleWatcher():
return "No port exposed in container" return ExcludedReasonNoPortContainer
case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container): case !r.Container.IsExplicit && docker.IsBlacklisted(r.Container):
return "Blacklisted (backend service or database)" return ExcludedReasonBlacklisted
case strings.HasPrefix(r.Container.ContainerName, "buildx_"): case strings.HasPrefix(r.Container.ContainerName, "buildx_"):
return "Buildx" return ExcludedReasonBuildx
} }
} else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer { } else if r.IsZeroPort() && r.Scheme != route.SchemeFileServer {
return "No port specified" return ExcludedReasonNoPortSpecified
} }
if strings.HasSuffix(r.Alias, "-old") { if strings.HasSuffix(r.Alias, "-old") {
return "Container renaming intermediate state" return ExcludedReasonOld
} }
return "" return ExcludedReasonNone
} }
func (r *Route) UseLoadBalance() bool { func (r *Route) UseLoadBalance() bool {
@@ -594,8 +637,8 @@ func (r *Route) Finalize() {
if isDocker { if isDocker {
scheme, port, ok := getSchemePortByImageName(cont.Image.Name) scheme, port, ok := getSchemePortByImageName(cont.Image.Name)
if ok { if ok {
if r.Scheme == "" { if r.Scheme == route.SchemeNone {
r.Scheme = route.Scheme(scheme) r.Scheme = scheme
} }
if pp == 0 { if pp == 0 {
pp = port pp = port
@@ -604,8 +647,8 @@ func (r *Route) Finalize() {
} }
if scheme, port, ok := getSchemePortByAlias(r.Alias); ok { if scheme, port, ok := getSchemePortByAlias(r.Alias); ok {
if r.Scheme == "" { if r.Scheme == route.SchemeNone {
r.Scheme = route.Scheme(scheme) r.Scheme = scheme
} }
if pp == 0 { if pp == 0 {
pp = port pp = port
@@ -620,7 +663,7 @@ func (r *Route) Finalize() {
} else { } else {
pp = preferredPort(cont.PrivatePortMapping) pp = preferredPort(cont.PrivatePortMapping)
} }
case r.Scheme == "https": case r.Scheme == route.SchemeHTTPS:
pp = 443 pp = 443
default: default:
pp = 80 pp = 80
@@ -628,10 +671,10 @@ func (r *Route) Finalize() {
} }
if isDocker { if isDocker {
if r.Scheme == "" { if r.Scheme == route.SchemeNone {
for _, p := range cont.PublicPortMapping { for _, p := range cont.PublicPortMapping {
if int(p.PrivatePort) == pp && p.Type == "udp" { if int(p.PrivatePort) == pp && p.Type == "udp" {
r.Scheme = "udp" r.Scheme = route.SchemeUDP
break break
} }
} }
@@ -649,14 +692,14 @@ func (r *Route) Finalize() {
} }
} }
if r.Scheme == "" { if r.Scheme == route.SchemeNone {
switch { switch {
case lp != 0: case lp != 0:
r.Scheme = "tcp" r.Scheme = route.SchemeTCP
case pp%1000 == 443: case pp%1000 == 443:
r.Scheme = "https" r.Scheme = route.SchemeHTTPS
default: // assume its http default: // assume its http
r.Scheme = "http" r.Scheme = route.SchemeHTTP
} }
} }

View File

@@ -113,7 +113,7 @@ func TestRouteValidate(t *testing.T) {
t.Run("InvalidScheme", func(t *testing.T) { t.Run("InvalidScheme", func(t *testing.T) {
r := &Route{ r := &Route{
Alias: "test", Alias: "test",
Scheme: "invalid", Scheme: 123,
Host: "example.com", Host: "example.com",
Port: route.Port{Proxy: 80}, Port: route.Port{Proxy: 80},
} }

View File

@@ -1,29 +1,82 @@
package route package route
import ( import (
"strconv"
"github.com/bytedance/sonic"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
) )
type Scheme string type Scheme uint8
var ErrInvalidScheme = gperr.New("invalid scheme") var ErrInvalidScheme = gperr.New("invalid scheme")
const ( const (
SchemeHTTP Scheme = "http" SchemeHTTP Scheme = 1 << iota
SchemeHTTPS Scheme = "https" SchemeHTTPS
SchemeTCP Scheme = "tcp" SchemeTCP
SchemeUDP Scheme = "udp" SchemeUDP
SchemeFileServer Scheme = "fileserver" SchemeFileServer
SchemeNone Scheme = 0
schemeReverseProxy = SchemeHTTP | SchemeHTTPS
schemeStream = SchemeTCP | SchemeUDP
schemeStrHTTP = "http"
schemeStrHTTPS = "https"
schemeStrTCP = "tcp"
schemeStrUDP = "udp"
schemeStrFileServer = "fileserver"
schemeStrUnknown = "unknown"
) )
func (s Scheme) Validate() gperr.Error { func (s Scheme) String() string {
switch s { switch s {
case SchemeHTTP, SchemeHTTPS, case SchemeHTTP:
SchemeTCP, SchemeUDP, SchemeFileServer: return schemeStrHTTP
return nil case SchemeHTTPS:
return schemeStrHTTPS
case SchemeTCP:
return schemeStrTCP
case SchemeUDP:
return schemeStrUDP
case SchemeFileServer:
return schemeStrFileServer
default:
return schemeStrUnknown
} }
return ErrInvalidScheme.Subject(string(s))
} }
func (s Scheme) IsReverseProxy() bool { return s == SchemeHTTP || s == SchemeHTTPS } func (s Scheme) MarshalJSON() ([]byte, error) {
func (s Scheme) IsStream() bool { return s == SchemeTCP || s == SchemeUDP } return strconv.AppendQuote(nil, s.String()), nil
}
func (s *Scheme) UnmarshalJSON(data []byte) error {
var v string
if err := sonic.Unmarshal(data, &v); err != nil {
return err
}
return s.Parse(v)
}
// Parse implements strutils.Parser
func (s *Scheme) Parse(v string) error {
switch v {
case schemeStrHTTP:
*s = SchemeHTTP
case schemeStrHTTPS:
*s = SchemeHTTPS
case schemeStrTCP:
*s = SchemeTCP
case schemeStrUDP:
*s = SchemeUDP
case schemeStrFileServer:
*s = SchemeFileServer
default:
return ErrInvalidScheme.Subject(v)
}
return nil
}
func (s Scheme) IsReverseProxy() bool { return s&schemeReverseProxy != 0 }
func (s Scheme) IsStream() bool { return s&schemeStream != 0 }