Skip to content

docs: RFC-108 - Multi-dataset incremental reads in Hudi Streamer#19299

Open
ashokkumar-allu wants to merge 1 commit into
apache:masterfrom
ashokkumar-allu:allu_oss_incr_multisource_rfc01
Open

docs: RFC-108 - Multi-dataset incremental reads in Hudi Streamer#19299
ashokkumar-allu wants to merge 1 commit into
apache:masterfrom
ashokkumar-allu:allu_oss_incr_multisource_rfc01

Conversation

@ashokkumar-allu

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

This PR adds a new RFC — RFC-108: Multi-dataset incremental reads in Hudi Streamer — proposing native multi-table incremental reads in Hudi Streamer so that N Hudi tables can be read incrementally into a single, atomic ingestion job (one commit, one checkpoint).

Related feature issue: Closes #19281

Summary and Changelog

Hudi Streamer (DeltaStreamer) can currently read incrementally from exactly one source per job. Pipelines that need to combine incremental changes from several Hudi tables into one target must run one Streamer job per source and merge downstream (drift, no cross-source atomicity, extra watermark/monitoring overhead) or fall back to expensive full-snapshot reads for all but one table.

This RFC proposes:

  • HoodieIncrMultiSource — a new source that reads N Hudi tables incrementally in a single batch with independent per-table checkpoints (each table advances on its own numInstantsPerFetch / missing-checkpoint strategy).
  • Multi-dataset InputBatch — additive List<T> batches alongside the existing Option<T> batch; getBatch() now fails loud if a multi-dataset batch would be silently narrowed, single-source callers are unaffected.
  • MultiDatasetTransformer contract, implemented by SqlFileBasedTransformer — merges the per-source datasets with a single SQL file referencing <SRC_0>, <SRC_1>, …; <SRC> is kept as an alias for the first dataset for backward compatibility; supports multi-statement SQL and CTE-style CREATE [OR REPLACE] TEMPORARY VIEW, with temp-view cleanup on both success and failure.
  • MultiTableCheckpointManager — persists per-table checkpoints as table1=ts1,table2=ts2 in deltastreamer.checkpoint.key; a bare timestamp continues to be honored as a legacy single-source checkpoint, so existing jobs migrate without checkpoint surgery.
  • StreamSync / SourceFormatAdapter wiring — startup validation that HoodieIncrMultiSource requires a MultiDatasetTransformer (resolved even inside a ChainedTransformer) to prevent a silent-data-loss path where only the last source would be written; per-dataset error-event handling on the multi-dataset row path.

Changelog:

  • rfc/rfc-108/rfc-108.md — new RFC document (Abstract, Background, Implementation, Rollout/Adoption Plan, Test Plan).
  • rfc/README.md — add RFC-108 entry with status UNDER REVIEW.

No code changes in this PR — this is the RFC document only.

Impact

  • User-facing: none from this PR (docs only). The proposed feature, when implemented, is opt-in via source.class=HoodieIncrMultiSource and does not alter behavior of existing single-source Streamer jobs, transformers, or checkpoints. API changes are additive; the checkpoint format degrades cleanly to the legacy single-timestamp form.
  • Performance: none from this PR.

Risk Level

none — RFC documentation only; no code or config changes.

Documentation Update

This PR is the design documentation for the proposed feature. No website updates are required until the implementation lands; user-facing configuration and the recommended OUTER JOIN / merge-payload patterns will be documented on the Hudi website when the feature is shipped.

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable (n/a — RFC doc only; the Test Plan section of the RFC covers the tests that will accompany the implementation)

@github-actions github-actions Bot added the size:M PR with lines of changes in (100, 300] label Jul 15, 2026
@hudi-bot

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for the RFC! This proposes native multi-table incremental reads in Hudi Streamer via a new HoodieIncrMultiSource, a multi-dataset InputBatch/MultiDatasetTransformer contract, and a backward-compatible multi-table checkpoint format. The design is well-organized and the backward-compatibility story is thoughtfully considered; a few areas — the lenient checkpoint parser's failure semantics, cross-table schema evolution, scalability of N reads per batch, and a missing Alternatives section — could use more detail (see inline comments). Once those are addressed, a Hudi committer or PMC member can take it from here for a deeper design review.

