"""
Ch9 配套代码 3 / 3 —— sentinel.conf 配置项解读

本脚本不连任何 Redis，只是把生产常用的 sentinel.conf 配置项
以「示例值 + 含义 + 调参建议 + 常见踩坑」的方式集中输出，方便速查。

直接运行：
    python 03_sentinel_config_explained.py
或者过滤某项：
    python 03_sentinel_config_explained.py monitor
"""

import sys
import textwrap
from dataclasses import dataclass


@dataclass
class ConfItem:
    name: str
    sample: str
    desc: str
    tuning: str
    pitfall: str


CONFIG: list[ConfItem] = [
    ConfItem(
        name="port",
        sample="port 26379",
        desc="哨兵自身监听的端口。客户端 / 兄弟哨兵都通过这个端口跟它说话。",
        tuning="一台机器跑多个哨兵实例时端口要错开（26379/26380/26381）。",
        pitfall="不要和 redis-server 端口冲突。",
    ),
    ConfItem(
        name="dir",
        sample='dir "/var/lib/redis-sentinel"',
        desc="哨兵的工作目录。哨兵会把当前已知的主从拓扑回写到 sentinel.conf。",
        tuning="必须可写。容器化时记得挂卷，否则重启后状态丢失。",
        pitfall="只读目录会让哨兵在故障转移后无法持久化新的主地址。",
    ),
    ConfItem(
        name="monitor",
        sample="sentinel monitor mymaster 127.0.0.1 6379 2",
        desc="""\
            告诉哨兵监控哪个主节点。4 个参数依次是：
              ① 主节点逻辑名（客户端 SDK 用这个名字找主）
              ② 主节点 IP
              ③ 主节点端口
              ④ quorum：判定 ODOWN 所需的最少哨兵数""",
        tuning="quorum 一般取 ⌈N/2⌉（5 个哨兵设 3，3 个哨兵设 2）。",
        pitfall="quorum 设成 1 等于绕过共识，回到单哨兵脑裂。",
    ),
    ConfItem(
        name="down-after-milliseconds",
        sample="sentinel down-after-milliseconds mymaster 30000",
        desc="哨兵 PING 多久没回复就把节点判为主观下线（SDOWN）。",
        tuning="""\
            在线核心业务 5000~10000ms 比较敏感，
            非核心 / 网络抖动多的环境保留默认 30000ms 更稳。""",
        pitfall="设得太小会因短时网络抖动频繁误切；太大故障恢复慢。",
    ),
    ConfItem(
        name="parallel-syncs",
        sample="sentinel parallel-syncs mymaster 1",
        desc="故障转移时，让多少个从节点 同时 SLAVEOF 新主进行同步。",
        tuning="""\
            1 是最稳妥（一个个排队同步，期间从可读旧数据），
            从节点多 + 主能扛同步压力时可以设到 2~3 加快收敛。""",
        pitfall="设得过大会让全部从同时全量同步，主节点 IO 被打爆 + 客户端读全部不可用。",
    ),
    ConfItem(
        name="failover-timeout",
        sample="sentinel failover-timeout mymaster 180000",
        desc="""\
            一次故障转移整个流程的超时时间，包含：
              · 选 Leader
              · 选新主
              · 让新主 SLAVEOF NO ONE
              · 重排其他从
            超时 = 视为本轮失败，下一轮选举可重新发起。""",
        tuning="默认 180s 一般够用；从节点特别多（>10）时可以放大到 300s。",
        pitfall="设得太小会让大集群一直「选举失败 → 重试」无法稳定。",
    ),
    ConfItem(
        name="auth-pass",
        sample="sentinel auth-pass mymaster s3cret!",
        desc="主从节点的连接密码。哨兵要能登入主从才能 INFO/SLAVEOF。",
        tuning="生产强烈推荐设密码，且哨兵自己的 requirepass 也要配。",
        pitfall="主和从的 requirepass / masterauth 必须一致，否则故障转移后从挂不上新主。",
    ),
    ConfItem(
        name="notification-script",
        sample="sentinel notification-script mymaster /opt/notify.sh",
        desc="任何 +sdown / +odown / +switch-master 等事件触发时，调用这个脚本。",
        tuning="一般用来做钉钉 / 邮件 / 短信告警。",
        pitfall="""\
            脚本必须 60s 内退出，否则哨兵会 SIGKILL；
            脚本异常退出码会被记日志但不影响故障转移流程。""",
    ),
    ConfItem(
        name="client-reconfig-script",
        sample="sentinel client-reconfig-script mymaster /opt/reload-lb.sh",
        desc="""\
            主切换完成后调用，常用于通知 LVS / Nginx / 配置中心
            把上游 Redis 地址换成新主。""",
        tuning="客户端用 Sentinel SDK 时这个脚本可以不配；只在有外部代理层时需要。",
        pitfall="脚本传入参数顺序：master_name role state from_ip from_port to_ip to_port",
    ),
    ConfItem(
        name="deny-scripts-reconfig",
        sample="sentinel deny-scripts-reconfig yes",
        desc="禁止通过 SENTINEL SET 命令在线修改 notification-script 等高危项。",
        tuning="生产必开，避免被攻破后用脚本路径注入恶意命令。",
        pitfall="老版本默认 no，升级后注意。",
    ),
    ConfItem(
        name="resolve-hostnames",
        sample="sentinel resolve-hostnames yes",
        desc="允许在 sentinel monitor 里写主机名而不是 IP（Redis 6.2+）。",
        tuning="K8s / 容器场景非常有用，IP 漂移时不用改配置。",
        pitfall="DNS 解析失败时哨兵会拒绝该主节点的故障转移。",
    ),
    ConfItem(
        name="announce-ip / announce-port",
        sample="sentinel announce-ip 10.0.0.5\nsentinel announce-port 26379",
        desc="哨兵向其他哨兵 / 客户端宣告自己时使用的 IP/端口。",
        tuning="跨 NAT / Docker 网络时必填，否则别人收到的是容器内网 IP 没法连。",
        pitfall="忘配会出现「能监控但客户端连不上」的诡异现象。",
    ),
]


