Skip to content

Report why an update action does not apply to a mixed index (#4930) - #4939

Open
batrived wants to merge 1 commit into
JanusGraph:masterfrom
batrived:fix/4930-mixed-index-update-status-guard
Open

batrived wants to merge 1 commit into
JanusGraph:masterfrom
batrived:fix/4930-mixed-index-update-status-guard

Conversation

@batrived

Copy link
Copy Markdown
Contributor

Fixes #4930.

A mixed index keeps a SchemaStatus per field instead of one status on the index, so updateIndex collects the fields whose status the action applies to. When no field matched, that empty set was passed to setStatus, which routed to setStatusVertex, whose precondition can never hold for a mixed index.

One correction to the issue: that precondition is Preconditions.checkArgument(condition) with no message argument, so the exception's message is null. The caller does not get a confusing message about RelationTypeVertex — it gets a bare IllegalArgumentException with no message at all. I found this while writing the test, which failed with a NullPointerException on e.getMessage() before the fix.

Re-running an idempotent schema script is enough to reach it, exactly as the issue describes: REGISTER_INDEX applies to INSTALLED and DISABLED, so it matches no field once the index is enabled. An index built on a property key created in the same management transaction is enabled immediately, so a script that creates an index and then registers it fails the second time it runs.

Change

The composite branch guards this a few lines earlier via isApplicableStatus. Worth noting that method never returns false — it throws — so the return null after it in the composite branch is unreachable, and the composite behaviour is "throw a descriptive error". This gives the mixed branch the equivalent check, and additionally reports the status of every field so it is clear which one blocked the action:

Update action [REGISTER_INDEX] cannot be invoked for index [nameMixed] because none of its fields has one of
the applicable statuses [INSTALLED, DISABLED]. The current status of each field is [name=ENABLED]

Two consequences worth your attention

The guard applies to every action, which is what makes a mixed index behave the way a composite index already does. Two of those are behaviour changes beyond the reported symptom, both of which I believe are improvements — but they are yours to judge:

  1. DISCARD_INDEX no longer clears the index backend before failing. Previously it called IndexSerializer.clearStore and then threw from setStatusVertex, so the documents were already gone while the status change had not happened.
  2. DROP_INDEX on a mixed index which is not DISCARDED is now rejected. This is the second problem you flagged in the issue as possibly deserving its own issue — schemaVertex.remove() used to run regardless, dropping the schema vertex and orphaning every document in the index backend. A composite index is already gated this way.

I also removed the !keySubset.isEmpty() check that REMOVE_STALE_ENTRIES made for itself, since the new guard runs earlier and makes it unreachable. If you would rather narrow the guard to only the status-changing actions, that check needs to come back.

Testing

MixedIndexUpdateStatusTest runs against the in-memory backend with the existing RecordingIndexProvider as the index backend. It covers the rejected case and asserts the message names the action, the index, the blocking status and the applicable statuses; plus two paths that must keep working, registering an INSTALLED index and disabling an ENABLED one.

I checked every existing caller of DISCARD_INDEX and DROP_INDEX in the test tree. The mixed-index sequences in JanusGraphIndexTest and ElasticsearchJanusGraphIndexTest all discard or mark-discarded before dropping, so they stay compatible; the sequences in JanusGraphTest are composite and relation indexes, which this does not touch. IndexSerializerTest still passes. Docker was unavailable to me, so I could not run the container-backed suites locally.


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

@batrived
batrived force-pushed the fix/4930-mixed-index-update-status-guard branch from e8f1099 to 7525f26 Compare August 13, 2026 17:10
@porunov
porunov requested a lite review from Copilot August 14, 2026 17:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes a failure mode when applying updateIndex actions to mixed indexes where no field matches the action’s applicable statuses, by rejecting early with a descriptive error (and adding regression tests).

Changes:

  • Add a precondition guard for mixed indexes to reject update actions that match no fields, including reporting per-field statuses.
  • Remove a now-redundant/late guard in the mixed REMOVE_STALE_ENTRIES path.
  • Add MixedIndexUpdateStatusTest covering the rejected case and two success paths (registering INSTALLED, disabling ENABLED).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
janusgraph-test/src/test/java/org/janusgraph/graphdb/database/management/MixedIndexUpdateStatusTest.java Adds regression tests ensuring a descriptive exception is thrown when an update action matches no mixed-index fields.
janusgraph-core/src/main/java/org/janusgraph/graphdb/database/management/ManagementSystem.java Adds an early guard for mixed-index updates when no fields match, and includes per-field status in the rejection message.

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

@porunov
porunov force-pushed the fix/4930-mixed-index-update-status-guard branch from 7525f26 to 89e3261 Compare September 21, 2026 00:07
@porunov

porunov commented Sep 21, 2026

Copy link
Copy Markdown
Member

Reviewed, rebased on current master (no conflicts) and force-pushed as a single commit. Copilot's one comment is applied — the inline comment now says the precondition carries no message at all, matching what you found while writing the test, rather than repeating the issue's guess about a RelationTypeVertex message.

On "no-op or clear error"

The issue asked for "either no-ops or reports a clear error, as it does for composite indexes", and I wanted to be sure which of those composite actually does before signing off on throwing. It throws: SchemaAction.isApplicableStatus never returns false, it raises IllegalArgumentException — which is why the return null after it in the composite branch is unreachable, as you noted. So throwing is the behaviour that matches, and the asymmetry the issue describes is genuine.

On the two behaviour changes

Both are improvements and I am happy to take them, but I checked each one rather than taking the reasoning on trust.

DISCARD_INDEX. The old order really was destructive-then-fail: IndexSerializer.clearStore(mixedIndex, ...) runs before setStatus, so on an index whose fields were all ENABLED the documents were deleted and then the call threw out of setStatusVertex, leaving the schema untouched. Rejecting before touching the index backend is strictly better.

DROP_INDEX. This is the one with the widest blast radius, so I enumerated every caller rather than sampling. JanusGraphBaseTest.dropIndex is the only helper and there are seven call sites: four in JanusGraphIndexTest (all mixed, all markIndexDiscarded or discardIndex first), one in ElasticsearchJanusGraphIndexTest (disableIndexdiscardIndexdropIndex), and two in JanusGraphTest which are composite and already gated. So nothing in the tree depended on the ungated behaviour, which matches what you reported.

On the third action this silently affects

Worth stating explicitly, since neither the issue nor the PR body mentions it: the guard also makes REINDEX fail fast when no field is in an applicable status. That is not a regression — IndexRepairJob.validateIndexStatus already rejects a mixed index with acceptableFields == 0, so the job would have failed anyway, just inside the worker after being submitted. The new behaviour is the same fail-fast rationale the REMOVE_STALE_ENTRIES check was added with, now applied uniformly, which is also why folding that check into the earlier one is right.

Verification

  • MixedIndexUpdateStatusTest 3/3.
  • BerkeleyLuceneTest 77/77 — the JanusGraphIndexTest mixed-index lifecycle suite. I checked the surefire report rather than just the count, and testDiscardAndDropRegisteredIndex, testDiscardManuallyAndDropRegisteredIndex, testDisableAndDiscardManuallyAndDropEnabledIndex, testWriteOnlyEnabledMixedIndex and testRemoveStaleEntriesMixedIndexActionApplicability all actually ran.
  • BerkeleyElasticsearchTest indexShouldNotExistAfterDeletion, indexShouldExistAfterCreation and testRemoveStaleEntriesMixedIndexActionApplicability 3/3 against a real Elasticsearch 9.5.4 container — the container gap you flagged in your own testing notes.
  • The relocated REMOVE_STALE_ENTRIES check keeps its contract: testRemoveStaleEntriesMixedIndexActionApplicability asserts IllegalArgumentException for an INSTALLED mixed index, and the earlier check raises the same type.
  • No test asserts on the old message text, so moving it costs nothing.

@porunov
porunov requested a balanced review from Copilot September 21, 2026 03:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 1 Medium severity · 1 Low severity

Open (2)
Resolved since last review (1)

@porunov
porunov force-pushed the fix/4930-mixed-index-update-status-guard branch from 89e3261 to 9a4883d Compare September 21, 2026 04:33
@porunov
porunov requested a balanced review from Copilot September 21, 2026 09:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
Resolved since last review (2)

@porunov
porunov force-pushed the fix/4930-mixed-index-update-status-guard branch from 9a4883d to 00e2d4e Compare September 21, 2026 09:51
@porunov
porunov requested a balanced review from Copilot September 21, 2026 10:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 2 Low severity

Open (2)
Resolved since last review (3)

@porunov
porunov force-pushed the fix/4930-mixed-index-update-status-guard branch from 00e2d4e to 6c8829e Compare September 21, 2026 11:11
@porunov
porunov requested a balanced review from Copilot September 21, 2026 11:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

Review effort: Lite
Findings: 3 Medium severity

Open (3)
Resolved since last review (2)

@porunov
porunov force-pushed the fix/4930-mixed-index-update-status-guard branch from 6c8829e to ca73124 Compare September 21, 2026 11:50
@porunov
porunov requested a balanced review from Copilot September 21, 2026 11:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

…ph#4930)

A mixed index keeps a SchemaStatus per field instead of one status on the index,
so updateIndex collects the fields whose status the action applies to. When no
field matched, the empty set was passed on to setStatus, which routed to
setStatusVertex, whose precondition can never hold for a mixed index. That
precondition carries no message, so the caller received a bare
IllegalArgumentException with nothing to act on.

Re-running an idempotent schema script is enough to reach this. REGISTER_INDEX
applies to INSTALLED and DISABLED, so it matches no field once the index is
enabled. An index built on a property key created in the same management
transaction is enabled immediately, so a script which creates an index and then
registers it fails the second time it runs.

The composite branch guards against this a few lines earlier with
isApplicableStatus, which throws a message naming the action and the status.
Give the mixed branch the equivalent check, and report the status of every field
so it is clear which one blocked the action. Render the applicable statuses in
the declaration order of the enum rather than through the iteration order of the
HashSet which holds them, so the message does not vary between runs.

Cover both of those with tests which fail without the guard: a discard of an
enabled index is rejected without the index backend having been cleared, and a
drop of an index which is not discarded is rejected with the index still present.
The sequence which must keep working, disable then discard then drop, is covered
too.

This makes a mixed index behave as a composite index already does for an action
which does not apply to it. It also means DISCARD_INDEX no longer clears the
index backend before failing, and DROP_INDEX on a mixed index which is not
DISCARDED is now rejected rather than dropping the schema vertex and orphaning
the documents.

The equivalent check that REMOVE_STALE_ENTRIES made for itself is now
unreachable, so remove it.

Signed-off-by: Balmukund Trivedi <btrivedipublic@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Oleksandr Porunov <alexandr.porunov@gmail.com>
Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>

@porunov porunov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@porunov porunov added this to the 1.2.0 milestone Sep 21, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

updateIndex on a mixed index fails an unrelated precondition when no key is in an applicable status

3 participants