"""
Ch8 配套代码 3 / 3 —— WAIT N timeout 演示

⚠️ WAIT 命令需要真实主从环境才能看到「>0」的返回值。
   单机环境（无 replica）会一直返回 0，脚本会自动检测并打印 mock 演示。

WAIT N timeout 含义：
   - N        : 期望至少有多少个 replica 接收到「当前已发出的所有写命令」
   - timeout  : 最多等待多少毫秒（0 = 无限等）
   - 返回值   : 实际确认的 replica 数（可能小于 N，如果超时）

典型用法：
   r.set("key", "value")
   acked = r.wait(numreplicas=1, timeout=100)   # 最多等 100ms
   if acked < 1:
       # 没等到至少 1 个 replica 确认 → 业务可重试 / 降级 / 报警

⚠️ WAIT 不是 quorum 写：
   - 主节点宕机仍可能丢数据（Redis 没有 fsync-then-ack 协议）
   - 它只保证「至少 N 个 replica 收到 + 应用了当前 offset」
"""

import time
import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)


def section(title: str) -> None:
    print("\n" + "=" * 70)
    print(title)
    print("=" * 70)


def detect_env() -> tuple[bool, int]:
    info = r.info("replication")
    role = info.get("role")
    n = info.get("connected_slaves", 0)
    print(f"  当前角色 : {role}")
    print(f"  在线 replica: {n}")
    is_real = (role == "master") and (n > 0)
    return is_real, n


def demo_real_wait():
    section("Demo: 真实主从环境下的 WAIT 演示")
    print("  场景：写一批 key 后调 WAIT，观察等待行为")
    for i in range(5):
        key = f"demo:wait:{i}"
        r.set(key, f"v{i}")

    print("\n  调用 r.wait(numreplicas=1, timeout=200)")
    t0 = time.perf_counter()
    acked = r.wait(num_replicas=1, timeout=200)
    elapsed = (time.perf_counter() - t0) * 1000
    print(f"    返回 = {acked}（确认数）")
    print(f"    耗时 = {elapsed:.1f} ms")
    if acked >= 1:
        print("    ✅ 至少 1 个 replica 已经同步到了当前 offset")
    else:
        print("    ⚠️ 200ms 内没有 replica 完成同步（要么没 replica，要么延迟太高）")

    print("\n  对比：r.wait(numreplicas=99, timeout=300)（要求过高）")
    t0 = time.perf_counter()
    acked = r.wait(num_replicas=99, timeout=300)
    elapsed = (time.perf_counter() - t0) * 1000
    print(f"    返回 = {acked}, 耗时 = {elapsed:.1f} ms")
    print("    ✅ WAIT 的语义：等不到也不会报错，最多等 timeout 毫秒就返回实际数")

    for i in range(5):
        r.delete(f"demo:wait:{i}")


def demo_mock_wait():
    section("Demo: 单机环境（无 replica）的 WAIT 演示")
    print("  调用真实的 WAIT 命令观察返回值（单机必为 0）：\n")
    r.set("demo:wait:single", "v1")
    t0 = time.perf_counter()
    acked = r.wait(num_replicas=1, timeout=100)
    elapsed = (time.perf_counter() - t0) * 1000
    print(f"    r.wait(1, 100) → 返回 {acked}, 耗时 ≈ {elapsed:.0f} ms")
    print(f"    （等不到任何 replica，等到 timeout 后返回 0）\n")
    r.delete("demo:wait:single")

    section("MOCK：典型主从环境下 WAIT 的行为")
    rows = [
        ("SET k v + WAIT 0 0",        "0",    "0",     "立即返回（不要求确认）"),
        ("SET k v + WAIT 1 100",      "1",    "5",     "1 个 replica 5ms 内确认"),
        ("SET k v + WAIT 2 100",      "2",    "12",    "2 个 replica 都确认"),
        ("SET k v + WAIT 3 100",      "2",    "100",   "只有 2 个 replica，等 100ms 超时返回 2"),
        ("大批量写 + WAIT 1 50",      "0",    "50",    "写得快 + 50ms 太短 → 超时返回 0"),
        ("大批量写 + WAIT 1 1000",    "1",    "320",   "等 1s 充足，1 个 replica 320ms 内追上"),
    ]
    print(f"  {'命令':<32} | {'返回':>4} | {'耗时(ms)':>8} | 说明")
    print(f"  {'-'*32}-+-{'-'*4}-+-{'-'*8}-+-{'-'*30}")
    for cmd, ret, ms, desc in rows:
        print(f"  {cmd:<32} | {ret:>4} | {ms:>8} | {desc}")


def show_wait_caveats():
    section("WAIT 的 3 个隐藏坑（必看）")
    print("""\
  ① WAIT 不保证「数据不丢」
     - 即便 WAIT 1 返回成功，如果主节点立刻宕机：
         · 那 1 个收到命令的 replica 可能恰好被切成新主 → 数据保留 ✅
         · 但 fail-over 由哨兵 / Cluster 决定，不一定选中那台 → 数据可能丢 ⚠️
     - 真要强一致，得用 etcd / ZooKeeper / Spanner 这种带 quorum 写的系统

  ② WAIT 会拖慢写延迟
     - 高 QPS 写路径慎用，否则整个集群吞吐被拉低
     - 关键写 + 偶发使用比较合理

  ③ WAIT 不指定具体 replica
     - 返回 N 表示「至少 N 个 replica 同步到当前 offset」
     - 但是哪 N 个？不知道。所以也不能用它做「特定从节点优先选主」

  💡 实践建议：
     - 大多数业务用默认异步复制 + 业务幂等就够了
     - 关键写入（订单创建、支付完成）用 WAIT 1 短超时
     - 强一致核心数据 → 直接所有读写都走主，放弃读写分离
     - 金融级写入安全 → WAIT + min-replicas-to-write 双保险\
""")


if __name__ == "__main__":
    try:
        section("环境检测")
        is_real, n = detect_env()
        if is_real:
            demo_real_wait()
        else:
            demo_mock_wait()
        show_wait_caveats()
        print("\n✅ 完成。把脚本拿到真实主从（1 主 N 从）跑能看到 WAIT 返回 > 0。")
    except redis.ConnectionError as e:
        print(f"❌ Redis 连接失败: {e}")
