fix: remove redundant ntfy messages and harden Uptime Kuma heartbeat

run_incremental_sync.py:
- Replace ntfy-pushing log_start/log_success with log_info. Liveness is
  owned by Uptime Kuma's heartbeat, so the startup and per-table success
  messages no longer duplicate Uptime's notifications.

uptime_kuma_utils.py (shared util, backward compatible):
- Run the heartbeat in a background daemon thread (new start()/stop()),
  decoupled from the sync loop so heartbeat network time/retries never
  delay syncing.
- Add retry (up to 3 attempts) and raise request timeout 5s -> 10s to
  tolerate Uptime Kuma's intermittent slow responses / transient 4xx.
- Keep send_heartbeat/check_and_send_heartbeat/send_stop_signal as
  backward-compatible wrappers; excel_sync_to_sql.py still uses them and
  now benefits from the longer timeout + retry.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-06-15 13:06:53 +08:00
parent 04779c5aea
commit dd27cbaf97
2 changed files with 112 additions and 82 deletions

View File

@@ -4,8 +4,8 @@ import sys
import pyodbc import pyodbc
from config import SQL_SERVER_CONN, ACCESS_DRIVER, SYNC_MAPPING, POLL_INTERVAL, LOG_TABLE_CONFIG, UPTIME_KUMA_CONFIG from config import SQL_SERVER_CONN, ACCESS_DRIVER, SYNC_MAPPING, POLL_INTERVAL, LOG_TABLE_CONFIG, UPTIME_KUMA_CONFIG
import db_utils import db_utils
from log_utils import (LoggerManager, log_success, log_error, log_warning, log_info, log_processing, from log_utils import (LoggerManager, log_error, log_warning, log_info, log_processing,
log_skip, log_critical, log_start, log_stop, log_file, log_database, log_sync) log_skip, log_critical, log_stop, log_file, log_database, log_sync)
from uptime_kuma_utils import UptimeKumaMonitor from uptime_kuma_utils import UptimeKumaMonitor
# 初始化日志管理器 # 初始化日志管理器
@@ -313,11 +313,11 @@ def process_sync_task():
file_error_count += 1 file_error_count += 1
continue continue
# 同步成功 # 同步成功 (仅本地日志记录, 不推送 ntfy; 服务存活状态由 Uptime Kuma 心跳负责)
msg = f"表 [{target_table}] 同步并校验通过: {len(inserted_pk_set)} 条入库" msg = f"表 [{target_table}] 同步并校验通过: {len(inserted_pk_set)} 条入库"
if removed_count > 0: if removed_count > 0:
msg += f", {removed_count} 条随源删除" msg += f", {removed_count} 条随源删除"
log_success(msg) log_info(msg)
file_success_count += 1 file_success_count += 1
file_total_records += len(record_ids) file_total_records += len(record_ids)
@@ -368,7 +368,8 @@ def process_sync_task():
# 使用 uptime_kuma_utils.UptimeKumaMonitor 替代原有实现 # 使用 uptime_kuma_utils.UptimeKumaMonitor 替代原有实现
if __name__ == "__main__": if __name__ == "__main__":
log_start("增量同步服务已启动 (配置驱动模式 + 逐主键落库校验)") # 仅本地日志记录, 不推送 ntfy 启动消息 (服务存活状态由 Uptime Kuma 心跳负责, 避免重复通知)
log_info("增量同步服务已启动 (配置驱动模式 + 逐主键落库校验)")
log_info(f"轮询间隔: {POLL_INTERVAL}") log_info(f"轮询间隔: {POLL_INTERVAL}")
log_info(f"监控配置: {len(SYNC_MAPPING)} 个文件") log_info(f"监控配置: {len(SYNC_MAPPING)} 个文件")
log_info(f"提交后复核: {'开启' if ENABLE_POST_COMMIT_VERIFY else '关闭'}") log_info(f"提交后复核: {'开启' if ENABLE_POST_COMMIT_VERIFY else '关闭'}")
@@ -376,16 +377,15 @@ if __name__ == "__main__":
log_info(f"心跳间隔: {UPTIME_KUMA_CONFIG['heartbeat_interval']}") log_info(f"心跳间隔: {UPTIME_KUMA_CONFIG['heartbeat_interval']}")
log_info("=" * 70) log_info("=" * 70)
# 启动时发送第一次心跳 # 启动后台心跳线程: 立即发送首跳, 之后按间隔周期发送 (与同步主循环解耦)
uptime_monitor.send_heartbeat() uptime_monitor.start()
try: try:
while True: while True:
try: try:
has_work = process_sync_task() has_work = process_sync_task()
# 检查是否需要发送心跳 # 心跳由后台线程独立周期发送, 此处无需再调用
uptime_monitor.check_and_send_heartbeat()
# 如果有工作,说明可能还有积压,休息短一点(0.1s) # 如果有工作,说明可能还有积压,休息短一点(0.1s)
# 如果没工作,休息标准间隔 # 如果没工作,休息标准间隔
@@ -399,5 +399,5 @@ if __name__ == "__main__":
log_critical(f"主循环崩溃: {e}") log_critical(f"主循环崩溃: {e}")
time.sleep(5) time.sleep(5)
finally: finally:
# 停止时发送心跳停止信号(可选) # 停止后台心跳线程, 并向 Uptime Kuma 发送 down 信号
uptime_monitor.send_stop_signal() uptime_monitor.stop()

