Skip to content

fix(backend): discard processed data whose sequence entry was edited - #7137

Draft
corneliusroemer-agent wants to merge 5 commits into
mainfrom
fix/discard-revoked-preprocessing-claims
Draft

corneliusroemer-agent wants to merge 5 commits into
mainfrom
fix/discard-revoked-preprocessing-claims

Conversation

@corneliusroemer-agent

@corneliusroemer-agent corneliusroemer-agent commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Rethinking this PR I'm now not sure we need it - it only affects reprocessing where a 10min delay on a few sequences isn't a big deal.

Fixes #7128.

When a user edits a sequence entry (which is only allowed when the current prepro version's entry is PROCESSED) we delet sequence_entries_preprocessed_data rows all pipeline versions of that accessionVersion. Preprocessing could already be underway for a a higher prepro version than current, when that delete happens. When that pipeline then submits it would get a 422 for the whole batch which would only get reprocessed once the cleanup job clears them (10min for PPX).

This isn't so bad as it only happens during reprocessing - so not sure we need to make this change here.

Changes

A result whose claim no longer exists is now discarded: WARN log, counted separately, and the summary line reads Updated N sequences to PROCESSED, discarded M results whose claim was revoked. The entry is handed out again and reprocessed, as it already was. Unknown accession versions and rows in an unexpected state still fail with 422.

Updated 0, discarded 100 repeating would indicate a lease threshold set below batch duration.

Not addressed: the update has no fencing token, so two pods of the same pipeline version can still race an edit and land pre-edit data on a fresh claim. That needs a claim id in the protocol. However, this should be a very rare issue and it would resolve on the next reprocessing so it's not permanent.

Alternatives considered

We could block editing a sequence when any entry greater or equal to current is IN_PROCESSING - however that would mean people could not edit while a reprocessing is in process which could be hours and is not a good user experience.

🚀 Preview: Add preview label to enable

Editing a sequence entry deletes its preprocessed data for all pipeline
versions, so a pipeline that is mid-flight on that entry can no longer
submit: insertProcessedData matched no row and threw, which rolled back
the entire submit-processed-data batch because the service is
@transactional. The unaffected entries in the batch stayed IN_PROCESSING
until the stale reaper cleared them.

Discard such a result instead, logging at WARN and reporting the count in
the summary log line. Unknown accession versions and rows in an
unexpected state still fail with 422.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude claude Bot added the backend related to the loculus backend component label Aug 23, 2026
@corneliusroemer
corneliusroemer marked this pull request as ready for review August 23, 2026 18:22
Copilot AI lite review requested due to automatic review settings August 23, 2026 18:22
@claude

claude Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Claude finished @corneliusroemer's task in 3m 6s —— View job


Review

  • Read PR diff and surrounding code
  • Check correctness of claim-revoked detection
  • Check transaction / concurrency semantics
  • Check test coverage
  • Post review

The approach is right: a WHERE ... AND status = IN_PROCESSING AND pipeline_version = ? update that matches 0 rows because the row was deleted underneath is not a client error, and turning the whole batch into a 422 was the actual harm in #7128. The narrowing to "entry still exists but has no row for this pipeline version" is a reasonable proxy given there's no claim id, and the honesty about the remaining fencing-token race in the description is appreciated.

Five things, in inline comments. Two are worth acting on before merge:

Files of a discarded result are still published to S3 (comment). processedFiles is populated before the insert and consumed after the loop, so a discarded entry's file IDs still reach setFileToPublic. The data is dropped but the files go public and end up orphaned. One-line fix.

No tests. The scenario from #7128 is directly constructible with the existing helpers — prepareDataTo(PROCESSED), extract with pipeline version 2, edit the entry (allowed, since the current version's entry is PROCESSED), then submit the version 2 results — and should assert 200 plus that the entry is handed out again. SubmitProcessedDataEndpointTest has three tests pinning the 422 paths; the new success path has none, and neither does the interaction with released files. Backend AGENTS.md asks for tests passing before commit, and this is a concurrency fix where a regression would be invisible in production (a silent 200).

The rest are smaller: the revoked check also swallows results for entries that were never in processing and costs two extra queries per failing row (comment) — the "never claimed" case is the one I'd most like a deliberate decision on, since it turns a real client bug into a silent success; plus a naming nit on the new boolean return and a suggestion to add a discard counter to SubmissionMetrics so the "lease threshold too low" condition is alertable rather than grep-only.

Not verified: I did not run ./gradlew test or ktlintFormat — no changes were made, so there was nothing new to check, and the existing 422 tests I traced by hand still hold under the new logic.
fix/discard-revoked-preprocessing-claims

Comment on lines +313 to +315
} else {
discardedAccessionVersions.add(submittedProcessedData.displayAccessionVersion())
}

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.

Files of a discarded result still get published.

validateFileMappingsAndCollectFileIds (line 377) writes into processedFiles before insertProcessedData runs, and the block at lines 320-331 later calls s3Service.setFileToPublic(fileId) for every entry that is already released. So when a result is discarded, its processed data is dropped but its files are still made public — and nothing references them anymore.

Reachable path: released entry gets reprocessed by pipeline version N+1 → that claim goes stale → cleanUpStaleSequencesInProcessing deletes the row → the pipeline submits late → discarded here, files published.

