Skip to content

GH Issue 1720: Run saveBatch and saveRuns transform scripts outside the save transaction - #8220

Merged
labkey-jeckels merged 17 commits into
developfrom
fb_batch_save_transforms_1720
Oct 29, 2026
Merged

labkey-jeckels merged 17 commits into
developfrom
fb_batch_save_transforms_1720

Conversation

@labkey-jeckels

@labkey-jeckels labkey-jeckels commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Rationale

The assay batch and run save APIs now run Standard assay transform scripts before opening their database transaction, so a script that calls back into the server can no longer deadlock with other work such as a folder delete (GH Issue 1720). This extends the GH Issue 1707 fix for single-run imports to requests that save several runs together: every run's scripts finish before the shared transaction opens, and if any script fails nothing is written.

It also fixes a regression from #8183 that broke BiologicsAssayFilesTest.testImportAssayWithFileFieldData (TeamCity failure): when an assay has a transform script, the uploaded data file's audit event was recorded outside the import transaction and lost its transaction ID.

Related Pull Requests

Changes

  • Save handlers can look up a request's data and materials without creating them
  • saveBatch and saveRuns run each run's transform scripts in a read-only first pass, then save the runs inside the transaction and record the scripts' output
  • A run whose data changes while its scripts run fails the save and asks the client to retry
  • saveBatch and saveRuns reject a request that lists the same run more than once
  • Standard assays configured for background upload get the same behavior through these APIs
  • Custom file-based assay save handlers keep running scripts inside the transaction
  • Audit events for files uploaded before a transform script runs are recorded in the import's transaction again; a failed import records them only for files it keeps

Tested locally: AssayTransformCallbackTest passes, including the new saveBatch and saveRuns cases and the new file audit cases.

…e transform output is recorded

A run edited while its scripts ran now fails the save instead of overwriting the edit, duplicate runs and missing batches are rejected before any script runs, and prepared inputs are keyed by file.
@labkey-jeckels labkey-jeckels self-assigned this Oct 22, 2026
@labkey-jeckels
labkey-jeckels marked this pull request as ready for review October 23, 2026 15:02
…port transaction

Events for files saved before the transform runs are held on the PreparedTransform; a failed import writes them only for files that still exist.
}

/** Builds the upload context handleRun would build for this run, without writing anything, and runs its transform scripts. */
private @Nullable PreparedTransform prepareTransform(ViewContext context, ExpProtocol protocol, JSONObject runJson) throws ExperimentException, ValidationException

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got very confused by all of these prepareTransform methods because of the similarities between the method names and the object name of the return value.

Consider renaming some of the private methods to bring out more distinction or inline one into the other.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, the overloads were the worst of it. The private pair is gone: prepareTransforms now reads each posted run once (readPostedRun) and hands it to runTransformScripts, which calls runCreator.prepareTransform, so that name now refers only to the run creator's method and the PreparedTransform it returns. I also renamed the snapshot helpers (SCRIPT_INPUT_KEYS, setScriptInputs/scriptInputsMatch) so "prepared" only refers to PreparedTransform objects.

/** Whether the run, read inside the transaction, still holds what its scripts saw before it. */
private boolean matchesPrepared(JSONObject runJson, @Nullable JSONObject serializedRun)
{
JSONObject seen = _preparedSerializations.get(runJson);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could some of this tracking and validation of the runs be handled inside of PreparedTransform?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the snapshot of what the scripts read onto PreparedTransform along with the check against it, so the handler no longer keeps a second map keyed by run JSON. PreparedTransform also now keeps only the inputs the participant visit resolver added, so the save no longer strips the posted inputs out and merges them back in. Which parts of the run JSON get snapshotted stays in the handler, since it depends on the save API's JSON shape and PreparedTransform also serves the non-JSON import paths.

}

/** For an import that failed before recording its held audits; cleanup may already have deleted some of the files. */
public static void addFileAuditEventsForRemainingFiles(@Nullable User user, @NotNull List<PendingFileAudit> audits)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems odd to me that auditing is handled here, although I can't think of a better place to track events.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's here because PreparedTransform is the one object that spans from before the import transaction opens until after it commits or rolls back: the events have to be held until the transaction exists, recorded inside it, and written on close() if the import never gets that far, the same lifecycle as the scripts' working directories. I did promote PendingFileAudit to a top-level type so the file writer and upload context don't reference it through PreparedTransform.

@labkey-jeckels

Copy link
Copy Markdown
Contributor Author

@labkey-klum I pushed edits. I had Claude add replies to your specific comments, which are wordier than I would have liked, but overall I tried to improve naming and consolidate a bit more. See what you think.

@labkey-klum labkey-klum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the last pass cleaned up some of those readability / complexity issues.

@labkey-jeckels
labkey-jeckels merged commit b6f457e into develop Oct 29, 2026
12 checks passed
@labkey-jeckels
labkey-jeckels deleted the fb_batch_save_transforms_1720 branch October 29, 2026 13:22
labkey-jeckels added a commit to LabKey/testAutomation that referenced this pull request Oct 29, 2026
… folder delete (#3265)

## Rationale
Adds test coverage for [GH Issue
1720](LabKey/internal-issues#1720): saving
assay batches or runs through the API must not hold a database
transaction while transform scripts run.

## Related Pull Requests
- LabKey/platform#8220

## Changes
- saveBatch and saveRuns variants of the folder-delete callback test
- A batch whose second run's script fails saves nothing and cleans up
the scripts' working directories
- Updating an existing run keeps its results when another run in the
request fails, and replaces them when the save succeeds

---------

Co-authored-by: Lum <klum@labkey.com>
XingY pushed a commit that referenced this pull request Oct 29, 2026
…he save transaction (#8220)

## Rationale
The assay batch and run save APIs now run Standard assay transform
scripts before opening their database transaction, so a script that
calls back into the server can no longer deadlock with other work such
as a folder delete ([GH Issue
1720](LabKey/internal-issues#1720)). This
extends the GH Issue 1707 fix for single-run imports to requests that
save several runs together: every run's scripts finish before the shared
transaction opens, and if any script fails nothing is written.

It also fixes a regression from #8183 that broke
`BiologicsAssayFilesTest.testImportAssayWithFileFieldData` ([TeamCity
failure](https://teamcity.labkey.org/buildConfiguration/LabKey_Trunk_Premium_ProductSuites_Biologics_BiologicsBPostgres/4231858)):
when an assay has a transform script, the uploaded data file's audit
event was recorded outside the import transaction and lost its
transaction ID.

## Related Pull Requests
- LabKey/testAutomation#3265

## Changes
- Save handlers can look up a request's data and materials without
creating them
- saveBatch and saveRuns run each run's transform scripts in a read-only
first pass, then save the runs inside the transaction and record the
scripts' output
- A run whose data changes while its scripts run fails the save and asks
the client to retry
- saveBatch and saveRuns reject a request that lists the same run more
than once
- Standard assays configured for background upload get the same behavior
through these APIs
- Custom file-based assay save handlers keep running scripts inside the
transaction
- Audit events for files uploaded before a transform script runs are
recorded in the import's transaction again; a failed import records them
only for files it keeps

Tested locally: AssayTransformCallbackTest passes, including the new
saveBatch and saveRuns cases and the new file audit cases.

---------

Co-authored-by: Lum <klum@labkey.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants