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]) */