feat(t15): 健康检查端点 /health(pg/redis/队列四态,DOWN 返503)

- 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 <noreply@anthropic.com>
This commit is contained in:
yixiong
2026-07-09 08:50:15 +08:00
co-authored by Claude
parent 43f7d30e46
commit 8e2c66b363
3 changed files with 139 additions and 0 deletions
@@ -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 堆积)
* <p>
* 聚合 PG / Redis 连通性与分发队列状态分布,输出整体 status:
* - UPPG + Redis 均连通
* - DEGRADEDPG 连通、Redis 异常(限流 fail-open、缓存降级,核心链路仍可用)
* - DOWN:PG 异常(消息落库不可用,核心受损)
* <p>
* 队列 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<String, Object> health() {
Map<String, Object> distQueue = new LinkedHashMap<>();
boolean pgUp;
try {
List<Map<String, Object>> rows = distQueueMapper.countByStatus();
// 四态计数初始化为 0(无记录的状态不出现在 GROUP BY 结果中)
long[] cnt = new long[4];
for (Map<String, Object> 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<String, Object> 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;
}
}
@@ -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 探针)
* <p>
* 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<String, Object> h = healthService.health();
// DOWN 返回 503,便探针摘流;UP/DEGRADED 返回 200
ctx.status("DOWN".equals(h.get("status")) ? 503 : 200);
ctx.render(Result.ok(h));
}
}
@@ -78,4 +78,12 @@ public interface DistQueueMapper extends BaseMapper<DistQueue> {
"locked_by = NULL, locked_at = NULL, updated_at = now() " + "locked_by = NULL, locked_at = NULL, updated_at = now() " +
"WHERE id = #{id} AND status = 3") "WHERE id = #{id} AND status = 3")
int redeliverDead(@Param("id") Long id); int redeliverDead(@Param("id") Long id);
/**
* 按状态聚合计数(健康检查/监控用,一次扫描拿全状态分布)
*
* @return 每项含 status0=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<java.util.Map<String, Object>> countByStatus();
} }