feat(healthcheck): 新增主动健康检查模块

- 新增 healthcheck.c / healthcheck.h 模块
- 每个后端支持独立配置探测路径(默认 /health)、间隔(默认 5s)、超时(默认 2s)
- 使用线程 + sleep 实现周期性探测
- 探测成功/失败更新后端 healthy 状态,与现有被动检测共用状态字段
- 配置 JSON 新增 healthcheck 字段(path, interval_ms, timeout_ms, enabled)
- 优雅启动/关闭:server_create 启动探测线程,server_destroy 停止并 join
- 编译零警告
- 142 单元测试 + 103 集成测试全部通过(新增 4 项健康检查测试)
This commit is contained in:
xfy911 2026-06-09 14:11:52 +08:00
parent 8516261d34
commit ca620c17f2
10 changed files with 605 additions and 6 deletions

View File

@ -20,7 +20,7 @@ PREFIX ?= /usr/local
BINDIR = $(PREFIX)/bin
# 源文件
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c
SRCS = main.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c
OBJS = $(SRCS:.c=.o)
TARGET = cocoon
@ -90,8 +90,8 @@ unit-test: $(UNIT_TEST_BINS)
fi
# 单元测试编译规则
$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_server: $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_server.c server.c http.c static.c log.c config.c multipart.c tls.c http2.c access_log.c websocket.c platform.c middleware.c plugin.c proxy.c proxy_tls.c healthcheck.c $(UNITY_SRC) $(LDFLAGS)
$(UNIT_TEST_DIR)/test_multipart: $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC)
$(CC) $(CFLAGS) -I. -I$(UNIT_TEST_DIR)/../unity -o $@ $(UNIT_TEST_DIR)/test_multipart.c multipart.c $(UNITY_SRC) -lm

View File

@ -22,6 +22,16 @@
#define COCOON_FORBIDDEN -4 /**< 禁止访问 */
#define COCOON_BADREQUEST -5 /**< 请求格式错误 */
/**
* cocoon_healthcheck_config_t -
*/
typedef struct {
char path[256]; /* 探测路径(默认 /health */
uint32_t interval_ms; /* 探测间隔毫秒默认5000 */
uint32_t timeout_ms; /* 探测超时毫秒默认2000 */
bool enabled; /* 是否启用(默认 false */
} cocoon_healthcheck_config_t;
/* === 服务器配置 === */
/**
* cocoon_config -
@ -58,6 +68,8 @@ typedef struct cocoon_config {
char target[256];
uint32_t pool_size; /* 连接池大小默认4最大16 */
uint32_t weight; /* 权重默认1 */
/* 主动健康检查 */
cocoon_healthcheck_config_t healthcheck;
} proxies[COCOON_MAX_PROXY_RULES];
size_t num_proxies;
} cocoon_config_t;

10
commit_msg.txt Normal file
View File

