feat(realtime): add primary-key in-memory support - #224
Conversation
6168759 to
6b9b215
Compare
8191180 to
3169bbf
Compare
wangyong9999
left a comment
There was a problem hiding this comment.
Two correctness issues found in the primary-key realtime path.
48a07b9 to
83c03c7
Compare
| PAIMON_RETURN_NOT_OK( | ||
| realtime_context_ | ||
| ->AdvanceMaterializedMaxSequenceNumber(partition_bucket_, last_sequence_number_) | ||
| .status()); |
There was a problem hiding this comment.
materialized_max_sequence_number seems to be used only to support writer handoff with uncommitted state retained in the same RealtimeContext. For failure recovery, the old context should be discarded, and the sequence number should be restored from the latest snapshot before replaying the data. Reusing this value would instead assign larger sequence numbers during replay. Could you confirm whether handoff with uncommitted state is a required use case? If not, I suggest removing this state and always restoring the sequence number from files.
There was a problem hiding this comment.
The materialized watermark is not for failure recovery: failure discards the context and restores from the snapshot. It supports successful sequential handoff, matching append realtime context/store reuse; primary-key mode must additionally continue the synthetic sequence. I personally prefer one RealtimeContext per writer with no handoff because it simplifies the design, but the current code retains state to match the existing append lifecycle.
zjw1111
left a comment
There was a problem hiding this comment.
Thanks for the extensive refactor. Besides the inline comments, could you also clean up the remaining style/reuse items before merge?
realtime_context_impl.cppstill uses string concatenation withstd::to_stringinstead offmt::format.primary_key_realtime_store.cppandprepared_key_value_reader.cppcontain rawnewoutside the documented private-constructor factory exception.realtime_primary_key_writer.husesstd::mapin its public signature without directly including<map>.
Could you also update the PR description to match the current implementation? It still mentions AppendRealtimeStoreCreateConfig, PrimaryKeyRealtimeStoreCreateConfig, and RealtimeStoreCreateConfig, which no longer exist, and still claims heap-based merging with constant query-reader cardinality even though the store now returns one reader per prepared batch.
| owner_->predicate_for_keys_, | ||
| data_file_path_factory)); | ||
| for (std::unique_ptr<KeyValueRecordReader>& reader : section_readers) { | ||
| readers->push_back(std::move(reader)); |
There was a problem hiding this comment.
Could we bound the realtime merge fan-in here? RealtimeTableScan groups the entire partition-bucket into one realtime split, and this loop flattens every section's sorted runs before combining them with all memory readers in one sort-merge reader. The loser tree advances every run during initialization, so the number of leaves and retained first batches grows with accumulated disk runs and memory batches; writer-local compaction is disabled on this path. One option is to merge each disk section first, concatenate the non-overlapping section readers into one disk run, and only then merge that run with memory readers. A documented hard fan-in limit would also prevent unbounded resource use.
There was a problem hiding this comment.
The disk side is fixed in 08a695c by composing the disk sections into one sorted disk reader before the final merge. The memory side is not fully fixed: fan-in remains unbounded by prepared-batch count, so many small writes increase final merge fan-in and retained first batches. This is a current performance/resource limitation rather than a correctness issue, and I am keeping this discussion open.
|
Additionally, you could also review the code to identify any unnecessary validations or overly defensive code generated by AI, and remove some of it where appropriate. |
|
Follow-up for review PRR_kwDOSj74F88AAAABK7352w and issue comment 5423886006: the style fixes are in 4781383, and the remaining raw new calls are only private-constructor Create factory exceptions. The PR description is now updated. Cleanup commit 824389e removes the duplicate private visible-offset check; real plugin/C Data boundary validations remain intentionally. |
Add typed primary-key store creation, an in-memory PK store, and a no-spill writer that materializes sealed mutations through MergeTreeWriter. Keep writer-local compaction disabled, preserve sequence progress across sequential writer handoff, and reject unsupported V1 table options.
Capture partition-bucket read views in realtime splits and merge PK memory readers with snapshot data by key range. Retain read views for reader lifetime, defer ticket consumption until vector reader construction succeeds, and apply predicates after PK deduplication.
Cover PK write and read, recovery, external compaction, supported concurrency, writer handoff, ticket lifecycle, plugin contracts, rolling files, and multi-partition and bucket restore.
824389e to
e448f7a
Compare
|
Thanks for streamlining the realtime test coverage. Could you make two follow-up cleanups before merge?
|
zjw1111
left a comment
There was a problem hiding this comment.
One test-scaffolding cleanup suggestion.
|
Follow-up to issue comment 5450741187: Both follow-ups are addressed. The remaining test-local transport schemas now use RealtimePrimaryKeyLayout::CreateSchema in a77b5f4; the tests provide only the value fields, while the layout helper supplies the transport fields. The intentional malformed-schema validation cases remain unchanged. I also synchronized the PR description with the current implementation and test scope. It now refers to RealtimePrimaryKeyLayout::CreateSchema / ValidateSchema, removes the obsolete fault-injection recovery coverage claim, keeps failure recovery as a caller contract, and reports the current totals of 39 focused core tests and 57 realtime integration tests. |
Purpose
Linked issue: #158
This PR extends the pluggable realtime read and write support introduced by #163 and the
RealtimeStoreAPI from #199 to fixed-bucket primary-key tables.Applications attach a
RealtimeContextto the existing file-store paths. ItsRealtimeStoreFactoryreceives aRealtimeStoreCreateRequestwith aRealtimeStoreModeand creates an append-only or primary-key store. The built-in primary-key store is storage-oriented: it retains written transport batches and returns one reader per stored write batch. Query views include both sealed and currently building batches. The framework owns offset and sequence preparation, sorting, visibility filtering, merge-on-read, and normal data-file writing.For primary-key reads, immutable memory read views are combined with the selected disk snapshot through
RealtimeSplit. The store applies theRealtimeQueryContext::read_schemaprojection to memory batches, including nested field-ID alignment within the current table schema. Reusing a store with a different transport schema is rejected, and realtime data evolution is not supported.For each primary-key partition-bucket with active memory, all selected disk splits are folded into one
RealtimeSplit; disk-only partition-buckets retain their ordinary disk splits. Overlapping runs are merged within each disk section, and the non-overlapping sections are concatenated into one sorted disk reader before the final disk-plus-memory merge-on-read. This keeps disk-side final merge fan-in bounded while ensuring that every disk run and memory mutation participates in one primary-key merge. Predicates that are unsafe before merge are evaluated after merge.Memory-side reader fan-in is not hard-bounded: every retained memory write batch contributes one reader to the final merge. Many small writes therefore increase final merge fan-in and the number of retained first batches. This is a current performance and resource limitation, not a correctness problem.
The main changes are:
RealtimeStoreimplementation;RealtimeStoreCreateRequest::mode;MergeTreeWritercan write;The current built-in primary-key implementation supports fixed-bucket tables with the deduplicate merge engine, full-row mutations, latest-snapshot recovery, concurrent readers, and synchronized writer operations. The supported lifecycle uses one active writer with its realtime context. Calls coordinating write, prepare-commit, commit, refresh, and reads may run concurrently as covered by the integration tests. Sequential writer handoff is supported after closing the prior writer. Reusing the same
RealtimeContextcontinues from retained in-memory progress, while recreating the context restores offsets and live-file sequence progress from the latest committed snapshot.Dynamic buckets, lookup or early merge-on-read, aggregation and partial-update merge engines, data evolution within an existing context/store, user sequence fields, read-optimized scans,
ignore_previous_files, custom write schemas, and recovery from a non-latest snapshot are not included.Writer-local compaction is force-disabled for realtime primary-key writers. User-provided compaction options are ignored,
num-sorted-run.stop-triggerbackpressure does not apply, and level-0 runs accumulate with each commit, so external compaction is required.The built-in primary-key store keeps realtime mutations entirely in memory and does not implement spill. Building and sealed batches remain retained until committed-offset refresh reclaims them, and immutable read views may keep reclaimed segments alive.
write-buffer-sizedoes not bound this usage. The publicRealtimeStorecontract still permits custom implementations to use their own spill strategy.Tests
Added unit coverage for:
MergeTreeWriterwith multiple readers, overlapping keys, and duplicate-key deduplication; andAdded integration coverage for:
Integration tests write real ORC data files through the normal
FileStoreWrite,PrepareCommitWithProgress, andCommitWithProgresspaths, then read the data back from the committed snapshot without aRealtimeContext.Validated with 39 focused core tests and all 57 realtime integration tests.
API and Format
This PR reuses the public realtime file-store APIs introduced by #163 and #199, including
RealtimeContext,RealtimeWriteBatch,PrepareCommitWithProgress,CommitWithProgress, realtime split planning, and snapshot refresh. It does not add a separate primary-key table API.Factories implement
RealtimeStoreFactory::Create(RealtimeStoreCreateRequest&&). The request carries the write or primary-key transport schema, options, memory pool, statistics mode, andRealtimeStoreMode; factories dispatch on the mode. For primary-key mode, the framework supplies batches with the primary-key transport schema and assigns offsets and sequence numbers before calling the store.statistics_modeconfigures the built-in append-only store; the built-in primary-key store does not collect or use in-memory min/max statistics.The store returns raw transport-batch readers. For primary-key queries,
offset_beginis ignored by the store. The store applies the requested transport projection, while the framework applies offset visibility, merge-on-read, predicates, and the final user projection.RealtimePrimaryKeyLayout::CreateSchemaandRealtimePrimaryKeyLayout::ValidateSchema, together with the layout indexes, define the transport schema used by write, commit, and query paths.No new data-file or commit-message format is introduced. Primary-key realtime writes produce normal merge-tree data files and commit messages. Realtime offsets continue to use the versioned snapshot metadata introduced by #163; they are framework-assigned progress identifiers, not primary-key sequence numbers.
Paimon serializes
WriteandSealForCommitfor each store. Existing immutable read views remain valid across later writes, seals, refresh, and committed-offset reclamation. Realtime split tickets remain process-local and single-success-use as defined by #199.CommitWithProgressfailures may be retried with the same arguments as documented by its public API. After a write or prepare-commit failure whose in-memory effects are unknown, the caller must discard the writer and realtime context, recreate them from the latest committed snapshot, and replay its external WAL. Refresh failures caused by removed or backward committed progress likewise require recreation; incomplete reclamation may be retried with the same snapshot. Overwrite, truncate, partition drop, rollback, and other progress-resetting operations likewise require coordinated recreation before writes continue.Existing non-realtime tables and append-realtime tables retain their previous execution paths.
Documentation
The public headers document store creation and mode dispatch, the primary-key transport schema, offset and sequence separation, read-schema projection, ownership, concurrency, and immutable-view behavior. The limitations and failure-recovery contract above describe the supported current implementation.
Generative AI tooling
Codex (GPT-5) was used for implementation, refactoring, tests, and PR text. Claude Code (Claude Opus 4.8) was used for review.