f1ab1958cec7cf4f14056692907e7518aa67273b
hiram
  Thu Apr 30 10:25:36 2026 -0700
use flock to ensure only one instance of this script is ever running at one time refs #31811

diff --git src/hg/utils/otto/userRequests/ottoRequestPush.py src/hg/utils/otto/userRequests/ottoRequestPush.py
index 699ccf08279..0842decfecd 100755
--- src/hg/utils/otto/userRequests/ottoRequestPush.py
+++ src/hg/utils/otto/userRequests/ottoRequestPush.py
@@ -1,35 +1,51 @@
 #!/usr/bin/env python3
 """
 ottoRequestPush.py - group fromDb/toDb identifiers from pending push
 requests (status=5) by clade.
 
 Output: dict[clade] -> sorted list of assembly identifiers, where each
 identifier is "<gcAccession>_<asmName>" for GenArk accessions, or the
 plain UCSC db name for native dbs.
 """
 
+import fcntl
 import os
 import re
 import subprocess
 import sys
 from collections import defaultdict
 
 scriptDir = os.path.dirname(os.path.abspath(__file__))
 cladeTsv = os.path.join(scriptDir, "dbDb.name.clade.tsv")
+lockPath = os.path.join(scriptDir, "ottoRequestPush.lock")
 gcPattern = re.compile(r"^GC[AF]_")
 
+
+def acquireSingletonLock():
+    """Ensure only one instance of this script runs at a time.  Holds an
+    exclusive flock on lockPath for the lifetime of the process; the
+    kernel releases it on exit (including crash / kill -9), so no stale
+    lock cleanup is needed.  Returns the open file handle, which the
+    caller must keep alive."""
+    fh = open(lockPath, "w")
+    try:
+        fcntl.flock(fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
+    except BlockingIOError:
+        sys.exit(0)
+    return fh
+
 def hgsql(query, db="hgcentraltest"):
     """Run hgsql -N -B and return rows as list of tuples (tab-split)."""
     out = subprocess.run(
         ["hgsql", "-N", "-B", "-e", query, db],
         check=True, capture_output=True, text=True,
     ).stdout
     return [tuple(line.split("\t")) for line in out.splitlines() if line]
 
 
 def loadDbDbClades():
     """Read dbDb.name.clade.tsv -> {dbName: clade}."""
     result = {}
     with open(cladeTsv) as fh:
         for line in fh:
             if line.startswith("#") or not line.strip():
@@ -143,30 +159,31 @@
     written.  Returns True on success, False if any step fails (the
     chain stops at the first failure)."""
     for cmd in makeChainCommands:
         print("# [%s] %s" % (cladeDir, cmd), file=sys.stderr)
         result = subprocess.run(
             cmd, shell=True, executable="/bin/bash", cwd=cladeDir,
         )
         if result.returncode != 0:
             print("# ERROR: exit %d from: %s -- stopping chain"
                   % (result.returncode, cmd), file=sys.stderr)
             return False
     return True
 
 
 def main():
+    lockFh = acquireSingletonLock()  # noqa: F841 -- keep ref alive
     requests = pendingRequests()
     if not requests:
         return
     dbs = set()
     for _, fromDb, toDb in requests:
         dbs.update((fromDb, toDb))
     accessions = {db for db in dbs if gcPattern.match(db)}
     dbDbClades = loadDbDbClades()
     genarkInfo = lookupGenark(accessions)
     grouped = groupByClade(dbs, dbDbClades, genarkInfo)
 
     def cladeOf(db):
         if gcPattern.match(db):
             info = genarkInfo.get(db)
             return info[1] if info else None