sanjeet006py opened a new pull request, #2574:
URL: https://github.com/apache/phoenix/pull/2574
<!--
Thanks for sending a pull request! Here are some tips for you:
1. If this is your first time, please read our contributor guidelines:
https://phoenix.apache.org/contributing.html
2. Ensure you have added or run the appropriate tests for your PR.
3. If the PR is unfinished, add '[WIP]' in your PR title, e.g., '[WIP]
PHOENIX-XXXX Your PR title ...'.
4. Be sure to keep the PR description updated to reflect all changes.
5. Please write your PR title to summarize what this PR proposes, and
prefix it with the JIRA id, e.g. 'PHOENIX-XXXX Your PR title ...'.
6. If possible, provide a concise example to reproduce the issue for a
faster review.
-->
### What changes were proposed in this pull request?
<!--
Please clarify what changes you are proposing. The purpose of this section
is to outline the changes and how this PR fixes the issue.
If possible, please consider writing useful notes for better and faster
reviews in your PR. See the examples below.
1. If you refactor some codes with changing classes, showing the class
hierarchy will help reviewers.
2. If you fix some SQL features, you can provide some references of other
DBMSes.
3. If there is design documentation, please add the link.
4. If there is a discussion in the mailing list, please add the link.
-->
This PR makes Phoenix's internal current-row read during secondary-index
maintenance TTL-mask exactly like an ordinary client read, so a
data table and its secondary index stay consistent after TTL expiry.
Index maintenance in IndexRegionObserver.preBatchMutateWithExceptions
reads the current on-disk row via getCurrentRowStates to rebuild
the correct index entry. That read was opened directly through
region.getScanner(scan), which bypasses the postScannerOpen coprocessor
hook — the only place a scan is normally wrapped in TTLRegionScanner. As a
result the internal read was not TTL-masked, so on a "partial
touch" upsert (an UPSERT that does not re-write any index-referenced
column) the index was rebuilt from
logically-expired-but-not-yet-compacted cells, while the data table
correctly expired them on read.
Changes:
1. Client side — ScanUtil.annotateMutationWithLiteralTTL (called from
MutationState) threads onto each mutation exactly what the client
read path sets as scan attributes: the empty-column CF/CQ (unconditionally
for any literal-TTL table/view), a view's compiled literal
TTL as the _TTL attribute (base tables rely on the CF-descriptor TTL), and
IS_STRICT_TTL=false only when the table/view is non-strict.
2. Server side — new ServerScanUtil (package
org.apache.phoenix.coprocessor, module phoenix-core-server) reproduces the
client read path
server-side: setInternalScanAttributes /
setInternalScanAttributesForPaging set the same empty-column/TTL/strict/paging
attributes, and
openRegionScanner wraps the scan in TTLRegionScanner + PagingRegionScanner
exactly as postScannerOpen does. IndexRegionObserver
captures the client-threaded attributes into the batch context
(extractLiteralTTLForInternalScan) and passes them into
getCurrentRowStates.
3. Anchor the masking clock at batchTimestamp — batchTimestamp is computed
before the current-row read, and the internal scan sets
scan.setTimeRange(0, batchTimestamp) so TTLRegionScanner's "current time"
equals the exact timestamp at which the index is rebuilt,
closing a sub-millisecond boundary window between the scan-open wall clock
and batchTimestamp. The half-open range drops nothing
current, since every pre-existing locked-row cell was written at ts <
batchTimestamp.
4. Companion correctness fix — moving getBatchTimestamp earlier means a
registered set could be structurally mutated outside its
synchronized block; batchesWithLastTimestamp now registers a defensive
TreeSet snapshot to avoid a ConcurrentModificationException,
staying conservative for the sleep/timestamp invariant (at most an extra
sleep, never a missed one).
Production files touched: IndexRegionObserver, ScanUtil, new
ServerScanUtil, MutationState, and a doc-comment update in MetaDataClient.
### Why are the changes needed?
<!--
Please clarify why the changes are needed. For instance,
1. If you propose a new API, clarify the use case for a new API.
2. If you fix a bug, you can clarify why it is a bug.
-->
This is a silent data-integrity/correctness bug. On a table or view with a
literal TTL and a secondary index, after TTL expiry the
secondary index can return a column value the data table no longer returns
— the two diverge with no error raised. The divergence
surfaces after logical TTL expiry and before major compaction physically
purges the expired cells.
The root cause is that the internal current-row read bypassed
TTLRegionScanner, so index rebuild saw expired cells the data-side read
masks. The affected paths are: secondary indexes (global covered, global
uncovered, and immutable indexes whose data/index storage
schemes differ) and the no-index current-row reads on a literal-TTL table
(atomic / ON DUPLICATE KEY upserts, returnResult upserts, and
row deletes). Conditional-TTL, non-TTL, and non-strict-TTL tables are
unaffected — masking is a no-op there and the read is
byte-identical to before.
### Does this PR introduce _any_ user-facing change?
<!--
Note that it means *any* user-facing change including all aspects such as
new features, bug fixes, or other behavior changes. Documentation-only updates
are not considered user-facing changes.
If yes, please clarify the previous behavior and the change this PR proposes
- provide the console output, description and/or an example to show the
behavior difference if possible.
If possible, please also clarify if this is a user-facing change compared to
the released Phoenix versions or within the unreleased branches such as master.
If no, write 'No'.
-->
Yes — a bug fix (behavior change) relative to master and released versions,
for tables/views with a literal TTL and a secondary index.
- Before: on a partial-touch upsert near/after the TTL boundary, the
secondary index could return a value that the data table no longer
returned (index resurrected a logically-expired value); data and index
disagreed.
- After: the internal current-row read is TTL-masked identically to a
client read and anchored at batchTimestamp, so the index is
rebuilt from the same masked state the data table exposes, and the two
stay consistent.
No API, syntax, or configuration change. No new config flag is introduced.
### How was this patch tested?
<!--
If tests were added, say they were added here. Please make sure to add some
test cases that check the changes thoroughly including negative and positive
cases if possible.
If it was tested in a way different from regular unit tests, please clarify
how you tested step by step, ideally copy and paste-able, so that other
reviewers can test and check, and descendants can verify in the future.
If tests were not added, please describe why they were not added and/or why
it was difficult to add.
-->
New integration test IndexDataTableSyncIT (parameterized over column-encoded
on/off), covering:
- base-table covered-column re-sync;
- uncovered-index indexed-column re-sync;
- view covered-column re-sync;
- non-strict table is not masked;
- no-index atomic (ON DUPLICATE KEY) upsert masks an expired row;
- UPSERT ... SELECT and UPSERT ... VALUES touches near the TTL boundary
keep data and index consistent;
- an immutable index with a differing storage scheme;
- a concurrency regression
(testConcurrentMajorCompactionDuringIndexWriteKeepsDataAndIndexConsistent)
where a data-table major
compaction runs during a slow index write — after the current-row read and
after the data locks are released, but before the index write
completes — and data/index must remain consistent.
### Was this patch authored or co-authored using generative AI tooling?
<!--
If generative AI tooling has been used in the process of authoring this
patch, please include the
phrase: 'Generated-by: ' followed by the name of the tool and its version.
If no, write 'No'.
Please refer to the [ASF Generative Tooling
Guidance](https://www.apache.org/legal/generative-tooling.html) for details.
-->
Generated-by: Claude Code (Anthropic Claude Opus 4.8)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]