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")