From f28667e23e59a689ddecad168170f4da0f60693a Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 2 Jan 2026 17:41:36 +0800 Subject: [PATCH 1/5] refactor: add context handling in various functions - Modified functions to accept context.Context as a parameter for better context management. - Updated Init methods in Proxmox and Config to use the provided context. - Adjusted UpdatePorts and NewProxmoxProvider to utilize the context for operations. --- internal/config/state.go | 2 +- internal/docker/container.go | 4 ++-- internal/idlewatcher/provider/proxmox.go | 4 ++-- internal/proxmox/config.go | 4 ++-- internal/route/provider/docker.go | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/config/state.go b/internal/config/state.go index ffc92dfd..8d0b5d41 100644 --- a/internal/config/state.go +++ b/internal/config/state.go @@ -292,7 +292,7 @@ func (state *state) initProxmox() error { errs := gperr.NewBuilder() for _, cfg := range proxmoxCfg { - if err := cfg.Init(); err != nil { + if err := cfg.Init(state.task.Context()); err != nil { errs.Add(err.Subject(cfg.URL)) } } diff --git a/internal/docker/container.go b/internal/docker/container.go index c56b7602..a36f4c87 100644 --- a/internal/docker/container.go +++ b/internal/docker/container.go @@ -91,14 +91,14 @@ func IsBlacklisted(c *types.Container) bool { return IsBlacklistedImage(c.Image) || isDatabase(c) } -func UpdatePorts(c *types.Container) error { +func UpdatePorts(ctx context.Context, c *types.Container) error { dockerClient, err := NewClient(c.DockerCfg) if err != nil { return err } defer dockerClient.Close() - inspect, err := dockerClient.ContainerInspect(context.Background(), c.ContainerID, client.ContainerInspectOptions{}) + inspect, err := dockerClient.ContainerInspect(ctx, c.ContainerID, client.ContainerInspectOptions{}) if err != nil { return err } diff --git a/internal/idlewatcher/provider/proxmox.go b/internal/idlewatcher/provider/proxmox.go index b3ac35d2..d781768f 100644 --- a/internal/idlewatcher/provider/proxmox.go +++ b/internal/idlewatcher/provider/proxmox.go @@ -25,14 +25,14 @@ const proxmoxStateCheckInterval = 1 * time.Second var ErrNodeNotFound = gperr.New("node not found in pool") -func NewProxmoxProvider(nodeName string, vmid int) (idlewatcher.Provider, error) { +func NewProxmoxProvider(ctx context.Context, nodeName string, vmid int) (idlewatcher.Provider, error) { node, ok := proxmox.Nodes.Get(nodeName) if !ok { return nil, ErrNodeNotFound.Subject(nodeName). Withf("available nodes: %s", proxmox.AvailableNodeNames()) } - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() lxcName, err := node.LXCName(ctx, vmid) diff --git a/internal/proxmox/config.go b/internal/proxmox/config.go index 4417c9f9..4972b5ac 100644 --- a/internal/proxmox/config.go +++ b/internal/proxmox/config.go @@ -32,7 +32,7 @@ func (c *Config) Client() *Client { return c.client } -func (c *Config) Init() gperr.Error { +func (c *Config) Init(ctx context.Context) gperr.Error { var tr *http.Transport if c.NoTLSVerify { // user specified @@ -56,7 +56,7 @@ func (c *Config) Init() gperr.Error { } c.client = NewClient(c.URL, opts...) - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() if err := c.client.UpdateClusterInfo(ctx); err != nil { diff --git a/internal/route/provider/docker.go b/internal/route/provider/docker.go index f9451ef8..9f3f111f 100755 --- a/internal/route/provider/docker.go +++ b/internal/route/provider/docker.go @@ -79,7 +79,7 @@ func (p *DockerProvider) loadRoutesImpl() (route.Routes, gperr.Error) { } if container.IsHostNetworkMode { - err := docker.UpdatePorts(container) + err := docker.UpdatePorts(ctx, container) if err != nil { errs.Add(gperr.PrependSubject(container.ContainerName, err)) continue From dd35a4159fa2150def048a24d3f7adb2aff72743 Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 2 Jan 2026 18:02:49 +0800 Subject: [PATCH 2/5] refactor(api/health): simplify health info type - Updated health-related functions to return simplified health information. - Introduced HealthStatusString type for correct swagger and schema generation. - Refactored HealthJSON structure to utilize the new HealthStatusString type. --- internal/api/v1/docs/swagger.json | 88 ++++++++++--------------------- internal/api/v1/docs/swagger.yaml | 54 ++++++++----------- internal/api/v1/health.go | 8 ++- internal/route/routes/query.go | 18 +++++++ internal/types/health.go | 83 ++++++++++++++--------------- 5 files changed, 112 insertions(+), 139 deletions(-) diff --git a/internal/api/v1/docs/swagger.json b/internal/api/v1/docs/swagger.json index 3ab3464a..da35ecc3 100644 --- a/internal/api/v1/docs/swagger.json +++ b/internal/api/v1/docs/swagger.json @@ -2956,43 +2956,6 @@ "x-nullable": false, "x-omitempty": false }, - "HealthInfo": { - "type": "object", - "properties": { - "detail": { - "type": "string", - "x-nullable": false, - "x-omitempty": false - }, - "latency": { - "description": "latency in microseconds", - "type": "number", - "x-nullable": false, - "x-omitempty": false - }, - "status": { - "type": "string", - "enum": [ - "healthy", - "unhealthy", - "napping", - "starting", - "error", - "unknown" - ], - "x-nullable": false, - "x-omitempty": false - }, - "uptime": { - "description": "uptime in milliseconds", - "type": "number", - "x-nullable": false, - "x-omitempty": false - } - }, - "x-nullable": false, - "x-omitempty": false - }, "HealthInfoWithoutDetail": { "type": "object", "properties": { @@ -3047,22 +3010,14 @@ "x-nullable": true }, "lastSeen": { + "description": "unix timestamp in seconds", "type": "integer", "x-nullable": false, "x-omitempty": false }, - "lastSeenStr": { - "type": "string", - "x-nullable": false, - "x-omitempty": false - }, "latency": { - "type": "number", - "x-nullable": false, - "x-omitempty": false - }, - "latencyStr": { - "type": "string", + "description": "latency in milliseconds", + "type": "integer", "x-nullable": false, "x-omitempty": false }, @@ -3072,30 +3027,22 @@ "x-omitempty": false }, "started": { + "description": "unix timestamp in seconds", "type": "integer", "x-nullable": false, "x-omitempty": false }, - "startedStr": { - "type": "string", - "x-nullable": false, - "x-omitempty": false - }, "status": { - "type": "string", + "$ref": "#/definitions/HealthStatusString", "x-nullable": false, "x-omitempty": false }, "uptime": { + "description": "uptime in seconds", "type": "number", "x-nullable": false, "x-omitempty": false }, - "uptimeStr": { - "type": "string", - "x-nullable": false, - "x-omitempty": false - }, "url": { "type": "string", "x-nullable": false, @@ -3108,11 +3055,32 @@ "HealthMap": { "type": "object", "additionalProperties": { - "$ref": "#/definitions/HealthInfo" + "$ref": "#/definitions/HealthStatusString" }, "x-nullable": false, "x-omitempty": false }, + "HealthStatusString": { + "type": "string", + "enum": [ + "unknown", + "healthy", + "napping", + "starting", + "unhealthy", + "error" + ], + "x-enum-varnames": [ + "StatusUnknownStr", + "StatusHealthyStr", + "StatusNappingStr", + "StatusStartingStr", + "StatusUnhealthyStr", + "StatusErrorStr" + ], + "x-nullable": false, + "x-omitempty": false + }, "HomepageCategory": { "type": "object", "properties": { diff --git a/internal/api/v1/docs/swagger.yaml b/internal/api/v1/docs/swagger.yaml index 23ebc20c..1865cedd 100644 --- a/internal/api/v1/docs/swagger.yaml +++ b/internal/api/v1/docs/swagger.yaml @@ -302,26 +302,6 @@ definitions: additionalProperties: {} type: object type: object - HealthInfo: - properties: - detail: - type: string - latency: - description: latency in microseconds - type: number - status: - enum: - - healthy - - unhealthy - - napping - - starting - - error - - unknown - type: string - uptime: - description: uptime in milliseconds - type: number - type: object HealthInfoWithoutDetail: properties: latency: @@ -351,32 +331,44 @@ definitions: - $ref: '#/definitions/HealthExtra' x-nullable: true lastSeen: + description: unix timestamp in seconds type: integer - lastSeenStr: - type: string latency: - type: number - latencyStr: - type: string + description: latency in milliseconds + type: integer name: type: string started: + description: unix timestamp in seconds type: integer - startedStr: - type: string status: - type: string + $ref: '#/definitions/HealthStatusString' uptime: + description: uptime in seconds type: number - uptimeStr: - type: string url: type: string type: object HealthMap: additionalProperties: - $ref: '#/definitions/HealthInfo' + $ref: '#/definitions/HealthStatusString' type: object + HealthStatusString: + enum: + - unknown + - healthy + - napping + - starting + - unhealthy + - error + type: string + x-enum-varnames: + - StatusUnknownStr + - StatusHealthyStr + - StatusNappingStr + - StatusStartingStr + - StatusUnhealthyStr + - StatusErrorStr HomepageCategory: properties: items: diff --git a/internal/api/v1/health.go b/internal/api/v1/health.go index 7bc35618..6fc19bfa 100644 --- a/internal/api/v1/health.go +++ b/internal/api/v1/health.go @@ -12,8 +12,6 @@ import ( _ "github.com/yusing/goutils/apitypes" ) -type HealthMap = map[string]routes.HealthInfo // @name HealthMap - // @x-id "health" // @BasePath /api/v1 // @Summary Get routes health info @@ -21,16 +19,16 @@ type HealthMap = map[string]routes.HealthInfo // @name HealthMap // @Tags v1,websocket // @Accept json // @Produce json -// @Success 200 {object} HealthMap "Health info by route name" +// @Success 200 {object} routes.HealthMap "Health info by route name" // @Failure 403 {object} apitypes.ErrorResponse // @Failure 500 {object} apitypes.ErrorResponse // @Router /health [get] func Health(c *gin.Context) { if httpheaders.IsWebsocket(c.Request.Header) { websocket.PeriodicWrite(c, 1*time.Second, func() (any, error) { - return routes.GetHealthInfo(), nil + return routes.GetHealthInfoSimple(), nil }) } else { - c.JSON(http.StatusOK, routes.GetHealthInfo()) + c.JSON(http.StatusOK, routes.GetHealthInfoSimple()) } } diff --git a/internal/route/routes/query.go b/internal/route/routes/query.go index 51d9c98a..50849c9a 100644 --- a/internal/route/routes/query.go +++ b/internal/route/routes/query.go @@ -17,6 +17,8 @@ type HealthInfoWithoutDetail struct { Latency time.Duration `json:"latency" swaggertype:"number"` // latency in microseconds } // @name HealthInfoWithoutDetail +type HealthMap = map[string]types.HealthStatusString // @name HealthMap + // GetHealthInfo returns a map of route name to health info. // // The health info is for all routes, including excluded routes. @@ -39,6 +41,14 @@ func GetHealthInfoWithoutDetail() map[string]HealthInfoWithoutDetail { return healthMap } +func GetHealthInfoSimple() map[string]types.HealthStatus { + healthMap := make(map[string]types.HealthStatus, NumAllRoutes()) + for r := range IterAll { + healthMap[r.Name()] = getHealthInfoSimple(r) + } + return healthMap +} + func getHealthInfo(r types.Route) HealthInfo { mon := r.HealthMonitor() if mon == nil { @@ -73,6 +83,14 @@ func getHealthInfoWithoutDetail(r types.Route) HealthInfoWithoutDetail { } } +func getHealthInfoSimple(r types.Route) types.HealthStatus { + mon := r.HealthMonitor() + if mon == nil { + return types.StatusUnknown + } + return mon.Status() +} + // ByProvider returns a map of provider name to routes. // // The routes are all routes, including excluded routes. diff --git a/internal/types/health.go b/internal/types/health.go index 797c08f3..be45b3f5 100644 --- a/internal/types/health.go +++ b/internal/types/health.go @@ -8,12 +8,12 @@ import ( "time" "github.com/bytedance/sonic" - strutils "github.com/yusing/goutils/strings" "github.com/yusing/goutils/task" ) type ( - HealthStatus uint8 + HealthStatus uint8 // @name HealthStatus + HealthStatusString string // @name HealthStatusString HealthCheckResult struct { Healthy bool `json:"healthy"` @@ -45,20 +45,16 @@ type ( HealthChecker } HealthJSON struct { - Name string `json:"name"` - Config *HealthCheckConfig `json:"config"` - Started int64 `json:"started"` - StartedStr string `json:"startedStr"` - Status string `json:"status"` - Uptime float64 `json:"uptime"` - UptimeStr string `json:"uptimeStr"` - Latency float64 `json:"latency"` - LatencyStr string `json:"latencyStr"` - LastSeen int64 `json:"lastSeen"` - LastSeenStr string `json:"lastSeenStr"` - Detail string `json:"detail"` - URL string `json:"url"` - Extra *HealthExtra `json:"extra,omitempty" extensions:"x-nullable"` + Name string `json:"name"` + Config *HealthCheckConfig `json:"config"` + Started int64 `json:"started"` // unix timestamp in seconds + Status HealthStatusString `json:"status"` + Uptime float64 `json:"uptime"` // uptime in seconds + Latency int64 `json:"latency"` // latency in milliseconds + LastSeen int64 `json:"lastSeen"` // unix timestamp in seconds + Detail string `json:"detail"` + URL string `json:"url"` + Extra *HealthExtra `json:"extra,omitempty" extensions:"x-nullable"` } // @name HealthJSON HealthJSONRepr struct { @@ -88,12 +84,12 @@ const ( StatusUnhealthy StatusError - StatusUnknownStr = "unknown" - StatusHealthyStr = "healthy" - StatusNappingStr = "napping" - StatusStartingStr = "starting" - StatusUnhealthyStr = "unhealthy" - StatusErrorStr = "error" + StatusUnknownStr HealthStatusString = "unknown" + StatusHealthyStr HealthStatusString = "healthy" + StatusNappingStr HealthStatusString = "napping" + StatusStartingStr HealthStatusString = "starting" + StatusUnhealthyStr HealthStatusString = "unhealthy" + StatusErrorStr HealthStatusString = "error" NumStatuses int = iota - 1 @@ -102,15 +98,15 @@ const ( ) var ( - StatusHealthyStr2 = strconv.Itoa(int(StatusHealthy)) - StatusNappingStr2 = strconv.Itoa(int(StatusNapping)) - StatusStartingStr2 = strconv.Itoa(int(StatusStarting)) - StatusUnhealthyStr2 = strconv.Itoa(int(StatusUnhealthy)) - StatusErrorStr2 = strconv.Itoa(int(StatusError)) + StatusHealthyStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusHealthy))) + StatusNappingStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusNapping))) + StatusStartingStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusStarting))) + StatusUnhealthyStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusUnhealthy))) + StatusErrorStr2 HealthStatusString = HealthStatusString(strconv.Itoa(int(StatusError))) ) func NewHealthStatusFromString(s string) HealthStatus { - switch s { + switch HealthStatusString(s) { case StatusHealthyStr, StatusHealthyStr2: return StatusHealthy case StatusUnhealthyStr, StatusUnhealthyStr2: @@ -126,7 +122,7 @@ func NewHealthStatusFromString(s string) HealthStatus { } } -func (s HealthStatus) String() string { +func (s HealthStatus) StatusString() HealthStatusString { switch s { case StatusHealthy: return StatusHealthyStr @@ -143,6 +139,11 @@ func (s HealthStatus) String() string { } } +// String implements fmt.Stringer. +func (s HealthStatus) String() string { + return string(s.StatusString()) +} + func (s HealthStatus) Good() bool { return s&HealthyMask != 0 } @@ -178,19 +179,15 @@ func (jsonRepr *HealthJSONRepr) MarshalJSON() ([]byte, error) { url = "" } return sonic.Marshal(HealthJSON{ - Name: jsonRepr.Name, - Config: jsonRepr.Config, - Started: jsonRepr.Started.Unix(), - StartedStr: strutils.FormatTime(jsonRepr.Started), - Status: jsonRepr.Status.String(), - Uptime: jsonRepr.Uptime.Seconds(), - UptimeStr: strutils.FormatDuration(jsonRepr.Uptime), - Latency: jsonRepr.Latency.Seconds(), - LatencyStr: strconv.Itoa(int(jsonRepr.Latency.Milliseconds())) + " ms", - LastSeen: jsonRepr.LastSeen.Unix(), - LastSeenStr: strutils.FormatLastSeen(jsonRepr.LastSeen), - Detail: jsonRepr.Detail, - URL: url, - Extra: jsonRepr.Extra, + Name: jsonRepr.Name, + Config: jsonRepr.Config, + Started: jsonRepr.Started.Unix(), + Status: HealthStatusString(jsonRepr.Status.String()), + Uptime: jsonRepr.Uptime.Seconds(), + Latency: jsonRepr.Latency.Milliseconds(), + LastSeen: jsonRepr.LastSeen.Unix(), + Detail: jsonRepr.Detail, + URL: url, + Extra: jsonRepr.Extra, }) } From 7eadec975266aac97bd502040ea81250bcca729d Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 2 Jan 2026 18:03:13 +0800 Subject: [PATCH 3/5] chore: remove unused utils/deep_equal.go --- internal/utils/deep_equal.go | 243 ----------------------------------- 1 file changed, 243 deletions(-) delete mode 100644 internal/utils/deep_equal.go diff --git a/internal/utils/deep_equal.go b/internal/utils/deep_equal.go deleted file mode 100644 index 72b5b12b..00000000 --- a/internal/utils/deep_equal.go +++ /dev/null @@ -1,243 +0,0 @@ -package utils - -import ( - "reflect" - "unsafe" -) - -// DeepEqual reports whether x and y are deeply equal. -// It supports numerics, strings, maps, slices, arrays, and structs (exported fields only). -// It's optimized for performance by avoiding reflection for common types and -// adaptively choosing between BFS and DFS traversal strategies. -func DeepEqual(x, y any) bool { - if x == nil || y == nil { - return x == y - } - - v1 := reflect.ValueOf(x) - v2 := reflect.ValueOf(y) - - if v1.Type() != v2.Type() { - return false - } - - return deepEqual(v1, v2, make(map[visit]bool), 0) -} - -// visit represents a visit to a pair of values during comparison -type visit struct { - a1, a2 unsafe.Pointer - typ reflect.Type -} - -// deepEqual performs the actual deep comparison with cycle detection -func deepEqual(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { - if !v1.IsValid() || !v2.IsValid() { - return v1.IsValid() == v2.IsValid() - } - - if v1.Type() != v2.Type() { - return false - } - - // Handle cycle detection for pointer-like types - if v1.CanAddr() && v2.CanAddr() { - addr1 := unsafe.Pointer(v1.UnsafeAddr()) - addr2 := unsafe.Pointer(v2.UnsafeAddr()) - typ := v1.Type() - v := visit{addr1, addr2, typ} - if visited[v] { - return true // already visiting, assume equal - } - visited[v] = true - defer delete(visited, v) - } - - switch v1.Kind() { - case reflect.Bool: - return v1.Bool() == v2.Bool() - - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v1.Int() == v2.Int() - - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v1.Uint() == v2.Uint() - - case reflect.Float32, reflect.Float64: - return floatEqual(v1.Float(), v2.Float()) - - case reflect.Complex64, reflect.Complex128: - c1, c2 := v1.Complex(), v2.Complex() - return floatEqual(real(c1), real(c2)) && floatEqual(imag(c1), imag(c2)) - - case reflect.String: - return v1.String() == v2.String() - - case reflect.Array: - return deepEqualArray(v1, v2, visited, depth) - - case reflect.Slice: - return deepEqualSlice(v1, v2, visited, depth) - - case reflect.Map: - return deepEqualMap(v1, v2, visited, depth) - - case reflect.Struct: - return deepEqualStruct(v1, v2, visited, depth) - - case reflect.Ptr: - if v1.IsNil() || v2.IsNil() { - return v1.IsNil() && v2.IsNil() - } - return deepEqual(v1.Elem(), v2.Elem(), visited, depth+1) - - case reflect.Interface: - if v1.IsNil() || v2.IsNil() { - return v1.IsNil() && v2.IsNil() - } - return deepEqual(v1.Elem(), v2.Elem(), visited, depth+1) - - default: - // For unsupported types (func, chan, etc.), fall back to basic equality - return v1.Interface() == v2.Interface() - } -} - -// floatEqual handles NaN cases properly -func floatEqual(f1, f2 float64) bool { - return f1 == f2 || (f1 != f1 && f2 != f2) // NaN == NaN -} - -// deepEqualArray compares arrays using DFS (since arrays have fixed size) -func deepEqualArray(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { - for i := range v1.Len() { - if !deepEqual(v1.Index(i), v2.Index(i), visited, depth+1) { - return false - } - } - return true -} - -// deepEqualSlice compares slices, choosing strategy based on size and depth -func deepEqualSlice(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { - if v1.IsNil() != v2.IsNil() { - return false - } - if v1.Len() != v2.Len() { - return false - } - if v1.IsNil() { - return true - } - - // Use BFS for large slices at shallow depth to improve cache locality - // Use DFS for small slices or deep nesting to reduce memory overhead - if shouldUseBFS(v1.Len(), depth) { - return deepEqualSliceBFS(v1, v2, visited, depth) - } - return deepEqualSliceDFS(v1, v2, visited, depth) -} - -// deepEqualSliceDFS uses depth-first traversal -func deepEqualSliceDFS(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { - for i := range v1.Len() { - if !deepEqual(v1.Index(i), v2.Index(i), visited, depth+1) { - return false - } - } - return true -} - -// deepEqualSliceBFS uses breadth-first traversal for better cache locality -func deepEqualSliceBFS(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { - length := v1.Len() - - // First, check all direct elements - for i := range length { - elem1, elem2 := v1.Index(i), v2.Index(i) - - // For simple types, compare directly - if isSimpleType(elem1.Kind()) { - if !deepEqual(elem1, elem2, visited, depth+1) { - return false - } - } - } - - // Then, recursively check complex elements - for i := range length { - elem1, elem2 := v1.Index(i), v2.Index(i) - - if !isSimpleType(elem1.Kind()) { - if !deepEqual(elem1, elem2, visited, depth+1) { - return false - } - } - } - - return true -} - -// deepEqualMap compares maps -func deepEqualMap(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { - if v1.IsNil() != v2.IsNil() { - return false - } - if v1.Len() != v2.Len() { - return false - } - if v1.IsNil() { - return true - } - - // Check all keys and values - for _, key := range v1.MapKeys() { - val1 := v1.MapIndex(key) - val2 := v2.MapIndex(key) - - if !val2.IsValid() { - return false // key doesn't exist in v2 - } - - if !deepEqual(val1, val2, visited, depth+1) { - return false - } - } - - return true -} - -// deepEqualStruct compares structs (exported fields only) -func deepEqualStruct(v1, v2 reflect.Value, visited map[visit]bool, depth int) bool { - typ := v1.Type() - - for i := range typ.NumField() { - field := typ.Field(i) - - // Skip unexported fields - if !field.IsExported() { - continue - } - - if !deepEqual(v1.Field(i), v2.Field(i), visited, depth+1) { - return false - } - } - - return true -} - -// shouldUseBFS determines whether to use BFS or DFS based on slice size and depth -func shouldUseBFS(length, depth int) bool { - // Use BFS for large slices at shallow depth (better cache locality) - // Use DFS for small slices or deep nesting (lower memory overhead) - return length > 100 && depth < 3 -} - -// isSimpleType checks if a type can be compared without deep recursion -func isSimpleType(kind reflect.Kind) bool { - if kind >= reflect.Bool && kind <= reflect.Complex128 { - return true - } - return kind == reflect.String -} From ef9ee0e1691b7f8320546a19cd604dc3c06e0bf4 Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 2 Jan 2026 18:04:08 +0800 Subject: [PATCH 4/5] feat(websocket): update goutils - deduplicate data to avoid unnecessary traffic --- goutils | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/goutils b/goutils index 486810fc..19965cc6 160000 --- a/goutils +++ b/goutils @@ -1 +1 @@ -Subproject commit 486810fc6a9ffb943597a27b279067880abc4ab3 +Subproject commit 19965cc6afc016fa41581bedbfc04695e2c726b4 From a4658caf02b55c63d6a7bb1dae7fbcf4baddbc87 Mon Sep 17 00:00:00 2001 From: yusing Date: Fri, 2 Jan 2026 21:56:34 +0800 Subject: [PATCH 5/5] refactor(config): correct logic in InitFromFile --- internal/config/events.go | 7 ------- internal/config/state.go | 12 ++++++++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/internal/config/events.go b/internal/config/events.go index 46755645..793a1036 100644 --- a/internal/config/events.go +++ b/internal/config/events.go @@ -3,12 +3,10 @@ package config import ( "errors" "fmt" - "io/fs" "sync" "time" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "github.com/yusing/godoxy/internal/common" config "github.com/yusing/godoxy/internal/config/types" "github.com/yusing/godoxy/internal/notif" @@ -62,11 +60,6 @@ func Load() error { cfgWatcher = watcher.NewConfigFileWatcher(common.ConfigFileName) initErr := state.InitFromFile(common.ConfigPath) - if errors.Is(initErr, fs.ErrNotExist) { - // log only - log.Warn().Msg("config file not found, using default config") - initErr = nil - } err := errors.Join(initErr, state.StartProviders()) if err != nil { logNotifyError("init", err) diff --git a/internal/config/state.go b/internal/config/state.go index 8d0b5d41..72633a5a 100644 --- a/internal/config/state.go +++ b/internal/config/state.go @@ -5,7 +5,9 @@ import ( "context" "crypto/tls" "crypto/x509" + "errors" "fmt" + "io/fs" "iter" "net/http" "os" @@ -19,7 +21,6 @@ import ( "github.com/yusing/godoxy/agent/pkg/agent" "github.com/yusing/godoxy/internal/acl" "github.com/yusing/godoxy/internal/autocert" - "github.com/yusing/godoxy/internal/common" config "github.com/yusing/godoxy/internal/config/types" "github.com/yusing/godoxy/internal/entrypoint" homepage "github.com/yusing/godoxy/internal/homepage/types" @@ -92,10 +93,13 @@ func Value() *config.Config { } func (state *state) InitFromFile(filename string) error { - data, err := os.ReadFile(common.ConfigPath) + data, err := os.ReadFile(filename) if err != nil { - state.Config = config.DefaultConfig() - return err + if errors.Is(err, fs.ErrNotExist) { + state.Config = config.DefaultConfig() + } else { + return err + } } return state.Init(data) }