Comment thread rfc/rfc-108/rfc-108.md
```

- Any value containing `=` is parsed as multi-table via a lenient parser that skips malformed entries
while preserving valid ones (so a single corrupt entry never causes the whole string to be

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The lenient parser silently skipping a malformed entry seems to interact dangerously with the per-table missing-checkpoint strategy. If a table's entry is dropped, that table has no checkpoint and falls back to its missing_checkpoint_strategy — and the default READ_LATEST would skip all unprocessed data (silent data loss), which contradicts the fail-loud philosophy used elsewhere in this design. Could you spell out exactly what happens to a table whose entry is skipped? @nsivabalan could you weigh in on whether silently dropping a checkpoint entry is safe here versus failing the batch?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md
| `hoodie.streamer.source.hudi.{table}.num_instants` | `HoodieIncrSourceConfig.NUM_INSTANTS_PER_FETCH` default | Per-table instants per fetch |
| `hoodie.streamer.source.hudi.{table}.missing_checkpoint_strategy` | `READ_LATEST` | `READ_LATEST` \| `READ_UPTO_LATEST_COMMIT` |
| `hoodie.streamer.source.hudi.{table}.skip_on_schema_failure` | `false` | On placeholder-schema resolution failure: `false` fails loud (checkpoint not advanced past a real failure); `true` returns an empty dataset and emits a `<table>.schema_resolution_failure` metric |
| `hoodie.streamer.source.hudi.{table}.partition.fields` / `.partition.extractor.class` | — | Optional per-table partitioning |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 How does this handle schema evolution across the N source tables over time? With a single SchemaProvider feeding one commit, it isn't clear what happens when one source table evolves its schema mid-stream while others don't, or how the empty-but-schema'd placeholder dataset stays in sync with an evolved source schema. It might be worth a dedicated subsection on per-table schema resolution and target-schema reconciliation.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md
2. For each table, performs a `HoodieIncrSource`-equivalent incremental read using that table's own
`numInstantsPerFetch`, missing-checkpoint strategy, hollow-commit handling, timeline management and
"already caught up" detection. Tables that ingest at different rates never block one another.
3. Returns a dataset for **every** configured table, in declaration order — an empty dataset carrying

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 "Tables ... never block one another" reads as a concurrency claim, but the sequence suggests the N incremental reads happen within a single batch on the driver. Are the per-table reads issued in parallel or sequentially, and is there an intended upper bound on N? A short note on the performance/resource profile as N grows (and whether one slow/large source stretches the whole batch) would strengthen the scalability story.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md

- **Impact on existing users.** None by default. The feature is inert unless a user sets
`source.class=HoodieIncrMultiSource` and declares source tables. Existing single-source jobs,
transformers, and checkpoints continue to work byte-for-byte.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The RFC doesn't include an Alternatives Considered section. It would help to briefly document approaches that were evaluated and rejected — e.g. orchestrating multiple single-source jobs with multi-writer/coordinated commits, a dedicated join/union source, or handling this at the transformer layer only — and why a new source plus InputBatch change is the preferred path.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md Outdated

A new source extending `Source<Dataset<Row>>` (SourceType `ROW`) — it extends `Source` directly
rather than `RowSource` so it can return a multi-dataset `InputBatch` without per-row sanitization
side effects. Per batch it:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Extending Source directly to skip per-row sanitization is a notable departure from RowSource. What sanitization is being bypassed (e.g. invalid/duplicate field-name handling), and could skipping it produce rows that later fail on write or violate downstream assumptions? It might be worth clarifying whether sanitization is re-applied after the transformer merge instead of dropped entirely.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md
- **Scope.** No change to Hudi's write/commit path or on-disk storage format; the only serialized
change is the contents of the Streamer's own `deltastreamer.checkpoint.key`.

## Rollout/Adoption Plan

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Could you expand on the failure/atomicity semantics within a batch? Specifically: if the incremental read of table K succeeds but table K+1's read (or the merge/write) fails, is the entire batch aborted with no checkpoints advanced, and are all per-table checkpoints persisted atomically together with the single commit? Making the all-or-nothing boundary explicit would clarify the core atomicity guarantee this RFC is built on.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md Outdated

### Motivating use cases

- **Incremental-transformation ETLs** for data-freshness initiatives, currently blocked by the absence

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The motivation is well argued conceptually, but the Motivating use cases section is a single fairly abstract bullet. Could you add one or two concrete scenarios (real tables/joins) and, if available, any evidence of demand — issue reports, scale figures, or the compute cost of the current snapshot-read workaround — to strengthen the case that this is worth the added surface area?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@github-actions github-actions Bot added size:L PR with lines of changes in (300, 1000] and removed size:M PR with lines of changes in (100, 300] labels Jul 15, 2026
@ashokkumar-allu
ashokkumar-allu force-pushed the allu_oss_incr_multisource_rfc01 branch from 1fb4a24 to 15fee89 Compare July 15, 2026 20:42

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for the RFC! This proposes native multi-table incremental reads in Hudi Streamer via a new HoodieIncrMultiSource, a MultiDatasetTransformer contract, and a backward-compatible multi-table checkpoint format. The design is well developed and the recent round of expansion addressed the earlier feedback nicely — a few areas could still use more detail around what the cross-source consistency guarantee actually is, how <SRC_N> placeholders bind when the table list changes, and operational checkpoint-override / table-set-lifecycle semantics (see inline comments). Once those are addressed, a Hudi committer or PMC member can take it from here for a deeper design review.

Comment thread rfc/rfc-108/rfc-108.md
downstream merge, the merge step periodically observes fact rows whose dimension row has not yet
been ingested and either drops them (inner join) or emits rows with null dimension fields (outer
join) that must be repaired on a later run. A native multi-source incremental read advances both
tables under one commit, so the join always sees a consistent slice.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Atomic checkpoint advancement isn't the same as temporal alignment of the data: each table reads its own window sized by its own num_instants from its own checkpoint, so two sources ingesting at different rates can still land at very different commit/event times within the same batch — the same drift a downstream OUTER JOIN sees today. Could you clarify what "consistent slice" actually guarantees, and whether per-table read windows should be bounded to a shared commit-time ceiling to deliver true cross-source temporal consistency?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md
`SqlFileBasedTransformer` implements it:

- Registers each input dataset as a unique Spark temp view (`HOODIE_SRC_TMP_TABLE_<uuid>`).
- SQL references datasets by index — `<SRC_0>`, `<SRC_1>`, … — with `<SRC>` kept as an alias for the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The <SRC_N> placeholders bind positionally to the order of ...multi.hudi.tables (line 208), while checkpoints bind by table name (line 235). If an operator reorders or edits the table list, the SQL's <SRC_0>/<SRC_1> silently rebind to different tables and the join becomes wrong — yet checkpoints keep resuming correctly by name, so nothing surfaces the mistake. Have you considered binding placeholders by table name/alias (e.g. <SRC:db.orders>) instead of by position?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md
- Any value containing `=` is parsed as multi-table via a lenient parser that skips malformed entries
while preserving valid ones (so a single corrupt entry never causes the whole string to be
misclassified as single-table and collapsed under a default key).
- A bare value with no `=` is treated as a legacy single checkpoint (mapped to a default table),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 How does the multi-table checkpoint interact with Streamer's operational checkpoint override (--checkpoint) and reset (hoodie.deltastreamer.checkpoint.reset_key)? A bare-timestamp override would parse as the legacy single form here and map to a single default table, which looks like it would drop the other tables' cursors on the next run. It'd help to specify override/reset semantics (and the expected value format) in multi-table mode.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md
misclassified as single-table and collapsed under a default key).
- A bare value with no `=` is treated as a legacy single checkpoint (mapped to a default table),
giving automatic backward compatibility and a clean upgrade path from an existing single-source job.
- The formatted output uses a deterministic key order (backed by a `TreeMap`) so the same set of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 The migration section covers single→multi upgrade, but not changing the table set over the life of a running job. Adding a table gives it no checkpoint entry (first-run → READ_LATEST skips history, or READ_UPTO_LATEST_COMMIT re-reads everything), and removing a table leaves a stale table=ts entry serialized in the TreeMap-backed string indefinitely. Could you document the intended behavior when ...multi.hudi.tables evolves — including whether stale entries are pruned and how a newly-added table should be seeded?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

Comment thread rfc/rfc-108/rfc-108.md
- **Routing.** In `fetchNextBatchFromSource`, when a multi-dataset transformer is present the full
`getBatches()` list is passed to `MultiDatasetTransformer#apply`; otherwise the existing
single-dataset path (`getBatch().map(...)`) is used unchanged. An empty-batch guard skips the
transformer when the source produced no data (rather than passing an empty list that would throw).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 There's some tension between step 3 (line 139), where the source always returns a dataset for every configured table, and this empty-batch guard that "skips the transformer when the source produced no data." If N datasets are always present, when does the guard actually fire — only when all N are empty? And in that all-caught-up case, does the batch still produce a commit and persist checkpoints, or is it a full no-op? Worth spelling out.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@voonhous

