Commit 4786d94
committed
[Python] Add UnboundedSource SDF wrapper (#19137)
Brings Java's ``UnboundedSource`` / ``UnboundedReader`` / ``CheckpointMark``
abstractions to the Python SDK as a Splittable-DoFn wrapper runnable on the
portable Fn API (DirectRunner / FnApiRunner). Wires the new source type into
``iobase.Read.expand()`` so ``p | beam.io.Read(my_unbounded_source)``
dispatches alongside the existing ``BoundedSource`` branch. Loosely inspired
by Java's ``Read.UnboundedSourceAsSDFWrapperFn``; the streaming-SDF template
followed for the process loop / watermark / defer plumbing is
``apache_beam.transforms.periodicsequence``.
addresses #19137
What's added
------------
``sdks/python/apache_beam/io/unbounded_source.py``
Public ABCs (``CheckpointMark``, ``UnboundedReader``, ``UnboundedSource``,
``ReadFromUnboundedSource``) plus the SDF wrapper internals
(``_UnboundedSourceRestriction``, ``_UnboundedSourceRestrictionCoder``,
``_UnboundedSourceRestrictionTracker``,
``_UnboundedSourceRestrictionProvider``).
``sdks/python/apache_beam/io/unbounded_source_test.py``
42 deterministic tests covering ABC contracts, restriction coder
round-trip, tracker state machine (claim / split / EOF / no-data /
check_done / progress / is_bounded), finalize idempotency, fan-out
via ``source.split``, source-watermark vs. record-event-time, finalize
vs. resume channel separation, tracker-internal exception close on
reader-method failures, DoFn generator close (unit + integration with
downstream raising ``Map``), cloudpickle round-trip, circular import
in three subprocess orderings, and an end-to-end DirectRunner pipeline.
What's changed in iobase.py
---------------------------
* ``Read.expand`` gains an ``UnboundedSource`` branch (function-local lazy
import to break the iobase <-> unbounded_source cycle) that delegates to
``ReadFromUnboundedSource``.
* ``Read.to_runner_api_parameter`` widens the source ``isinstance`` to
``(BoundedSource, UnboundedSource)``, writing ``READ.urn`` +
``ReadPayload(is_bounded=UNBOUNDED)``. Decode rides the existing
``PICKLED_SOURCE`` URN on ``SourceBase``. Runner-side
``IsBounded.UNBOUNDED`` dispatch in
``bundle_processor.IMPULSE_READ_TRANSFORM`` remains W2 -- execution
flows through the composite's expanded ``Impulse | Map | SDF-ParDo``.
Correctness covered
-------------------
* Data-path watermark uses ``reader.get_watermark()`` (Java
``Read.java:594`` parity), not the per-record event time. Holder is
``(value, record_ts, source_wm)``.
* Restriction has separate ``checkpoint_mark`` (resume) and
``finalization_checkpoint_mark`` (commit hook) channels; coder is a
fixed 5-tuple.
* Reader is closed on every exit path -- tracker-internal close on EOF
/ split / reader-method exception; DoFn ``finally`` defense-in-depth
for yield / downstream raise via the SDF wrapper's private chain
(isinstance guard + warning log if the chain ever moves upstream).
* EOF advances the watermark estimator to ``MAX_TIMESTAMP`` so
downstream event-time windows can close.
* ``UnboundedSource.split(desired_num_splits=20, options)`` is honoured;
returned sub-sources are validated as ``UnboundedSource`` (raises
``TypeError`` outside the split-refusal ``except``); on
split-exception the provider falls back to a single restriction with
WARNING.
* ``default_output_coder`` reaches the output PCollection via
``coders.registry.register_coder`` + ``element_type``.
* ``ReadFromUnboundedSource`` validates ``poll_interval_seconds > 0``.
Out of scope (tracked under #19137)
-----------------------------------
Listed exhaustively in the module docstring at
``sdks/python/apache_beam/io/unbounded_source.py``:
* Record-id-based deduplication (Java's ``ValueWithRecordId``).
* Backlog-byte reporting (``restriction_size`` is constant 1;
``current_progress`` is binary 0.0 / 1.0).
* Dynamic split fractions / runner-initiated work stealing.
* Source-specific checkpoint coders threaded through the SDF
restriction coder (checkpoint marks are pickled today regardless of
the source's ``get_checkpoint_mark_coder``).
* Reader caching across bundles (Java uses a Guava cache).
* ``EmptyUnboundedSource`` terminal-state marker (this PoC uses an
``is_done`` flag).
* Runner-side ``IsBounded.UNBOUNDED`` dispatch in
``bundle_processor.IMPULSE_READ_TRANSFORM``.
Tests: 42/42 ``unbounded_source_test.py``, 16/16 ``iobase_test.py``,
yapf + isort clean.1 parent 5a65f6e commit 4786d94
4 files changed
Lines changed: 2060 additions & 2 deletions
File tree
- sdks/python/apache_beam/io
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
919 | 919 | | |
920 | 920 | | |
921 | 921 | | |
922 | | - | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
923 | 927 | | |
924 | 928 | | |
925 | 929 | | |
| |||
945 | 949 | | |
946 | 950 | | |
947 | 951 | | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
948 | 962 | | |
949 | 963 | | |
950 | 964 | | |
| |||
986 | 1000 | | |
987 | 1001 | | |
988 | 1002 | | |
989 | | - | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
990 | 1012 | | |
991 | 1013 | | |
992 | 1014 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
220 | 220 | | |
221 | 221 | | |
222 | 222 | | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
223 | 301 | | |
224 | 302 | | |
0 commit comments