#!/usr/bin/env python3 """Typified examples for the connected-index prototypes (bench/examples/README.md). run-example ID SCALE ENGINE[,ENGINE..] [--phases load,queries,writes,concurrency] [--knob k=v ..] [--no-instr] [--keep] [--validate] run-example --list | --check | --coverage [--ready] SCALE is S, M, L (100k, 1M, 10M fact rows) or a row count for development. ENGINE: native, sift-unconnected, sift-off, connected:. Needs PGHOST and PGPORT of a scratch cluster (eval "$(bench/examples/cluster.sh start)"); never the system server. """ import argparse import binascii import json import os import subprocess import sys import time sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib')) import exlib # noqa: E402 PHASES = 'load,queries,writes' OUT = os.environ.get('EX_OUT', os.path.join(exlib.ROOT, '..', 'out', 'examples')) def tip(): try: return subprocess.run(['git', '-C', exlib.ROOT, 'rev-parse', '--short=12', 'HEAD'], capture_output=True, text=True).stdout.strip() or 'nogit' except OSError: return 'nogit' def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument('id', nargs='?') ap.add_argument('scale', nargs='?') ap.add_argument('engine', nargs='?') ap.add_argument('--phases', default=PHASES) ap.add_argument('--knob', action='append', default=[]) ap.add_argument('--no-instr', action='store_true') ap.add_argument('--keep', action='store_true') ap.add_argument('--validate', action='store_true', help='native and sift-unconnected, load+queries+writes, oracle must hold: marks the example ready') ap.add_argument('--seconds', type=int, default=20) ap.add_argument('--set', action='append', default=[], help='a SET statement for the measured (not the oracle) sessions, e.g. --set "SET max_parallel_workers_per_gather = 0"') ap.add_argument('--patterns', default='', help='writes: only the first batch of each of these write patterns (scale runs)') ap.add_argument('--only', default='', help='the query ids to run (bisecting a mismatch)') ap.add_argument('--txnmix', action='store_true', help='concurrency: one more reader that inserts rows in a transaction and reads its own rows (parallel plans see them)') ap.add_argument('--skip', default='', help='concurrency: batch ids left out (bisecting a mismatch)') ap.add_argument('--exact-oracle', action='store_true', help='the oracle runs the main query text, ignoring `-- oracle:` rewrites (small scales only: those plans can be quadratic)') ap.add_argument('--out', default=OUT) ap.add_argument('--list', action='store_true') ap.add_argument('--check', action='store_true') ap.add_argument('--coverage', action='store_true') ap.add_argument('--ready', action='store_true') a = ap.parse_args() if a.list: rd = exlib.ready_ids() for i in exlib.example_ids(): ex = exlib.Example(i) print('%-28s %-7s %s' % (i, 'ready' if exlib.is_ready(ex, rd) else 'draft', ex.meta['class'])) return 0 if a.check: problems = exlib.check_registry() print('\n'.join(problems) or 'registry ok: %d examples' % len(exlib.example_ids())) return 1 if problems else 0 if a.coverage: print(exlib.coverage(a.ready)) return 0 if not (a.id and a.scale and (a.engine or a.validate)): ap.error('ID SCALE ENGINE are required') if not os.environ.get('PGHOST') or not os.environ.get('PGPORT'): sys.exit('run-example: PGHOST and PGPORT of a scratch cluster are required (eval "$(bench/examples/cluster.sh start)")') os.environ['EX_ONLY'] = a.only os.environ['EX_PATTERNS'] = a.patterns os.environ['EX_EXACT_ORACLE'] = '1' if a.exact_oracle else '' os.environ['EX_SETTINGS'] = ';'.join(a.set) ex = exlib.Example(a.id) engines = ['native', 'sift-unconnected'] if a.validate else a.engine.split(',') phases = ('load,queries,writes' if a.validate else a.phases).split(',') knobs = dict(k.split('=', 1) for k in a.knob) run_id = '%s-%s-%s' % (tip(), time.strftime('%Y%m%dT%H%M%SZ', time.gmtime()), binascii.hexlify(os.urandom(2)).decode()) out = os.path.join(a.out, run_id) os.makedirs(out, exist_ok=True) tables = {n: [] for n in ('queries', 'writes', 'load', 'concurrency')} failed = [] for name in engines: eng = exlib.engine_of(name) r = exlib.Run(ex, a.scale, eng, knobs, out, a.keep) lead = dict(example=ex.id, scale=a.scale, engine=name) try: r.load() if 'queries' in phases: r.run_queries(instr=not a.no_instr) if 'writes' in phases: r.run_writes() if 'concurrency' in phases: r.run_concurrency(a.seconds, skip=a.skip.split(','), txnmix=a.txnmix) finally: r.drop() for t, rows in (('queries', r.queryrows), ('writes', r.writerows), ('load', r.loadrows), ('concurrency', r.concrows)): tables[t] += [dict(lead, **row) for row in rows] failed += ['%s %s: queries %s' % (name, ex.id, row['query']) for row in r.queryrows if not row['rows_equal']] failed += ['%s %s: connection %s was not filled (sift.col_connections fill is not `filled`)' % (name, ex.id, d) for d in r.refused] failed += ['%s %s: queries %s was not answered by the found flag (header `-- found: yes`)' % (name, ex.id, row['query']) for row in r.queryrows if not row['found_ok']] failed += ['%s %s: queries %s lost its plan node (header `-- plan:`)' % (name, ex.id, row['query']) for row in r.queryrows if not row['plan_ok']] failed += ['%s %s: queries %s has a trivial oracle (no rows or zeros): fix the filter or add `-- empty: ok`' % (name, ex.id, row['query']) for row in r.queryrows if row['trivial']] failed += ['%s %s: writes %s mismatches %s %s' % (name, ex.id, row['batch'], row['mismatches'], row['error']) for row in r.writerows if not row['oracle_ok']] failed += ['%s %s: concurrency %s' % (name, ex.id, row['detail'] or row['final_mismatches']) for row in r.concrows if not row['ok']] for t, rows in tables.items(): exlib.write_tsv(os.path.join(out, t + '.tsv'), rows, {}) json.dump(dict(run_id=run_id, example=ex.id, digest=ex.digest(), scale=a.scale, engines=engines, knobs=r.k, phases=phases, maintenance=os.environ.get('EX_MAINTENANCE', 'off'), failed=failed), open(os.path.join(out, 'run.json'), 'w'), indent=1) print('%s %s %s: %s %s' % (ex.id, a.scale, ','.join(engines), 'FAIL' if failed else 'oracle ok', out)) for f in failed: print(' ' + f) if a.validate and not failed: exlib.mark_ready(ex, run_id, engines) return 1 if failed else 0 if __name__ == '__main__': sys.exit(main())