demoprep / tests /ts_table_create_deepdive.py
mikeboone's picture
perf(deploy): scope TS connection to demo DB + monthly-rotating database (250s -> <1s table imports)
21ee86d
Raw
History Blame Contribute Delete
8.92 kB
#!/usr/bin/env python3
"""
ts_table_create_deepdive.py — isolate WHY logical-table TML import takes ~250s.
Hypotheses tested (2026-08-10 deep dive):
H1 "instance" : sebe (staging, measured ~250s in July) is unhealthy; prod
secloud is fine. -> run the same probe on both envs.
H2 "scoping" : the app's connection TML has no `database` property, so the
connection sees ~507 Snowflake DBs; table create scales with
visible external metadata. -> compare an unscoped vs a
database-pinned connection on the SAME instance.
Each probe: create a throwaway connection (variant-specific), TML-import ONE
trivial 2-column DONT_INDEX table against a pre-existing empty physical table,
time it, then delete the logical table + the throwaway connection.
Usage:
python scratch/ts_table_create_deepdive.py --env "secloud - primary" --variant unscoped
python scratch/ts_table_create_deepdive.py --env "sebe - se" --variant scoped
python scratch/ts_table_create_deepdive.py --env "sebe - se" --variant both
Physical table: creates DEMOBUILD.ZPERFTEST.DDPROBE1 in Snowflake if missing
(empty, 2 columns). Snowflake + TS credentials come from .env / Supabase admin
settings exactly like the app.
"""
import argparse
import os
import sys
import time
import requests
import yaml
from dotenv import load_dotenv
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, ROOT)
load_dotenv(os.path.join(ROOT, ".env"))
from supabase_client import get_admin_setting # noqa: E402
DB = os.getenv("DD_PROBE_DB", "DEMOBUILD")
SCHEMA = os.getenv("DD_PROBE_SCHEMA", "ZPERFTEST")
PHYS_TABLE = "DDPROBE1"
TABLE_TML = """\
guid: null
table:
name: {name}
db: {db}
schema: {schema}
db_table: {phys}
connection:
name: {connection}
columns:
- name: ID
db_column_name: ID
properties:
column_type: MEASURE
aggregation: SUM
index_type: DONT_INDEX
db_column_properties:
data_type: INT64
- name: LABEL
db_column_name: LABEL
properties:
column_type: ATTRIBUTE
index_type: DONT_INDEX
db_column_properties:
data_type: VARCHAR
"""
def resolve_env(label):
i = 1
while True:
lbl = os.getenv(f"TS_ENV_{i}_LABEL", "").strip()
if not lbl:
return None, None
if lbl == label:
return (os.getenv(f"TS_ENV_{i}_URL", "").strip().rstrip("/"),
os.getenv(f"TS_ENV_{i}_KEY_VAR", "").strip())
i += 1
def get_private_key_pem():
raw = get_admin_setting("SNOWFLAKE_KP_PK")
if not raw.startswith("-----BEGIN"):
import base64
try:
raw = base64.b64decode(raw).decode("utf-8")
except Exception:
pass
return raw
def connection_tml(name, scoped_db=None):
props = [
{"key": "accountName", "value": get_admin_setting("SNOWFLAKE_ACCOUNT")},
{"key": "user", "value": get_admin_setting("SNOWFLAKE_KP_USER")},
{"key": "private_key", "value": get_private_key_pem()},
{"key": "passphrase", "value": get_admin_setting("SNOWFLAKE_KP_PASSPHRASE", required=False)},
{"key": "role", "value": get_admin_setting("SNOWFLAKE_ROLE")},
{"key": "warehouse", "value": get_admin_setting("SNOWFLAKE_WAREHOUSE")},
]
if scoped_db:
props.append({"key": "database", "value": scoped_db})
return yaml.dump({
"guid": None,
"connection": {
"name": name,
"type": "RDBMS_SNOWFLAKE",
"authentication_type": "KEY_PAIR",
"properties": props,
"description": "deep-dive probe — safe to delete",
},
}, default_flow_style=False, sort_keys=False)
def ensure_physical_table():
from snowflake_auth import get_snowflake_connection
conn = get_snowflake_connection()
cur = conn.cursor()
cur.execute(f"CREATE DATABASE IF NOT EXISTS {DB}")
cur.execute(f"CREATE SCHEMA IF NOT EXISTS {DB}.{SCHEMA}")
cur.execute(f"CREATE TABLE IF NOT EXISTS {DB}.{SCHEMA}.{PHYS_TABLE} (ID NUMBER, LABEL VARCHAR)")
cur.close()
conn.close()
print(f"[snowflake] {DB}.{SCHEMA}.{PHYS_TABLE} ready")
def auth_session(url, user, secret):
s = requests.Session()
s.headers.update({"Content-Type": "application/json", "X-Requested-By": "ThoughtSpot"})
t0 = time.time()
r = s.post(f"{url}/api/rest/2.0/auth/token/full",
json={"username": user, "secret_key": secret, "validity_time_in_sec": 3600}, timeout=60)
print(f"[auth] HTTP {r.status_code} in {time.time()-t0:.2f}s")
r.raise_for_status()
s.headers["Authorization"] = f"Bearer {r.json()['token']}"
return s
def tml_import(s, url, tmls, timeout=360):
t0 = time.time()
r = s.post(f"{url}/api/rest/2.0/metadata/tml/import",
json={"metadata_tmls": tmls, "import_policy": "PARTIAL", "create_new": True},
timeout=timeout)
elapsed = time.time() - t0
status, guid, err = None, None, None
if r.status_code == 200:
obj = (r.json() or [{}])[0]
resp = obj.get("response", obj)
status = (resp.get("status") or {}).get("status_code")
err = (resp.get("status") or {}).get("error_message")
guid = (resp.get("header") or {}).get("id_guid")
return elapsed, r.status_code, status, guid, err
def delete_logical_table(s, url, guid):
r = s.post(f"{url}/api/rest/2.0/metadata/delete",
json={"metadata": [{"type": "LOGICAL_TABLE", "identifier": guid}]}, timeout=120)
print(f"[cleanup] delete table {guid}: HTTP {r.status_code}")
def delete_connection(s, url, guid):
# NB: connection/delete, NOT metadata/delete (CONNECTION not in that enum)
r = s.post(f"{url}/api/rest/2.0/connection/delete",
json={"connection_identifier": guid}, timeout=120)
print(f"[cleanup] delete connection {guid}: HTTP {r.status_code}")
def run_variant(s, url, variant, suffix):
scoped = DB if variant == "scoped" else None
conn_name = f"ZPERF_DD_{variant.upper()}_{suffix}"
print(f"\n=== VARIANT {variant} — connection {conn_name}"
+ (f" (database pinned to {DB})" if scoped else " (no database property, app-style)") + " ===")
t0 = time.time()
elapsed, http, status, conn_guid, err = None, None, None, None, None
r = s.post(f"{url}/api/rest/2.0/metadata/tml/import",
json={"metadata_tmls": [connection_tml(conn_name, scoped)], "import_policy": "PARTIAL"},
timeout=300)
conn_create_s = time.time() - t0
obj = (r.json() or [{}])[0] if r.status_code == 200 else {}
resp = obj.get("response", obj)
conn_guid = (resp.get("header") or {}).get("id_guid")
conn_status = (resp.get("status") or {}).get("status_code")
print(f"[conn-create] HTTP {r.status_code} status={conn_status} in {conn_create_s:.2f}s guid={conn_guid}")
if not conn_guid:
print(f"[conn-create] FAILED: {str(resp)[:500]}")
return None
table_name = f"DDT_{variant.upper()}_{suffix}"
tml = TABLE_TML.format(name=table_name, db=DB, schema=SCHEMA, phys=PHYS_TABLE, connection=conn_name)
print(f"[import] creating logical table {table_name} ({len(tml)} bytes) ...")
elapsed, http, status, table_guid, err = tml_import(s, url, [tml])
flag = " <-- GATEWAY TIMEOUT" if http in (502, 503, 504) else ""
print(f"[import] HTTP {http} status={status} in {elapsed:.1f}s{flag}" + (f" err={err}" if err else ""))
if table_guid:
delete_logical_table(s, url, table_guid)
delete_connection(s, url, conn_guid)
return {"variant": variant, "conn_create_s": round(conn_create_s, 2),
"import_s": round(elapsed, 1), "http": http, "status": status}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--env", required=True, help='TS env label from .env (e.g. "sebe - se")')
ap.add_argument("--variant", choices=["unscoped", "scoped", "both"], default="both")
args = ap.parse_args()
url, secret = resolve_env(args.env)
user = os.getenv("TEST_USER") or os.getenv("THOUGHTSPOT_USERNAME")
if not (url and user and secret):
raise SystemExit(f"Could not resolve env '{args.env}' from .env")
print(f"Host: {url}\nUser: {user}\nSource: {DB}.{SCHEMA}.{PHYS_TABLE}")
ensure_physical_table()
s = auth_session(url, user, secret)
suffix = time.strftime("%H%M%S")
variants = ["unscoped", "scoped"] if args.variant == "both" else [args.variant]
results = [run_variant(s, url, v, suffix) for v in variants]
print("\n" + "=" * 64)
print(f"{'variant':12}{'conn create':>12}{'table import':>14} http/status")
for res in results:
if res:
print(f"{res['variant']:12}{res['conn_create_s']:>11}s{res['import_s']:>13}s {res['http']}/{res['status']}")
print("=" * 64)
if __name__ == "__main__":
main()