9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
559 lines
20 KiB
Python
559 lines
20 KiB
Python
#!/usr/bin/env python3
|
||
"""Reproducible Buzz relay Redis bus scaling harness.
|
||
|
||
The harness isolates the relay's Redis fan-out boundary, not client rendering or
|
||
DB ingest. It compares the pre-rewrite global-firehose shape (every pod receives
|
||
every published event) with the multi-tenant scoped-topic shape (a pod receives
|
||
only community topics it has retained because it has local subscribers).
|
||
|
||
Default scenario:
|
||
* 64 communities publish an equal one-second-equivalent event count.
|
||
* one target community has local subscribers on every relay pod.
|
||
* all other communities are irrelevant to those pods.
|
||
|
||
Modes:
|
||
* redis: measured Redis PUB/SUB delivery using only Python stdlib (default).
|
||
* model: deterministic no-service arithmetic model.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import os
|
||
import socket
|
||
import threading
|
||
import time
|
||
from dataclasses import dataclass
|
||
from typing import BinaryIO
|
||
from urllib.parse import urlparse
|
||
from uuid import UUID
|
||
|
||
BUZZ_PREFIX = "buzz"
|
||
OLD_GLOBAL_CHANNEL = f"{BUZZ_PREFIX}:global"
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Scenario:
|
||
pods: int
|
||
communities: int
|
||
events_per_community_per_sec: float
|
||
subscribed_communities: int
|
||
interested_pods_per_subscribed_community: int
|
||
|
||
@property
|
||
def total_event_rate(self) -> float:
|
||
return self.communities * self.events_per_community_per_sec
|
||
|
||
@property
|
||
def subscribed_event_rate(self) -> float:
|
||
return self.subscribed_communities * self.events_per_community_per_sec
|
||
|
||
def old_global_firehose(self) -> tuple[float, float, float]:
|
||
"""Return (cluster ingress/sec, avg pod ingress/sec, irrelevant pct)."""
|
||
cluster = self.pods * self.total_event_rate
|
||
per_pod = self.total_event_rate
|
||
irrelevant = max(self.total_event_rate - self.subscribed_event_rate, 0.0)
|
||
irrelevant_pct = 100.0 * irrelevant / self.total_event_rate
|
||
return cluster, per_pod, irrelevant_pct
|
||
|
||
def scoped_bus(self) -> tuple[float, float, float]:
|
||
"""Return (cluster ingress/sec, avg pod ingress/sec, irrelevant pct)."""
|
||
interested_pods = min(self.pods, self.interested_pods_per_subscribed_community)
|
||
cluster = (
|
||
self.subscribed_communities
|
||
* interested_pods
|
||
* self.events_per_community_per_sec
|
||
)
|
||
per_pod = cluster / self.pods
|
||
return cluster, per_pod, 0.0
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class Measurement:
|
||
pods: int
|
||
old_cluster: int
|
||
old_avg_pod: float
|
||
old_irrelevant_pct: float
|
||
new_cluster: int
|
||
new_avg_pod: float
|
||
new_irrelevant_pct: float
|
||
reduction: float
|
||
published: int
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class RedisAddress:
|
||
host: str
|
||
port: int
|
||
db: int
|
||
username: str | None = None
|
||
password: str | None = None
|
||
|
||
|
||
def fmt(value: float) -> str:
|
||
if abs(value - round(value)) < 1e-9:
|
||
return f"{int(round(value)):,}"
|
||
return f"{value:,.2f}"
|
||
|
||
|
||
def community_id(index: int) -> str:
|
||
"""Deterministic UUIDs matching the relay's topic format."""
|
||
return str(UUID(int=index + 1))
|
||
|
||
|
||
def scoped_global_channel(community: str) -> str:
|
||
# Mirrors crates/buzz-pubsub/src/topic.rs EventTopic::Global:
|
||
# format!("buzz:{}:global", self.community_id)
|
||
return f"{BUZZ_PREFIX}:{community}:global"
|
||
|
||
|
||
def encode_command(*parts: object) -> bytes:
|
||
encoded = [str(part).encode("utf-8") for part in parts]
|
||
frame = [f"*{len(encoded)}\r\n".encode("ascii")]
|
||
for part in encoded:
|
||
frame.append(f"${len(part)}\r\n".encode("ascii"))
|
||
frame.append(part)
|
||
frame.append(b"\r\n")
|
||
return b"".join(frame)
|
||
|
||
|
||
def _read_line(stream: BinaryIO) -> bytes:
|
||
line = stream.readline()
|
||
if not line:
|
||
raise EOFError("Redis connection closed")
|
||
if not line.endswith(b"\r\n"):
|
||
raise ValueError(f"malformed Redis line: {line!r}")
|
||
return line[:-2]
|
||
|
||
|
||
def read_resp(stream: BinaryIO) -> object:
|
||
prefix = stream.read(1)
|
||
if not prefix:
|
||
raise EOFError("Redis connection closed")
|
||
if prefix == b"+":
|
||
return _read_line(stream).decode("utf-8")
|
||
if prefix == b"-":
|
||
raise RuntimeError(_read_line(stream).decode("utf-8"))
|
||
if prefix == b":":
|
||
return int(_read_line(stream))
|
||
if prefix == b"$":
|
||
length = int(_read_line(stream))
|
||
if length == -1:
|
||
return None
|
||
data = stream.read(length)
|
||
terminator = stream.read(2)
|
||
if len(data) != length or terminator != b"\r\n":
|
||
raise ValueError("malformed Redis bulk string")
|
||
return data.decode("utf-8")
|
||
if prefix == b"*":
|
||
length = int(_read_line(stream))
|
||
if length == -1:
|
||
return None
|
||
return [read_resp(stream) for _ in range(length)]
|
||
raise ValueError(f"unknown Redis RESP prefix: {prefix!r}")
|
||
|
||
|
||
class RedisClient:
|
||
def __init__(self, address: RedisAddress, timeout: float) -> None:
|
||
self._address = address
|
||
self._sock = socket.create_connection((address.host, address.port), timeout=timeout)
|
||
self._sock.settimeout(timeout)
|
||
self._stream = self._sock.makefile("rwb")
|
||
self._authenticate_and_select()
|
||
|
||
def _authenticate_and_select(self) -> None:
|
||
if self._address.password:
|
||
if self._address.username:
|
||
self.command("AUTH", self._address.username, self._address.password)
|
||
else:
|
||
self.command("AUTH", self._address.password)
|
||
if self._address.db:
|
||
self.command("SELECT", self._address.db)
|
||
|
||
def command(self, *parts: object) -> object:
|
||
self._stream.write(encode_command(*parts))
|
||
self._stream.flush()
|
||
return read_resp(self._stream)
|
||
|
||
def close(self) -> None:
|
||
try:
|
||
self._stream.close()
|
||
finally:
|
||
self._sock.close()
|
||
|
||
def __enter__(self) -> "RedisClient":
|
||
return self
|
||
|
||
def __exit__(self, *_exc: object) -> None:
|
||
self.close()
|
||
|
||
|
||
class RedisSubscriber(threading.Thread):
|
||
def __init__(
|
||
self,
|
||
*,
|
||
pod: int,
|
||
address: RedisAddress,
|
||
channels: list[str],
|
||
relevant_communities: set[str],
|
||
timeout: float,
|
||
) -> None:
|
||
super().__init__(name=f"redis-sub-pod-{pod}", daemon=True)
|
||
self.address = address
|
||
self.channels = channels
|
||
self.relevant_communities = relevant_communities
|
||
self.timeout = timeout
|
||
self.ready = threading.Event()
|
||
self.stop_requested = threading.Event()
|
||
self.error: BaseException | None = None
|
||
self.received = 0
|
||
self.irrelevant = 0
|
||
self._sock: socket.socket | None = None
|
||
|
||
def run(self) -> None:
|
||
try:
|
||
sock = socket.create_connection((self.address.host, self.address.port), timeout=self.timeout)
|
||
self._sock = sock
|
||
sock.settimeout(0.2)
|
||
stream = sock.makefile("rwb")
|
||
try:
|
||
if self.address.password:
|
||
if self.address.username:
|
||
self._write_and_read(stream, "AUTH", self.address.username, self.address.password)
|
||
else:
|
||
self._write_and_read(stream, "AUTH", self.address.password)
|
||
if self.address.db:
|
||
self._write_and_read(stream, "SELECT", self.address.db)
|
||
|
||
stream.write(encode_command("SUBSCRIBE", *self.channels))
|
||
stream.flush()
|
||
subscribed = 0
|
||
while subscribed < len(self.channels):
|
||
frame = read_resp(stream)
|
||
if isinstance(frame, list) and frame and frame[0] == "subscribe":
|
||
subscribed += 1
|
||
self.ready.set()
|
||
|
||
while not self.stop_requested.is_set():
|
||
try:
|
||
frame = read_resp(stream)
|
||
except socket.timeout:
|
||
continue
|
||
if not (isinstance(frame, list) and len(frame) >= 3 and frame[0] == "message"):
|
||
continue
|
||
payload = str(frame[2])
|
||
community = payload.split("|", 1)[0]
|
||
self.received += 1
|
||
if community not in self.relevant_communities:
|
||
self.irrelevant += 1
|
||
finally:
|
||
stream.close()
|
||
sock.close()
|
||
except OSError as exc:
|
||
if not self.stop_requested.is_set():
|
||
self.error = exc
|
||
self.ready.set()
|
||
except BaseException as exc:
|
||
self.error = exc
|
||
self.ready.set()
|
||
|
||
@staticmethod
|
||
def _write_and_read(stream: BinaryIO, *parts: object) -> object:
|
||
stream.write(encode_command(*parts))
|
||
stream.flush()
|
||
return read_resp(stream)
|
||
|
||
def stop(self) -> None:
|
||
self.stop_requested.set()
|
||
if self._sock is not None:
|
||
try:
|
||
self._sock.shutdown(socket.SHUT_RDWR)
|
||
except OSError:
|
||
pass
|
||
try:
|
||
self._sock.close()
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
def parse_redis_url(raw: str) -> RedisAddress:
|
||
parsed = urlparse(raw)
|
||
if parsed.scheme not in {"redis", "rediss"}:
|
||
raise ValueError(f"unsupported Redis URL scheme: {parsed.scheme!r}")
|
||
if parsed.scheme == "rediss":
|
||
raise ValueError("rediss:// is not supported by this stdlib harness; use redis://")
|
||
return RedisAddress(
|
||
host=parsed.hostname or "127.0.0.1",
|
||
port=parsed.port or 6379,
|
||
db=int(parsed.path.lstrip("/") or "0"),
|
||
username=parsed.username,
|
||
password=parsed.password,
|
||
)
|
||
|
||
|
||
def publish_scenario(
|
||
*,
|
||
address: RedisAddress,
|
||
channels_by_community: list[str],
|
||
events_per_community: int,
|
||
timeout: float,
|
||
) -> None:
|
||
with RedisClient(address, timeout) as redis:
|
||
for community_index, channel in enumerate(channels_by_community):
|
||
community = community_id(community_index)
|
||
for event_index in range(events_per_community):
|
||
redis.command("PUBLISH", channel, f"{community}|{event_index}")
|
||
|
||
|
||
def wait_until_ready(subscribers: list[RedisSubscriber], timeout: float) -> None:
|
||
for sub in subscribers:
|
||
if not sub.ready.wait(timeout):
|
||
stop_subscribers(subscribers)
|
||
raise TimeoutError(f"subscriber {sub.name} did not SUBSCRIBE within {timeout}s")
|
||
if sub.error is not None:
|
||
stop_subscribers(subscribers)
|
||
raise RuntimeError(f"subscriber {sub.name} failed: {sub.error}")
|
||
|
||
|
||
def wait_for_counts(subscribers: list[RedisSubscriber], expected_total: int, timeout: float) -> None:
|
||
deadline = time.monotonic() + timeout
|
||
while time.monotonic() < deadline:
|
||
if any(sub.error for sub in subscribers):
|
||
break
|
||
if sum(sub.received for sub in subscribers) >= expected_total:
|
||
return
|
||
time.sleep(0.01)
|
||
actual = sum(sub.received for sub in subscribers)
|
||
raise TimeoutError(f"timed out waiting for {expected_total} pub/sub deliveries; got {actual}")
|
||
|
||
|
||
def stop_subscribers(subscribers: list[RedisSubscriber]) -> None:
|
||
for sub in subscribers:
|
||
sub.stop()
|
||
for sub in subscribers:
|
||
sub.join(timeout=1.0)
|
||
|
||
|
||
def measure_redis_for_pods(args: argparse.Namespace, pods: int) -> Measurement:
|
||
events_per_community = int(args.events_per_community_per_sec)
|
||
if events_per_community != args.events_per_community_per_sec:
|
||
raise ValueError("--mode redis requires integer --events-per-community-per-sec")
|
||
address = parse_redis_url(args.redis_url)
|
||
communities = [community_id(i) for i in range(args.communities)]
|
||
relevant = set(communities[: args.subscribed_communities])
|
||
interested = pods if args.interested_pods_per_subscribed_community == 0 else min(
|
||
pods, args.interested_pods_per_subscribed_community
|
||
)
|
||
|
||
with RedisClient(address, args.redis_timeout) as redis:
|
||
assert redis.command("PING") == "PONG"
|
||
|
||
old_subs = [
|
||
RedisSubscriber(
|
||
pod=pod,
|
||
address=address,
|
||
channels=[OLD_GLOBAL_CHANNEL],
|
||
relevant_communities=relevant,
|
||
timeout=args.redis_timeout,
|
||
)
|
||
for pod in range(pods)
|
||
]
|
||
for sub in old_subs:
|
||
sub.start()
|
||
wait_until_ready(old_subs, args.redis_timeout)
|
||
publish_scenario(
|
||
address=address,
|
||
channels_by_community=[OLD_GLOBAL_CHANNEL for _ in communities],
|
||
events_per_community=events_per_community,
|
||
timeout=args.redis_timeout,
|
||
)
|
||
wait_for_counts(old_subs, pods * args.communities * events_per_community, args.redis_timeout)
|
||
stop_subscribers(old_subs)
|
||
|
||
new_subs = [
|
||
RedisSubscriber(
|
||
pod=pod,
|
||
address=address,
|
||
channels=[scoped_global_channel(community) for community in communities[: args.subscribed_communities]],
|
||
relevant_communities=relevant,
|
||
timeout=args.redis_timeout,
|
||
)
|
||
for pod in range(interested)
|
||
]
|
||
for sub in new_subs:
|
||
sub.start()
|
||
wait_until_ready(new_subs, args.redis_timeout)
|
||
publish_scenario(
|
||
address=address,
|
||
channels_by_community=[scoped_global_channel(community) for community in communities],
|
||
events_per_community=events_per_community,
|
||
timeout=args.redis_timeout,
|
||
)
|
||
wait_for_counts(new_subs, interested * args.subscribed_communities * events_per_community, args.redis_timeout)
|
||
stop_subscribers(new_subs)
|
||
|
||
old_cluster = sum(sub.received for sub in old_subs)
|
||
new_cluster = sum(sub.received for sub in new_subs)
|
||
old_irrelevant = sum(sub.irrelevant for sub in old_subs)
|
||
new_irrelevant = sum(sub.irrelevant for sub in new_subs)
|
||
published = args.communities * events_per_community
|
||
return Measurement(
|
||
pods=pods,
|
||
old_cluster=old_cluster,
|
||
old_avg_pod=old_cluster / pods,
|
||
old_irrelevant_pct=100.0 * old_irrelevant / old_cluster if old_cluster else 0.0,
|
||
new_cluster=new_cluster,
|
||
new_avg_pod=new_cluster / pods,
|
||
new_irrelevant_pct=100.0 * new_irrelevant / new_cluster if new_cluster else 0.0,
|
||
reduction=old_cluster / new_cluster if new_cluster else float("inf"),
|
||
published=published,
|
||
)
|
||
|
||
|
||
def model_measurements(args: argparse.Namespace, pods_values: list[int]) -> list[Measurement]:
|
||
rows = []
|
||
for pods in pods_values:
|
||
scenario = Scenario(
|
||
pods=pods,
|
||
communities=args.communities,
|
||
events_per_community_per_sec=args.events_per_community_per_sec,
|
||
subscribed_communities=args.subscribed_communities,
|
||
interested_pods_per_subscribed_community=args.interested_pods_per_subscribed_community or pods,
|
||
)
|
||
old_cluster, old_pod, old_irrelevant = scenario.old_global_firehose()
|
||
new_cluster, new_pod, new_irrelevant = scenario.scoped_bus()
|
||
rows.append(
|
||
Measurement(
|
||
pods=pods,
|
||
old_cluster=int(old_cluster),
|
||
old_avg_pod=old_pod,
|
||
old_irrelevant_pct=old_irrelevant,
|
||
new_cluster=int(new_cluster),
|
||
new_avg_pod=new_pod,
|
||
new_irrelevant_pct=new_irrelevant,
|
||
reduction=old_cluster / new_cluster if new_cluster else float("inf"),
|
||
published=int(scenario.total_event_rate),
|
||
)
|
||
)
|
||
return rows
|
||
|
||
|
||
def redis_measurements(args: argparse.Namespace, pods_values: list[int]) -> list[Measurement]:
|
||
return [measure_redis_for_pods(args, pods) for pods in pods_values]
|
||
|
||
|
||
def assert_scaling(args: argparse.Namespace, rows: list[Measurement]) -> None:
|
||
ideal = args.communities / args.subscribed_communities
|
||
min_reduction = ideal * args.min_reduction_ratio
|
||
for row in rows:
|
||
if row.reduction < min_reduction:
|
||
raise AssertionError(
|
||
f"pods={row.pods}: reduction {row.reduction:.2f}× < required {min_reduction:.2f}×"
|
||
)
|
||
if row.new_irrelevant_pct > args.max_scoped_irrelevant_pct:
|
||
raise AssertionError(
|
||
f"pods={row.pods}: scoped irrelevant {row.new_irrelevant_pct:.2f}% > "
|
||
f"allowed {args.max_scoped_irrelevant_pct:.2f}%"
|
||
)
|
||
|
||
|
||
def print_rows(args: argparse.Namespace, rows: list[Measurement]) -> None:
|
||
print("Buzz relay Redis bus scaling harness")
|
||
print("====================================")
|
||
print(
|
||
"scenario: "
|
||
f"{args.communities} communities × {fmt(args.events_per_community_per_sec)} events/s, "
|
||
f"{args.subscribed_communities} subscribed community topic(s), "
|
||
f"interested pods per subscribed community = "
|
||
f"{args.interested_pods_per_subscribed_community or 'all pods'}, "
|
||
f"mode={args.mode}"
|
||
)
|
||
if args.mode == "redis":
|
||
print(f"redis: {args.redis_url}")
|
||
print()
|
||
print(
|
||
"| pods | old global cluster ingress/s | old avg pod ingress/s | "
|
||
"new scoped cluster ingress/s | new avg pod ingress/s | reduction | old irrelevant/pod | new irrelevant/pod |"
|
||
)
|
||
print("|---:|---:|---:|---:|---:|---:|---:|---:|")
|
||
for row in rows:
|
||
print(
|
||
f"| {row.pods} | {fmt(row.old_cluster)} | {fmt(row.old_avg_pod)} | "
|
||
f"{fmt(row.new_cluster)} | {fmt(row.new_avg_pod)} | {row.reduction:,.1f}× | "
|
||
f"{row.old_irrelevant_pct:.2f}% | {row.new_irrelevant_pct:.2f}% |"
|
||
)
|
||
print()
|
||
print("Interpretation:")
|
||
print(
|
||
"- Old relay/global bus: every pod receives every community's event, so "
|
||
"cluster pub/sub ingress = pods × total_event_rate."
|
||
)
|
||
print(
|
||
"- New relay/scoped bus: a pod retains only server-resolved community topics "
|
||
"with local subscribers, so ingress = interested_pods × subscribed_community_rate."
|
||
)
|
||
print(
|
||
"- The assertion checks the bus-bound scaling claim; end-to-end latency/DB "
|
||
"capacity should be measured separately with a live relay stack."
|
||
)
|
||
|
||
|
||
def run(args: argparse.Namespace) -> int:
|
||
if args.communities <= 0:
|
||
raise ValueError("--communities must be positive")
|
||
if args.subscribed_communities <= 0:
|
||
raise ValueError("--subscribed-communities must be positive")
|
||
if args.subscribed_communities > args.communities:
|
||
raise ValueError("--subscribed-communities cannot exceed --communities")
|
||
pods_values = [int(p.strip()) for p in args.pods.split(",") if p.strip()]
|
||
if not pods_values or any(p <= 0 for p in pods_values):
|
||
raise ValueError("--pods must contain positive integers")
|
||
|
||
rows = model_measurements(args, pods_values) if args.mode == "model" else redis_measurements(args, pods_values)
|
||
print_rows(args, rows)
|
||
if args.assert_scaling:
|
||
assert_scaling(args, rows)
|
||
print()
|
||
print(
|
||
"assertion: PASS "
|
||
f"(reduction ≥ {args.min_reduction_ratio:.0%} of ideal, "
|
||
f"scoped irrelevant ≤ {args.max_scoped_irrelevant_pct:.2f}%)"
|
||
)
|
||
return 0
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("--mode", choices=["redis", "model"], default="redis")
|
||
parser.add_argument("--pods", default="1,2,4", help="comma-separated pod counts")
|
||
parser.add_argument("--communities", type=int, default=64)
|
||
parser.add_argument("--events-per-community-per-sec", type=float, default=100.0)
|
||
parser.add_argument("--subscribed-communities", type=int, default=1)
|
||
parser.add_argument(
|
||
"--interested-pods-per-subscribed-community",
|
||
type=int,
|
||
default=0,
|
||
help="0 means all pods are interested in the subscribed community",
|
||
)
|
||
parser.add_argument("--redis-url", default=os.environ.get("REDIS_URL", "redis://127.0.0.1:6379/0"))
|
||
parser.add_argument("--redis-timeout", type=float, default=10.0)
|
||
parser.add_argument(
|
||
"--no-assert-scaling",
|
||
dest="assert_scaling",
|
||
action="store_false",
|
||
help="print measurements without enforcing the scaling invariant",
|
||
)
|
||
parser.set_defaults(assert_scaling=True)
|
||
parser.add_argument(
|
||
"--min-reduction-ratio",
|
||
type=float,
|
||
default=0.95,
|
||
help="minimum observed reduction as a fraction of ideal communities/subscribed",
|
||
)
|
||
parser.add_argument("--max-scoped-irrelevant-pct", type=float, default=0.0)
|
||
return run(parser.parse_args())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|