Files
zlareb1andClaude Sonnet 4.6 8694d70550 keeper stress: improve exception logging across gates, faults, and sampler
Replace silent `except Exception: pass` and unexplained early-return paths with
descriptive messages so test failures surface real causes instead of silently
skipping important checks.

- `gates/base.py`: `backlog_drains`, `config_converged`, `ephemerals_gone_within`,
  `ready_expect` — log first failure with `_warned` flag to avoid log spam;
  `_aggregate_totals` and `_conf_members_count` print node name and error on each
  exception; `coord_timeouts_match` early-return prints explain which precondition
  was not met (no ctx, no cluster, missing config dir, no XML files, unreadable file)
- `faults/base.py`: `_step_background_schedule`, `_step_parallel`, `apply_step` —
  print when sub-steps list is empty or `kind` field is missing or unrecognised;
  `_step_ensure_paths` prints when paths list is empty and logs `mntr` read failures
- `sampler.py`: `_loop` prints flush errors instead of swallowing them

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-03-21 16:08:02 +00:00

808 lines
27 KiB
Python

import os
import re
import time
from pathlib import Path
from kazoo.client import KazooClient
from keeper.framework.core.settings import (
DEFAULT_ERROR_RATE,
DEFAULT_P95_MS,
DEFAULT_P99_MS,
)
from keeper.framework.core.util import leader_or_first, parse_bool, wait_until
from keeper.framework.io.probes import (
any_ephemerals,
ch_trace_log,
count_leaders,
four,
lgif,
mntr,
prom_metrics,
ready,
srvr_kv,
wchp_paths,
wchs_total,
)
from keeper.framework.io.prom_parse import parse_prometheus_text
from keeper.workloads.adapter import servers_arg
# Gate registry
_GATE_HANDLERS = {}
def register_gate(name):
"""Decorator to register a gate handler function."""
def decorator(func):
_GATE_HANDLERS[name] = func
return func
return decorator
def _extract_param(g, key, default=None, type_fn=None):
"""Helper to extract and optionally convert a parameter from gate config."""
val = g.get(key, default)
if val is None or val is default:
return default
return type_fn(val) if type_fn else val
# Core gate functions (used directly and by handlers)
def single_leader(nodes, timeout_s=180):
wait_until(
lambda: count_leaders(nodes) == 1,
timeout_s=timeout_s,
interval=0.5,
desc="single_leader failed",
)
def backlog_drains(nodes, max_s=120):
# Best-effort: wait until watches drop near baseline
deadline = time.time() + max(1, int(max_s))
last = None
_warned = False
while time.time() < deadline:
try:
cur = sum(wchs_total(n) for n in nodes)
if last is not None and cur <= last:
if cur <= 5:
return
last = cur
except Exception as e:
if not _warned:
print(f"Watch count probe failed while waiting for backlog to drain, will keep retrying: {e}")
_warned = True
time.sleep(1.0)
def _aggregate_totals(nodes, targets, aggregate, per_node_values_fn):
"""Aggregate metric values across nodes (sum or max)."""
agg = str(aggregate or "sum").strip().lower()
totals = {k: 0.0 for k in (targets or {})}
for n in nodes or []:
try:
for k, val in per_node_values_fn(n):
if k not in targets:
continue
v = float(val) if val is not None else 0.0
totals[k] = max(totals[k], v) if agg == "max" else totals[k] + v
except Exception as e:
print(f"Skipping metrics aggregation for node {getattr(n, 'name', n)}: {e}")
continue
return totals
def _conf_members_count(node):
"""Count server entries in 4lw conf output."""
try:
txt = four(node, "conf") or ""
return sum(1 for line in txt.splitlines() if line.strip().startswith("server."))
except Exception as e:
print(f"4lw 'conf' query failed on node {getattr(node, 'name', node)}, treating as 0 cluster members: {e}")
return 0
def config_members_len_eq(nodes, expected):
# Compare against leader's conf and assert equals expected
if not nodes:
raise AssertionError("no nodes")
target = leader_or_first(nodes)
cnt = _conf_members_count(target)
# If 4lw 'conf' is unavailable, we cannot strictly validate; be lenient in stress env
if int(cnt) <= 0:
return
if int(cnt) != int(expected):
raise AssertionError(f"config_members_len_eq: got {cnt}, expected {expected}")
def config_converged(nodes, timeout_s=30):
# Ensure all nodes show the same conf members count as leader
if not nodes:
raise AssertionError("no nodes")
target = leader_or_first(nodes)
expected = _conf_members_count(target)
# If 4lw 'conf' is unavailable on this build, fall back to topology size for smoke runs
if expected <= 0:
expected = len(nodes)
deadline = time.time() + max(1, int(timeout_s))
_warned = False
while time.time() < deadline:
try:
ok = True
for n in nodes:
cnt = _conf_members_count(n)
# Treat unknown (<=0) as inconclusive rather than failure
if cnt > 0 and cnt != expected:
ok = False
break
if ok:
return
except Exception as e:
if not _warned:
print(f"Cluster conf convergence check failed, will keep retrying: {e}")
_warned = True
time.sleep(0.5)
raise AssertionError("config_converged: mismatch across nodes")
def coord_timeouts_match(nodes, ctx, startup=None, shutdown=None):
"""Validate that coordination startup/shutdown timeouts match expectations.
Best-effort: parse generated keeper_config_*.xml from instances_dir/configs/<cluster_name>/.
If values are missing or files not found, treat as non-fatal (pass).
"""
if ctx is None:
print("Skipping coordination timeout validation: no metrics context available to locate keeper config files")
return
cluster = ctx.get("cluster")
if cluster is None:
print("Skipping coordination timeout validation: cluster object not found in metrics context")
return
cname = os.environ.get("KEEPER_CLUSTER_NAME", "")
inst_dir = Path(getattr(cluster, "instances_dir", ""))
conf_dir = inst_dir / "configs" / (cname or "")
if not conf_dir.exists():
print(f"Skipping coordination timeout validation: keeper config directory not found at {conf_dir}")
return
# Read one config (all nodes share the same coord settings)
paths = sorted(conf_dir.glob("keeper_config_*.xml"))
if not paths:
print(f"Skipping coordination timeout validation: no keeper_config_*.xml files found in {conf_dir}")
return
try:
txt = paths[0].read_text(encoding="utf-8")
except OSError as e:
print(f"Skipping coordination timeout validation: could not read config file {paths[0]}: {e}")
return
if startup is not None:
exp = int(startup)
m = re.search(r"<startup_timeout>\s*([0-9]+)\s*</startup_timeout>", txt)
if m and int(m.group(1)) != exp:
raise AssertionError(
f"coord_timeouts_match: startup_timeout={m.group(1)} != {exp}"
)
if shutdown is not None:
exp = int(shutdown)
m = re.search(r"<shutdown_timeout>\s*([0-9]+)\s*</shutdown_timeout>", txt)
if m and int(m.group(1)) != exp:
raise AssertionError(
f"coord_timeouts_match: shutdown_timeout={m.group(1)} != {exp}"
)
def error_rate_le(summary, max_ratio=DEFAULT_ERROR_RATE):
"""Assert error rate is below threshold."""
summary = summary or {}
errs = float(summary.get("errors", 0) or 0)
ops = float(summary.get("ops", 0) or 0)
if ops <= 0:
return
ratio = errs / ops
if ratio > float(max_ratio):
raise AssertionError(
f"error_rate_le: ratio {ratio:.4f} exceeds {max_ratio:.4f}"
)
def srvr_thresholds_le(nodes, metrics, aggregate="sum"):
"""Assert srvr metrics are below thresholds."""
targets = dict(metrics or {})
if not targets:
return
def _values(n):
sk = srvr_kv(n) or {}
for k in targets:
if k in sk:
yield k, sk[k]
totals = _aggregate_totals(nodes, targets, aggregate, _values)
for k, thr in targets.items():
if float(totals.get(k, 0.0)) > float(thr):
raise AssertionError(
f"srvr_thresholds_le: {k}={totals.get(k, 0.0)} exceeds {thr}"
)
def rps_ge(summary, min_rps=0.0):
"""Assert requests per second is above threshold."""
summary = summary or {}
dur = float(summary.get("duration_s", 0) or 0)
ops = float(summary.get("ops", 0) or 0)
rps = ops / dur if dur > 0 else 0.0
if rps < float(min_rps):
raise AssertionError(f"rps_ge: {rps:.3f} < {min_rps:.3f}")
def ops_ge(summary, min_ops=0.0):
"""Assert total ops is above threshold."""
summary = summary or {}
ops = float(summary.get("ops", 0) or 0)
if ops < float(min_ops):
raise AssertionError(f"ops_ge: {ops:.0f} < {min_ops:.0f}")
def _zk_count_descendants(zk, path):
try:
if not zk.exists(path):
return 0
children = zk.get_children(path)
total = 0
for c in children:
sub = (path.rstrip("/") + "/" + c) if path != "/" else "/" + c
total += 1
total += _zk_count_descendants(zk, sub)
return total
except Exception:
return 0
def count_paths(nodes, prefixes):
"""Count znodes under given prefixes."""
hosts = [h.strip() for h in (servers_arg(nodes) or "").split() if h.strip()]
hostlist = ",".join(hosts) if hosts else "localhost:9181"
zk = None
results = []
try:
zk = KazooClient(hosts=hostlist, timeout=10.0)
zk.start(timeout=10.0)
for p in prefixes or []:
cnt = _zk_count_descendants(zk, str(p))
print(f"[keeper] count_paths prefix={p} count={cnt}")
results.append((str(p), cnt))
except Exception:
for p in prefixes or []:
print(f"[keeper] count_paths prefix={p} count=0 (no_zk_conn)")
results.append((str(p), 0))
finally:
if zk:
try:
zk.stop()
zk.close()
except Exception:
pass
# Debug output
if parse_bool(os.environ.get("KEEPER_DEBUG")) and results:
out = (
Path(__file__).parents[4]
/ "tests"
/ "stress"
/ "keeper"
/ "tests"
/ "keeper_counts.txt"
)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(
"\n".join(f"{p},{c}" for p, c in results) + "\n", encoding="utf-8"
)
def election_time_le(ctx, max_s=10.0):
"""Assert election time is below threshold."""
v = (ctx or {}).get("election_time_s")
if v is None:
raise AssertionError("election_time_le: missing election_time_s in context")
if float(v) > float(max_s):
raise AssertionError(f"election_time_le: {float(v):.3f}s exceeded {max_s:.3f}s")
def log_sanity_ok(nodes, allow=None, limit_rows=1000):
# Scan recent trace logs for severe indicators. Allowlist can silence benign patterns.
allow = [re.compile(p, re.IGNORECASE) for p in (allow or []) if str(p).strip()]
disallow_patterns = [
re.compile(p, re.IGNORECASE)
for p in [
"AddressSanitizer|UBSAN|Sanitizer:",
"heap-(use-after-free|buffer-overflow|overflow)",
"Segmentation fault|SIGSEGV|std::terminate|terminate called",
"CHECK FAILED|assert(ion)? failed|FATAL",
]
]
offenders = []
for n in nodes or []:
try:
txt = ch_trace_log(n, limit_rows=limit_rows) or ""
except Exception:
txt = ""
if not txt:
continue
for line in txt.splitlines():
s = line.strip()
if not s:
continue
if allow and any(r.search(s) for r in allow):
continue
if any(r.search(s) for r in disallow_patterns):
offenders.append((n.name, s))
if len(offenders) >= 5:
break
if len(offenders) >= 5:
break
if offenders:
sample = "; ".join([f"{a}: {b[:160]}" for a, b in offenders])
raise AssertionError(f"log_sanity_ok: found severe log lines: {sample}")
def _max_latency(summary, keys):
"""Get maximum latency value from summary for given keys."""
try:
summary = summary or {}
vals = [float(summary.get(k, 0) or 0) for k in (keys or [])]
return max(vals) if vals else 0.0
except Exception:
return 0.0
def p99_le(summary, max_ms=DEFAULT_P99_MS):
"""Assert p99 latency is below threshold."""
summary = summary or {}
val = _max_latency(summary, ["read_p99_ms", "write_p99_ms"])
if val > float(max_ms):
raise AssertionError(f"p99_le: {val:.3f}ms exceeds {float(max_ms):.3f}ms")
def p95_le(summary, max_ms=DEFAULT_P95_MS):
"""Assert p95 latency is below threshold."""
summary = summary or {}
val = _max_latency(summary, ["read_p95_ms", "write_p95_ms"])
if val > float(max_ms):
raise AssertionError(f"p95_le: {val:.3f}ms exceeds {float(max_ms):.3f}ms")
def watch_delta_within(nodes, ctx, max_delta=None, pct=None):
"""Assert watch count delta is within threshold."""
cur_total = sum(int(wchs_total(n) or 0) for n in (nodes or []))
base_total = int((ctx or {}).get("watch_baseline_total", 0) or 0)
if base_total <= 0:
return # No baseline, treat as non-fatal
delta = abs(cur_total - base_total)
if pct is not None and float(pct) >= 0:
allowed = base_total * (float(pct) / 100.0)
if delta > allowed:
raise AssertionError(
f"watch_delta_within: |{cur_total}-{base_total}| > {allowed:.1f} (pct {pct}%)"
)
return
threshold = int(max_delta) if max_delta is not None else 100
if delta > threshold:
raise AssertionError(
f"watch_delta_within: |{cur_total}-{base_total}| > {threshold}"
)
def no_watcher_hotspot(nodes, ctx=None, max_share=0.95, max_path_share=None):
"""Assert no single node has excessive watcher share."""
totals = [int(wchs_total(n) or 0) for n in (nodes or [])]
total = sum(totals)
if total > 0:
worst = max(totals)
share = float(worst) / float(total)
if share > float(max_share):
raise AssertionError(
f"no_watcher_hotspot: worst_share={share:.3f} exceeds {max_share:.3f}"
)
if max_path_share is not None:
agg = {}
for n in nodes or []:
for k, v in (wchp_paths(n) or {}).items():
agg[k] = agg.get(k, 0) + int(v)
t = sum(agg.values())
if t > 0:
w = max(agg.values())
s = float(w) / float(t)
if s > float(max_path_share):
raise AssertionError(
f"no_watcher_hotspot: worst_path_share={s:.3f} exceeds {max_path_share:.3f}"
)
def ephemerals_gone_within(nodes, max_s=60):
deadline = time.time() + max(1, int(max_s))
_warned = False
while time.time() < deadline:
try:
if not any(any_ephemerals(n) for n in nodes):
return
except Exception as e:
if not _warned:
print(f"Ephemeral node probe failed while waiting for sessions to expire, will keep retrying: {e}")
_warned = True
time.sleep(0.5)
def ready_expect(nodes, leader, ok=True, timeout_s=60):
deadline = time.time() + max(1, int(timeout_s))
_warned = False
while time.time() < deadline:
try:
r = ready(leader)
if bool(r) == bool(ok):
return
except Exception as e:
if not _warned:
print(f"Keeper readiness probe failed, will keep retrying for up to {int(timeout_s)}s: {e}")
_warned = True
time.sleep(0.5)
raise AssertionError(
f"ready_expect: expected {bool(ok)} but condition not met in {int(timeout_s)}s"
)
def lgif_monotone(nodes, ctx=None):
"""Assert lgif metrics are monotonically increasing."""
if not ctx:
return
for n in nodes or []:
cur = lgif(n) or {}
base = ((ctx.get("_metrics_cache_baseline") or {}).get(n.name) or {}).get(
"lgif"
)
if not base:
continue
dec = []
for k, bv in base.items():
try:
bvi, cv = int(bv), int(cur.get(k, bv))
if cv < bvi:
dec.append(k)
except (ValueError, TypeError):
continue
if dec:
raise AssertionError(
f"lgif_monotone: decreased keys: {','.join(sorted(dec)[:5])}"
)
def fourlw_enforces(nodes, allow=None, deny=None):
allow = [str(x).strip() for x in (allow or []) if str(x).strip()]
deny = [str(x).strip() for x in (deny or []) if str(x).strip()]
for n in nodes or []:
for cmd in allow:
out = four(n, cmd)
if not str(out).strip():
raise AssertionError(f"fourlw_enforces: allow {cmd} returned empty")
for cmd in deny:
out = four(n, cmd)
if str(out).strip() and (
"Mode:" in out or "zk_" in out or "watch" in out or "connections" in out
):
raise AssertionError(f"fourlw_enforces: deny {cmd} returned response")
def health_precheck(nodes, timeout_s=0):
"""Verify cluster is healthy: has leader and all nodes respond to probes."""
if not nodes:
raise AssertionError("health_precheck: no nodes")
if timeout_s and int(timeout_s) > 0:
deadline = time.time() + float(timeout_s)
while True:
if count_leaders(nodes) >= 1:
break
if time.time() >= deadline:
raise AssertionError("health_precheck: no leader")
time.sleep(0.5)
else:
if count_leaders(nodes) < 1:
raise AssertionError("health_precheck: no leader")
for n in nodes:
m = mntr(n)
if not isinstance(m, dict) or not m:
kv = srvr_kv(n)
if not isinstance(kv, dict) or not kv:
raise AssertionError("health_precheck: mntr/srvr empty")
def prom_thresholds_le(nodes, metrics, aggregate="sum"):
"""Assert Prometheus metrics are below thresholds."""
targets = dict(metrics or {})
if not targets:
return
def _values(n):
for r in parse_prometheus_text(prom_metrics(n)):
name = r.get("name", "")
if name in targets:
yield name, r.get("value", 0.0)
totals = _aggregate_totals(nodes, targets, aggregate, _values)
for k, thr in targets.items():
if float(totals.get(k, 0.0)) > float(thr):
raise AssertionError(
f"prom_thresholds_le: {k}={totals.get(k, 0.0)} exceeds {thr}"
)
def _gate_mntr_print(nodes):
"""Print mntr stats for each node."""
for n in nodes or []:
try:
m = mntr(n) or {}
print(
f"[keeper] mntr node={n.name} zk_znode_count={m.get('zk_znode_count', 0)} "
f"zk_ephemerals_count={m.get('zk_ephemerals_count', 0)} "
f"zk_watch_count={m.get('zk_watch_count', 0)} zk_data_size={m.get('zk_approximate_data_size', 0)}"
)
except Exception:
print(f"[keeper] mntr node={getattr(n, 'name', 'node')} error")
def _gate_print_summary(summary):
"""Print benchmark summary."""
s = summary or {}
ops = int(s.get("ops", 0) or 0)
errs = int(s.get("errors", 0) or 0)
rp50, rp95, rp99 = (
float(s.get("read_p50_ms", 0) or 0),
float(s.get("read_p95_ms", 0) or 0),
float(s.get("read_p99_ms", 0) or 0),
)
wp50, wp95, wp99 = (
float(s.get("write_p50_ms", 0) or 0),
float(s.get("write_p95_ms", 0) or 0),
float(s.get("write_p99_ms", 0) or 0),
)
extra = []
if s.get("read_ratio") is not None:
extra.append(f"read_ratio={float(s['read_ratio']):.3f}")
if s.get("write_ratio") is not None:
extra.append(f"write_ratio={float(s['write_ratio']):.3f}")
print(
f"[keeper] bench_summary ops={ops} errors={errs} read_p99={rp99:.3f}ms write_p99={wp99:.3f}ms {' '.join(extra)}".strip()
)
# Debug output
if parse_bool(os.environ.get("KEEPER_DEBUG")):
out = (
Path(__file__).parents[4]
/ "tests"
/ "stress"
/ "keeper"
/ "tests"
/ "keeper_summary.txt"
)
out.parent.mkdir(parents=True, exist_ok=True)
content = (
f"ops={ops} errors={errs} "
f"read_p50_ms={rp50:.3f} read_p95_ms={rp95:.3f} read_p99_ms={rp99:.3f} "
f"write_p50_ms={wp50:.3f} write_p95_ms={wp95:.3f} write_p99_ms={wp99:.3f}"
)
if extra:
content += " " + " ".join(extra)
out.write_text(content + "\n", encoding="utf-8")
# Gate handlers (registered with decorators)
@register_gate("single_leader")
def _gate_single_leader(g, nodes, leader, ctx, summary):
return single_leader(nodes, timeout_s=_extract_param(g, "timeout_s", 180, int))
@register_gate("backlog_drains")
def _gate_backlog_drains(g, nodes, leader, ctx, summary):
return backlog_drains(nodes, max_s=_extract_param(g, "max_s", 120, int))
@register_gate("error_rate_le")
def _gate_error_rate_le(g, nodes, leader, ctx, summary):
return error_rate_le(summary, max_ratio=_extract_param(g, "max_ratio", DEFAULT_ERROR_RATE, float))
@register_gate("p99_le")
def _gate_p99_le(g, nodes, leader, ctx, summary):
return p99_le(summary, max_ms=_extract_param(g, "max_ms", DEFAULT_P99_MS, int))
@register_gate("p95_le")
def _gate_p95_le(g, nodes, leader, ctx, summary):
return p95_le(summary, max_ms=_extract_param(g, "max_ms", DEFAULT_P95_MS, int))
@register_gate("watch_delta_within")
def _gate_watch_delta_within(g, nodes, leader, ctx, summary):
md_val = g.get("max_delta")
md = int(md_val) if md_val is not None else None
pct_val = g.get("pct")
pct = float(pct_val) if pct_val is not None else None
return watch_delta_within(nodes, ctx, max_delta=md, pct=pct)
@register_gate("no_watcher_hotspot")
def _gate_no_watcher_hotspot(g, nodes, leader, ctx, summary):
mps_val = g.get("max_path_share")
mps = float(mps_val) if mps_val is not None else None
return no_watcher_hotspot(
nodes, ctx, max_share=_extract_param(g, "max_share", 0.95, float), max_path_share=mps
)
@register_gate("ephemerals_gone_within")
def _gate_ephemerals_gone_within(g, nodes, leader, ctx, summary):
return ephemerals_gone_within(nodes, max_s=_extract_param(g, "max_s", 60, int))
@register_gate("ready_expect")
def _gate_ready_expect(g, nodes, leader, ctx, summary):
return ready_expect(
nodes, leader,
ok=_extract_param(g, "ok", True, bool),
timeout_s=_extract_param(g, "timeout_s", 60, int)
)
@register_gate("lgif_monotone")
def _gate_lgif_monotone(g, nodes, leader, ctx, summary):
return lgif_monotone(nodes, ctx)
@register_gate("fourlw_enforces")
def _gate_fourlw_enforces(g, nodes, leader, ctx, summary):
return fourlw_enforces(nodes, allow=g.get("allow"), deny=g.get("deny"))
@register_gate("health_precheck")
def _gate_health_precheck(g, nodes, leader, ctx, summary):
return health_precheck(nodes, timeout_s=_extract_param(g, "timeout_s", 30, int))
@register_gate("prom_thresholds_le")
def _gate_prom_thresholds_le(g, nodes, leader, ctx, summary):
return prom_thresholds_le(
nodes, g.get("metrics") or {}, aggregate=str(g.get("aggregate", "sum"))
)
@register_gate("srvr_thresholds_le")
def _gate_srvr_thresholds_le(g, nodes, leader, ctx, summary):
return srvr_thresholds_le(
nodes, g.get("metrics") or {}, aggregate=str(g.get("aggregate", "sum"))
)
@register_gate("rps_ge")
def _gate_rps_ge(g, nodes, leader, ctx, summary):
return rps_ge(summary, min_rps=_extract_param(g, "min_rps", 0, float))
@register_gate("ops_ge")
def _gate_ops_ge(g, nodes, leader, ctx, summary):
return ops_ge(summary, min_ops=_extract_param(g, "min_ops", 0, float))
@register_gate("count_paths")
def _gate_count_paths(g, nodes, leader, ctx, summary):
return count_paths(nodes, g.get("prefixes") or [])
@register_gate("config_converged")
def _gate_config_converged(g, nodes, leader, ctx, summary):
return config_converged(nodes, timeout_s=_extract_param(g, "timeout_s", 30, int))
@register_gate("config_members_len_eq")
def _gate_config_members_len_eq(g, nodes, leader, ctx, summary):
return config_members_len_eq(nodes, expected=_extract_param(g, "expected", 3, int))
@register_gate("election_time_le")
def _gate_election_time_le(g, nodes, leader, ctx, summary):
return election_time_le(ctx, max_s=_extract_param(g, "max_s", 10, float))
@register_gate("log_sanity_ok")
def _gate_log_sanity_ok(g, nodes, leader, ctx, summary):
return log_sanity_ok(nodes, allow=g.get("allow"))
@register_gate("coord_timeouts_match")
def _gate_coord_timeouts_match(g, nodes, leader, ctx, summary):
return coord_timeouts_match(
nodes, ctx, startup=g.get("startup"), shutdown=g.get("shutdown")
)
@register_gate("mntr_print")
def _gate_dispatch_mntr_print(g, nodes, leader, ctx, summary):
return _gate_mntr_print(nodes)
@register_gate("print_summary")
def _gate_dispatch_print_summary(g, nodes, leader, ctx, summary):
return _gate_print_summary(summary)
@register_gate("sync_read_barrier")
def _gate_sync_read_barrier(g, nodes, leader, ctx, summary):
"""Test sync read barrier semantics.
This gate verifies that sync operations create a read barrier ensuring
all previous writes are visible to subsequent reads.
TODO: Implement sync read barrier test logic.
"""
print("[keeper][gate] sync_read_barrier: not yet implemented, skipping")
return True
@register_gate("linearizable_reads_ok")
def _gate_linearizable_reads_ok(g, nodes, leader, ctx, summary):
"""Test linearizable read guarantees.
This gate verifies that reads are linearizable (consistent ordering)
when quorum_reads is enabled.
TODO: Implement linearizable reads test logic.
"""
print("[keeper][gate] linearizable_reads_ok: not yet implemented, skipping")
return True
@register_gate("acl_matrix_ok")
def _gate_acl_matrix_ok(g, nodes, leader, ctx, summary):
"""Test ACL (Access Control List) matrix correctness.
This gate verifies that ACL permissions are correctly enforced
across different authentication schemes (world, auth, digest).
TODO: Implement ACL matrix test logic.
"""
print("[keeper][gate] acl_matrix_ok: not yet implemented, skipping")
return True
@register_gate("acl_digest_rotate_ok")
def _gate_acl_digest_rotate_ok(g, nodes, leader, ctx, summary):
"""Test ACL digest rotation correctness.
This gate verifies that ACL digest rotation works correctly
and doesn't break existing permissions.
TODO: Implement ACL digest rotation test logic.
"""
print("[keeper][gate] acl_digest_rotate_ok: not yet implemented, skipping")
return True
@register_gate("lin_register_ok")
def _gate_lin_register_ok(g, nodes, leader, ctx, summary):
"""Test linearizable register semantics.
This gate verifies that register operations (read/write) maintain
linearizability guarantees under concurrent access and faults.
TODO: Implement linearizable register test logic.
"""
print("[keeper][gate] lin_register_ok: not yet implemented, skipping")
return True
def apply_gate(gate, nodes, leader, ctx, summary):
"""Apply a validation gate check."""
gtype = (gate.get("type") or "").strip()
handler = _GATE_HANDLERS.get(gtype)
if handler:
return handler(gate, nodes, leader, ctx, summary)
raise AssertionError(f"unknown gate type: {gtype}")