Skip to content

Report a bulk update which failed because the document is missing (#4926) - #4942

Open
batrived wants to merge 2 commits into
JanusGraph:masterfrom
batrived:fix/4926-bulk-404-update-discarded
Open

Report a bulk update which failed because the document is missing (#4926)#4942
batrived wants to merge 2 commits into
JanusGraph:masterfrom
batrived:fix/4926-bulk-404-update-discarded

Conversation

@batrived

Copy link
Copy Markdown
Contributor

Fixes #4926.

pairErrorsWithSubmittedMutation removed every bulk item whose status is 404 from the failure list, whatever operation produced it. That is right for a delete — deleting an absent document leaves the index in the state the deletion asked for — but an update returning 404 is a document_missing_exception, and the write did not happen.

Combined with mutate() withholding the upsert whenever a mutation has both deletions and additions, changing a SINGLE cardinality indexed property on an element whose document is absent silently no-ops, permanently, with no exception, no log line and no metric. mutate returns normally, so even indexProvider.<name>.mutate.exceptions stays at zero.

Change

This implements option (1) from the issue. RequestBytes already reads request.getRequestType() to decide on the retry_on_conflict parameter but did not keep it, so retaining it makes the operation available where the failure is interpreted. The exemption is then limited to delete items.

Why I did not also do option (2)

Option (2) — always supplying the upsert — would make the 404 impossible rather than merely visible, and I started there. But mutation.getAdditions() for a SINGLE cardinality value change holds only the changed property, not every indexed property of the element. Upserting from it would create a document containing just that one field, so the element becomes findable by the changed property while still missing the others. That trades a detectable failure for a silently partial document, which I think is the worse of the two.

Making the 404 impossible properly means upserting a document built from the element's full indexed state, which is what restore() already does, and that is a larger change than this fix. Surfacing the error keeps the existing repair paths — transaction log recovery and reindex — in charge of rebuilding the document. Happy to follow up with the upsert version if you would rather have it, or to reconsider if you think the partial document is acceptable.

Behaviour change to be aware of

A mutation which previously returned normally now raises a PermanentBackendException. For a user whose index has already diverged, commits touching those elements will start failing visibly where they used to pass. That is the intent — the alternative is that the divergence stays invisible — but it is a real change in what a running deployment sees, so it may deserve a changelog note if you want one; say so and I will add it.

Testing

RestClientBulkItemStatusTest covers the delete exemption, an update 404, an index 404, the deletion-plus-addition shape that mutate() actually produces for a SINGLE cardinality value change, a mixed batch where only the genuinely failed item is reported, and a batch where an exempt delete and a reported update arrive together.

I confirmed four of these fail against master with "Expected java.io.IOException to be thrown, but nothing was thrown", which is the silent discard. The existing RestClientRetryTest still passes, so the retry path that also consumes this method is unaffected. Docker was unavailable to me, so I could not run the container-backed Elasticsearch suites locally.

Note on overlap

This touches pairErrorsWithSubmittedMutation and RequestBytes, which my PR #4935 for #4925 also modifies nearby, so whichever merges second will need a small rebase.


For all changes:

  • Is there an issue associated with this PR? Is it referenced in the commit message?
  • Does your PR body contain #xyz where xyz is the issue number you are trying to resolve?
  • Has your PR been rebased against the latest commit within the target branch (typically master)?
  • Is your initial contribution a single, squashed commit?

For code changes:

  • Have you written and/or updated unit tests to verify your changes?
  • If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under ASF 2.0? — no new dependencies
  • If applicable, have you updated the LICENSE.txt file, including the main LICENSE.txt file in the root of this repository? — not applicable
  • If applicable, have you updated the NOTICE.txt file, including the main NOTICE.txt file found in the root of this repository? — not applicable

…nusGraph#4926)

pairErrorsWithSubmittedMutation removed every bulk item whose status is 404 from
the failure list, whatever operation produced it. That is correct for a delete,
because deleting an absent document leaves the index in the state the deletion
asked for. An update which returns 404 is a document_missing_exception: the write
did not happen.

Changing the value of a SINGLE cardinality indexed property produces a deletion
of the old value and an addition of the new one against the same document, with
isNew false. Both become update operations, and because the mutation has
deletions, mutate() withholds the upsert document. If the Elasticsearch document
is absent, both items return 404, both were discarded, and the property was
never indexed. Every later update to that element took the same path, so the
document was never recreated. The element stayed invisible to the mixed index
while present in the storage backend, and nothing reported it: mutate returned
normally, so even the mutate.exceptions metric stayed at zero.

Retain the request type on RequestBytes, which already reads it to decide on the
retry_on_conflict parameter but did not keep it, and limit the exemption to
delete items.

Signed-off-by: Balmukund Trivedi <btrivedipublic@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@batrived
batrived force-pushed the fix/4926-bulk-404-update-discarded branch from eba16d7 to 2e97a6f Compare August 12, 2026 17:04
… delete

The previous commit exempted only RequestType.DELETE from the rule that a 404 in
a bulk item is a lost write. mutate() sends a field deletion as an UPDATE which
runs the parameterized deletion script, so that exemption is too narrow: a
document with no fields left to delete is already the state the mutation asked
for, and reporting it fails index maintenance which removes stale records and any
transaction which deletes a field of a document another transaction removed.

Carry the distinction on the mutation instead of reading it off the request type.
A whole document deletion and a field deletion both only take content out of the
index. An addition is what a 404 loses, and mutate() withholds the upsert from
the addition once the mutation also has deletions, which is the case the previous
commit set out to report.

Signed-off-by: Balmukund Trivedi <btrivedipublic@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@batrived
batrived force-pushed the fix/4926-bulk-404-update-discarded branch from 2e97a6f to 3053b67 Compare August 13, 2026 17:10
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.

Elasticsearch: bulk items failing with HTTP 404 are silently discarded, so updates to a missing document are dropped permanently

1 participant