79837f643d9b21431d679ea7216f7e68150b2adf hiram Mon May 11 15:34:40 2026 -0700 making the ottoRequestView CGI efficient and avoiding repeatedly scanning files to get numbers that are not changing refs #31811 diff --git src/hg/utils/otto/userRequests/ottoRequestView.cgi src/hg/utils/otto/userRequests/ottoRequestView.cgi index 8c880859803..da94c6f41bb 100644 --- src/hg/utils/otto/userRequests/ottoRequestView.cgi +++ src/hg/utils/otto/userRequests/ottoRequestView.cgi @@ -18,54 +18,61 @@ import time import urllib.parse from datetime import datetime ALLOWED_IP = '128.114.198.5' HGDB_CONF = '/usr/local/apache/cgi-bin/hg.conf' TRASH = '/data/apache/trash' DB = 'hgcentraltest' TABLE = 'ottoRequest' # Galaxy queue status panel - snapshot is refreshed by ottoRequestWatch.sh # (cron, every 11 minutes), CGI just reads it. CACHE_PATH = '/data/apache/trash/ottoRequestGalaxyStatus.json' CACHE_TTL = 1800 # seconds; older than this -> show "stale" instead +# featureBits coverage snapshot - append-only file maintained by +# featureBitsSnapshot.py (cron, via ottoRequestWatch.sh). fb.*.txt +# values are immutable once an alignment completes so no TTL is needed; +# featureBitsPct() falls back to an NFS read on a snapshot miss. +FB_SNAPSHOT_PATH = '/data/apache/trash/ottoRequestFeatureBitsPct.json' + # from README.txt in this directory STATUS_NAMES = { 0: 'received by API', 1: 'acknowledged, email sent', 2: 'galaxy job started', 3: 'galaxy done, download started', 4: 'downloaded, track files made', 5: 'symlinks ready, awaiting push', 6: 'push complete', 7: 'ERROR', 8: 'COMPLETE (final email sent)', } COLS = ['id', 'requestType', 'fromDb', 'toDb', 'email', 'comment', 'requestTime', 'status', 'buildDir', 'completeTime'] # featureBits coverage lookup roots HIVE_GENOMES = '/hive/data/genomes' ASMHUB_ROOT = HIVE_GENOMES + '/asmHubs' # in-process caches; one CGI invocation only, but rows reuse same accessions _buildDirCache = {} _fbPctCache = {} _genarkAsmName = {} # populated up-front by loadGenarkNames() +_fbSnapshot = {} # populated up-front by loadFeatureBitsSnapshot() def forbidden(msg): sys.stdout.write("Status: 403 Forbidden\r\n") sys.stdout.write("Content-Type: text/plain; charset=utf-8\r\n\r\n") sys.stdout.write(msg + "\n") sys.exit(0) def checkIp(): remote = os.environ.get('REMOTE_ADDR', '') if remote != ALLOWED_IP: forbidden(f"Access denied for {remote!r}; this page is restricted.") @@ -165,38 +172,61 @@ src = acc[:3] sub = 'refseqBuild' if src == 'GCF' else 'genbankBuild' digits = acc[4:].split('.', 1)[0] if len(digits) >= 9: result = (f'{ASMHUB_ROOT}/{sub}/{src}/' f'{digits[0:3]}/{digits[3:6]}/{digits[6:9]}/' f'{acc}_{asmName}') else: candidate = f'{HIVE_GENOMES}/{acc}' if os.path.isdir(candidate): result = candidate _buildDirCache[acc] = result return result +def loadFeatureBitsSnapshot(): + """Populate _fbSnapshot from the JSON file written by + featureBitsSnapshot.py via cron. Silent no-op if the file is + missing or malformed - featureBitsPct() falls back to an NFS read + on a snapshot miss, so the page still renders correctly.""" + try: + with open(FB_SNAPSHOT_PATH) as f: + data = json.load(f) + except (OSError, ValueError): + return + _fbSnapshot.update(data.get('pct') or {}) + + def featureBitsPct(srcAcc, qryAcc): """Return percentage from fb.<srcAcc>.chain<qryAcc>Link.txt (% of srcAcc - covered by chains to qryAcc), or '' if unavailable.""" + covered by chains to qryAcc), or '' if unavailable. + + Two-tier lookup: the precomputed cron snapshot first (pure dict + lookup, no I/O); on miss falls back to the NFS file read so + freshly-completed rows still show a value before the next cron + tick promotes them.""" if not srcAcc or not qryAcc: return '' key = (srcAcc, qryAcc) if key in _fbPctCache: return _fbPctCache[key] + snapKey = f'{srcAcc}\t{qryAcc}' + if snapKey in _fbSnapshot: + pct = _fbSnapshot[snapKey] + _fbPctCache[key] = pct + return pct bdir = hubBuildDir(srcAcc) pct = '' if bdir: # GenArk builds keep lastz under trackData/, UCSC native under bed/ sub = 'trackData' if '/asmHubs/' in bdir else 'bed' # chain<QryAcc>Link.txt: first letter of query is capitalized # (matches the ${dstDb^} convention in installLinks). No-op for # GCA_*/GCF_* accessions; converts hg38 -> Hg38 for native dbs. QryAcc = qryAcc[:1].upper() + qryAcc[1:] path = (f'{bdir}/{sub}/lastz.{qryAcc}/' f'fb.{srcAcc}.chain{QryAcc}Link.txt') try: with open(path) as f: txt = f.read() m = re.search(r'\(([\d.]+)%\)', txt) @@ -397,27 +427,28 @@ except RuntimeError as e: rows = [] error = (error + ' / ' if error else '') + f"fetch failed: {e}" # one bulk lookup of GenArk asmNames so hubBuildDir() avoids NFS readdir fromIdx = COLS.index('fromDb') toIdx = COLS.index('toDb') gcAccs = set() for r in rows: for idx in (fromIdx, toIdx): if idx < len(r): v = r[idx] if v.startswith('GCA_') or v.startswith('GCF_'): gcAccs.add(v) loadGenarkNames(gcAccs) + loadFeatureBitsSnapshot() galaxyStatus = loadGalaxyStatus() renderPage(rows, info=info, error=error, galaxyStatus=galaxyStatus) if __name__ == '__main__': try: main() except Exception as e: sys.stdout.write("Content-Type: text/plain; charset=utf-8\r\n\r\n") sys.stdout.write(f"ottoRequestView.cgi error: {e}\n")