59e2bbf2d3a99e5f18436a756e1e5c8b64308e33
braney
  Tue Sep 8 09:21:02 2026 -0700
Copy a track collection's hub file when the program that writes it asks for a copy, instead of on every session load.

cartCopyLocalHubs ran on every session load, in every CGI and in four more places in
hgSession. For each customComposite-<db> cart variable it copied the collection's hub
file to a fresh trash name and registered the copy in hgcentral.hubStatus to get a hub
id. Nothing removed the old row. 81% of the 3.1 million rows in hubStatus on the RR are
these dead registrations, and each load also took the central_hubStatus advisory lock
and wrote to a MyISAM table, which serializes every concurrent load of a shared session
that carries a collection.

The copy itself is needed. A saved session's hub file lives under sessionDataDir and
every load of that session names it, so hgCollection must not write it in place. It was
just being made for every load, and almost no load is followed by an edit. hgCollection
is the only program that writes one of these files, so it now asks for its own copy:
main() calls cartRequestLocalHubCopy() before it opens the cart, and cartNew() makes the
copy for it.

That has to be a property of the program rather than of the request, and the copy has to
be made at cart open, above hubConnectLoadHubs. Copying the file gives the hub a new id,
the hubs are loaded during cart open, and trackList carries that id in every track name,
so a copy made any later leaves printTrackDbListToHub looking for the collection under
an id trackList does not have and the hub file comes out with a header and no tracks. A
condition that instead tries to work out whether this particular request will write
cannot be correct either: "cmd" is not in hgTracks' excludeVars, so it persists in the
cart and hgCollection dispatches on the cart rather than on the CGI variable, a settings
file can carry it, and a command-line run has no SCRIPT_NAME to test.

Three other parts. copyLocalHubs skips a hub the cart already owns, so repeated edits
reuse one file and one hub id rather than renumbering the hub on every drag; the test
requires a plain file, because saveTrackFile leaves the trash name behind as a symbolic
link to the durable copy and writing through that link would rewrite the saved session's
own file. copyLocalHubs also screens the path with isServerUserFilePath before opening
it, the way getHubName does, and selects on customComposite-<db> with the dash so that
the names it acts on are the ones fileNameCartVarPrefixes screens. saveTrackFile copies
when the source is a local hub outside trash that is not under this session's own
directory, so loading one session and saving it under a new name gives the new session
its own file instead of a reference into the first one's directory. pathIsUnderDir is no
longer static in trashDir.c.

All of it is behind the hg.conf gate collectionHubCopyOnWrite, default off, which
reproduces the old behavior exactly. Retiring the gate is not uniform: the body of
cartCopyLocalHubsOnSessionLoad is the old behavior and that function and its five
callers go away with the gate, while the other three tests lose only the gate term.

Measured on hgcentraltest with one binary. With the gate off, five loads of a session
carrying a collection add five hubStatus rows and three later edits add none; with it
on, the five loads add none and the first edit adds one. Saving a loaded session costs
two rows off and one on. The resulting hub file is byte identical either way apart from
the hub id, and the file the saved session names is unchanged by md5 and mtime,
including after an edit made straight after a save, and including when the cart names a
trash symbolic link into session storage.

refs #38273

diff --git src/hg/lib/sessionData.c src/hg/lib/sessionData.c
index 97d1895b279..dbf31ac2ea4 100644
--- src/hg/lib/sessionData.c
+++ src/hg/lib/sessionData.c
@@ -344,76 +344,106 @@
             char *newDbTableName = saveTrashTable(tableName, sessionDataDbPrefix, dbSuffix);
             if (newDbTableName)
                 {
                 updateSessionDataTablePaths(newDbTableName, sessionDir);
                 char *newString = replaceChars(*retString, tableName, newDbTableName);
                 freez(retString);
                 *retString = newString;
                 freeMem(newDbTableName);
                 }
             }
         freeMem(tableName);
         }
     }
 }
 
+INLINE boolean cartVarIsLocalHub(char *cartVar)
+/* Return TRUE if cartVar starts with "customComposite-" or "hubQuickLift-". */
+{
+return startsWith(quickLiftCartName "-", cartVar) || startsWith(customCompositeCartName "-", cartVar);
+}
+
 static char *newCtTrashFile()
 /* Alloc and return the name of a new trash file to hold custom track metadata. */
 {
 struct tempName tn;
 trashDirFile(&tn, "ct", CT_PREFIX, ".ctfile");
 return cloneString(tn.forCgi);
 }
 