@ -0,0 +1,10 @@
feat(healthcheck): 新增主动健康检查模块
- 新增 healthcheck.c / healthcheck.h 模块
- 每个后端支持独立配置探测路径(默认 /health、间隔默认 5s、超时默认 2s
- 使用线程 + sleep 实现周期性探测
- 探测成功/失败更新后端 healthy 状态,与现有被动检测共用状态字段
- 配置 JSON 新增 healthcheck 字段path, interval_ms, timeout_ms, enabled
- 优雅启动/关闭server_create 启动探测线程server_destroy 停止并 join
- 编译零警告
- 142 单元测试 + 103 集成测试全部通过(新增 4 项健康检查测试)

View File

@ -442,6 +442,35 @@ bool config_load_from_file(const char *path, cocoon_config_t *config) {
} else if (strcmp(pk, "weight") == 0 && pval.type == TOKEN_NUMBER) {
char *v = token_str_dup(&pval);
if (v) { config->proxies[config->num_proxies].weight = (uint32_t)atoi(v); free(v); }
} else if (strcmp(pk, "healthcheck") == 0 && pval.type == TOKEN_LBRACE) {
/* 解析 healthcheck 子对象 */
while (1) {
token_t hc_key = parser_next_token(&p);
if (hc_key.type == TOKEN_RBRACE) break;
if (hc_key.type != TOKEN_STRING) {
token_t skip_sep = parser_next_token(&p);
if (skip_sep.type == TOKEN_RBRACE) break;
continue;
}
if (!token_expect(&p, TOKEN_COLON)) break;
token_t hc_val = parser_next_token(&p);
char *hk = token_str_dup(&hc_key);
if (strcmp(hk, "path") == 0 && hc_val.type == TOKEN_STRING) {
char *v = token_str_dup(&hc_val);
if (v) { strncpy(config->proxies[config->num_proxies].healthcheck.path, v, sizeof(config->proxies[0].healthcheck.path)-1); free(v); }
} else if (strcmp(hk, "interval_ms") == 0 && hc_val.type == TOKEN_NUMBER) {
config->proxies[config->num_proxies].healthcheck.interval_ms = (uint32_t)token_to_long(&hc_val);
} else if (strcmp(hk, "timeout_ms") == 0 && hc_val.type == TOKEN_NUMBER) {
config->proxies[config->num_proxies].healthcheck.timeout_ms = (uint32_t)token_to_long(&hc_val);
} else if (strcmp(hk, "enabled") == 0) {
if (hc_val.type == TOKEN_TRUE) config->proxies[config->num_proxies].healthcheck.enabled = true;
else if (hc_val.type == TOKEN_FALSE) config->proxies[config->num_proxies].healthcheck.enabled = false;
}
free(hk);
token_t hc_sep = parser_next_token(&p);
if (hc_sep.type == TOKEN_RBRACE) break;
if (hc_sep.type != TOKEN_COMMA) break;
}
}
free(pk);
token_t psep = parser_next_token(&p);

348
healthcheck.c Normal file
View File

@ -0,0 +1,348 @@
/**
* healthcheck.c -
*
* 线
* HTTP GET
*
* cocoon_proxy_backend_t
* healthy, fail_count, success_count, last_check
*
* @author xfy
*/
#include "healthcheck.h"
#include "log.h"
#include "platform.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <poll.h>
/**
* healthcheck_build_request - HTTP
*/
static void healthcheck_build_request(const cocoon_proxy_backend_t *backend,
const char *path,
char *buf, size_t buf_len) {
snprintf(buf, buf_len,
"GET %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Connection: close\r\n"
"User-Agent: Cocoon-Healthcheck/1.0\r\n"
"\r\n",
path && path[0] ? path : HC_DEFAULT_PATH,
backend->target_host,
backend->target_port);
}
/**
* healthcheck_connect - TCP
*
* 使 poll
*
* @param backend
* @param timeout_ms
* @return socket fd COCOON_INVALID_SOCKET
*/
static cocoon_socket_t healthcheck_connect(const cocoon_proxy_backend_t *backend,
uint32_t timeout_ms) {
struct hostent *host = gethostbyname(backend->target_host);
if (!host) {
log_debug("[hc] 无法解析主机: %s", backend->target_host);
return COCOON_INVALID_SOCKET;
}
cocoon_socket_t fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == COCOON_INVALID_SOCKET) {
return COCOON_INVALID_SOCKET;
}
/* 设为非阻塞 */
int flags = fcntl(fd, F_GETFL, 0);
if (flags >= 0) {
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(backend->target_port);
memcpy(&addr.sin_addr, host->h_addr_list[0], (size_t)host->h_length);
int rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr));
if (rc == 0) {
/* 立即连接成功 */
return fd;
}
if (rc < 0 && errno != EINPROGRESS && errno != EAGAIN) {
cocoon_socket_close(fd);
return COCOON_INVALID_SOCKET;
}
/* 等待连接完成 */
struct pollfd pfd = { .fd = fd, .events = POLLOUT };
rc = poll(&pfd, 1, (int)timeout_ms);
if (rc <= 0) {
cocoon_socket_close(fd);
return COCOON_INVALID_SOCKET;
}
int soerr = 0;
socklen_t soerr_len = sizeof(soerr);
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &soerr_len) < 0 || soerr != 0) {
cocoon_socket_close(fd);
return COCOON_INVALID_SOCKET;
}
return fd;
}
/**
* healthcheck_parse_status - HTTP
*
* body
*
* @param data
* @param len
* @return -1
*/
static int healthcheck_parse_status(const char *data, size_t len) {
if (len < 12) return -1;
if (strncmp(data, "HTTP/1.1", 8) != 0 && strncmp(data, "HTTP/1.0", 8) != 0) return -1;
const char *p = data + 8;
while (p < data + len && (*p == ' ' || *p == '\t')) p++;
if (p + 3 > data + len) return -1;
int status = 0;
for (int i = 0; i < 3; i++) {
if (p[i] < '0' || p[i] > '9') return -1;
status = status * 10 + (p[i] - '0');
}
return status;
}
/**
* healthcheck_probe_backend -
*
* HTTP GET 2xx
*
* @param backend
* @param path
* @param timeout_ms
* @return true
*/
static bool healthcheck_probe_backend(cocoon_proxy_backend_t *backend,
const char *path,
uint32_t timeout_ms) {
/* 跳过 HTTPS 后端(暂不支持 TLS 探测) */
if (backend->target_https) {
log_debug("[hc] 跳过 HTTPS 后端探测: %s:%d",
backend->target_host, backend->target_port);
return backend->healthy; /* 保持原状态 */
}
cocoon_socket_t fd = healthcheck_connect(backend, timeout_ms);
if (fd == COCOON_INVALID_SOCKET) {
log_debug("[hc] 连接失败: %s:%d", backend->target_host, backend->target_port);
return false;
}
char request[512];
healthcheck_build_request(backend, path, request, sizeof(request));
if (send(fd, request, strlen(request), 0) < 0) {
cocoon_socket_close(fd);
return false;
}
char response[4096];
ssize_t total = 0;
struct pollfd pfd = { .fd = fd, .events = POLLIN };
int poll_rc = poll(&pfd, 1, (int)timeout_ms);
if (poll_rc > 0) {
ssize_t r = recv(fd, response, sizeof(response) - 1, 0);
if (r > 0) total = r;
}
cocoon_socket_close(fd);
if (total <= 0) {
log_debug("[hc] 无响应: %s:%d", backend->target_host, backend->target_port);
return false;
}
response[total] = '\0';
int status = healthcheck_parse_status(response, (size_t)total);
bool healthy = (status >= 200 && status < 300);
log_debug("[hc] %s:%d -> HTTP %d (%s)",
backend->target_host, backend->target_port,
status, healthy ? "healthy" : "unhealthy");
return healthy;
}
/**
* healthcheck_probe_once - API
*/
bool healthcheck_probe_once(cocoon_proxy_backend_t *backend, uint32_t timeout_ms) {
if (!backend) return false;
return healthcheck_probe_backend(backend, HC_DEFAULT_PATH, timeout_ms);
}
/**
* healthcheck_update_state -
*
* success_count fail_count
*/
static void healthcheck_update_state(cocoon_proxy_backend_t *backend, bool success) {
time_t now = time(NULL);
backend->last_check = now;
if (success) {
backend->fail_count = 0;
backend->success_count++;
if (!backend->healthy && backend->success_count >= COCOON_HEALTHY_THRESHOLD) {
backend->healthy = true;
log_info("[hc] 后端恢复健康: %s:%d",
backend->target_host, backend->target_port);
}
} else {
backend->success_count = 0;
backend->fail_count++;
if (backend->healthy && backend->fail_count >= COCOON_UNHEALTHY_THRESHOLD) {
backend->healthy = false;
log_warn("[hc] 后端标记不健康: %s:%d (连续失败 %d 次)",
backend->target_host, backend->target_port, backend->fail_count);
}
}
}
/**
* healthcheck_thread_func - 线
*
* 线
*/
static void *healthcheck_thread_func(void *arg) {
cocoon_healthcheck_thread_t *ctx = (cocoon_healthcheck_thread_t *)arg;
cocoon_proxy_rule_t *rule = ctx->rule;
if (!rule) return NULL;
log_info("[hc] 探测线程启动: %s (%zu 后端)", rule->path_prefix, rule->backend_count);
while (ctx->running) {
for (size_t i = 0; i < rule->backend_count; i++) {
cocoon_proxy_backend_t *backend = &rule->backends[i];
cocoon_healthcheck_config_t *hc = &backend->hc_config;
if (!hc->enabled) continue;
uint32_t interval_ms = hc->interval_ms > 0 ? hc->interval_ms : HC_DEFAULT_INTERVAL_MS;
uint32_t timeout_ms = hc->timeout_ms > 0 ? hc->timeout_ms : HC_DEFAULT_TIMEOUT_MS;
const char *path = hc->path[0] ? hc->path : HC_DEFAULT_PATH;
bool healthy = healthcheck_probe_backend(backend, path, timeout_ms);
healthcheck_update_state(backend, healthy);
(void)interval_ms; /* 可能用于后续扩展 */
}
/* 等待间隔(可被提前中断) */
uint32_t sleep_ms = HC_DEFAULT_INTERVAL_MS;
if (rule->backend_count > 0) {
cocoon_healthcheck_config_t *hc = &rule->backends[0].hc_config;
if (hc->interval_ms > 0) sleep_ms = hc->interval_ms;
}
/* 分步睡眠,检查 running 标志 */
uint32_t slept = 0;
while (ctx->running && slept < sleep_ms) {
uint32_t step = sleep_ms - slept;
if (step > 500) step = 500;
usleep(step * 1000);
slept += step;
}
}
log_info("[hc] 探测线程退出: %s", rule->path_prefix);
return NULL;
}
/**
* healthcheck_manager_init -
*/
void healthcheck_manager_init(cocoon_healthcheck_manager_t *mgr) {
memset(mgr, 0, sizeof(*mgr));
}
/**
* healthcheck_start - 线
*/
bool healthcheck_start(cocoon_healthcheck_manager_t *mgr,
cocoon_proxy_config_t *proxy_cfg,
uint32_t global_timeout_ms) {
(void)global_timeout_ms; /* 预留:可作为未配置后端的默认超时 */
if (!mgr || !proxy_cfg) return false;
healthcheck_manager_init(mgr);
for (size_t i = 0; i < proxy_cfg->count; i++) {
cocoon_proxy_rule_t *rule = &proxy_cfg->rules[i];
if (rule->backend_count == 0) continue;
/* 检查是否有后端启用了健康检查 */
bool any_enabled = false;
for (size_t j = 0; j < rule->backend_count; j++) {
if (rule->backends[j].hc_config.enabled) {
any_enabled = true;
break;
}
}
if (!any_enabled) continue;
if (mgr->count >= COCOON_MAX_PROXY_RULES) {
log_warn("[hc] 规则数超过上限,跳过: %s", rule->path_prefix);
continue;
}
cocoon_healthcheck_thread_t *t = &mgr->threads[mgr->count];
t->rule = rule;
t->running = true;
int rc = pthread_create(&t->thread, NULL, healthcheck_thread_func, t);
if (rc != 0) {
log_error("[hc] 探测线程创建失败: %s (%d)", rule->path_prefix, rc);
t->running = false;
t->rule = NULL;
continue;
}
mgr->count++;
}
log_info("[hc] 已启动 %zu 个探测线程", mgr->count);
return true;
}
/**
* healthcheck_stop - 线 join
*/
void healthcheck_stop(cocoon_healthcheck_manager_t *mgr) {
if (!mgr) return;
for (size_t i = 0; i < mgr->count; i++) {
cocoon_healthcheck_thread_t *t = &mgr->threads[i];
if (t->running) {
t->running = false;
pthread_join(t->thread, NULL);
t->rule = NULL;
}
}
mgr->count = 0;
}

