zjw1111 commented on code in PR #213:
URL: https://github.com/apache/paimon-cpp/pull/213#discussion_r3813342667


##########
src/paimon/core/operation/file_store_commit_impl.cpp:
##########
@@ -929,6 +929,19 @@ Result<int64_t> FileStoreCommitImpl::CommitWithProgress(
         commit_messages.push_back(realtime_commit.commit_message);
     }
 
+    PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
+                           snapshot_manager_->LatestSnapshot());
+    PAIMON_ASSIGN_OR_RAISE(RealtimeOffsetMap committed_offsets,
+                           
RealtimeCommitProperties::ReadOffsets(latest_snapshot, fs_));
+    PAIMON_ASSIGN_OR_RAISE(bool ranges_committed, 
RealtimeCommitProperties::AreRangesCommitted(

Review Comment:
   Question on the idempotency model here, not a change request.
   
   This short-circuit decides "already committed" purely from realtime 
offset-range coverage. It does not look at `(commitUser, commitIdentifier)`, 
and it does not run `CheckFilesExistence`. Both differ from the non-realtime 
path in this same file: `FilterAndCommit` calls `CheckFilesExistence` and 
`FilterCommitted`, and `FilterCommitted` resolves the committed identifier via 
`LatestSnapshotOfUser(commit_user_)` — matching Java's 
`FileStoreCommitImpl.filterCommitted`. `CommitWithProgress` -> `Commit` -> 
`TryCommit` bypasses both.
   
   Two consequences I want to check are intentional:
   
   1. Two writers sharing the same offset ranges are indistinguishable here. If 
a zombie writer replays ranges that the current writer already committed, this 
returns `latest_snapshot->Id()` and silently drops `commit_messages`, whereas 
before this patch `RealtimeCommitProperties::Build` would have rejected the 
commit as non-contiguous. Coverage of the offset range is treated as proof that 
*this* writer's files are already in the snapshot, which only holds if offsets 
are globally owned by one writer.
   2. Without `CheckFilesExistence`, a retry after partial cleanup can be 
accepted on offset coverage alone even if the referenced data files are gone.
   
   Is offset coverage intended to be the sole idempotency key for realtime 
commits — i.e. offset ownership is guaranteed single-writer by contract above 
this layer — or is `(commitUser, commitIdentifier)` + `CheckFilesExistence` 
still expected to be layered in later? The surviving `TODO(xinyu.lxy): Support 
failure recovery and idempotent retry for real-time commits.` in `TryCommit` 
suggests the latter, but this block reads like the former.



##########
src/paimon/core/realtime/arrow_realtime_store.cpp:
##########
@@ -264,16 +389,23 @@ Status ArrowRealtimeStore::Write(RealtimeWriteBatch&& 
write_batch) {
     }
     std::shared_ptr<arrow::StructArray> struct_array =
         checked_pointer_cast<arrow::StructArray>(data);
+    PAIMON_ASSIGN_OR_RAISE(std::optional<BatchStatistics> statistics,

Review Comment:
   Question on the intended scope of the "optional pruning aid" contract, not a 
change request.
   
   `MayMatch` states it explicitly:
   
   ```cpp
   // Statistics are only an optional pruning aid. An unsupported predicate or 
incomplete
   // statistic must retain the batch to avoid false negatives.
   return !result.ok() || result.value();
   ```
   
   and the read path is tested for it 
(`TestMissingStatisticsRetainsNonMatchingBatch` tolerates `stored.min_values == 
nullptr`).
   
   The write path here does the opposite: `CollectStatistics` failure 
propagates out of `Write` and fails the write. `CollectStatistics` depends on 
`arrow::compute::MinMax` and `arrow::StructArray::Make`, so any 
type/kernel/allocation failure there turns into a write failure. Net effect: 
`realtime.store.stats-mode=full` introduces a write failure mode that `none` 
does not have, for data that is otherwise perfectly writable — statistics are 
optional for pruning but mandatory for ingestion.
   
   Is the "optional" contract meant to apply only to reads, with statistics 
collection being a hard requirement on write? Or should a failure here degrade 
to "no statistics for this batch" the way a missing statistic already degrades 
on the read side?



##########
src/paimon/core/operation/expire_snapshots.cpp:
##########
@@ -157,8 +158,22 @@ Result<int32_t> ExpireSnapshots::ExpireUntil(int64_t 
earliest_snapshot_id,
     std::vector<Snapshot> retained_snapshots;
     PAIMON_ASSIGN_OR_RAISE(Snapshot snapshot, 
snapshot_manager_->LoadSnapshot(end_exclusive_id));
     retained_snapshots.push_back(snapshot);
+    std::set<std::string> retained_offset_files;
+    PAIMON_ASSIGN_OR_RAISE(std::vector<Snapshot> all_snapshots,

Review Comment:
   Perf point worth confirming — flagging only, no change requested.
   
   `GetAllSnapshots()` is now called unconditionally on every expire round, 
purely to build `retained_offset_files`. Before this patch the retained side of 
`ExpireUntil` only did a point `LoadSnapshot(end_exclusive_id)`, so this adds a 
full snapshot-directory listing plus one snapshot read per retained snapshot, 
i.e. O(snapshot count) extra IO per expire round. On object storage that is a 
listing round-trip plus N GETs, and it happens even for tables that never 
produced a `.offsets` file at all — the common non-realtime case pays the full 
cost to build an empty set.
   
   Please confirm this is acceptable for tables with a large retained snapshot 
count, or that you have measured it and it is noise relative to the manifest 
work already done in this loop.



##########
src/paimon/core/operation/file_store_commit_impl.cpp:
##########
@@ -929,6 +929,19 @@ Result<int64_t> FileStoreCommitImpl::CommitWithProgress(
         commit_messages.push_back(realtime_commit.commit_message);
     }
 
+    PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
+                           snapshot_manager_->LatestSnapshot());

Review Comment:
   Perf point worth confirming — flagging only, no change requested.
   
   `LatestSnapshot()` here (line 933) and `ReadOffsets()` (line 935) are both 
repeated further down the same commit path: `TryCommitOnce` does 
`snapshot_manager_->LatestSnapshot()` again at line 978, and 
`RealtimeCommitProperties::Build` re-reads the offsets file. So a single 
realtime commit now does two latest-snapshot resolutions and two `.offsets` 
reads, where the offsets read is a metadata-file GET on object storage.
   
   That is on the hot path for every realtime commit, and since `Commit` is 
invoked with `/*retry_on_conflict=*/false` (line 949) there is no retry loop to 
amortize it against.
   
   Please confirm the duplication is intentional (e.g. you want the coverage 
check against a snapshot read strictly before the commit attempt, independent 
of what `TryCommit` observes) rather than an accidental double read.



##########
src/paimon/core/realtime/arrow_realtime_store_test.cpp:
##########
@@ -86,6 +94,21 @@ class ArrowRealtimeStoreTest : public testing::Test {
         return c_schema;
     }
 
+    std::vector<int64_t> ReadIds(const BatchReader::ReadBatchWithBitmap& 
batch) const {
+        std::shared_ptr<arrow::Array> array =
+            arrow::ImportArray(batch.first.first.get(), 
batch.first.second.get()).ValueOrDie();
+        std::shared_ptr<arrow::StructArray> struct_array =
+            std::static_pointer_cast<arrow::StructArray>(array);

Review Comment:
   `docs/code-style.md` requires `checked_pointer_cast<T>` instead of 
`std::static_pointer_cast<T>`, and the example given there is literally this 
cast:
   
   ```cpp
   // Good
   auto struct_array = checked_pointer_cast<arrow::StructArray>(array);
   ```
   
   Applies to line 103 as well. Please switch both to `checked_pointer_cast`.



##########
src/paimon/core/realtime/arrow_realtime_store_test.cpp:
##########
@@ -86,6 +94,21 @@ class ArrowRealtimeStoreTest : public testing::Test {
         return c_schema;
     }
 
+    std::vector<int64_t> ReadIds(const BatchReader::ReadBatchWithBitmap& 
batch) const {
+        std::shared_ptr<arrow::Array> array =
+            arrow::ImportArray(batch.first.first.get(), 
batch.first.second.get()).ValueOrDie();
+        std::shared_ptr<arrow::StructArray> struct_array =
+            std::static_pointer_cast<arrow::StructArray>(array);
+        std::shared_ptr<arrow::Int64Array> ids =
+            
std::static_pointer_cast<arrow::Int64Array>(struct_array->field(/*id=*/1));

Review Comment:
   Nit: the parameter of `arrow::StructArray::field` is named `pos`, not `id`. 
`.clang-tidy` enables `bugprone-argument-comment`, so this mismatched comment 
is a lint violation. Please use `/*pos=*/1`.



##########
include/paimon/realtime/realtime_store.h:
##########
@@ -40,6 +40,14 @@ namespace paimon {
 class MemoryPool;
 class Predicate;
 
+/// Statistics collected by a `RealtimeStore` for query predicate pushdown.
+enum class StatisticsMode {

Review Comment:
   Question on the intended semantics of this enum name.
   
   `StatisticsMode` is declared in top-level `namespace paimon` (and 
forward-declared in `core_options.h`), with a fully generic name. But 
everything about it is realtime-specific: it lives in `realtime_store.h`, the 
doc comment says "collected by a `RealtimeStore`", the option is 
`realtime.store.stats-mode`, and the accessor is 
`GetRealtimeStoreStatisticsMode()`.
   
   The values are also a strict subset of Java's `metadata.stats-mode`, which 
is `none | counts | truncate(n) | full` with default `truncate(16)` 
(`CoreOptions.java`); Rust `core_options.rs` mirrors that set. Here it is `NONE 
| FULL` only, and `FULL` does not truncate STRING/BINARY min/max.
   
   So: is `StatisticsMode` meant to eventually carry the same semantics as 
Java's `metadata.stats-mode` (in which case the missing `counts`/`truncate` 
values and the realtime-only header placement are staging steps), or is it 
deliberately a realtime-private concept that just happens to share the name? 
Judging by the name alone a reader will assume the former; judging by the 
implementation it is the latter. Worth pinning down now, since the name is 
already exposed at `paimon` scope and hard to walk back later.



##########
src/paimon/core/core_options.h:
##########
@@ -107,6 +108,8 @@ class PAIMON_EXPORT CoreOptions {
     std::optional<int64_t> GetScanSnapshotId() const;
     std::optional<int64_t> GetScanTimestampMillis() const;
     int64_t GetRealtimeReadViewTtlMillis() const;
+    /// Returns the statistics mode used by real-time memory indexers.

Review Comment:
   "memory indexers" is stale terminology. The concept is `RealtimeStore` now — 
the option is `realtime.store.stats-mode`, the accessor is 
`GetRealtimeStoreStatisticsMode`, the enum doc in `realtime_store.h` says 
"collected by a `RealtimeStore`", and this PR itself renames indexer -> store 
elsewhere (e.g. `TestReusesIndexer...` -> `TestReusesStore...` in 
`realtime_context_test.cpp`).
   
   Suggest: `/// Returns the statistics mode used by real-time stores.`



##########
src/paimon/core/operation/expire_snapshots.cpp:
##########
@@ -169,10 +184,22 @@ Result<int32_t> ExpireSnapshots::ExpireUntil(int64_t 
earliest_snapshot_id,
         PAIMON_ASSIGN_OR_RAISE(Snapshot snapshot, 
snapshot_manager_->LoadSnapshot(id));
         PAIMON_RETURN_NOT_OK(CleanUnusedManifests(snapshot.BaseManifestList(), 
skipping_sets));
         
PAIMON_RETURN_NOT_OK(CleanUnusedManifests(snapshot.DeltaManifestList(), 
skipping_sets));
+        std::optional<std::string> offsets_path =
+            RealtimeCommitProperties::GetOffsetsPath(snapshot);
+        if (offsets_path) {
+            expired_offset_files.insert(offsets_path.value());
+        }
         auto status = fs_->Delete(snapshot_manager_->SnapshotPath(id));
         // delete quietly will ignore any status error
         (void)status;
     }
+    for (const std::string& offsets_path : expired_offset_files) {
+        if (retained_offset_files.count(offsets_path) == 0) {

Review Comment:
   This `retained_offset_files` guard is the one thing preventing a live 
`.offsets` file from being deleted, and nothing in the test suite exercises the 
case it exists for.
   
   Sharing happens because `RealtimeCommitProperties::Build` forward-copies 
`kOffsetsKey` when `realtime_ranges.empty()`, so a non-realtime snapshot 
(compaction being the obvious one) inherits the `.offsets` path of the realtime 
snapshot before it. Expiring the realtime snapshot then puts that path into 
`expired_offset_files` while a retained snapshot still references it — exactly 
what the `count(...) == 0` check must catch.
   
   `TestRealtimeOffsetFileLifecycle` does not reach that state: it uses two 
distinct `.offsets` paths and never commits a compaction. Across 
`test/inte/realtime_write_inte_test.cpp`, `ExpireSnapshots()` only appears in 
that one test and `CompactAndCommit` only in a different one, so no test ever 
has an expiring and a retained snapshot pointing at the same `.offsets` file. 
If the guard regresses, the failure is silent data loss — the offsets file is 
gone and the realtime data referenced by the retained snapshot becomes 
unreadable — and CI stays green.
   
   Please add a test: realtime commit -> compaction commit (inherits the same 
`.offsets` via the empty-ranges forward-copy) -> expire the realtime snapshot 
-> assert the `.offsets` file still exists and `ReadOffsets` on the retained 
snapshot still returns the committed ranges.



-- 
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]

Reply via email to