def render(item: ConfItem) -> str:
    lines = []
    lines.append("┌" + "─" * 72)
    lines.append(f"│ ▎{item.name}")
    lines.append("├" + "─" * 72)
    lines.append("│ 示例：")
    for ln in item.sample.splitlines():
        lines.append(f"│   {ln}")
    lines.append("│ 含义：")
    desc = textwrap.dedent(item.desc).strip()
    for ln in desc.splitlines():
        lines.append(f"│   {ln}")
    lines.append("│ 调参建议：")
    tuning = textwrap.dedent(item.tuning).strip()
    for ln in tuning.splitlines():
        lines.append(f"│   {ln}")
    lines.append("│ 踩坑提醒：")
    pitfall = textwrap.dedent(item.pitfall).strip()
    for ln in pitfall.splitlines():
        lines.append(f"│   ⚠️ {ln}")
    lines.append("└" + "─" * 72)
    return "\n".join(lines)


def main() -> None:
    keyword = sys.argv[1].lower() if len(sys.argv) > 1 else None
    print("=" * 74)
    print(" sentinel.conf 关键配置项速查 ".center(74, "="))
    print("=" * 74)

    shown = 0
    for item in CONFIG:
        if keyword and keyword not in item.name.lower():
            continue
        print(render(item))
        print()
        shown += 1

    if keyword and shown == 0:
        print(f"⚠️ 没有匹配 {keyword!r} 的配置项")
        print(f"   可用项：{', '.join(c.name for c in CONFIG)}")
        return

    print("─" * 74)
    print("📌 完整生产模板（3 哨兵）")
    print("─" * 74)
    print(textwrap.dedent("""\
        port 26379
        dir "/var/lib/redis-sentinel"
        sentinel monitor mymaster 10.0.0.10 6379 2
        sentinel auth-pass mymaster s3cret!
        sentinel down-after-milliseconds mymaster 10000
        sentinel parallel-syncs mymaster 1
        sentinel failover-timeout mymaster 180000
        sentinel deny-scripts-reconfig yes
        sentinel announce-ip 10.0.0.20
    """))


if __name__ == "__main__":
    main()
