26009fa434f2595ded075145fd322dc85dc6d518 braney Tue Sep 8 08:57:37 2026 -0700 ts: give each ticket sandbox its own trackDb cache Every user's CGIs write their trackDb cache into one shared cacheTrackDbDir. That is wrong for a park in two ways. The cached image is a shared-memory dump whose layout is tied to TRACKDB_VERSION in the binaries that wrote it, so a park built from a branch that touches the cache shares a directory with the live sandbox and with everybody else. And the writer leaves a name.txt beside each image and opens it with mustOpen, so whoever wrote it first owns the file and the next writer's CGI dies on a permission error - after the image itself has landed, so the next request succeeds and the failure reads as intermittent. Each ticket now gets NNNNN/trackDbCache inside its own sandbox. Unlike the udc cache this is derived from the code rather than data, so it is treated the opposite way: freeze clears it, since the binaries it belongs to have just been replaced, remove takes it with the sandbox, and conf retrofits it onto an instance frozen before this existed. refs #37867 diff --git src/utils/ts/ts src/utils/ts/ts index 54daf528004..3170ba94d5c 100755 --- src/utils/ts/ts +++ src/utils/ts/ts @@ -1,31 +1,32 @@ #!/bin/bash # # ts - "ticket sandbox": park a UCSC Genome Browser Redmine ticket as its own # frozen browser instance on hgwdev. refs #37867 # # A per-ticket Apache instance, owned by the developer, bound to a loopback high # port, reached by an ssh tunnel (yours or a colleague's - all hgwdev users # share the loopback, so anyone with an hgwdev account can tunnel in). The DB # and /gbdb are shared (hg.conf points at hgwdev MySQL; /gbdb is shared NFS) and # trash is shared with the live CGIs, so only the *code* is frozen: a full copy # of cgi-bin-$USER + htdocs-$USER plus a rewritten hg.conf. Data is left -# shared on purpose (freeze the code, share the data). The one exception is the -# udc cache: each ticket gets its own subtree under the shared trash, so one -# instance cannot serve another a cache entry it wrote (see setUdcDir). The -# rewritten hg.conf also drops the cookie domain, or a parked instance could not -# hold a cart at all (see setCookieDomain). +# shared on purpose (freeze the code, share the data). Two caches are the +# exception, because a cache written by one instance is otherwise read by all of +# them: the udc cache gets its own subtree under the shared trash (see +# setUdcDir), and the trackDb cache gets its own directory inside the sandbox +# (see setTrackDbCacheDir). The rewritten hg.conf also drops the cookie domain, +# or a parked instance could not hold a cart at all (see setCookieDomain). # # Each instance answers on two ports: plain http on its registered port, and # https on that port plus 1000, from a self-signed certificate shared by every # park. Both serve the same frozen code, so the pair is how you tell whether a # CGI behaves differently under TLS - Apache sets HTTPS=on only for the https # listener, and a CGI that reads it, such as one deciding whether to mark a # cookie Secure, takes the other branch there. Nothing about the http side # changed, and "ts conf NNNNN" adds https to a park frozen before it existed. # # The parked instances and the port registry live under $TS_ROOT, by default # $HOME/ticketSandboxes. On hgwdev, point that at a large local pool (a freeze # is a few GB): mkdir /data/home/$USER/ticketSandboxes and symlink it from # $HOME, or set TS_ROOT. # # Subcommands (NNNNN = Redmine ticket number): @@ -163,30 +164,33 @@ "$LIVE_HTDOCS/" "$dir/$HTDOCS_NAME/" # Rewritten, frozen hg.conf: # - include ../cgi-bin/hg.conf -> absolute (avoid self-include loop; the # shared base config is data-side and stays live by design) # - browser.documentRoot -> this ticket's frozen htdocs-$USER sed -e 's#^[[:space:]]*include[[:space:]]\+\.\./cgi-bin/hg\.conf#include '"$BASE_HGCONF"'#' \ -e 's#^[[:space:]]*browser\.documentRoot[[:space:]]*=.*#browser.documentRoot='"$dir"'/'"$HTDOCS_NAME"'#' \ "$LIVE_CGI/hg.conf" > "$dir/cgi-bin/hg.conf" # Per-ticket marker: a densely tiled "RM NNNNN" watermark on the page # background, so it stays visible in the gaps around the content and it is # obvious which frozen instance you are looking at. browser.style # (cart.c:2993) injects the stylesheet ; appended last so it wins. makeMarker "$tkt" "$dir" printf '\n# ts: per-ticket background marker (refs #37867)\nbrowser.style=/style/tsMarker.css\n' \ >> "$dir/cgi-bin/hg.conf" + # the images are keyed to the binaries we are replacing, so drop them + rm -rf "$dir/trackDbCache" + setTrackDbCacheDir "$tkt" "$dir" setUdcDir "$tkt" "$dir" setCookieDomain "$tkt" "$dir" echo "Freeze complete ($(du -sh "$dir/cgi-bin" "$dir/$HTDOCS_NAME" 2>/dev/null | awk '{print $1}' | paste -sd'+'))." } # --- generate the per-ticket background watermark image + stylesheet --------- # Small tile => the label repeats many times across the page, so at least some # copies land in the margins that the content does not cover. makeMarker() { local tkt="$1" dir="$2" local png="$dir/$HTDOCS_NAME/style/tsMarker.png" local css="$dir/$HTDOCS_NAME/style/tsMarker.css" if command -v convert >/dev/null 2>&1; then convert -size 150x70 xc:none -gravity center \ -fill 'rgba(200,40,40,0.28)' -pointsize 17 -weight 700 \ @@ -210,30 +214,53 @@ # its own subtree. It still lives in the shared trash, so the trash cleaner # ages it out like any other udc cache, and it is still data, not code: sync # does not clear it. Appended to hg.conf, so it overrides the udc.cacheDir in # the included base config (the last assignment of a name wins). udcDir() { echo "$SHARED_TRASH/udcCache/ts/$1"; } setUdcDir() { local tkt="$1" dir="$2" mkdir -p "$(udcDir "$tkt")" # freeze() writes hg.conf fresh, but conf() calls this on an existing one grep -q '^# ts: per-ticket udc cache' "$dir/cgi-bin/hg.conf" && return 0 printf '\n# ts: per-ticket udc cache (refs #37867)\nudc.cacheDir=%s\n' \ "$(udcDir "$tkt")" >> "$dir/cgi-bin/hg.conf" } +# --- give the ticket its own trackDb cache ----------------------------------- +# The trackDb cache is a shared-memory image of the parsed trackDb, and every +# user's CGIs write into one cacheTrackDbDir. Two things about that are wrong +# for a park. The image layout is tied to TRACKDB_VERSION in the frozen +# binaries, so a park built from a branch that changes the cache shares a +# directory with the live sandbox and with other developers. And the writer +# leaves a name.txt beside each image and opens it for write, so whoever wrote +# it first owns it and the next writer's CGI dies on a permission error, after +# the image itself has landed - which makes the failure look intermittent. +# Give each ticket its own directory, inside the sandbox so remove takes it +# along with everything else. Unlike the udc cache this is derived from the +# code rather than data, so a re-freeze clears it. +tdbCacheDir() { echo "$(tsDir "$1")/trackDbCache"; } + +setTrackDbCacheDir() { + local tkt="$1" dir="$2" + mkdir -p "$(tdbCacheDir "$tkt")" + # freeze() writes hg.conf fresh, but conf() calls this on an existing one + grep -q '^# ts: per-ticket trackDb cache' "$dir/cgi-bin/hg.conf" && return 0 + printf '\n# ts: per-ticket trackDb cache (refs #37867)\ncacheTrackDbDir=%s\n' \ + "$(tdbCacheDir "$tkt")" >> "$dir/cgi-bin/hg.conf" +} + # --- let the cart survive on a loopback host --------------------------------- # The shared config sets central.domain=.ucsc.edu, and cartWriteCookie (cart.c) # puts that on the cart cookie. A parked instance answers on localhost, so the # browser drops a cookie scoped to .ucsc.edu, and every request gets a fresh # cart. Track visibility falls back to its trackDb default, quietly: the page # still renders, so a scripted run measures the defaults and looks like a clean # pass. Clicking through a park by hand hides it, because hgTracks puts the # hgsid in its own page links; a run that navigates by URL has nothing to carry. # An empty value leaves the domain attribute off the cookie altogether, which is # what a host-only cookie needs, and it works whether the instance is reached as # localhost or as 127.0.0.1. The login cookies follow it too, through # getCookieDomainString() in wikiLink.c. Appended to hg.conf, so it overrides # the value in the included base config (the last assignment of a name wins). setCookieDomain() { local tkt="$1" dir="$2" @@ -390,30 +417,31 @@ # Rewrite httpd.conf from the current template without touching the frozen code. # "sync" also re-freezes, which is what you want after more work on the live # sandbox, but not when the freeze is the whole point of the park and only the # config template moved on. cmd_conf() { local tkt="$1"; validTkt "$tkt" local port; port="$(regPort "$tkt")" [[ -n "$port" ]] || die "RM $tkt not found in registry" [[ -d "$(tsDir "$tkt")" ]] || die "no sandbox directory for RM $tkt" local running=""; [[ -n "$(pidOf "$tkt")" ]] && running=1 [[ -n "$running" ]] && stopHttpd "$tkt" ensureCert # retrofits https onto a pre-https park writeConf "$tkt" "$port" setUdcDir "$tkt" "$(tsDir "$tkt")" # retrofit a sandbox frozen before these existed + setTrackDbCacheDir "$tkt" "$(tsDir "$tkt")" setCookieDomain "$tkt" "$(tsDir "$tkt")" if [[ -n "$running" ]]; then startHttpd "$tkt" else echo "RM $tkt httpd.conf rewritten (httpd was not running)." fi } cmd_start() { validTkt "$1"; startHttpd "$1"; } cmd_stop() { validTkt "$1"; stopHttpd "$1"; } cmd_port() { # print the port for a ticket (used by the laptop-side ts wrapper) # "ts port NNNNN" keeps printing the http port on its own, because a laptop # may still hold a copy of ts.mac from before https existed. The https port # is a second argument away rather than a second line. @@ -454,31 +482,32 @@ [[ -s "$REG" ]] || { echo "No parked tickets."; return 0; } printf '%-8s %-6s %-6s %-11s %-8s %s\n' RM PORT HTTPS CREATED STATUS NOTE while IFS=$'\t' read -r tkt port created note; do [[ -z "$tkt" ]] && continue local status="stopped"; [[ -n "$(pidOf "$tkt")" ]] && status="running" printf '%-8s %-6s %-6s %-11s %-8s %s\n' \ "$tkt" "$port" "$(sslPort "$port")" "$created" "$status" "$note" done < "$REG" } cmd_remove() { local tkt="$1"; validTkt "$tkt" local port; port="$(regPort "$tkt")" [[ -n "$port" ]] || die "RM $tkt not found in registry" stopHttpd "$tkt" || true - rm -rf "$(tsDir "$tkt")" # trash is a symlink: removed, not followed + rm -rf "$(tsDir "$tkt")" # takes the ticket's trackDb cache with it; + # trash is a symlink: removed, not followed rm -rf "$(udcDir "$tkt")" # the ticket's udc cache lives inside the shared trash # drop the registry row local tmp; tmp="$(mktemp)" awk -F'\t' -v r="$tkt" '$1!=r' "$REG" > "$tmp" && mv "$tmp" "$REG" echo "RM $tkt removed." } # --- dispatch ---------------------------------------------------------------- [[ $# -ge 1 ]] || usage sub="$1"; shift || true case "$sub" in create) [[ $# -ge 1 ]] || usage; cmd_create "$@";; sync) [[ $# -eq 1 ]] || usage; cmd_sync "$1";; conf) [[ $# -eq 1 ]] || usage; cmd_conf "$1";; start) [[ $# -eq 1 ]] || usage; cmd_start "$1";;