Repository: rust-lang/futures-rs (Rust)

Issue:
try_run_one/run_until_stalled cannot handle subsequently spawned futures
In #1537/#1437 to answer #1369, we introduced `LocalPool::try_run_one`/`LocalPool::run_until_stalled` to partially progress a LocalPool.

Now, I used them myself and noticed that they do not worked as described when the spawned futures spawn futures themselves.

In the following example it only prints `begin`. Instead of also printing out `inner` and `end 5`.

```rust
let mut pool = LocalPool::new();
let mut spawner = pool.spawner();

spawner.clone().spawn_local(async move {
    println!("begin");

    let inner = spawner.spawn_local_with_handle(async {
        println!("inner");
        5
    }).unwrap();

    println!("end {}", inner.await);
}).unwrap();

pool.run_until_stalled();
```

This is because `inner.await` makes the internal `poll` function of the outer future return `Pending`. `run_until_stalled` will see this `Pending` and return because it thinks it can not make any more progress, which is wrong because there are new futures which have been enqueued and which can be advanced.

The same happens with `pool.try_run_one`.

For both function, this behavior is against what their documentation describes. Because in both cases the function returns __even if progress can be made__!

```rust
/// Runs all tasks and returns after completing one future or until no more progress
/// can be made. Returns `true` if one future was completed, `false` otherwise.
/// [...]
/// This function will not block the calling thread and will return the moment
/// that there are no tasks left for which progress can be made or after exactly one
/// task was completed; [...]
pub fn try_run_one(&mut self) -> bool;

/// Runs all tasks in the pool and returns if no more progress can be made
/// on any task.
/// [...]
/// This function will not block the calling thread and will return the moment
/// that there are no tasks left for which progress can be made;
/// [...]
pub fn run_until_stalled(&mut self);
```




Discussion:

Patch:
diff --git a/futures-executor/src/local_pool.rs b/futures-executor/src/local_pool.rs
index 9985dca5b9..1fd9c03cf4 100644
--- a/futures-executor/src/local_pool.rs
+++ b/futures-executor/src/local_pool.rs
@@ -193,12 +193,20 @@ impl LocalPool {
     /// Though only one task will be completed, progress may be made on multiple tasks.
     pub fn try_run_one(&mut self) -> bool {
         poll_executor(|ctx| {
-            let ret = self.poll_pool_once(ctx);
+            loop {
+                let ret = self.poll_pool_once(ctx);
 
-            // return if we really have executed a future
-            match ret {
-                Poll::Ready(Some(_)) => true,
-                _ => false
+                // return if we have executed a future
+                if let Poll::Ready(Some(_)) = ret {
+                    return true;
+                }
+
+                // if there are no new incoming futures
+                // then there is no feature that can make progress
+                // and we can return without having completed a single future
+                if self.incoming.borrow().is_empty() {
+                    return false;
+                }
             }
         })
     }
@@ -229,17 +237,9 @@ impl LocalPool {
     /// of the pool's run or poll methods. While the function is running, all tasks
     /// in the pool will try to make progress.
     pub fn run_until_stalled(&mut self) {
-        poll_executor(|ctx| {
-            loop {
-                let result = self.poll_pool_once(ctx);
-
-                // if there are no more ready futures exit
-                match result {
-                    Poll::Pending | Poll::Ready(None) => return,
-                    _ => continue
-                }
-            }
-        })
+        poll_executor(|ctx| { 
+            let _ = self.poll_pool(ctx);
+        });
     }
 
     // Make maximal progress on the entire pool of spawned task, returning `Ready`


Test patch:
diff --git a/futures-executor/tests/local_pool.rs b/futures-executor/tests/local_pool.rs
index 44b0a76ff0..9663d03a66 100644
--- a/futures-executor/tests/local_pool.rs
+++ b/futures-executor/tests/local_pool.rs
@@ -179,6 +179,29 @@ fn try_run_one_returns_on_no_progress() {
     assert_eq!(cnt.get(), ITER);
 }
 
+#[test]
+fn try_run_one_runs_sub_futures() {
+    let mut pool = LocalPool::new();
+    let mut spawn = pool.spawner();
+    let cnt = Rc::new(Cell::new(0));
+
+    let mut inner_spawner = spawn.clone();
+    let cnt1 = cnt.clone();
+    spawn.spawn_local_obj(Box::pin(poll_fn(move |_| {
+        cnt1.set(cnt1.get() + 1);
+        
+        let cnt2 = cnt1.clone();
+        inner_spawner.spawn_local_obj(Box::pin(lazy(move |_|{
+            cnt2.set(cnt2.get() + 1)
+        })).into()).unwrap();
+
+        Poll::Pending
+    })).into()).unwrap();
+
+    pool.try_run_one();
+    assert_eq!(cnt.get(), 2);
+}
+
 #[test]
 fn run_until_stalled_returns_if_empty() {
     let mut pool = LocalPool::new();
@@ -203,6 +226,29 @@ fn run_until_stalled_returns_multiple_times() {
     assert_eq!(cnt.get(), 2);
 }
 
+#[test]
+fn run_until_stalled_runs_spawned_sub_futures() {
+    let mut pool = LocalPool::new();
+    let mut spawn = pool.spawner();
+    let cnt = Rc::new(Cell::new(0));
+
+    let mut inner_spawner = spawn.clone();
+    let cnt1 = cnt.clone();
+    spawn.spawn_local_obj(Box::pin(poll_fn(move |_| {
+        cnt1.set(cnt1.get() + 1);
+        
+        let cnt2 = cnt1.clone();
+        inner_spawner.spawn_local_obj(Box::pin(lazy(move |_|{
+            cnt2.set(cnt2.get() + 1)
+        })).into()).unwrap();
+
+        Poll::Pending
+    })).into()).unwrap();
+
+    pool.run_until_stalled();
+    assert_eq!(cnt.get(), 2);
+}
+
 #[test]
 fn run_until_stalled_executes_all_ready() {
     const ITER: usize = 200;