From 26d21ebfcb2a056d58407c49487c99415017976b Mon Sep 17 00:00:00 2001 From: Noxiven <41963680+Noxiven@users.noreply.github.com> Date: Wed, 10 Jun 2026 21:10:07 +0800 Subject: [PATCH] feat: implement probe services for scheduler and worker --- .github/workflows/build_backend.yml | 2 +- .github/workflows/build_image.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- README.md | 11 ++-- README_zh.md | 11 ++-- config.example.yaml | 2 + docs/docs.go | 24 ++++++++ docs/swagger.json | 24 ++++++++ docs/swagger.yaml | 15 +++++ go.mod | 2 +- internal/apps/health/routers.go | 55 ++++++++++++++++++ internal/cmd/scheduler.go | 5 ++ internal/cmd/worker.go | 5 ++ internal/config/model.go | 2 + internal/probe/server.go | 71 +++++++++++++++++++++++ internal/router/middlewares.go | 9 ++- internal/router/router.go | 1 + 17 files changed, 228 insertions(+), 15 deletions(-) create mode 100644 internal/probe/server.go diff --git a/.github/workflows/build_backend.yml b/.github/workflows/build_backend.yml index 44885366..5d88d96d 100644 --- a/.github/workflows/build_backend.yml +++ b/.github/workflows/build_backend.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.26" check-latest: true - name: Install dependencies diff --git a/.github/workflows/build_image.yml b/.github/workflows/build_image.yml index 47c0c964..29ad9cb6 100644 --- a/.github/workflows/build_image.yml +++ b/.github/workflows/build_image.yml @@ -29,7 +29,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.26" check-latest: true # download Go modules diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 9055760c..4aec2afc 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -39,7 +39,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.25" + go-version: "1.26" check-latest: true - name: Install dependencies diff --git a/README.md b/README.md index ec19b726..18fdbff1 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [中文](./README_zh.md) [![License: Apache2.0](https://img.shields.io/badge/License-Apache2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![Go Version](https://img.shields.io/badge/Go-1.25.5-blue.svg)](https://golang.org/) +[![Go Version](https://img.shields.io/badge/Go-1.26-blue.svg)](https://golang.org/) [![Next.js](https://img.shields.io/badge/Next.js-16-black.svg)](https://nextjs.org/) [![React](https://img.shields.io/badge/React-19-blue.svg)](https://reactjs.org/) @@ -52,7 +52,7 @@ LINUX DO Credit is a credit service platform built for the Linux Do community, a ## 🛠️ Tech Stack ### Backend -- **[Go 1.25.5](https://go.dev/doc)** - Primary development language +- **[Go 1.26](https://go.dev/doc)** - Primary development language - **[GIN](https://github.com/gin-gonic/gin)** - Web Framework - **[GORM](https://github.com/go-gorm/gorm)** - ORM Framework - **[Redis](https://github.com/redis/redis)** - Cache and session store @@ -70,7 +70,7 @@ LINUX DO Credit is a credit service platform built for the Linux Do community, a ## 📋 Requirements -- **Go** >= 1.25.5 +- **Go** >= 1.26 - **Node.js** >= 18.0 - **PostgreSQL** >= 18 - **Redis** >= 6.0 @@ -136,7 +136,8 @@ pnpm dev - **Frontend Interface**: http://localhost:3000 - **API Documentation**: http://localhost:8000/swagger/index.html -- **Health Check**: http://localhost:8000/api/health +- **Health Check**: http://localhost:8000/api/v1/health +- **Readiness Check**: http://localhost:8000/api/v1/ready ## ⚙️ Configuration @@ -145,6 +146,8 @@ pnpm dev | Option | Description | Example | |--------|-------------|---------| | `app.addr` | Backend service listening address | `:8000` | +| `worker.port` | Worker probe port | `8001` | +| `scheduler.port` | Scheduler probe port | `8002` | | `oauth2.client_id` | OAuth2 Client ID | `your_client_id` | | `database.host` | PostgreSQL database host | `127.0.0.1` | | `database.port` | PostgreSQL database port | `5432` | diff --git a/README_zh.md b/README_zh.md index 58a0ed9e..e5a98768 100644 --- a/README_zh.md +++ b/README_zh.md @@ -5,7 +5,7 @@ [English](./README.md) [![License: Apache2.0](https://img.shields.io/badge/License-Apache2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -[![Go Version](https://img.shields.io/badge/Go-1.25.5-blue.svg)](https://golang.org/) +[![Go Version](https://img.shields.io/badge/Go-1.26-blue.svg)](https://golang.org/) [![Next.js](https://img.shields.io/badge/Next.js-16-black.svg)](https://nextjs.org/) [![React](https://img.shields.io/badge/React-19-blue.svg)](https://reactjs.org/) @@ -52,7 +52,7 @@ LINUX DO Credit 是一个为 Linux Do 社区打造的积分服务平台,旨在 ## 🛠️ 技术栈 ### 后端 -- **[Go 1.25.5](https://go.dev/doc)** - 主要开发语言 +- **[Go 1.26](https://go.dev/doc)** - 主要开发语言 - **[GIN](https://github.com/gin-gonic/gin)** - Web 框架 - **[GORM](https://github.com/go-gorm/gorm)** - ORM 框架 - **[Redis](https://github.com/redis/redis)** - 缓存和会话存储 @@ -70,7 +70,7 @@ LINUX DO Credit 是一个为 Linux Do 社区打造的积分服务平台,旨在 ## 📋 环境要求 -- **Go** >= 1.25.5 +- **Go** >= 1.26 - **Node.js** >= 18.0 - **PostgreSQL** >= 18 - **Redis** >= 6.0 @@ -136,7 +136,8 @@ pnpm dev - **前端界面**: http://localhost:3000 - **API 文档**: http://localhost:8000/swagger/index.html -- **健康检查**: http://localhost:8000/api/health +- **健康检查**: http://localhost:8000/api/v1/health +- **就绪检查**: http://localhost:8000/api/v1/ready ## ⚙️ 配置说明 @@ -145,6 +146,8 @@ pnpm dev | 配置项 | 说明 | 示例 | |--------|------|------| | `app.addr` | 后端服务监听地址 | `:8000` | +| `worker.port` | Worker 探针端口 | `8001` | +| `scheduler.port` | Scheduler 探针端口 | `8002` | | `oauth2.client_id` | OAuth2 客户端 ID | `your_client_id` | | `database.host` | PostgreSQL 数据库地址 | `127.0.0.1` | | `database.port` | PostgreSQL 数据库端口 | `5432` | diff --git a/config.example.yaml b/config.example.yaml index 91176135..945c4139 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -105,6 +105,7 @@ log: # Scheduler scheduler: + port: 8002 # scheduler probe port update_user_gamification_scores_task_cron: "0 2 * * *" dispute_auto_refund_dispatch_interval_seconds: 3 auto_refund_expired_disputes_task_cron: "0 0 * * *" @@ -115,6 +116,7 @@ scheduler: # Worker worker: + port: 8001 # worker probe port concurrency: 20 strict_priority: false queues: diff --git a/docs/docs.go b/docs/docs.go index 6c5a06cf..46e2b3ec 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1443,6 +1443,30 @@ const docTemplate = `{ } } }, + "/api/v1/ready": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, "/api/v1/redenvelope/claim": { "post": { "consumes": [ diff --git a/docs/swagger.json b/docs/swagger.json index 98c4939f..905a7701 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1434,6 +1434,30 @@ } } }, + "/api/v1/ready": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/util.ResponseAny" + } + } + } + } + }, "/api/v1/redenvelope/claim": { "post": { "consumes": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d6268ff4..f42068c2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1438,6 +1438,21 @@ paths: $ref: '#/definitions/util.ResponseAny' tags: - payment + /api/v1/ready: + get: + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/util.ResponseAny' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/util.ResponseAny' + tags: + - health /api/v1/redenvelope/{id}: get: parameters: diff --git a/go.mod b/go.mod index 306c988e..23fd4571 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/linux-do/credit -go 1.25.5 +go 1.26 require ( github.com/ClickHouse/clickhouse-go/v2 v2.37.2 diff --git a/internal/apps/health/routers.go b/internal/apps/health/routers.go index 375501c1..8a1bb4ee 100644 --- a/internal/apps/health/routers.go +++ b/internal/apps/health/routers.go @@ -20,6 +20,9 @@ import ( "net/http" "github.com/gin-gonic/gin" + "github.com/linux-do/credit/internal/config" + "github.com/linux-do/credit/internal/db" + "github.com/linux-do/credit/internal/logger" "github.com/linux-do/credit/internal/util" ) @@ -31,3 +34,55 @@ import ( func Health(c *gin.Context) { c.JSON(http.StatusOK, util.OKNil()) } + +// Ready godoc +// @Tags health +// @Produce json +// @Success 200 {object} util.ResponseAny +// @Failure 500 {object} util.ResponseAny +// @Router /api/v1/ready [get] +func Ready(c *gin.Context) { + ctx := c.Request.Context() + + if config.Config.Database.Enabled { + sqlDB, err := db.DB(ctx).DB() + if err != nil { + logger.ErrorF(ctx, "[Ready] PostgreSQL check failed: %v", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, util.Err("PostgreSQL not ready")) + return + } + if err := sqlDB.PingContext(ctx); err != nil { + logger.ErrorF(ctx, "[Ready] PostgreSQL check failed: %v", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, util.Err("PostgreSQL not ready")) + return + } + } + + if config.Config.Redis.Enabled { + if db.Redis == nil { + logger.ErrorF(ctx, "[Ready] Redis check failed: client is nil") + c.AbortWithStatusJSON(http.StatusInternalServerError, util.Err("Redis not ready")) + return + } + if err := db.Redis.Ping(ctx).Err(); err != nil { + logger.ErrorF(ctx, "[Ready] Redis check failed: %v", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, util.Err("Redis not ready")) + return + } + } + + if config.Config.ClickHouse.Enabled { + if db.ChConn == nil { + logger.ErrorF(ctx, "[Ready] ClickHouse check failed: client is nil") + c.AbortWithStatusJSON(http.StatusInternalServerError, util.Err("ClickHouse not ready")) + return + } + if err := db.ChConn.Ping(ctx); err != nil { + logger.ErrorF(ctx, "[Ready] ClickHouse check failed: %v", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, util.Err("ClickHouse not ready")) + return + } + } + + c.JSON(http.StatusOK, util.OKNil()) +} diff --git a/internal/cmd/scheduler.go b/internal/cmd/scheduler.go index c7b4bd03..d9bf9b37 100644 --- a/internal/cmd/scheduler.go +++ b/internal/cmd/scheduler.go @@ -19,6 +19,8 @@ package cmd import ( "log" + "github.com/linux-do/credit/internal/config" + "github.com/linux-do/credit/internal/probe" "github.com/linux-do/credit/internal/task/scheduler" "github.com/spf13/cobra" @@ -29,6 +31,9 @@ var schedulerCmd = &cobra.Command{ Short: "credit Scheduler", Run: func(cmd *cobra.Command, args []string) { log.Println("[Scheduler] 启动定时任务调度服务") + if err := probe.Start(config.Config.Scheduler.Port); err != nil { + log.Fatalf("[Scheduler] 启动探针服务失败: %v", err) + } if err := scheduler.StartScheduler(); err != nil { log.Fatalf("[调度器] 启动失败: %v", err) } diff --git a/internal/cmd/worker.go b/internal/cmd/worker.go index 90105fc4..caa87c58 100644 --- a/internal/cmd/worker.go +++ b/internal/cmd/worker.go @@ -19,6 +19,8 @@ package cmd import ( "log" + "github.com/linux-do/credit/internal/config" + "github.com/linux-do/credit/internal/probe" "github.com/linux-do/credit/internal/task/worker" "github.com/spf13/cobra" @@ -29,6 +31,9 @@ var workerCmd = &cobra.Command{ Short: "credit Worker", Run: func(cmd *cobra.Command, args []string) { log.Println("[Worker] 启动任务处理服务") + if err := probe.Start(config.Config.Worker.Port); err != nil { + log.Fatalf("[Worker] 启动探针服务失败: %v", err) + } if err := worker.StartWorker(); err != nil { log.Fatalf("[工作器] 启动失败: %v", err) } diff --git a/internal/config/model.go b/internal/config/model.go index f8fac649..947e816a 100644 --- a/internal/config/model.go +++ b/internal/config/model.go @@ -147,6 +147,7 @@ type logConfig struct { // schedulerConfig 定时任务配置 type schedulerConfig struct { + Port int `mapstructure:"port"` UpdateUserGamificationScoresTaskCron string `mapstructure:"update_user_gamification_scores_task_cron"` DisputeAutoRefundDispatchIntervalSeconds int `mapstructure:"dispute_auto_refund_dispatch_interval_seconds"` AutoRefundExpiredDisputesTaskCron string `mapstructure:"auto_refund_expired_disputes_task_cron"` @@ -158,6 +159,7 @@ type schedulerConfig struct { // workerConfig 工作配置 type workerConfig struct { + Port int `mapstructure:"port"` Concurrency int `mapstructure:"concurrency"` StrictPriority bool `mapstructure:"strict_priority"` Queues []QueueConfig `mapstructure:"queues"` diff --git a/internal/probe/server.go b/internal/probe/server.go new file mode 100644 index 00000000..a19d4ecf --- /dev/null +++ b/internal/probe/server.go @@ -0,0 +1,71 @@ +/* +Copyright 2025 linux.do + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package probe + +import ( + "errors" + "fmt" + "log" + "net" + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + "github.com/linux-do/credit/internal/apps/health" + "github.com/linux-do/credit/internal/config" +) + +// Start launches a lightweight HTTP probe server for non-API processes. +func Start(port int) error { + if port <= 0 { + return fmt.Errorf("invalid probe port: %d", port) + } + + if config.Config.App.IsProduction() { + gin.SetMode(gin.ReleaseMode) + } + + addr := net.JoinHostPort("", strconv.Itoa(port)) + listener, err := net.Listen("tcp", addr) + if err != nil { + return err + } + + engine := gin.New() + engine.Use(gin.Recovery()) + + apiV1Router := engine.Group(config.Config.App.APIPrefix + "/v1") + { + apiV1Router.GET("/health", health.Health) + apiV1Router.GET("/ready", health.Ready) + } + + server := &http.Server{ + Handler: engine, + ReadHeaderTimeout: 5 * time.Second, + } + + go func() { + if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Printf("[Probe] serve failed: %v\n", err) + } + }() + + log.Printf("[Probe] listening on :%d\n", port) + return nil +} diff --git a/internal/router/middlewares.go b/internal/router/middlewares.go index b10b4267..422ce170 100644 --- a/internal/router/middlewares.go +++ b/internal/router/middlewares.go @@ -68,9 +68,7 @@ func loggerMiddleware() gin.HandlerFunc { latency := end.Sub(start) // 打印日志 - // 排除健康检查接口 - healthPath := config.Config.App.APIPrefix + "/v1/health" - if c.Request.URL.Path != healthPath { + if !isProbePath(c.Request.URL.Path) { logger.InfoF( ctx, "[LoggerMiddleware] %s %s\nStartTime: %s\nEndTime: %s\nLatency: %d\nClientIP: %s\nResponse: %d %d", @@ -92,3 +90,8 @@ func loggerMiddleware() gin.HandlerFunc { } } } + +func isProbePath(path string) bool { + return path == config.Config.App.APIPrefix+"/v1/health" || + path == config.Config.App.APIPrefix+"/v1/ready" +} diff --git a/internal/router/router.go b/internal/router/router.go index b4b1ab63..05cbc377 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -129,6 +129,7 @@ func Serve() { { // Health apiV1Router.GET("/health", health.Health) + apiV1Router.GET("/ready", health.Ready) // OAuth apiV1Router.GET("/oauth/login", oauth.GetLoginURL)