Suggested change
} else {
discardedAccessionVersions.add(submittedProcessedData.displayAccessionVersion())
}
} else {
discardedAccessionVersions.add(submittedProcessedData.displayAccessionVersion())
processedFiles.remove(
AccessionVersion(submittedProcessedData.accession, submittedProcessedData.version),
)
}

Fix this →

Comment on lines +510 to 528
private fun claimWasRevoked(accessionVersion: AccessionVersionInterface, pipelineVersion: Long): Boolean {
val sepd = SequenceEntriesPreprocessedDataTable
val claimExists = sepd
.select(sepd.accessionColumn)
.where {
sepd.accessionVersionEquals(accessionVersion) and
(sepd.pipelineVersionColumn eq pipelineVersion)
}
.limit(1)
.count() > 0
if (claimExists) {
return false
}
return SequenceEntriesTable
.select(SequenceEntriesTable.accessionColumn)
.where { SequenceEntriesTable.accessionVersionIsIn(listOf(accessionVersion)) }
.limit(1)
.count() > 0
}

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.

Two concerns here.

1. This also silently swallows results for entries that were never in processing. The predicate is "entry exists, but no preprocessed row for this pipeline version". An entry in RECEIVED that was never handed out to pipeline version N matches that just as well as an edited entry. So a pipeline submitting results for an entry it never claimed now gets a 200 instead of the previous 422 ... does not exist or is not awaiting any processing results. That's an error signal we lose for a genuine client bug.

The three existing 422 tests still pass (non-existent accession/version fail the SequenceEntriesTable lookup; the PROCESSED entry has a row for the same pipeline version), so nothing catches this. Worth a deliberate decision + a test either way — see the top-level comment.

2. Duplicated query work on the failure path. throwInsertFailedException (line 618) already selects every preprocessed row for the accession version and reasons about status/pipeline version. claimWasRevoked issues two more queries to answer an overlapping question, and it runs per row — during an edit sweep (the #7128 scenario) that's the common path, so a 100-row batch costs 200 extra round trips in a method that was otherwise deliberately written to batch its DB lookups (see the comment on line 296).

Folding the revoked check into throwInsertFailedException would reuse the one select it already does and keep all "why did the update not match" reasoning in one place: if the select returns no rows at all, check SequenceEntriesTable once and signal "revoked" instead of throwing.

organism: Organism,
pipelineVersion: Long,
) {
): Boolean {

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.

if (insertProcessedData(...)) at the call site (line 310) doesn't say what the boolean means — a reader has to jump here to find out that true means stored and false means discarded. The KDoc that explained this was removed in c940c33, so consider letting the name carry it instead, e.g. storeProcessedDataUnlessClaimRevoked(...), or returning a two-value enum. Either way the if/else at line 310 becomes readable on its own.


log.info {
"Updated ${processedAccessionVersions.size} sequences to $PROCESSED. " +
"Updated ${processedAccessionVersions.size} sequences to $PROCESSED, " +

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.

The PR description notes that a repeating Updated 0, discarded 100 signals a lease threshold set below batch duration — that's exactly the kind of thing operators should be able to alert on, and a log line makes it grep-only. SubmissionMetrics already has the Counter pattern (recordProcessedSequencesStored, line 348); a matching recordDiscardedProcessedResults(organism, count) would be a small addition and makes the condition observable.

Copilot AI 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.

Pull request overview

This PR fixes a failure mode in the backend preprocessing submission flow where processed results for a sequence are rejected (422) if the sequence entry was edited while a higher preprocessing pipeline version was still running. The change treats “missing claim row for this pipeline version” as a revoked claim and discards the result instead of failing the whole batch.

Changes:

  • Make insertProcessedData(...) return Boolean to indicate whether a processed result was stored or discarded due to a revoked claim.
  • Add a revoked-claim detection query (claimWasRevoked) and log/summary reporting for discarded results.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 307 to +315
submittedProcessedDataBatch.forEach { submittedProcessedData ->
val processingResult = submittedProcessedData.processingResult()

insertProcessedData(submittedProcessedData, organism, pipelineVersion)
processedAccessionVersions.add(submittedProcessedData.displayAccessionVersion())
processingResultCounts.merge(processingResult, 1, Int::plus)
if (insertProcessedData(submittedProcessedData, organism, pipelineVersion)) {
processedAccessionVersions.add(submittedProcessedData.displayAccessionVersion())
processingResultCounts.merge(processingResult, 1, Int::plus)
} else {
discardedAccessionVersions.add(submittedProcessedData.displayAccessionVersion())
}
corneliusroemer-agent and others added 2 commits August 23, 2026 18:39
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A discarded result's file IDs were collected before the result was
stored, so its files were still made public for an already released
entry while nothing referenced them any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.assertStatusIs(Status.RECEIVED)
}

// A discarded result must not publish its files, they end up referenced by nothing.

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.

Suggested change
// A discarded result must not publish its files, they end up referenced by nothing.

).andExpect(status().isNoContent)
convenienceClient.approveProcessedSequenceEntries(listOf(AccessionVersion(accession, 1)))

// Pipeline version 2 submits a result for the released entry without holding a claim on it.

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.

Suggested change
// Pipeline version 2 submits a result for the released entry without holding a claim on it.

@corneliusroemer corneliusroemer changed the title fix(backend): discard processed data whose claim was revoked fix(backend): discard processed data whose sequence entry was edited Aug 24, 2026
@corneliusroemer
corneliusroemer marked this pull request as draft August 24, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend related to the loculus backend component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

editing a sequence entry breaks in-flight preprocessing for it

3 participants