From 8e2c66b3631bc1d50d3b8cb4ac6c2bc925117a22 Mon Sep 17 00:00:00 2001 From: yixiong Date: Thu, 9 Jul 2026 08:50:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(t15):=20=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E7=AB=AF=E7=82=B9=20/health=EF=BC=88pg/redis/?= =?UTF-8?q?=E9=98=9F=E5=88=97=E5=9B=9B=E6=80=81=EF=BC=8CDOWN=20=E8=BF=9450?= =?UTF-8?q?3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DistQueueMapper: +countByStatus (GROUP BY status 一次拿全) - HealthService(新): 聚合 pg 连通/redis ping/队列四态,status UP/DEGRADED/DOWN - HealthController(新): GET /health,DOWN 返 503 便 LB 摘流;免租户鉴权(PathWhitelist) - 联调:status=UP,pg/redis=UP,distQueue 四态 0,HTTP 200 Co-Authored-By: Claude --- .../java/com/imutil/common/HealthService.java | 95 +++++++++++++++++++ .../imutil/controller/HealthController.java | 36 +++++++ .../com/imutil/mapper/DistQueueMapper.java | 8 ++ 3 files changed, 139 insertions(+) create mode 100644 src/main/java/com/imutil/common/HealthService.java create mode 100644 src/main/java/com/imutil/controller/HealthController.java diff --git a/src/main/java/com/imutil/common/HealthService.java b/src/main/java/com/imutil/common/HealthService.java new file mode 100644 index 0000000..67f2451 --- /dev/null +++ b/src/main/java/com/imutil/common/HealthService.java @@ -0,0 +1,95 @@ +package com.imutil.common; + +import com.imutil.mapper.DistQueueMapper; +import lombok.extern.slf4j.Slf4j; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +import java.time.OffsetDateTime; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 健康检查服务(对齐设计:监控告警 - 中间件连通、dist_queue 堆积) + *

+ * 聚合 PG / Redis 连通性与分发队列状态分布,输出整体 status: + * - UP:PG + Redis 均连通 + * - DEGRADED:PG 连通、Redis 异常(限流 fail-open、缓存降级,核心链路仍可用) + * - DOWN:PG 异常(消息落库不可用,核心受损) + *

+ * 队列 pending/processing/dead 计数供运维判断是否需干预: + * - pending 持续上涨 → worker 处理不过去,考虑扩容 + * - processing 长期不归零且无 done 增长 → 工作线程可能卡死 + * - dead 积累 → 需排查死信或重发 + * + * @author imutil + */ +@Slf4j +@Component +public class HealthService { + + @Inject + private DistQueueMapper distQueueMapper; + + @Inject + private JedisPool jedisPool; + + /** + * 采集健康指标 + * + * @return 含 status / pg / redis / distQueue / timestamp + */ + public Map health() { + Map distQueue = new LinkedHashMap<>(); + boolean pgUp; + try { + List> rows = distQueueMapper.countByStatus(); + // 四态计数初始化为 0(无记录的状态不出现在 GROUP BY 结果中) + long[] cnt = new long[4]; + for (Map row : rows) { + int st = ((Number) row.get("status")).intValue(); + long c = ((Number) row.get("cnt")).longValue(); + if (st >= 0 && st < 4) { + cnt[st] = c; + } + } + distQueue.put("pending", cnt[0]); + distQueue.put("processing", cnt[1]); + distQueue.put("done", cnt[2]); + distQueue.put("dead", cnt[3]); + pgUp = true; + } catch (Exception e) { + log.warn("健康检查 PG 查询失败 : {}", e.getMessage()); + distQueue.put("error", e.getMessage()); + pgUp = false; + } + + boolean redisUp; + try (Jedis j = jedisPool.getResource()) { + redisUp = "PONG".equals(j.ping()); + } catch (Exception e) { + log.warn("健康检查 Redis ping 失败 : {}", e.getMessage()); + redisUp = false; + } + + String status; + if (pgUp && redisUp) { + status = "UP"; + } else if (pgUp) { + status = "DEGRADED"; + } else { + status = "DOWN"; + } + + Map r = new LinkedHashMap<>(); + r.put("status", status); + r.put("pg", pgUp ? "UP" : "DOWN"); + r.put("redis", redisUp ? "UP" : "DOWN"); + r.put("distQueue", distQueue); + r.put("timestamp", OffsetDateTime.now().toString()); + return r; + } +} diff --git a/src/main/java/com/imutil/controller/HealthController.java b/src/main/java/com/imutil/controller/HealthController.java new file mode 100644 index 0000000..b9048d3 --- /dev/null +++ b/src/main/java/com/imutil/controller/HealthController.java @@ -0,0 +1,36 @@ +package com.imutil.controller; + +import com.imutil.common.HealthService; +import com.imutil.model.Result; +import org.noear.solon.annotation.Controller; +import org.noear.solon.annotation.Inject; +import org.noear.solon.annotation.Mapping; +import org.noear.solon.core.handle.Context; +import org.noear.solon.core.handle.MethodType; + +import java.util.Map; + +/** + * 健康检查端点(运维 / LB 探针) + *

+ * GET /health:返回 PG/Redis 连通 + 分发队列四态计数。 + * 免租户鉴权(见 {@link com.imutil.common.PathWhitelist})。 + * HTTP 状态码:200 = UP/DEGRADED(核心可用);503 = DOWN(PG 异常,核心受损), + * 便于 LB/k8s 探针据状态码摘流。 + * + * @author imutil + */ +@Controller +public class HealthController { + + @Inject + private HealthService healthService; + + @Mapping(value = "/health", method = MethodType.GET) + public void health(Context ctx) throws Throwable { + Map h = healthService.health(); + // DOWN 返回 503,便探针摘流;UP/DEGRADED 返回 200 + ctx.status("DOWN".equals(h.get("status")) ? 503 : 200); + ctx.render(Result.ok(h)); + } +} diff --git a/src/main/java/com/imutil/mapper/DistQueueMapper.java b/src/main/java/com/imutil/mapper/DistQueueMapper.java index 58f9a96..cb13c2a 100644 --- a/src/main/java/com/imutil/mapper/DistQueueMapper.java +++ b/src/main/java/com/imutil/mapper/DistQueueMapper.java @@ -78,4 +78,12 @@ public interface DistQueueMapper extends BaseMapper { "locked_by = NULL, locked_at = NULL, updated_at = now() " + "WHERE id = #{id} AND status = 3") int redeliverDead(@Param("id") Long id); + + /** + * 按状态聚合计数(健康检查/监控用,一次扫描拿全状态分布) + * + * @return 每项含 status(0=pending/1=processing/2=done/3=dead)与 cnt + */ + @org.apache.ibatis.annotations.Select("SELECT status, count(*) AS cnt FROM dist_queue GROUP BY status") + java.util.List> countByStatus(); }