383ff66de9a1a37635a1d079cccdeb9842056524
braney
  Tue Sep 8 09:22:26 2026 -0700
ottoMonitor: correct the job counts in the README and the survey header, refs #38101

The README's "WHAT IT ASKS" section said fifteen jobs write a log or a named
file, nine leave only a directory mtime, and eight leave nothing.  Those add to
32, but the monitor watches 40.  The real split is twenty-four, eight and
eight, which is what ottoMonitor.py -v reports.  The old "fifteen" counted only
the external jobs with a positive stamp and dropped the nine internal ones that
also write a per-run file.  The old "nine" came from counting the "dir mtime"
notes in ottoMonitorStamps.tsv, which catches ottoGitVsHive, but that job is
blind, not directory-mtime.  The same off-by-one is fixed in the traps section.

The two surveys cover all 47 jobs in otto.crontab while the monitor watches 40,
so the README now says where the other seven went: they are the Cell Browser
jobs, marked monitor=no in ottoOwners.tsv.

ottoFailureSignatures.tsv opened by saying the 18 internal jobs were not
surveyed yet, three sections above a fully surveyed section for exactly those
18 jobs.  The sentence was left over from the draft before that section was
added.

Also drop a dead clause in sourceIsUp().  "code == 226 or code == 0 and False"
reduces to "code == 226", because and binds tighter than or, so it read as if a
curl that could not connect were handled specially when it was not.  Behavior
is unchanged: curl() returns 0 when curl itself failed, and 0 means the source
did not answer.

These are the two items the 2026-09-08 code review asked for, both in files
that tell a future editor to read them before touching a stamp glob.

diff --git src/hg/utils/otto/ottoMonitor/ottoMonitor.py src/hg/utils/otto/ottoMonitor/ottoMonitor.py
index 947f6c5b136..618587b0e10 100755
--- src/hg/utils/otto/ottoMonitor/ottoMonitor.py
+++ src/hg/utils/otto/ottoMonitor/ottoMonitor.py
@@ -193,32 +193,34 @@
         path = sourceUrl[len("file:"):]
         if not os.access(path, os.R_OK):
             return(None, "cannot read " + path)
         if path.endswith("curl.config"):
             code = curl(["-K", path, "-r", "0-0"])
             return(code == 200 or code == 206, "curl -K %s -> %d" % (path, code))
         with open(path) as fh:
             for line in fh:
                 line = line.strip()
                 if line.startswith("http") or line.startswith("ftp"):
                     code = curl(["-L", "-r", "0-0", line])
                     return(code in (200, 206), "config url -> %d" % code)
         return(None, "no url found in " + path)
 
     if sourceUrl.startswith("ftp:"):
+        # An ftp listing has no HTTP code; curl reports 226 for a completed
+        # transfer.  Anything else, 0 included, means the source did not answer.
         code = curl([sourceUrl])
-        return(code == 226 or code == 0 and False, "ftp -> %d" % code)
+        return(code == 226, "ftp -> %d" % code)
 
     code = curl(["-I", sourceUrl])
     if code == 200:
         return(True, "HEAD -> 200")
     # HEAD is refused by clinGenCspec, insight and lovd, and g2p and omim
     # redirect.  A ranged GET answers all five.
     code2 = curl(["-L", "-r", "0-0", sourceUrl])
     return(code2 in (200, 206), "HEAD -> %d, GET -> %d" % (code, code2))
 
 
 def readTable(path, wanted):
     """Read a tab separated table with a leading comment block.  Returns a dict
     keyed on the first column, plus the comment lines, so the ottoOnDuty header
     can be found without a second pass."""
     rows = {}