PHOENIX-7996 Enable server-side maintenance of immutable indexes by default - #2611
Open
lokiore wants to merge 3 commits into
Open
PHOENIX-7996 Enable server-side maintenance of immutable indexes by default#2611lokiore wants to merge 3 commits into
lokiore wants to merge 3 commits into
Conversation
…efault Flip DEFAULT_SERVER_SIDE_IMMUTABLE_INDEXES_ENABLED to true so that immutable, global, non-transactional secondary indexes are maintained server-side by IndexRegionObserver (PHOENIX-7426) without an explicit opt-in. The client stops generating index mutations for these tables and instead ships the serialized IndexMaintainer, which the region server uses to build index updates exactly once. Immutable data tables that declare a ROW_TIMESTAMP column stay client-maintained regardless of the flag. Server-side maintenance stamps every data cell with the server batch timestamp, which would overwrite the user-supplied ROW_TIMESTAMP value and silently drop rows on ROW_TIMESTAMP range scans (the same reason a mutable ROW_TIMESTAMP table with an index is rejected via CANNOT_CREATE_INDEX_ON_MUTABLE_TABLE_WITH_ROWTIMESTAMP). The decision is centralized in IndexUtil.isServerSideImmutableIndexMaintenanceEnabled and applied at every data-table gate that reads the flag so the client and server agree on which side maintains a given table. Tests that assert client-side index mutation accounting pin the flag off at the driver level; the index end-to-end helpers derive their expectation from the live flag. Generated-by: Claude Code (Opus 4.8) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k covered columns on partial immutable upserts Two correctness fixes needed once server-side maintenance of immutable indexes is on by default: - IndexRegionObserver skipped the current-row read-back for immutable batches, so a partial upsert that omitted a covered column built the index from the partial mutation alone and dropped that column from the index while it survived in the data table. Force a read-back for immutable covered-global batches when any enabled mutation omits an indexed or covered column, resolved to on-disk qualifiers so full-row and single-cell upserts keep the fast path. - The immutable server-serialize filter matched IndexType.GLOBAL only, so an uncovered global immutable index whose storage scheme matched the data table was serialized by neither client nor server and went unmaintained. Match IndexUtil.isGlobalIndex so uncovered global indexes are serialized to the region server too. Generated-by: Claude Code (Opus 4.8) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed global indexes Server-side maintenance of an immutable global index skips the current-row read-back. A partial upsert that omits an indexed column then builds the index entry from the partial mutation alone. The covered case was already handled; an uncovered global index has the same hazard: the omitted indexed column is materialized as null, so a spurious null-keyed index entry is written while the data row keeps the earlier value. A point lookup self-heals through the index read-repair join-back, but a server-side aggregate over the index rows returns the wrong count. Generalize the read-back predicate to union the required on-disk columns of both covered and uncovered global maintainers, and fire the read-back for immutable batches carrying either kind of global index when a partial upsert omits one of those columns. Columns resolve to their on-disk qualifiers per the storage scheme, so full-row upserts and single-cell tables keep the fast path. Add GlobalIndexCheckerIT#testPartialRowUpdateForImmutableUncovered covering the scenario via the index path. Generated-by: Claude Code (Opus 4.8) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lokiore
marked this pull request as ready for review
September 1, 2026 23:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changes were proposed in this pull request?
This flips the client default
DEFAULT_SERVER_SIDE_IMMUTABLE_INDEXES_ENABLEDfromfalsetotrue. With the flag enabled, immutable, global, non-transactional secondary indexes are maintained server-side byIndexRegionObserver(added in PHOENIX-7426) rather than by the client. The client no longer generates index mutations for these tables; it ships the serializedIndexMaintainerand lets the region server build the index updates exactly once.To keep the flip safe, immutable data tables that declare a
ROW_TIMESTAMPcolumn continue to be maintained client-side regardless of the flag. The decision is centralized in a new helper,IndexUtil.isServerSideImmutableIndexMaintenanceEnabled(...), and applied at every data-table gate that reads the flag:IndexUtil.getClientMaintainedIndexesIndexMaintainer.maintainedLocalOrGlobalIndexesWithoutMatchingStorageScheme(theINDEX_UUIDgate)MutationState.filterIndexCheckerMutationsDeleteCompiler.isMaintainedOnClient(signature extended to take the data table so the guard resolvesROW_TIMESTAMPagainst the data table, not a projected or index table)IndexMetaDataCacheClient.setMetaDataOnMutations(the send-metadata gate)Routing all gates through the same helper keeps the client and server in agreement on which side maintains a given table; a disagreement would cause a
ROW_TIMESTAMPtable to be maintained on both sides.Enabling server-side maintenance by default also surfaced two paths that must be corrected so the server maintains every affected index correctly:
IndexRegionObserverskips the current-row read-back for immutable batches. A partial upsert that omits an indexed, covered, or index-WHERE column then builds the index entry from the partial mutation alone. For a covered index this drops the column, so it is lost from the index while it survives in the data table (silent divergence, the same class of hazard as PHOENIX-7961). For an uncovered index the omitted indexed column is materialized as null, so a spurious null-keyed index entry is written while the data row keeps the earlier value: a point lookup self-heals through the index read-repair join-back, but a server-side aggregate over the index rows returns the wrong result. The read-back gate now forces a read-back for immutable batches carrying a covered or uncovered global index when any enabled mutation omits one of that index's on-disk columns. Columns are resolved to their on-disk qualifiers per the data table storage scheme, so full-row upserts and single-cell tables keep the fast path with no read-back. The gate trades a bounded in-memory column scan (only on the immutable global-index write path) for avoiding a disk read-back on the common full-row-upsert case.IndexMaintainermatchedIndexType.GLOBALonly. An uncovered global immutable index whose storage scheme matched the data table was therefore serialized by neither the client (which returns no client-maintained indexes for these tables when the flag is on) nor the server (whose filter dropped it), so it went unmaintained. The filter now matchesIndexUtil.isGlobalIndex, which covers bothGLOBALandUNCOVERED_GLOBAL.Why are the changes needed?
PHOENIX-7426 added server-side maintenance of immutable-table indexes behind a flag that defaulted to off. Enabling it by default removes per-batch client index-mutation generation for immutable tables and lets
IndexRegionObserver(the default index path for years,DEFAULT_INDEX_REGION_OBSERVER_ENABLED=true) build the index updates, reducing client-side work and mutation payload.The
ROW_TIMESTAMPcarve-out is required for correctness. Server-side maintenance re-stamps every data cell — including theROW_TIMESTAMPcolumn — with the server batch timestamp, overwriting the user-suppliedROW_TIMESTAMPvalue.ROW_TIMESTAMPrange predicates push an HBase scanTimeRange, so re-stamped cells fall outside it and rows are silently dropped on range reads (SCN-based visibility breaks for the same reason). This is the same hazard behindCANNOT_CREATE_INDEX_ON_MUTABLE_TABLE_WITH_ROWTIMESTAMP, which already forbids the mutable variant; the immutable variant was safe only because it was client-maintained.The two additional fixes are required because the default flip is the point at which the server becomes responsible for these indexes: without the read-back, a partial immutable upsert would serve stale/missing covered values or a wrong-keyed uncovered entry from the index; without the broadened serialize filter, an uncovered global immutable index would receive no updates at all.
Does this PR introduce any user-facing change?
Yes. Immutable, global, non-transactional secondary indexes are now maintained server-side by default (previously client-side unless the flag was set explicitly).
Upgrade notes:
IndexRegionObserverpath, which is enabled by default; on a default cluster a new client talking to an already-upgraded (or default-configured) server loses no index data.phoenix.index.region.observer.enabled=false). Clusters still on that configuration must move off it before adopting this default.ROW_TIMESTAMPcolumn are unaffected — they remain client-maintained.phoenix.server.side.immutable.indexes.enabled=false.How was this patch tested?
RowTimestampIT— regression lock; asserts raw-scan cell timestamps equal the userROW_TIMESTAMPon both data and index tables for the immutable case. Passes with the guard.GlobalIndexCheckerIT#testPartialRowUpdateForImmutable— regression lock for the read-back fix on a ONE_CELL_PER_COLUMN immutable covered index; a full upsert is followed by a partial upsert that omits a covered column, and the test asserts the index read still returns the earlier value. Fails without the read-back fix (expected:<abcd> but was:<null>) and passes with it.GlobalIndexCheckerIT#testPartialRowUpdateForImmutableUncovered— regression lock for the read-back fix on a ONE_CELL_PER_COLUMN immutable uncovered index; a full upsert is followed by a partial upsert that omits the indexed column, and the test asserts (via the index path) that the row still resolves under its original key, that a count over the index is exactly 1, and thatIS NULLreturns nothing. Fails without the read-back fix (the count returns0) and passes with it.UncoveredGlobalImmutableNonTxIndexITandUncoveredGlobalImmutableNonTxIndex2IT— pass with the broadened serialize filter (uncovered global immutable indexes are now maintained).GlobalImmutableNonTxIndexIT(covered global) stays green, confirming no regression to the covered path.ServerSideImmutableIndexITandClientSideImmutableIndexIT— pass.PhoenixMetricsIT,PhoenixLoggingMetricsIT, andPhoenixTableLevelMetricsIT#testMetricsWithIndexUsage— pass; the flag is pinned off at the driver level for the assertions that account for client-side index mutations.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)