Repository navigation
[fix][meta] Find the records written with a partition key in Oxia secondary-index scans - #26920
Conversation
…ondary-index scans OxiaMetadataStore.storeScanByIndex listed the index with client.list(..., UseIndex), which reads every shard, then fetched each listed key with storeGet(key, opts). Without a partition key in opts, that get goes to the shard of the record's key, but a record written with Option.PartitionKey lives on the shard of its partition key. On an Oxia namespace with more than one shard, the records whose partition key and key hash to different shards were listed, not found, and dropped. Every PIP-473 transaction record has a partition key and TxnMetadataStore scans them without one: on a 3-shard Oxia, listWritesBySegment returned 12 of 30 write records and listOpenByDeadlineRange 3 of 15 open transactions. Read the records in the scan instead: client.rangeScan(..., UseIndex) returns the value, version and key of each record from the shard that holds it. Skip the entries of records deleted after the scan read their index entry, which Oxia returns as not found, without a version or a value. A consumer exception stops the scan and fails it with that exception, as before. TxnMetadataStore.listOpsByTxn and scanAbortedTxns now pass the partition key their records share (the transaction, the segment), so these scans read one shard instead of all of them.
codelipenghui
left a comment
There was a problem hiding this comment.
The native indexed range scan looks like the right fix for missing partition-keyed records: returning values from the shards holding them avoids re-routing each primary key without its original partition key.
One compatibility question about the ordering statement in the description, based on head 1cc87bcfcb9d65746890caf8046a1ac1db81b7e2:
- ScanConsumer's Javadoc explicitly covers both
scanChildrenandscanByIndexand says: “The store invokes … onNext for each record (in key order).” - In Oxia client 0.9.5,
listwithout a partition key usesinternalListMultiShards, which combines the returned primary keys and callslist.sort(CompareWithSlash.INSTANCE). The previous Pulsar implementation chained its reads/callbacks sequentially, preserving that order among the records it actually returned. - The native range scan's multi-shard consumer forwards serialized arrivals without globally sorting them, so this changes observable ordering on that path.
This is narrower than saying every old scan guaranteed primary-key order: an explicit partition key takes the single-shard list path without that client-side sort, and the Javadoc does not distinguish primary-key order from secondary-index order.
Could we clarify the intended ordering contract and reconcile the Javadoc with the implementation? If unordered indexed scans are intended, please make that explicit and address compatibility with existing consumers; if ordering is required, it needs to be preserved. I am not suggesting unconditional buffering/sorting of all returned values without considering the memory and streaming-latency cost.
This is based on source inspection only; no tests were run, and no consumer regression has been reproduced.
…he filtered topic listing ScanConsumer said that a scan returns its records in key order. On an Oxia namespace with several shards, the scans pass on the records of each shard as they arrive: scanChildren since it was added, and scanByIndex now that it reads the records in the index range scan. Say that the records come in no particular order, in ScanConsumer and in MetadataStore.scanByIndex. No consumer of scanByIndex depends on the order: the transaction ones collect sets, maps or flags, or act on each record on its own. The order a user sees is the one of the property-filtered scalable topic listing of the admin API, which returned the records in the order of the scan. Sort it by path, the order of the unfiltered listing on every store.
codelipenghui
left a comment
There was a problem hiding this comment.
Reviewed head 358e23227196f5edebf798f200bf26fab71c231f.
The native indexed scan fixes the partition-key routing problem, and the updated ordering documentation plus caller-side topic-list sorting addresses my earlier concern. I am requesting changes for one introduced timeout/liveness regression, detailed inline.
Validation: all 158 focused tests passed (83 metadata and 75 broker tests). An isolated A/B probe using the actual pre-PR adapter reproduced the original omission (8/30 records before, 30/30 after). However, after stopping the initialized three-shard Oxia server with a configured one-second request timeout, the pre-PR adapter failed after 1,007 ms with one error callback, while this head remained pending after 5,006 ms with no terminal callback.
Please preserve a bounded retry deadline for index scans and add regression coverage for persistent retryable shard failures. No additional actionable findings were identified.
The Oxia client retries the range scan of a shard it can't reach with no deadline, and keeps to itself the failures of the attempts that got no response, so a scan of an unavailable shard never ended. list(), which the scan replaced, fails after the request timeout of the client. IndexScan fails the scan with a TimeoutException when it doesn't end within the request timeout of the client, the one it was built with so that the client config file can set it. The consumer and the future get exactly one terminal callback, and the scan stops at the next record the client passes on after that. The timeout is cancelled when the scan ends.
codelipenghui
left a comment
There was a problem hiding this comment.
Reviewed the follow-up at b1ee6cbba0dd333fbea9df175df6afb7915f8f10. The previous timeout finding is resolved: index scans now use the configured deadline, select exactly one terminal callback, and suppress late callbacks.
All 160 focused tests passed (85 metadata and 75 broker tests). An independent probe through the production factory/config-file path verified that a one-second timeout fails the outage scan after 1,002 ms with exactly one error callback; the healthy three-shard scan still returns all 30 records.
No actionable findings identified within the reviewed scope. Approving the code changes; required CI checks should pass before merging.
Motivation
With an Oxia metadata store whose namespace has more than one shard,
scanByIndexdrops the records written withOption.PartitionKey. Every PIP-473 transaction record has a partition key (its transaction or its segment), andTxnMetadataStorescans them through secondary indexes without one. So on a sharded Oxia, these all work from incomplete lists:The cause is in
OxiaMetadataStore.storeScanByIndex:client.list(from, to, UseIndex(...)), which reads every shard.storeGet(key, opts). Without a partition key inopts, the Oxia client sends that get to the shard of the record's key. A record written with a partition key lives on the shard of its partition key.MetadataStoreSecondaryIndexTestruns on a single-shard Oxia without partition keys, andOxiaPartitionKeyTest(3 shards) only reads records back with the partition key they were written with, so neither caught this.Example
On a 3-shard Oxia (
oxia/oxia:nightly-beta), on master:With the Oxia CLI on the same data, the index list returns all 30 keys. A
getwithout the partition key finds 8 records; with it, all 30.Through
TxnMetadataStore, with 30 transactions that each wrote and acked on one segment (15 open, 15 committed):listWritesBySegmentlistAcksBySegmentSubscriptionlistOpenByDeadlineRangelistFinalizedByStateAndTimeRangescanAbortedTxnsfound 9 of 30, and in one runlistOpsByTxnreturned none of the transaction's ops.Modifications
OxiaMetadataStore.storeScanByIndexreads the records in the index scan withclient.rangeScan(from, to, consumer, {UseIndex(indexName), [PartitionKey]}). Each shard returns the matching records it holds, with their key, value and version. The bounds are unchanged. The server has returned the primary key with index range-scan results since secondary indexes were added (Secondary indexes implementation oxia-db/oxia#544).GetResultwith an empty value and a zeroVersion. In a test that deleted indexed records while scanning them, 1,529 of the 16,753 entries the client returned were like this.getskipped them by returning nothing.onNextreturnsfalse, and the client then callsonCompleted), and the scan fails with that exception, as before.TxnMetadataStore.listOpsByTxnandscanAbortedTxnspass the partition key their records share (the transaction, the segment), so these scans read one shard instead of all of them.scanByIndexdoesn't promise an order, and no caller relies on one.Verifying this change
OxiaPartitionKeyTest(3 shards): a scan across 30 partition keys returns every record (8 to 13 of 30 before the change), and a scan scoped to one partition key returns that partition's records.OxiaMetadataStoreScanByIndexTest(mocked Oxia client): the scan passes the index and the partition key torangeScan, skips a not-found entry, and fails with a consumer's exception.TxnMetadataStoreShardedOxiaTest(3 shards): each of the six index queries returns the records of all 30 transactions. Without the store change, the four cross-partition queries fail and the two partition-keyed ones pass. Without either change, all six fail.MetadataStoreSecondaryIndexTest(60 tests, every provider),MetadataCacheSecondaryIndexTest,TxnMetadataStoreTest,MetadataTransactionBufferTest,MetadataPendingAckStoreTest,TransactionCoordinatorV5Test,ScalableTopicPropertyIndexTestandV5Transaction*pass.quickCheckpasses.Does this pull request potentially affect one of the following parts:
If the box was checked, please highlight the changes