+static char *localHubSessionDataPath(char *varName, char *sessionDir)
+/* Alloc and return a fresh path under sessionDir for a track collection or quickLift hub file.
+ * Used when the hub being saved belongs to some other session, so its own name cannot be
+ * reused.  Mint a new trash name and map it into sessionDir the way a trash file's own name
+ * would be mapped, so the result has the same shape as every other saved hub path. */
+{
+struct tempName tn;
+if (startsWith(quickLiftCartName "-", varName))
+    trashDirDateFile(&tn, "quickLift", "hub", ".txt");
+else
+    trashDirDateFile(&tn, "hgComposite", "hub", ".txt");
+return sessionDataPathFromTrash(tn.forCgi, sessionDir);
+}
+
 static char *saveTrackFile(struct cart *cart, char *varName, char *oldFile,
                            char *sessionDataDbPrefix, char *dbSuffix, char *sessionDir)
 /* oldFile contains custom track lines or track collection hub trackDb; scan for trashDir paths
  * and/or customTrash tables and move files and tables to safe locations per sessionDataDbPrefix and
  * sessionDir.  If oldFile does not exist or has already been saved, return NULL. */
 {
 char *newFile = NULL;
 if (fileExists(oldFile))
     {
-    if (isTrashPath(oldFile))
+    // A local hub file outside trash belongs to another session: this one was loaded and is now
+    // being saved under a new name, or is another user's session being re-saved.  Copy it so that
+    // each session owns its own hub file.  Under copy-on-write nothing copies these on load, so
+    // this is the only place the split happens.  refs #38273
+    boolean fromOtherSession = (cartCollectionHubCopyOnWrite() &&
+                                !isTrashPath(oldFile) && isNotEmpty(sessionDir) &&
+                                cartVarIsLocalHub(varName) &&
+                                !pathIsUnderDir(sessionDir, oldFile));
+    if (isTrashPath(oldFile) || fromOtherSession)
         {
         struct lineFile *lf = lineFileOpen(oldFile, TRUE);
-        if (isNotEmpty(sessionDir))
+        if (fromOtherSession)
+            newFile = localHubSessionDataPath(varName, sessionDir);
+        else if (isNotEmpty(sessionDir))
             newFile = sessionDataPathFromTrash(oldFile, sessionDir);
         else
             newFile = newCtTrashFile();
         if (fileExists(newFile))
             fprintf(stderr, "saveTrackFile: new file '%s' already exists", newFile);
         makeDirsForFile(newFile);
         FILE *newF = mustOpen(newFile, "w");
         char *line;
         while (lineFileNext(lf, &line, NULL))
             {
             char *s = skipLeadingSpaces(line);
             if (*s != '\0' && *s != '#')
                 {
                 char *trackLine = cloneString(line);
                 saveTrashPaths(&trackLine, sessionDir, FALSE);
                 saveDbTableName(&trackLine, sessionDataDbPrefix, dbSuffix, sessionDir);
                 fprintf(newF, "%s\n", trackLine);
                 freeMem(trackLine);
                 }
             else
                 fprintf(newF, "%s\n", line);
             }
         carefulClose(&newF);
         fprintf(stderr, "Wrote new file %s\n", newFile);
-        if (isNotEmpty(sessionDir))
+        if (isNotEmpty(sessionDir) && !fromOtherSession)
             {
             if (unlink(oldFile) != 0)
                 errnoAbort("saveTrackFile: unlink(oldFile='%s') failed", oldFile);
             if (symlink(newFile, oldFile) != 0)
                 errnoAbort("saveTrackFile: symlink(newFile='%s', oldFile='%s') failed",
                            newFile, oldFile);
             fprintf(stderr, "symlinked %s to %s\n", oldFile, newFile);
             }
         cartSetString(cart, varName, newFile);
         }
     }
 else
     cartRemove(cart, varName);
 return newFile;
 }
@@ -430,36 +460,30 @@
     {
     if (sessionDataDir[0] != '/')
         errAbort("config setting sessionDataDir must be an absolute path (starting with '/')");
     char *userHash = md5HexForString(encUserName);
     userHash[2] = '\0';
     char *sessionHash = md5HexForString(encSessionName);
     sessionHash[8] = '\0';
     struct dyString *dy = dyStringCreate("%s/%s/%s/%s",
                                          sessionDataDir, userHash, encUserName, sessionHash);
     dir = dyStringCannibalize(&dy);
     freeMem(sessionHash);
     }
 return dir;
 }
 
-INLINE boolean cartVarIsLocalHub(char *cartVar)
-/* Return TRUE if cartVar starts with "customComposite-" or "hubQuickLift-". */
-{
-return startsWith(quickLiftCartName "-", cartVar) || startsWith(customCompositeCartName "-", cartVar);
-}
-
 static char *dayOfMonthString()
 /* Return a two-character string with the current day of the month [01..31].  Do not free.
  * (Yeah, not [0..30]!  See man 3 localtime.) */
 {
 static char dayString[16];
 time_t now = time(NULL);
 struct tm *tm = localtime(&now);
 safef(dayString, sizeof dayString, "%02u", tm->tm_mday);
 return dayString;
 }
 
 void sessionDataSaveSession(struct cart *cart, char *encUserName, char *encSessionName,
                             char *dbSuffix)
 /* If hg.conf specifies safe places to store files and/or tables that belong to user sessions,
  * then scan cart for trashDir files and/or customTrash tables, store them in safe locations,