dabla commented on code in PR #62922:
URL: https://github.com/apache/airflow/pull/62922#discussion_r4248810310


##########
task-sdk/src/airflow/sdk/bases/xcom.py:
##########
@@ -566,3 +567,166 @@ def delete(
             ),
         )
         cls.purge(xcom_result)
+
+
+def _normalize_index(index: int, length: int) -> int:
+    """Map a sequence index, negative ones included, onto a position in ``[0, 
length)``."""
+    if index < 0:
+        index += length
+    if not (0 <= index < length):
+        raise IndexError(index)
+    return index
+
+
+class XComIterable(Sequence):
+    """
+    An iterable that lazily fetches XCom values one by one instead of loading 
all at once.
+
+    This is a read-only :class:`collections.abc.Sequence` over the 
``return_value_<index>`` XComs an
+    iterated task pushed, one per index: the values are written by the 
producing task's runner as
+    each sub-task finishes (see ``IterableOperator.axcom_push``), and the 
iterable only ever reads
+    them. Nothing on this class mutates the underlying XComs.
+
+    Indexing follows the usual sequence rules, negative indices included: 
``result[-1]`` is the last
+    value. Iterations that were skipped pushed nothing and are left out, as 
the XComs of skipped
+    mapped task instances are: ``length`` counts every input item, ``skipped`` 
lists the indices
+    that were skipped, and positions in the sequence run over the others only. 
An iteration that
+    returned ``None`` pushed nothing either but keeps its position: reading it 
gives ``None``,
+    where ``.expand()`` would not count such a mapped task instance at all.
+
+    Every element is a remote fetch, so random access costs one XCom read per 
element, and so do
+    iterating and slicing: N elements are N requests. Reading several in one 
request needs an
+    Execution API endpoint that takes a list of keys, which does not exist yet.
+    """
+
+    def __init__(
+        self,
+        task_id: str,
+        dag_id: str,
+        run_id: str,
+        map_index: int | None = None,
+        length: int | None = None,
+        skipped: Sequence[int] = (),
+    ):
+        self.task_id = task_id
+        self.dag_id = dag_id
+        self.run_id = run_id
+        self.map_index = map_index
+        self.length = length or 0
+        self.skipped: list[int] = sorted(skipped)
+
+    def _index_of(self, position: int) -> int:
+        """Map ``position`` in the sequence to its input index, stepping over 
the skipped indices."""
+        index = _normalize_index(position, len(self))
+        for skipped_index in self.skipped:
+            if skipped_index > index:
+                break
+            index += 1
+        return index
+
+    def __iter__(self) -> Iterator[Any]:
+        return _XComIterator(self)
+
+    def __len__(self) -> int:
+        return self.length - len(self.skipped)
+
+    async def alen(self) -> int:
+        """Async twin of ``len(self)``, for readers on the event loop that 
take a length before each read."""
+        return len(self)
+
+    def __getitem__(self, key: int | slice) -> Any | Sequence[Any]:
+        """Allow direct indexing so this works like a sequence."""
+        from airflow.sdk.execution_time.xcom import XCom
+
+        if isinstance(key, slice):
+            # TODO: This issues one XCom.get_one call per element — N 
round-trips for a full slice.
+            # XComIterable stores results under distinct keys (return_value_0, 
return_value_1, …)
+            # with the same map_index, so the existing GetXComSequenceSlice 
endpoint (which ranges
+            # over map_index for a single key) cannot be reused.  A new POST 
endpoint that accepts
+            # a list of keys and returns values in a single query is needed; 
once that lands, replace
+            # this loop with a single batched fetch.
+            start, stop, step = key.indices(len(self))
+            return [self[i] for i in range(start, stop, step)]
+
+        return XCom.get_one(

Review Comment:
   Right, reproduced through `loop.previous`: the outer read went out with the 
flag, the item reads without it. `XComIterable` now carries 
`previous_iteration`, set by `LoopContextAccessor._result` when it reads an 
iterable for the previous iteration, and passes it on in its item reads, sync 
and async; only when set, so reads outside a loop call the XCom backend with 
the same arguments as before. `aget_one` takes the same private 
`_previous_iteration` as `get_one` for the async read. The new test in 
`test_loop.py` checks the outer read and both item reads for the current and 
the previous iteration (4c9ce91d92). A local read benchmark (old vs new, 3000 
reads) showed no measurable difference.
   
   Drafted-by: Claude Opus 5.5; reviewed by @dabla before posting



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