Skip to content

PHOENIX-7996 Enable server-side maintenance of immutable indexes by default - #2611

Open
lokiore wants to merge 3 commits into
apache:masterfrom
lokiore:enable-server-side-immutable-indexes-default
Open

PHOENIX-7996 Enable server-side maintenance of immutable indexes by default#2611
lokiore wants to merge 3 commits into
apache:masterfrom
lokiore:enable-server-side-immutable-indexes-default

Conversation

@lokiore

@lokiore lokiore commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This flips the client default DEFAULT_SERVER_SIDE_IMMUTABLE_INDEXES_ENABLED from false to true. With the flag enabled, immutable, global, non-transactional secondary indexes are maintained server-side by IndexRegionObserver (added in PHOENIX-7426) rather than by the client. The client no longer generates index mutations for these tables; it ships the serialized IndexMaintainer and lets the region server build the index updates exactly once.

To keep the flip safe, immutable data tables that declare a ROW_TIMESTAMP column 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.getClientMaintainedIndexes
  • IndexMaintainer.maintainedLocalOrGlobalIndexesWithoutMatchingStorageScheme (the INDEX_UUID gate)
  • MutationState.filterIndexCheckerMutations
  • DeleteCompiler.isMaintainedOnClient (signature extended to take the data table so the guard resolves ROW_TIMESTAMP against 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_TIMESTAMP table 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:

  • Partial upserts on immutable global indexes could lose or misbuild an index column. IndexRegionObserver skips 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.
  • Uncovered global immutable indexes with a matching storage scheme were maintained by nobody. The immutable server-serialize filter in IndexMaintainer matched IndexType.GLOBAL only. 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 matches IndexUtil.isGlobalIndex, which covers both GLOBAL and UNCOVERED_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_TIMESTAMP carve-out is required for correctness. Server-side maintenance re-stamps every data cell — including the ROW_TIMESTAMP column — with the server batch timestamp, overwriting the user-supplied ROW_TIMESTAMP value. ROW_TIMESTAMP range predicates push an HBase scan TimeRange, 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 behind CANNOT_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:

  • Upgrade region servers before clients. Server-side maintenance rides the generic IndexRegionObserver path, 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.
  • Silent index-data loss is only reachable on the deprecated legacy-Indexer configuration (phoenix.index.region.observer.enabled=false). Clusters still on that configuration must move off it before adopting this default.
  • Immutable tables with a ROW_TIMESTAMP column are unaffected — they remain client-maintained.
  • The previous behavior can be restored by setting phoenix.server.side.immutable.indexes.enabled=false.

How was this patch tested?

  • RowTimestampIT — regression lock; asserts raw-scan cell timestamps equal the user ROW_TIMESTAMP on 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 that IS NULL returns nothing. Fails without the read-back fix (the count returns 0) and passes with it.
  • UncoveredGlobalImmutableNonTxIndexIT and UncoveredGlobalImmutableNonTxIndex2IT — 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.
  • ServerSideImmutableIndexIT and ClientSideImmutableIndexIT — pass.
  • PhoenixMetricsIT, PhoenixLoggingMetricsIT, and PhoenixTableLevelMetricsIT#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)

lokesh-khurana and others added 3 commits August 27, 2026 17:01
…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
lokiore marked this pull request as ready for review September 1, 2026 23:19
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.

1 participant