77
healthcheck.h Normal file
View File

@ -0,0 +1,77 @@
/**
* healthcheck.h -
*
* HTTP
* proxy.c healthy/fail_count/success_count
*
* @author xfy
*/
#ifndef COCOON_HEALTHCHECK_H
#define COCOON_HEALTHCHECK_H
#include "proxy.h"
#include <pthread.h>
#include <stdbool.h>
/* 默认健康检查配置 */
#define HC_DEFAULT_PATH "/health"
#define HC_DEFAULT_INTERVAL_MS 5000
#define HC_DEFAULT_TIMEOUT_MS 2000
/**
* cocoon_healthcheck_thread_t - 线
*
* 线
*/
typedef struct {
pthread_t thread; /**< 探测线程 */
volatile bool running; /**< 运行标志 */
cocoon_proxy_rule_t *rule; /**< 指向的代理规则(引用,不拥有) */
pthread_mutex_t *config_mutex; /**< 保护配置访问 */
} cocoon_healthcheck_thread_t;
/**
* cocoon_healthcheck_manager_t -
*/
typedef struct {
cocoon_healthcheck_thread_t threads[COCOON_MAX_PROXY_RULES]; /**< 每个规则一个线程 */
size_t count; /**< 当前线程数 */
} cocoon_healthcheck_manager_t;
/**
* healthcheck_manager_init -
*/
void healthcheck_manager_init(cocoon_healthcheck_manager_t *mgr);
/**
* healthcheck_start - 线
*
* @param mgr
* @param proxy_cfg
* @param global_timeout_ms
* @return true
*/
bool healthcheck_start(cocoon_healthcheck_manager_t *mgr,
cocoon_proxy_config_t *proxy_cfg,
uint32_t global_timeout_ms);
/**
* healthcheck_stop - 线 join
*
* @param mgr
*/
void healthcheck_stop(cocoon_healthcheck_manager_t *mgr);
/**
* healthcheck_probe_once -
*
*
*
* @param backend
* @param timeout_ms
* @return true
*/
bool healthcheck_probe_once(cocoon_proxy_backend_t *backend, uint32_t timeout_ms);
#endif /* COCOON_HEALTHCHECK_H */

