blackmwk commented on code in PR #2620: URL: https://github.com/apache/iceberg-rust/pull/2620#discussion_r4034191748
########## docs/rfcs/0003_stateful_transaction.md: ########## @@ -0,0 +1,418 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> + +# RFC: Stateful Transactions and Snapshot Production + +**Status:** Draft +**Target:** Apache Iceberg Rust (`iceberg-rust`) + +## 1. Motivation and Scope + +Replaying transaction actions after a catalog conflict is sufficient for simple +metadata updates, but snapshot-producing actions benefit from retaining work +across attempts. FastAppend and merging operations such as RowDelta need stable +execution identity, reusable metadata processing, and ownership of generated +files that may require cleanup. + +This RFC establishes a stateful action model and a shared foundation for snapshot +production. The transaction owns catalog refresh and replay; each action execution +owns retry-persistent state; snapshot-producing actions use persistent producers +as that state. + +The proposal fixes lifetimes, ownership, the core action interface, and the +responsibilities of snapshot producers. It does not prescribe cache layouts, +conflict-validation predicates, or the complete semantics of each operation. +Those can be implemented incrementally within these boundaries. + +## 2. Transaction and Action State Model + +### 2.1 Three Lifetimes + +| Lifetime | Responsibility | +| --- | --- | +| Transaction | Catalog refresh, retry policy, action ordering, replay, and the terminal result | +| Action execution | Immutable operation intent and exclusively owned retry-persistent state | +| Attempt | Execution against the current transaction-local table and construction of that attempt's result | + +An action's intent does not change during replay. Its state survives attempts of +one logical execution, while values derived from a particular base are local to +an attempt or reused only under an appropriate validity check. + +### 2.2 Stateful Action Interface + +Each action declares an associated state type. The following interface captures +the lifecycle; the heterogeneous storage adapters are implementation details. + +```rust +#[async_trait] +pub(crate) trait TransactionAction: Send + Sync + 'static { + type State: Send + Sync + 'static; + + /// Fresh state for one logical execution; infallible and table-independent. + fn new_state(&self) -> Self::State; + + /// One attempt against the current transaction-local table. + async fn commit( + &self, + state: &mut Self::State, + table: &Table, + ) -> Result<ActionCommit>; + + /// Best-effort cleanup after the transaction reaches a terminal result. + async fn finish_commit( + &self, + _state: &mut Self::State, + _table: &Table, + _status: CommitStatus, + ) {} +} +``` + +The action is borrowed immutably, its execution state mutably, and the table per +attempt. Stateless actions use `State = ()`. State is created when an action is +added to a transaction. Table-dependent initialization happens during execution; +once resolved, logical snapshot identity remains stable across retries. + +`CommitStatus` describes terminal cleanup safety (§6). Neither the action nor its +state needs to control the transaction's catalog retry loop. + +The action interface and its storage machinery remain `pub(crate)`: external +crates cannot define transaction actions, so the erasure and adapter details +are internal rather than public API commitments. + +### 2.3 Action Entries and Cloning + +A transaction stores one entry per action, pairing immutable intent with its +execution state. Conceptually: + +```rust +struct ActionEntry<A: TransactionAction> { + action: Arc<A>, + state: A::State, +} +``` + +The transaction must preserve that pairing when storing heterogeneous entries. +A typed entry erased behind an adapter and separately erased action/state values +with a checked pairing are both acceptable implementations. + +Retry reuses the same entry state. Cloning a transaction creates a new execution +of the action plan: it may share immutable actions, but creates fresh state via +`new_state()`. Mutable retry state and generated execution identity are not shared +between transaction executions. + +## 3. Retry and Replay Workflow + +`Transaction::commit` owns refresh, retry, and replay. Each attempt starts from a +catalog base, executes actions in application order, and submits their combined +updates and requirements as one `TableCommit`. + +```mermaid +flowchart TD + Start["Begin transaction commit"] --> Refresh["Load latest catalog base"] + Refresh --> Init["Initialize attempt-local table and update accumulators"] + Init --> Action["Execute next action with retained state"] + Action --> Apply["Apply ActionCommit locally and accumulate changes"] + Apply --> More{"More actions?"} + More -->|Yes| Action + More -->|No| Submit["Build TableCommit and submit to catalog"] + Submit -->|Success| Committed["Committed"] + Submit -->|Retryable conflict and budget remains| Retry["Retain state and artifacts; back off"] + Retry --> Refresh + Submit -->|Definitive failure; no retry| Failed["Failed"] + Submit -->|Indeterminate result| Unknown["Unknown"] + Action -->|Definitive execution failure| Failed + Apply -->|Definitive local apply failure| Failed + Committed --> Finish["Call finish_commit on every entry"] + Failed --> Finish + Unknown --> Finish +``` + +Later actions observe earlier actions' transaction-local results, even though +none of those results has yet been committed to the catalog. After a conflict, +the next attempt rebuilds the local table from the refreshed base and replays +the complete action sequence. It does not continue accumulating changes on the +previous attempt's local result. + +The diagram omits refresh errors and other error branches for clarity. The +transaction/catalog layer determines retryability and whether a failed request +definitively did not commit. An unresolved catalog outcome is `Unknown`; it must +not be treated as a known conflict or definite failure. + +Replaying an action does not require repeating all of its I/O or metadata +processing. The same persistent state is passed into each attempt, allowing the +action to reuse valid work while producing a new result for the current base. + +## 4. Persistent Snapshot Producers + +### 4.1 Producers as Action State + +Snapshot-producing actions use persistent producers as their associated state: + +| Producer | Role | Initial consumer | +| --- | --- | --- | +| `SimpleSnapshotProducer` | Append-shaped snapshot production | `FastAppendAction` | +| `MergingSnapshotProducer` | Snapshot production involving data/delete file additions and removals | `RowDeltaAction` | + +An action expresses operation intent and selects the checks required by its +semantics and configuration. The producer supplies reusable validation and +metadata-processing capabilities, retains work across retries, and constructs +the snapshot result for an attempt. + +The producers are independent concrete types, not an inheritance hierarchy. +`MergingSnapshotProducer` does not contain `SimpleSnapshotProducer`. Each producer +owns the persistent state needed for its responsibilities; the transaction does +not need to know that an action's state is a producer. + +### 4.2 Shared Snapshot Production + +Common mechanics are shared through stateless helpers: snapshot-ID generation, +metadata path generation, summary construction, manifest-list writing, and +snapshot/update/requirement assembly. These helpers operate on explicit inputs +and do not own retry state or select operation policy. + +This replaces the attempt-scoped producer and generic hook-trait pipeline with +concrete producers and shared functions. Identity, path allocation, reuse, and +artifact ownership remain producer responsibilities. Whether common persistent +fields are extracted into a shared struct is an implementation choice. + +### 4.3 From Action Intent to a Snapshot + +Snapshot production has three logical stages. These describe responsibilities, +not a requirement to put all stages into one `apply()` method. + +1. **The action selects and invokes validation.** For example, a RowDelta action + may require referenced files to remain live or reject conflicting changes + since a starting snapshot. The producer provides shared validation capabilities + and reusable history processing; the action determines which checks apply. + The concrete conflict predicates are outside this RFC. + +2. **The producer prepares the resulting manifests.** It obtains the current + transaction-local manifest set, applies removals through manifest filtering, + generates or reuses manifests for additions, and assembles the complete + resulting set. Filtering updates manifest entries to reflect file removals; + it does not read Parquet files to execute row-level deletes. Unaffected + manifests can be retained, and valid transformation results can be reused. + +3. **The producer constructs the attempt's snapshot.** It determines parent, + sequence number, applicable row-ID allocation, and summary from the current + table and resulting changes. It writes an attempt-specific manifest list and + returns an `ActionCommit`. The transaction applies that result locally and + eventually submits the combined `TableCommit`. + +For example, if the current set is `M1, M2`, an action removes a file described +by `M1` and adds file `X`, the producer may produce `M1′, M2, MX`: `M1′` reflects +the removal, `M2` is unchanged, and `MX` describes the addition. Manifest filter +managers and history-processing helpers support this workflow without defining +the public action semantics themselves. + +## 5. Reuse Across Attempts + +> Retries recompute base-dependent snapshot composition, while reusing previously +> completed work whenever all semantic inputs that produced it remain unchanged. + +### 5.1 Retained Work and Attempt-Specific Results + +| Work or state | Behavior across retry/rebase | +| --- | --- | +| Action intent and resolved logical identity | Preserve for the execution | +| Added manifests and per-manifest transformation results | Reuse while all relevant inputs remain unchanged | +| Metadata reads and processed validation history | Reuse under the same semantic validity rule | +| Parent, sequence number, and row-ID allocation | Determine from the current attempt's table | +| Complete resulting manifest set and snapshot summary | Recompute for the current attempt | +| Manifest list, `ActionCommit`, and `TableCommit` | Construct for the current attempt | + +The main reuse opportunities are expensive reads, decoding, and manifest +materialization. Reconstructing the complete manifest set does not require +discarding those results, nor does it prohibit caching the current snapshot's +input manifests under an appropriate identity. + +An immutable file is a useful reuse boundary, but file identity alone is not +necessarily the full semantic input to a computation. Operation intent, relevant +metadata context, and inherited values must also remain compatible. Each attempt +must establish its required validation coverage and current-state checks; cached +history or an earlier successful check cannot substitute for that obligation. + +This RFC identifies reuse opportunities without requiring a particular cache +inventory, key type, or storage layer. Correct recomputation is an acceptable +fallback whenever reuse cannot be established safely. + +One assumption deserves explicit statement because it is format-dependent. +Reusing a generated added manifest across attempts is valid under current +formats because the attempt-sensitive values — committing snapshot ID, data +sequence number, first-row-id — are inherited through manifest-list metadata +rather than embedded in the manifest file, so a rebase changes what the +manifest inherits without invalidating its content. A future format that +embeds base-dependent values directly into manifests would add those values +to the manifest's semantic inputs and shrink this reuse accordingly. That is +a seam inside producers and their writers; it changes what may be reused, not +the ownership, replay, or cleanup architecture. + +### 5.2 Logical Identity Versus Snapshot Representation + +A stable snapshot ID identifies one logical snapshot-producing execution. It +does not imply that an uncommitted snapshot's representation is stable across +attempts. + +In a multi-action transaction, action B can observe a snapshot produced locally +by action A. After rebase, A retains its snapshot ID but may produce a different +parent, sequence number, manifest set, and manifest-list location. B must use +that new representation. + +Committed snapshots have immutable representations. Transaction-local snapshots +must instead be treated as attempt-dependent until committed. Any reuse involving +them must be guarded by the relevant representation or semantic inputs, or the +work must be recomputed. Snapshot ID alone is insufficient for that purpose. + +### 5.3 Multi-Action Replay Example + +Consider A appending file X and B removing a file described by manifest M1. +Assume the relevant transformation inputs remain compatible across the rebase, +and a concurrent commit adds an unrelated manifest M3. + +| Stage | First attempt | After rebase | +| --- | --- | --- | +| Catalog base | `M1, M2` | `M3, M1, M2` | +| A: append X | Write `MX`; assemble `MX, M1, M2` | Reuse `MX`; assemble `MX, M3, M1, M2` | +| B: remove file | Transform `M1 → M1′`; retain `MX, M2` | Reuse valid results for `M1, MX, M2`; evaluate `M3` | +| B: resulting set | `MX, M1′, M2` | `MX, M3, M1′, M2` if M3 is unaffected | +| Snapshot construction | A and B each write a manifest list | A and B each write a new manifest list | + +A and B replay in full, but neither necessarily rewrites its materialized +manifests. B consumes A's current output, not A's previous attempt's complete +manifest set. If relevant inputs change or validation finds a conflict, the +affected work is recomputed or the action fails according to its semantics. + +## 6. Artifact Ownership and Terminal Cleanup + +Producers create metadata before catalog commit succeeds. Each producer must +track its generated artifacts, including those written by owned helper +components. Caller-provided data or delete files do not become producer-owned +merely because the action references them. + +Generated metadata paths are write-once: a new attempt must not overwrite files +from an earlier attempt. Reused artifacts keep their existing paths. Artifacts +are retained during retry and considered for cleanup only when the transaction +reaches a terminal result. + +```rust +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CommitStatus { + Committed, + Failed, + Unknown, Review Comment: > Unknown is a kind of failure with one special case: if verification failed but snapshot may have committed to the catalog successfully, we should not clean up nor retry. This seems odd to me, in what case verfication failed but commit succeeded? When I see the `Unknown` status for first time, one case come up to my mind: 1. All verification passed 2. When committing to catalog, network error happens. In this case, we are not sure if remote catalog has successfully applied the transaction. The catalog implementation should try its best to complete it, but it has some dependency, for example the api should be idempotent, and network recovered eventually. I agree that we could keep this, please add comments to explain the case. -- 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] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
