e2c9cfc1bb6a71ba957b8f2b674491595a232353
hiram
  Fri Jul 24 09:51:39 2026 -0700
refactor the ottoRequest table access with relay requests from Euro/Asia nodes to the RR system refs #31811

diff --git src/hg/hubApi/liftOver.c src/hg/hubApi/liftOver.c
index 64c723f5ca9..eec643b08d3 100644
--- src/hg/hubApi/liftOver.c
+++ src/hg/hubApi/liftOver.c
@@ -133,109 +133,58 @@
     }
 jsonWriteListEnd(jw);
 jsonWriteNumber(jw, "totalLiftOvers", totalRows);
 int chainListCount = slCount(chainList);
 jsonWriteNumber(jw, "itemsReturned", chainListCount);
 liftOverChainFreeList(&chainList);
 
 /* if no chain rows for this pair, check ottoRequest for any existing
  * row (any status) so the user is told their pair has already been
  * submitted instead of being allowed to create a duplicate row */
 if (chainListCount == 0 && isNotEmpty(fromDb) && isNotEmpty(toDb))
     {
     char *ottoTable = cfgOption("ottoTable");
     if (isNotEmpty(ottoTable))
         {
-        struct sqlConnection *ottoConn = hConnectOtto();
+        struct sqlConnection *ottoConn = hConnectCentral();
         if (sqlTableExists(ottoConn, ottoTable))
             {
             struct dyString *pq = newDyString(0);
             sqlDyStringPrintf(pq,
                 "SELECT id, status, requestTime FROM %s "
                 "WHERE requestType='liftOver' AND "
                 "((fromDb='%s' AND toDb='%s') OR (fromDb='%s' AND toDb='%s')) "
                 "ORDER BY requestTime DESC LIMIT 1",
                 ottoTable, fromDb, toDb, toDb, fromDb);
             char **row;
             struct sqlResult *sr = sqlGetResult(ottoConn, dyStringCannibalize(&pq));
             if ((row = sqlNextRow(sr)) != NULL)
                 {
                 jsonWriteBoolean(jw, "pending", TRUE);
                 jsonWriteNumber(jw, "pendingStatus", sqlSigned(row[1]));
                 jsonWriteString(jw, "pendingRequestTime", row[2]);
                 }
             sqlFreeResult(&sr);
             }
-        hDisconnectOtto(&ottoConn);
+        hDisconnectCentral(&ottoConn);
         }
     }
 
 apiFinishOutput(0, NULL, jw);
 hDisconnectCentral(&conn);
 }
 
