@@ -480,6 +480,32 @@ def async_step_recv(
480480 max_get : int | None = None ,
481481 timeout : float | None = None ,
482482 ) -> TensorDictBase :
483+ """Collects step results from the pool.
484+
485+ Args:
486+ min_get (int, optional): minimum number of results to collect
487+ before returning. Defaults to ``self.min_get``.
488+ env_index (int, optional): if provided, waits for the result of
489+ this specific env instead of forming a batch; ``min_get``,
490+ ``max_get`` and ``timeout`` are ignored.
491+
492+ Keyword Args:
493+ max_get (int, optional): maximum number of results to return.
494+ ``None`` (default) drains everything available once
495+ ``min_get`` is satisfied.
496+ timeout (float, optional): deadline in seconds for the entire
497+ call, measured from call entry. If fewer than ``min_get``
498+ results are available when the deadline expires, a
499+ :class:`TimeoutError` is raised; results collected so far are
500+ put back and remain available to the next call. Once
501+ ``min_get`` is satisfied, the call returns at the deadline
502+ with whatever is ready (up to ``max_get``). ``timeout=0``
503+ polls without blocking. ``None`` (default) waits
504+ indefinitely.
505+
506+ Returns:
507+ TensorDictBase: the stacked results, carrying ``"env_index"``.
508+ """
483509 raise NotImplementedError
484510
485511 @abc .abstractmethod
@@ -497,6 +523,18 @@ def async_step_and_maybe_reset_recv(
497523 max_get : int | None = None ,
498524 timeout : float | None = None ,
499525 ) -> tuple [TensorDictBase , TensorDictBase ]:
526+ """Collects step-and-maybe-reset results from the pool.
527+
528+ ``min_get``, ``env_index``, ``max_get`` and ``timeout`` behave as in
529+ :meth:`async_step_recv`; in particular ``timeout`` bounds the entire
530+ call and raises :class:`TimeoutError` (without losing results) if
531+ fewer than ``min_get`` results arrive in time.
532+
533+ Returns:
534+ tuple of (TensorDictBase, TensorDictBase): the stacked step
535+ results and the stacked roots of the next steps (reset where
536+ needed), both carrying ``"env_index"``.
537+ """
500538 raise NotImplementedError
501539
502540 @abc .abstractmethod
@@ -516,6 +554,17 @@ def async_reset_recv(
516554 max_get : int | None = None ,
517555 timeout : float | None = None ,
518556 ) -> TensorDictBase :
557+ """Collects reset results from the pool.
558+
559+ ``min_get``, ``env_index``, ``max_get`` and ``timeout`` behave as in
560+ :meth:`async_step_recv`; in particular ``timeout`` bounds the entire
561+ call and raises :class:`TimeoutError` (without losing results) if
562+ fewer than ``min_get`` results arrive in time.
563+
564+ Returns:
565+ TensorDictBase: the stacked reset results, carrying
566+ ``"env_index"``.
567+ """
519568 raise NotImplementedError
520569
521570 def stats (self , * , reset : bool = False ) -> dict [str , float | int ]:
@@ -1118,12 +1167,33 @@ def _receive_futures(futures, min_get, max_get, timeout):
11181167 limit = len (futures ) if max_get is None else max_get
11191168 pending = set (futures )
11201169 completed = []
1121- deadline_timer = None
1170+ # The deadline is anchored at call entry and bounds the entire call,
1171+ # including the wait for the first result.
1172+ deadline_timer = (
1173+ None
1174+ if timeout is None
1175+ else timeit ("async_env_future_batch_deadline" ).start ()
1176+ )
11221177 while pending and len (completed ) < min_get :
1123- done , pending = wait (pending , return_when = FIRST_COMPLETED )
1178+ if deadline_timer is None :
1179+ done , pending = wait (pending , return_when = FIRST_COMPLETED )
1180+ else :
1181+ remaining = timeout - deadline_timer .elapsed ()
1182+ # A non-positive remaining still polls: wait(timeout=0)
1183+ # returns whatever is already done without blocking.
1184+ done , pending = wait (
1185+ pending , timeout = max (remaining , 0.0 ), return_when = FIRST_COMPLETED
1186+ )
1187+ if not done :
1188+ # Completed futures stay in the pool's pending lists (the
1189+ # caller only removes returned ones), so no result is
1190+ # lost and state stays consistent.
1191+ raise TimeoutError (
1192+ f"async recv timed out: { len (completed )} /{ min_get } "
1193+ f"results after { timeout } s; completed futures remain "
1194+ f"available to the next call."
1195+ )
11241196 completed .extend (list (done )[: limit - len (completed )])
1125- if deadline_timer is None and timeout is not None :
1126- deadline_timer = timeit ("async_env_future_batch_deadline" ).start ()
11271197 while pending and len (completed ) < limit :
11281198 done = {future for future in pending if future .done ()}
11291199 if not done :
0 commit comments