Copy link
Copy Markdown
Member

@ashok-allu, can you please claim the RFC first like so in a separate PR: #19303

Thanks!

Propose native multi-table incremental reads in Hudi Streamer via a new
HoodieIncrMultiSource that reads N Hudi tables incrementally in a single
batch with independent per-table checkpoints, a MultiDatasetTransformer
contract implemented by SqlFileBasedTransformer for merging datasets with
a single SQL file referencing <SRC_0>, <SRC_1>, ..., and a
backward-compatible multi-table checkpoint format managed by
MultiTableCheckpointManager. Enables a single, atomic ingestion job -
one commit, one checkpoint - for related datasets.

The RFC-108 entry in rfc/README.md is claimed separately in the split-out
claim PR to keep this content PR reviewable in isolation.
@ashokkumar-allu
ashokkumar-allu force-pushed the allu_oss_incr_multisource_rfc01 branch from 15fee89 to 63c738a Compare July 16, 2026 19:03
@ashokkumar-allu

Copy link
Copy Markdown
Contributor Author

@voonhous thanks for the pointer! I've split this into two PRs following the pattern of #19303:

Happy to hold this content PR until the claim PR is merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L PR with lines of changes in (300, 1000]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multi-dataset incremental reads in Hudi Streamer

5 participants