-static char *thisHostName()
-/* Return this machine's own hostname via gethostname().  Unlike hHttpHost(),
- * which reflects the client-supplied HTTP_HOST/Host: header, this can't be
- * spoofed by the request and doesn't change depending on which round-robin
- * name (e.g. genome.ucsc.edu) the client used to reach this box -- using
- * hHttpHost() here made onGenomeRRMachine() misclassify RR machines reached
- * via the genome.ucsc.edu name, sending them into an infinite self-relay
- * loop in fetchGbMembersFromCentral(). */
-{
-static char host[256];
-static boolean init = FALSE;
-if (!init)
-    {
-    if (gethostname(host, sizeof(host)) != 0)
-        host[0] = '\0';
-    init = TRUE;
-    }
-return host;
-}
-
-static boolean inUcscEduDomain()
-/* Return TRUE if this machine is configured as belonging to the ucsc.edu
- * domain, per hg.conf's central.domain setting (the same setting used for
- * the cross-host login cookie domain).  This is deployment config rather
- * than something derived from the machine's own OS hostname/DNS: a box's
- * local/cloud-assigned hostname and its UCSC-facing name (e.g.
- * genome-euro.ucsc.edu) can be unrelated DNS labels with no way to bridge
- * them via gethostname()/getaddrinfo(). */
-{
-char *domain = cfgOption(CFG_CENTRAL_DOMAIN);
-return (domain != NULL && strstr(domain, ".ucsc.edu") != NULL);
-}
-
-static boolean onGenomeRRMachine()
-/* Return TRUE if running on one of the genome.ucsc.edu round-robin
- * machines (hgw0, hgw1, hgw2, ...), which carry SQL grants on
- * hgcentral.gbMembers.  Only meaningful within inUcscEduDomain(). */
-{
-if (hIsPrivateHost())	// stay on localhost for hgwdev and hgwbeta
-    return TRUE;
-char *hostName = thisHostName();
-if (isEmpty(hostName))
-    return FALSE;
-if (startsWith("hgwbeta", hostName))
-    return TRUE;
-if (!startsWith("hgw", hostName))
-    return FALSE;
-char afterHgw = hostName[3];
-return (afterHgw >= '0' && afterHgw <= '9');
-}
-
 static boolean fetchGbMembersFromCentral(char *userName, char **retEmail, char **retRealName)
 /* Relay to genome.ucsc.edu's own loginStatus endpoint to get email/realName
  * for userName, forwarding this request's Cookie header so genome.ucsc.edu
  * authenticates the same session.  Used on ucsc.edu hosts that lack SQL
  * grants on hgcentral.gbMembers.  Returns FALSE on any failure. */
 {
 char *cookieHeader = getenv("HTTP_COOKIE");
 if (isEmpty(cookieHeader))
     return FALSE;
 
 struct dyString *reqHeader = dyStringNew(0);
 dyStringPrintf(reqHeader, "Cookie: %s\r\n", cookieHeader);
 char *url = "https://genome.ucsc.edu/cgi-bin/hubApi/liftOver/loginStatus";
 int sd = netOpenHttpExt(url, "GET", reqHeader->string);
 dyStringFree(&reqHeader);
@@ -280,49 +229,42 @@
 if (userName != NULL)
     {
     // Get both email and realName from gbMembers table
     char *email = NULL;
     char *realName = NULL;
 
     if (inUcscEduDomain() && !onGenomeRRMachine())
         {
         // dev sandboxes, hgwbeta, etc: no local grants on gbMembers,
         // relay to genome.ucsc.edu instead
         fetchGbMembersFromCentral(userName, &email, &realName);
         }
     else
         {
         // RR machines, and anything entirely outside ucsc.edu: unchanged
-        struct sqlConnection *sc = NULL;
-        if (privateHost)
-	    sc = hConnectCentral();
-        else
-	    sc = hConnectOtto();
+        struct sqlConnection *sc = hConnectCentral();
         struct dyString *query = sqlDyStringCreate("select email, realName from gbMembers where userName = '%s'", userName);
         struct sqlResult *sr = sqlGetResult(sc, dyStringCannibalize(&query));
         char **row = sqlNextRow(sr);
 
         if (row != NULL)
             {
             email = cloneString(row[0] ? row[0] : "");
             realName = cloneString(row[1] ? row[1] : "");
             }
         sqlFreeResult(&sr);
-        if (privateHost)
         hDisconnectCentral(&sc);
-        else
-	    hDisconnectOtto(&sc);
         }
 
     // Build logout URL with returnto parameter
     char *returnTo = cgiOptionalString("returnTo");
     struct dyString *logoutUrl = dyStringNew(0);
     dyStringPrintf(logoutUrl, "%s?hgLogin.do.displayLogout=1", hgLoginLink);
     if (isNotEmpty(returnTo))
         {
         char *encodedReturnUrl = cgiEncodeFull(returnTo);
 
         dyStringPrintf(logoutUrl, "&returnto=%s", encodedReturnUrl);
         freeMem(encodedReturnUrl);
         }
 
     jsonWriteString(jw, "userName", userName);
@@ -544,123 +486,61 @@
 struct dbDb *toDb = hDbDb(toGenome);
 if (toDb == NULL)
     {
     toDb = genarkLiftOverDb(toGenome);
     }
 if ( (fromDb == NULL) || (fromDb == NULL) )
     {
     if ( (fromDb == NULL) && (toDb == NULL) )
 	    apiErrAbort(err400, err400Msg, "can not find either 'fromGenome=%s' or 'toGenome=%s' for endpoint '/liftOver", fromGenome, toGenome);
         else
 	    apiErrAbort(err400, err400Msg, "can not find 'fromoGenome=%s' for endpoint '/liftOver", fromGenome);
     if (toDb == NULL)
         apiErrAbort(err400, err400Msg, "can not find 'toGenome=%s' for endpoint '/liftOver", toGenome);
     }
 
-/* duplicate-row guard: any existing row in ottoRequest for this pair
- * (either direction, any status) blocks resubmission.  The form's JS
- * already shows a "pending" panel for this case via the listExisting
- * endpoint; this is the backstop for clients that bypass the form. */
-{
-char *dupOttoTable = cfgOption("ottoTable");
-if (isNotEmpty(dupOttoTable))
-    {
-    struct sqlConnection *conn = hConnectOtto();
-    if (sqlTableExists(conn, dupOttoTable))
-        {
-        struct dyString *dq = newDyString(0);
-        sqlDyStringPrintf(dq,
-            "SELECT COUNT(*) FROM %s WHERE requestType='liftOver' AND "
-            "((fromDb='%s' AND toDb='%s') OR (fromDb='%s' AND toDb='%s'))",
-            dupOttoTable, fromGenome, toGenome, toGenome, fromGenome);
-        int dupCount = sqlQuickNum(conn, dyStringCannibalize(&dq));
-        hDisconnectOtto(&conn);
-        if (dupCount > 0)
+/* Record the request in the ottoRequest table: duplicate-row guard, daily
+ * rate-limit guard, then an atomic insert.  Done locally (this host has
+ * hgcentral write grants) or relayed to genome.ucsc.edu (it doesn't) --
+ * see inUcscEduDomain()/onGenomeRRMachine(). */
+char *ottoStatus;
+if (inUcscEduDomain() && !onGenomeRRMachine())
+    ottoStatus = relaySubmitOttoRequest("liftOver", fromGenome, toGenome, email, comment);
+else
+    ottoStatus = submitOttoRequest("liftOver", fromGenome, toGenome, email, comment);
+
+if (sameString(ottoStatus, "duplicate"))
     apiErrAbort(err409, err409Msg,
         "A request for %s <-> %s has already been submitted "
         "and is on record.  Duplicates are not accepted.",
         fromGenome, toGenome);
-        }
-    else
-        hDisconnectOtto(&conn);
-    }
-}
-
-/* per-email daily rate limit, per requestType, calendar-day server time */
+else if (sameString(ottoStatus, "rateLimited"))
+    {
     char *limitStr = cfgOption("liftDailyLimit");
-int dailyLimit = isNotEmpty(limitStr) ? atoi(limitStr) : 0;
-if (dailyLimit > 0)
-    {
-    char *limitOttoTable = cfgOption("ottoTable");
-    if (isNotEmpty(limitOttoTable))
-        {
-        struct sqlConnection *conn = hConnectOtto();
-        if (sqlTableExists(conn, limitOttoTable))
-            {
-            struct dyString *q = newDyString(0);
-            sqlDyStringPrintf(q,
-                "SELECT COUNT(*) FROM %s "
-                "WHERE requestType='liftOver' AND email='%s' "
-                "AND DATE(requestTime) = CURDATE()",
-                limitOttoTable, email);
-            int todayCount = sqlQuickNum(conn, dyStringCannibalize(&q));
-            hDisconnectOtto(&conn);
-            if (todayCount >= dailyLimit)
     apiErrAbort(err429, err429Msg,
-                    "Daily limit reached: %d liftOver requests per day. "
+        "Daily limit reached: %s liftOver requests per day. "
         " Please try again tomorrow.",
-                    dailyLimit);
-            }
-        else
-            hDisconnectOtto(&conn);
-        }
+        isNotEmpty(limitStr) ? limitStr : "the daily limit of");
     }
+else if (sameString(ottoStatus, "error"))
+    apiErrAbort(err500, err500Msg, "internal error recording liftOver request");
 
 char *toAddr = cfgOption("chainFileRequestEmail");
 char *fromAddr = cfgOption("apiFromEmail");
 
 if (isNotEmpty(toAddr) && isNotEmpty(fromAddr))
     {
     char nowTime[256];
     time_t seconds = clock1();
     struct tm *timeNow = localtime(&seconds);
     strftime(nowTime, sizeof nowTime, "%Y-%m-%d %H:%M:%S", timeNow);
 
     struct dyString *msg = newDyString(0);
     /* may need to encode these inputs to make them safe */
     dyStringPrintf(msg, "%s\nLift over request\nfrom: %s\nto: %s\nemail '%s'\ncomment: '%s'", nowTime, fromGenome, toGenome, email, comment);
 
     /* some kind of response here back to the request page */
     struct jsonWrite *jw = apiStartOutput();
     jsonWriteString(jw, "msg", dyStringCannibalize(&msg));
     apiFinishOutput(0,NULL,jw);
-    char *ottoTable = cfgOption("ottoTable");	/* probably ottoRequest */
-    if (isNotEmpty(ottoTable))
-        {
-        struct sqlConnection *conn = hConnectOtto();
-        if (sqlTableExists(conn, ottoTable))
-	    {
-            /* Atomic insert with duplicate check - prevents race condition */
-            struct dyString *update = newDyString(0);
-            sqlDyStringPrintf(update,
-                "INSERT INTO %s (requestType, fromDb, toDb, email, comment, requestTime, status, buildDir) "
-                "SELECT 'liftOver', '%s', '%s', '%s', '%s', now(), 0, '' "
-                "WHERE NOT EXISTS ("
-                "  SELECT 1 FROM %s WHERE requestType='liftOver' AND "
-                "  ((fromDb='%s' AND toDb='%s') OR (fromDb='%s' AND toDb='%s'))"
-                ")",
-                ottoTable, fromGenome, toGenome, email, comment,
-                ottoTable, fromGenome, toGenome, toGenome, fromGenome);
-            int rowsAffected = sqlUpdateRows(conn, dyStringCannibalize(&update), NULL);
-            if (rowsAffected == 0)
-                {
-                hDisconnectOtto(&conn);
-                apiErrAbort(err409, err409Msg,
-                    "A request for %s <-> %s has already been submitted "
-                    "and is on record.  Duplicates are not accepted.",
-                    fromGenome, toGenome);
-                }
-	    }
-        hDisconnectOtto(&conn);
-        }
     }
 }	/*	void apiLiftRequest(char *words[MAX_PATH_INFO])	*/