8f801f0b7d0b28d3adbc5f26fc7cb8976ac2830f hiram Fri May 22 13:48:48 2026 -0700 add the featureBits measurements to the user notification email refs #31811 diff --git src/hg/utils/otto/userRequests/ottoRequestWatch.sh src/hg/utils/otto/userRequests/ottoRequestWatch.sh index 78868ee3593..98a5fd13074 100755 --- src/hg/utils/otto/userRequests/ottoRequestWatch.sh +++ src/hg/utils/otto/userRequests/ottoRequestWatch.sh @@ -6,30 +6,31 @@ # web-server service user). Picks up requests that ottoRequest.py has # acknowledged (status=1) and drives them through alignment setup # and workflow monitoring. # # Phase 1: new requests needing alignment setup - status=1 AND buildDir='' # run ottoRequestAlign.sh to set up and launch the workflow # Phase 2: in-progress requests needing workflow monitoring # run workflowMonitor.sh to poll Galaxy and install results # 0 pending, 1 notified, 2 in progress, 3 galaxy done, 4 tracks complete, # 5 ready to push, 6 push is done, 7 problems, # 8 final notification has been sent == process is complete ### cron job entry: #9,20,31,42,53 * * * * ~/kent/src/hg/utils/otto/userRequests/ottoRequestWatch.sh set -eEu -o pipefail +umask 002 export scriptDir=$(cd "$(dirname "$0")" && pwd) ############################################################################## ### singleton lock - only one instance at a time ### Open lockPath on FD 9 for the lifetime of the shell, then take a ### non-blocking exclusive lock. Kernel releases the lock on exit ### (normal, error, or kill -9), so no stale lock cleanup is needed. ### Exit 0 silently if another instance holds the lock so cron doesn't ### email on every overlapping tick. PID is written to the file for ### information only see the holder via: ### cat ottoRequestWatch.lock (the PID) ### lsof ottoRequestWatch.lock (the locking process) ############################################################################## export lockPath="${scriptDir}/ottoRequestWatch.lock" @@ -42,30 +43,69 @@ # truncates via a separate FD; FD 9 keeps its position 0 from <>, so # the printf below starts writing at the beginning of the empty file. : >"${lockPath}" printf "%d\n" "$$" >&9 ############################################################################## ############################################################################## ### errors - set error status in the table function setErrorStatus() { id="${1}" /cluster/bin/x86_64/hgsql -N -e \ "UPDATE ottoRequest SET status=7 WHERE id=${id};" hgcentraltest } ############################################################################## +############################################################################## +### getFeatureBitsPct - get percentage coverage from featureBits file +### args: srcDb dstDb buildDir +### returns percentage string (e.g., "45.2%") or empty string if not found +### mimics the featureBitsPct() function from ottoRequestView.cgi +############################################################################## +function getFeatureBitsPct() { + local srcDb="${1}" + local dstDb="${2}" + local buildDir="${3}" + local DstDb="${dstDb^}" # first letter capitalized + + if [[ -z "${srcDb}" || -z "${dstDb}" || -z "${buildDir}" ]]; then + return + fi + + # determine subdirectory: trackData for GenArk, bed for UCSC native + local sub + if [[ "${srcDb}" == GC* ]]; then + sub="trackData" + else + sub="bed" + fi + + # construct path to featureBits file +# local fbFile="${buildDir}/${sub}/lastz.${dstDb}/fb.${srcDb}.chain${DstDb}Link.txt" + local fbFile="${buildDir}/fb.${srcDb}.chain${DstDb}Link.txt" + + if [[ -f "${fbFile}" ]]; then + # extract percentage from file using grep + sed (matches Python regex r'\(([\d.]+)%\)') + local pct + pct="$(grep -oE '\([0-9.]+%\)' "${fbFile}" 2>/dev/null | sed 's/[()]//g' | head -1)" + if [[ -n "${pct}" ]]; then + printf "%s" "${pct}" + fi + fi +} +############################################################################## + ############################################################################## ### liftOverUrl - build the public download URL for an over.chain.gz file ### args: srcDb dstDb ### GenArk: https://hgdownload.soe.ucsc.edu/hubs/<3>/<3>/<3>/<3>//liftOver/To.over.chain.gz ### UCSC native: https://hgdownload.soe.ucsc.edu/goldenPath//liftOver/To.over.chain.gz ### DstDb is dstDb with the first letter upper-cased (matches the ### filename convention used in installLinks()). ### verifies the URL with a curl HEAD before printing; returns 1 if ### the URL does not resolve to a 2xx response. ############################################################################## function liftOverUrl() { local srcDb="${1}" local dstDb="${2}" local DstDb="${dstDb^}" local fileName="${srcDb}To${DstDb}.over.chain.gz" @@ -464,36 +504,70 @@ if [ -s "${buildDir}/successInvocationId.txt" ]; then invocationId=$(cut -f2 "${buildDir}/successInvocationId.txt") if ! "${scriptDir}/galaxyCleanup.py" "${profileJson}" "${invocationId}"; then printf "# WARNING: galaxy cleanup failed for request %s\n" "${reqId}" 1>&2 fi fi if ! fromUrl="$(liftOverUrl "${fromDb}" "${toDb}")"; then setErrorStatus "${reqId}" continue fi if ! toUrl="$(liftOverUrl "${toDb}" "${fromDb}")"; then setErrorStatus "${reqId}" continue fi + # source the kegAlign.sh variables to get targetDb, queryDb, and swapDir + targetDb="" queryDb="" swapDir="" + if [[ -f "${buildDir}/kegAlign.sh" ]]; then + source <(grep -E '^export (swapDir|targetDb|queryDb)=' "${buildDir}/kegAlign.sh" 2>/dev/null || true) + fi + # get featureBits coverage percentages for both directions + # determine which direction is which and use appropriate build directory + fromToPct="" toFromPct="" + if [[ -n "${targetDb}" && -n "${queryDb}" && -n "${swapDir}" ]]; then + if [[ "${fromDb}" == "${targetDb}" ]]; then + # fromDb -> toDb uses buildDir, toDb -> fromDb uses swapDir + fromToPct="$(getFeatureBitsPct "${fromDb}" "${toDb}" "${buildDir}")" + toFromPct="$(getFeatureBitsPct "${toDb}" "${fromDb}" "${swapDir}")" + else + # fromDb -> toDb uses swapDir, toDb -> fromDb uses buildDir + fromToPct="$(getFeatureBitsPct "${fromDb}" "${toDb}" "${swapDir}")" + toFromPct="$(getFeatureBitsPct "${toDb}" "${fromDb}" "${buildDir}")" + fi + else + # fallback: try both directions with buildDir only (may not find swap direction) + fromToPct="$(getFeatureBitsPct "${fromDb}" "${toDb}" "${buildDir}")" + toFromPct="$(getFeatureBitsPct "${toDb}" "${fromDb}" "${buildDir}")" + fi + + # construct coverage info for email + coverageInfo="" + if [[ -n "${fromToPct}" || -n "${toFromPct}" ]]; then + coverageInfo=" +Chain coverage (% of genome covered by alignments): + ${fromDb} -> ${toDb}: ${fromToPct:-"not available"} + ${toDb} -> ${fromDb}: ${toFromPct:-"not available"} +" + fi + sendNotification "${reqId}" \ "from UCSC: liftOverRequest complete: ${fromDb}<->${toDb}" \ "Your lift over request is complete: From: ${fromDb} To: ${toDb} comment: ${comment} submitted: ${requestTime} - +${coverageInfo} The lift.over files are available at these links: ${fromUrl} ${toUrl} " /cluster/bin/x86_64/hgsql -N -e \ "UPDATE ottoRequest SET status=8, completeTime=now() WHERE id=${reqId};" hgcentraltest done < <(/cluster/bin/x86_64/hgsql -N -B -e \ "SELECT id, fromDb, toDb, comment, requestTime, buildDir FROM ottoRequest \ WHERE status = 6 AND requestType = 'liftOver';" hgcentraltest)