Files
shopdb-flask/scripts/loadtest/sample_resources.py
benjamin.baker 0d5b9730e3 Add load test harness for pool and fleet sizing
Adds the scripts used to size the connection pool and validate fleet polling under load: burst arrival and mixed workload drivers, pool and resource samplers, a UI probe, host seed/drop SQL, and a gevent Dockerfile. Hosts are addressed by compose service name (api:5001) rather than a local path, so the harness runs against any stack rather than one machine.

loadtest_token.py mints a scoped collector token named LOADTEST and prints the secret once, then revokes only that token by name so a real collector token is never touched. envkeyfallback is false, so the harness needs a genuine managed token rather than an env key.
2026-09-07 12:39:53 -04:00

81 lines
2.6 KiB
Python

"""Sample container CPU and memory during a sizing run.
WHY: the sizing table claims specific vCPU and RAM figures. Those can only be
confirmed by watching the container under load, not by reasoning about greenlet
size. This records peak RSS and CPU so a claimed "4 GB is enough" is either
demonstrated or falsified.
Runs on the HOST (needs the docker CLI), unlike the generators.
Usage:
python sample_resources.py --container shopdb-sizing-api --seconds 120
"""
import argparse
import json
import re
import statistics
import subprocess
import time
def parse_size(text):
"""Docker stats prints like '1.234GiB' / '512MiB'. Return megabytes."""
match = re.match(r'^([0-9.]+)\s*([KMGT]?i?B)$', text.strip())
if not match:
return 0.0
value = float(match.group(1))
unit = match.group(2).upper().replace('I', '')
factors = {'B': 1.0 / (1024 * 1024), 'KB': 1.0 / 1024, 'MB': 1.0,
'GB': 1024.0, 'TB': 1024.0 * 1024.0}
return value * factors.get(unit, 1.0)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--container', required=True)
parser.add_argument('--seconds', type=int, default=120)
parser.add_argument('--interval', type=float, default=2.0)
arguments = parser.parse_args()
cpu_samples = []
memory_samples = []
deadline = time.monotonic() + arguments.seconds
while time.monotonic() < deadline:
try:
output = subprocess.check_output(
['docker', 'stats', '--no-stream', '--format',
'{{.CPUPerc}}|{{.MemUsage}}', arguments.container],
stderr=subprocess.DEVNULL, text=True).strip()
except subprocess.CalledProcessError:
break
if not output:
break
cpu_text, memory_text = output.split('|', 1)
cpu_samples.append(float(cpu_text.replace('%', '').strip()))
used = memory_text.split('/')[0]
memory_samples.append(parse_size(used))
time.sleep(arguments.interval)
def summarize(values):
if not values:
return {'samples': 0}
ordered = sorted(values)
index = min(int(len(ordered) * 0.95), len(ordered) - 1)
return {
'samples': len(values),
'median': round(statistics.median(values), 1),
'p95': round(ordered[index], 1),
'max': round(max(values), 1),
}
print(json.dumps({
'container': arguments.container,
'cpu_percent': summarize(cpu_samples),
'memory_mb': summarize(memory_samples),
}, indent=2))
if __name__ == '__main__':
main()