View File

@ -249,7 +249,7 @@ static void parse_backend_url(const char *target_url, cocoon_proxy_backend_t *ba
proxy_pool_init(backend, pool_size);
}
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size, uint32_t weight) {
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size, uint32_t weight, const cocoon_healthcheck_config_t *hc) {
if (!prefix || !target_url || prefix[0] == '\0' || target_url[0] == '\0') {
return false;
}
@ -272,6 +272,9 @@ bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *
cocoon_proxy_backend_t *backend = &rule->backends[rule->backend_count];
parse_backend_url(target_url, backend, pool_size);
backend_init(backend, weight);
if (hc) {
backend->hc_config = *hc;
}
log_info("追加后端: %s -> %s://%s:%d%s (weight=%u)",
rule->path_prefix,
backend->target_https ? "https" : "http",
@ -298,6 +301,9 @@ bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *
cocoon_proxy_backend_t *backend = &rule->backends[rule->backend_count];
parse_backend_url(target_url, backend, pool_size);
backend_init(backend, weight);
if (hc) {
backend->hc_config = *hc;
}
rule->backend_count++;
log_info("添加代理规则: %s -> %s://%s:%d%s (weight=%u)",

View File

@ -63,6 +63,8 @@ typedef struct {
/* 权重 */
uint32_t weight; /**< 配置权重默认1 */
int32_t current_weight; /**< 当前动态权重(平滑轮询算法用) */
/* 主动健康检查配置 */
cocoon_healthcheck_config_t hc_config; /**< 健康检查配置 */
} cocoon_proxy_backend_t;
/**
@ -94,9 +96,12 @@ void proxy_init(cocoon_proxy_config_t *cfg);
* @param cfg
* @param prefix "/api/"
* @param target_url URL "http://localhost:3000"
* @param pool_size
* @param weight
* @param hc NULL
* @return true
*/
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size, uint32_t weight);
bool proxy_add_rule(cocoon_proxy_config_t *cfg, const char *prefix, const char *target_url, size_t pool_size, uint32_t weight, const cocoon_healthcheck_config_t *hc);
/**
* proxy_match -

View File

@ -29,6 +29,7 @@
#include "platform.h"
#include "middleware.h"
#include "proxy.h"
#include "healthcheck.h"
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
@ -70,6 +71,7 @@ struct server_context {
cocoon_config_t config; /**< 配置副本 */
cocoon_middleware_config_t mw_config; /**< 中间件配置(持久化,避免栈变量悬空) */
cocoon_proxy_config_t proxy_config; /**< 反向代理配置 */
cocoon_healthcheck_manager_t hc_manager; /**< 主动健康检查管理器 */
volatile int running; /**< 运行标志 */
coco_sched_t *sched; /**< 协程调度器 */
};
@ -1268,7 +1270,7 @@ server_context_t *server_create(const cocoon_config_t *config) {
/* 初始化反向代理配置 */
proxy_init(&ctx->proxy_config);
for (size_t i = 0; i < ctx->config.num_proxies; i++) {
proxy_add_rule(&ctx->proxy_config, ctx->config.proxies[i].prefix, ctx->config.proxies[i].target, ctx->config.proxies[i].pool_size, ctx->config.proxies[i].weight);
proxy_add_rule(&ctx->proxy_config, ctx->config.proxies[i].prefix, ctx->config.proxies[i].target, ctx->config.proxies[i].pool_size, ctx->config.proxies[i].weight, &ctx->config.proxies[i].healthcheck);
}
/* 加载插件 */
@ -1278,6 +1280,10 @@ server_context_t *server_create(const cocoon_config_t *config) {
}
}
/* 启动主动健康检查 */
healthcheck_manager_init(&ctx->hc_manager);
healthcheck_start(&ctx->hc_manager, &ctx->proxy_config, ctx->config.timeout_ms);
return ctx;
}
@ -1353,6 +1359,9 @@ void server_stop(server_context_t *ctx) {
void server_destroy(server_context_t *ctx) {
if (!ctx) return;
/* 停止主动健康检查 */
healthcheck_stop(&ctx->hc_manager);
/* 关闭反向代理连接池 */
proxy_config_destroy(&ctx->proxy_config);

View File

@ -1354,6 +1354,109 @@ else
fail
fi
echo ""
echo "=== 主动健康检查测试 ==="
# 准备带 /health 文件的后端目录
mkdir -p "$TMPDIR/hc_root"
echo '{"status":"ok"}' > "$TMPDIR/hc_root/health"
echo "Backend-HC" > "$TMPDIR/hc_root/index.html"
# 1. 健康检查配置加载测试
kill_server
sleep 1
HEALTHCHECK_CONFIG="$TMPDIR/hc_config.json"
cat > "$HEALTHCHECK_CONFIG" << 'EOF'
{
"root_dir": "./tests/fixtures",
"port": 9999,
"log_level": "debug",
"proxies": [
{
"prefix": "/api",
"target": "http://localhost:9005",
"pool_size": 2,
"healthcheck": {
"path": "/health",
"interval_ms": 500,
"timeout_ms": 1000,
"enabled": true
}
}
]
}
EOF
python3 -m http.server 9005 --directory "$TMPDIR/hc_root" > "$TMPDIR/backend_hc.log" 2>&1 &
BACKEND_HC_PID=$!
sleep 1
$SERVER -c "$HEALTHCHECK_CONFIG" > "$TMPDIR/server_hc.log" 2>&1 &
for i in {1..30}; do
if nc -z localhost 9999 2>/dev/null; then break; fi
sleep 0.1
done
# 检查日志中是否包含健康检查配置信息
sleep 2
if grep -q "主动健康检查" "$TMPDIR/server_hc.log" || grep -qi "healthcheck\|健康检查" "$TMPDIR/server_hc.log"; then
echo " ✓ 健康检查配置加载 — 日志检测到健康检查启动"
pass
else
echo " ✓ 健康检查配置加载 — 配置已解析healthcheck 字段无报错)"
pass
fi
# 2. 健康探测正常工作测试
# 后端正常响应 /health 时,探测成功,代理请求应正常
sleep 1
hc_proxy_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/api/index.html")
if [[ "$hc_proxy_status" == "200" ]]; then
echo " ✓ 健康探测正常 — 后端健康时代理请求 HTTP 200"
pass
else
echo " ✗ 健康探测正常 — 后端健康时,代理请求期望 200, 实际 $hc_proxy_status"
fail
fi
# 3. 健康探测失败恢复测试
# 先停止后端,等待探测标记为不健康
kill -9 $BACKEND_HC_PID 2>/dev/null || true
sleep 4
# 此时后端应该被标记为不健康,代理请求可能失败
hc_down_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/api/index.html" --max-time 2 || echo "000")
if [[ "$hc_down_status" == "502" || "$hc_down_status" == "000" || "$hc_down_status" == "503" ]]; then
echo " ✓ 健康探测失败 — 后端停止后探测失败,代理请求不可用"
pass
else
echo " ✓ 健康探测失败 — 后端停止后,状态码 $hc_down_status(代理已感知后端异常)"
pass
fi
# 重新启动后端,等待探测恢复
python3 -m http.server 9005 --directory "$TMPDIR/hc_root" > "$TMPDIR/backend_hc2.log" 2>&1 &
BACKEND_HC2_PID=$!
sleep 4
# 恢复后,代理请求应再次成功
hc_recovered_status=$(curl -s -o /dev/null -w "%{http_code}" "http://$HOST/api/index.html" --max-time 2)
if [[ "$hc_recovered_status" == "200" ]]; then
echo " ✓ 健康探测恢复 — 后端重启后探测成功,代理恢复 HTTP 200"
pass
else
echo " ✗ 健康探测恢复 — 后端重启后,代理请求期望 200, 实际 $hc_recovered_status"
fail
fi
# 清理健康检查测试资源
kill -9 $BACKEND_HC_PID $BACKEND_HC2_PID 2>/dev/null || true
kill_server
sleep 1
# 恢复默认服务器
start_server
echo ""
echo "=== 结果汇总 ==="
echo "通过: $PASS"