"""
Ch5 配套代码 3 / 3 —— 8 种淘汰策略实测

通过 CONFIG SET 把 maxmemory 调小，写入超量数据，观察不同策略下哪些 Key 被踢出。
对比 4 种典型策略：noeviction / allkeys-lru / allkeys-lfu / volatile-ttl。
"""

import time
import redis

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

VALUE = "x" * 1024
PREFIX = "evict:"
N_KEYS = 200
KEEP_HOT = ("evict:hot:1", "evict:hot:2", "evict:hot:3")
ORIGINAL = {}


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


def save_original_config() -> None:
    ORIGINAL["maxmemory"] = r.config_get("maxmemory")["maxmemory"]
    ORIGINAL["maxmemory-policy"] = r.config_get("maxmemory-policy")["maxmemory-policy"]


def restore_config() -> None:
    r.config_set("maxmemory", ORIGINAL.get("maxmemory", "0"))
    r.config_set("maxmemory-policy", ORIGINAL.get("maxmemory-policy", "noeviction"))


def cleanup() -> None:
    keys = r.keys(PREFIX + "*")
    if keys:
        r.delete(*keys)


def fill_and_observe(policy: str, with_ttl: bool = False) -> None:
    cleanup()
    r.config_set("maxmemory", 0)
    r.config_set("maxmemory-policy", policy)

    for i in range(20):
        key = f"{PREFIX}cold:{i}"
        if with_ttl:
            r.set(key, VALUE, ex=60)
        else:
            r.set(key, VALUE)
    for hot in KEEP_HOT:
        r.set(hot, VALUE)
        for _ in range(50):
            r.get(hot)

    used = int(r.info("memory")["used_memory"])
    target = used + 100 * 1024
    r.config_set("maxmemory", target)
    print(f"  策略={policy:<18} 容量上限={target} bytes (~{target//1024}KB)")

    evicted_before = int(r.info("stats")["evicted_keys"])

    fail_count = 0
    for i in range(N_KEYS):
        try:
            key = f"{PREFIX}new:{i}"
            if with_ttl:
                r.set(key, VALUE, ex=120)
            else:
                r.set(key, VALUE)
        except redis.ResponseError as e:
            fail_count += 1
            if fail_count <= 1:
                print(f"  ⚠ 写入报错（OOM 写报错预期出现 in noeviction）: {e}")

    time.sleep(0.1)
    evicted_after = int(r.info("stats")["evicted_keys"])
    cold_remain = sum(1 for i in range(20) if r.exists(f"{PREFIX}cold:{i}"))
    hot_remain = sum(1 for k in KEEP_HOT if r.exists(k))
    new_remain = sum(1 for i in range(N_KEYS) if r.exists(f"{PREFIX}new:{i}"))

    print(f"     淘汰数 = {evicted_after - evicted_before:>4} | "
          f"冷数据剩 {cold_remain:>2}/20 | 热点剩 {hot_remain}/3 | 新写入剩 {new_remain}/{N_KEYS} | OOM 报错 {fail_count}")


def main() -> None:
    section("8 种淘汰策略对比（重点看 4 种）")
    save_original_config()

    try:
        print("\n  [测试 1] noeviction：内存满时直接拒写")
        fill_and_observe("noeviction", with_ttl=False)

        print("\n  [测试 2] allkeys-lru：踢最久未访问，热点应保留")
        fill_and_observe("allkeys-lru", with_ttl=False)

        print("\n  [测试 3] allkeys-lfu：踢访问频次低的，热点应保留")
        fill_and_observe("allkeys-lfu", with_ttl=False)

        print("\n  [测试 4] volatile-ttl：仅淘汰带 TTL 且 TTL 最小的")
        fill_and_observe("volatile-ttl", with_ttl=True)

        print("\n  💡 观察要点：")
        print("     - noeviction 下「新写入剩 N」会很少（因为大量被拒）")
        print("     - allkeys-lru / lfu 下「热点剩 3/3」（被反复 GET 过）")
        print("     - volatile-ttl 下淘汰的是 TTL 较短的，hot 没设 TTL 不参与")
    finally:
        cleanup()
        restore_config()
        print("\n  已恢复 maxmemory 与 maxmemory-policy 原始值")


if __name__ == "__main__":
    try:
        main()
    except redis.ConnectionError as e:
        print(f"❌ Redis 连接失败: {e}")