View File

@@ -2,44 +2,58 @@
Uptime Kuma 心跳监控工具 Uptime Kuma 心跳监控工具
用于向 Uptime Kuma 服务发送心跳信号,监控服务运行状态。 用于向 Uptime Kuma 服务发送心跳信号,监控服务运行状态。
设计要点:
- 推荐通过 start() 启动后台心跳线程, 与业务主循环解耦 ——
心跳的网络耗时 / 重试不会阻塞业务逻辑。
- 单次心跳自带重试 (MAX_RETRIES), 容忍 Uptime Kuma 服务偶发的慢响应 / 超时 / 4xx。
- send_heartbeat() / check_and_send_heartbeat() / send_stop_signal() 保留为
向后兼容的同步接口 (excel_sync_to_sql.py 等仍在使用)。
""" """
import time import time
import threading
import requests import requests
# 单次请求超时(秒) —— 适当放宽, 容忍 Uptime Kuma 偶发的慢响应
REQUEST_TIMEOUT = 10
# 单次心跳的最大尝试次数 (含首次)
MAX_RETRIES = 3
# 重试间隔(秒)
RETRY_BACKOFF = 2
# stop() 时等待后台线程退出的最长时间(秒); 线程为 daemon, 超时也不会阻塞进程退出
STOP_JOIN_TIMEOUT = 40
class UptimeKumaMonitor: class UptimeKumaMonitor:
""" """
Uptime Kuma 心跳监控器 Uptime Kuma 心跳监控器
示例: 推荐 (后台线程模式, 与业务循环解耦)::
from uptime_kuma_utils import UptimeKumaMonitor
# 初始化监控器
monitor = UptimeKumaMonitor({ monitor = UptimeKumaMonitor({
'enabled': True, 'enabled': True,
'push_url': 'https://uptimekuma.example.com/api/push/xxx', 'push_url': 'https://uptimekuma.example.com/api/push/xxx',
'heartbeat_interval': 59 'heartbeat_interval': 59
}) })
monitor.start() # 启动后台线程: 立即发首跳, 之后按间隔周期发送
try:
... # 业务主循环
finally:
monitor.stop() # 停止线程并发送一次 down 信号
# 启动时发送首次心跳 向后兼容 (同步模式, 不推荐新代码使用)::
monitor.send_heartbeat()
# 主循环中定期发送心跳 monitor.send_heartbeat() # 同步发一次 (带重试)
while True: monitor.check_and_send_heartbeat() # 按间隔节流后同步发送
monitor.check_and_send_heartbeat()
# ... 执行任务 ...
# 停止时发送停止信号
monitor.send_stop_signal() monitor.send_stop_signal()
""" """
def __init__(self, config): def __init__(self, config):
""" """
初始化监控器
Args: Args:
config (dict): 配置字典包含: config (dict): 配置字典, 包含:
- enabled (bool): 是否启用心跳 - enabled (bool): 是否启用心跳
- push_url (str): Uptime Kuma 推送 URL - push_url (str): Uptime Kuma 推送 URL
- heartbeat_interval (int): 心跳间隔(秒) - heartbeat_interval (int): 心跳间隔(秒)
@@ -50,89 +64,105 @@ class UptimeKumaMonitor:
self.heartbeat_interval = self.config.get('heartbeat_interval', 60) self.heartbeat_interval = self.config.get('heartbeat_interval', 60)
self._last_heartbeat_time = 0 self._last_heartbeat_time = 0
self._logger = None self._logger = None
# 后台线程相关
self._thread = None
self._stop_event = threading.Event()
def set_logger(self, logger_func): def set_logger(self, logger_func):
""" """设置日志记录函数 (如 log_warning)。"""
设置日志记录函数
Args:
logger_func: 日志记录函数,如 log_warning, log_info 等
"""
self._logger = logger_func self._logger = logger_func
def _log(self, func_name, message): def _log(self, message):
"""内部日志记录方法"""
if self._logger: if self._logger:
self._logger(message) self._logger(message)
def send_heartbeat(self): # ================= 单次请求 + 重试 =================
"""
发送心跳信号到 Uptime Kuma
Returns: def _do_request(self, status, msg):
bool: 是否成功发送 """发送一次心跳请求, 返回是否成功 (HTTP 2xx)。失败时记录日志。"""
try:
params = {'status': status, 'msg': msg, 'ping': ''}
response = requests.get(self.push_url, params=params, timeout=REQUEST_TIMEOUT)
response.raise_for_status()
return True
except Exception as e:
self._log(f"心跳发送失败 (status={status}): {e}")
return False
def _send_with_retry(self, status='up', msg='OK'):
"""
带重试的心跳发送: 任一尝试成功即视为成功。
成功发送 'up' 时更新最近心跳时间。返回是否最终成功。
""" """
if not self.enabled or not self.push_url: if not self.enabled or not self.push_url:
return False return False
for attempt in range(1, MAX_RETRIES + 1):
try: if self._do_request(status, msg):
params = { if status == 'up':
'status': 'up',
'msg': 'OK',
'ping': ''
}
response = requests.get(self.push_url, params=params, timeout=5)
response.raise_for_status()
self._last_heartbeat_time = time.time() self._last_heartbeat_time = time.time()
return True return True
except Exception as e: if attempt < MAX_RETRIES:
self._log('warning', f"心跳发送失败: {e}") time.sleep(RETRY_BACKOFF)
return False return False
def send_stop_signal(self): # ================= 后台线程模式 (推荐) =================
"""
发送停止信号到 Uptime Kuma
Returns: def start(self):
bool: 是否成功发送 """
启动后台心跳线程: 立即发送一次, 之后按 heartbeat_interval 周期发送。
与业务主循环完全解耦, 心跳的网络耗时 / 重试不会阻塞业务。
重复调用安全 (已在运行则直接返回)。
""" """
if not self.enabled or not self.push_url: if not self.enabled or not self.push_url:
return False return
if self._thread is not None and self._thread.is_alive():
return
self._stop_event.clear()
self._thread = threading.Thread(
target=self._heartbeat_loop, daemon=True, name='uptime-kuma-heartbeat')
self._thread.start()
try: def _heartbeat_loop(self):
params = {'status': 'down', 'msg': 'Service stopped'} # 启动立即发一次
response = requests.get(self.push_url, params=params, timeout=5) self._send_with_retry()
response.raise_for_status() # 周期发送, 直到 stop() 触发 _stop_event
return True # Event.wait(interval) 在超时返回 False (继续发), 被置位时返回 True (退出)
except Exception as e: while not self._stop_event.wait(self.heartbeat_interval):
# 停止信号失败不影响主逻辑 self._send_with_retry()
def stop(self):
"""停止后台心跳线程, 并向 Uptime Kuma 发送一次 down 信号。"""
self._stop_event.set()
if self._thread is not None:
self._thread.join(timeout=STOP_JOIN_TIMEOUT)
self._thread = None
self.send_stop_signal()
# ================= 向后兼容的同步接口 =================
def send_heartbeat(self):
"""同步发送一次心跳 (自带重试)。向后兼容用法。"""
return self._send_with_retry()
def send_stop_signal(self):
"""发送停止(down)信号。失败不影响主逻辑。"""
if not self.enabled or not self.push_url:
return False return False
return self._do_request('down', 'Service stopped')
def check_and_send_heartbeat(self): def check_and_send_heartbeat(self):
""" """按间隔节流后同步发送心跳。向后兼容用法。"""
检查是否需要发送心跳,如果需要则发送
Returns:
bool: 是否发送了心跳
"""
if not self.enabled: if not self.enabled:
return False return False
if time.time() - self._last_heartbeat_time >= self.heartbeat_interval:
time_since_last = time.time() - self._last_heartbeat_time
if time_since_last >= self.heartbeat_interval:
return self.send_heartbeat() return self.send_heartbeat()
return False return False
def get_time_since_last_heartbeat(self): def get_time_since_last_heartbeat(self):
""" """获取距离上次心跳的时间(秒)。"""
获取距离上次心跳的时间(秒)
Returns:
float: 距离上次心跳的秒数
"""
return time.time() - self._last_heartbeat_time return time.time() - self._last_heartbeat_time
@property @property
def last_heartbeat_time(self): def last_heartbeat_time(self):
"""获取上次心跳时间戳""" """获取上次心跳时间戳"""
return self._last_heartbeat_time return self._last_heartbeat_time