codeconsole commented on code in PR #16690:
URL: https://github.com/apache/grails-core/pull/16690#discussion_r4332046546


##########
grails-async/rxjava3/src/main/groovy/org/grails/async/factory/rxjava3/RxPromiseFactory.groovy:
##########
@@ -76,24 +76,27 @@ class RxPromiseFactory extends AbstractPromiseFactory {
 
     @Override
     <T> Promise<T> onComplete(List<Promise<T>> promises, Closure<T> callable) {
-        new RxPromise<T>(this, Observable.concat(
-            promises.collect { Promise p ->
-                if (p instanceof BoundPromise) {
-                    return Observable.just(((BoundPromise) p).value) as 
Observable<T>
-                }
-                else {
-                    return ((RxPromise) p).toObservable() as Observable<T>
-                }
-            }
-        ).toList())
-         .onComplete(callable)
+        new RxPromise<T>(this, values(promises)).onComplete(callable)
     }
 
     @Override
     <T> Promise<List<T>> onError(List<Promise<T>> promises, Closure<?> 
callable) {
-        new RxPromise(this, Observable.concat(
-                promises.collect { ((RxPromise<T>) it).toObservable() }
-        ).toList())
-        .onError(callable) as Promise<List<T>>
+        new RxPromise(this, values(promises)).onError(callable) as 
Promise<List<T>>
+    }
+
+    /**
+     * Concatenates the values of the given promises into one list, in order, 
with a {@code null} value restored
+     * from the sentinel it travels through the stream as.
+     */
+    private static <T> Single<List<T>> values(List<Promise<T>> promises) {
+        List<Observable<T>> sources = promises.collect { Promise<T> p ->
+            if (p instanceof BoundPromise) {
+                return Observable.just(RxPromise.wrap(((BoundPromise) 
p).value)) as Observable<T>
+            }
+            return ((RxPromise<T>) p).toObservable()

Review Comment:
   `toObservable()` returns the promise's source, not its result. It's the bug 
from my #16619 review, and it's on `7.0.x` too: before this PR, `onComplete` 
and `onError` already read `toObservable()`.
   - **A task:** the source is the cold `Single.create(..)`, so subscribing to 
it here runs the task a second time, and the list gets the second run's value.
   - **A `then` or `onComplete` promise:** the source is `subject.map(..)` on 
the previous promise's `subject`. That subject never completes, because the 
constructor's observer sets `finished` but doesn't pass `onComplete` on. So 
`concat` waits forever, and `isDone()` on the chained promise stays `false`.
   
   I ran the four features below on this branch with JDK 17:
   
   | Call | At a474a77242 | With the two lines below |
   |---|---|---|
   | `Promises.onComplete([task]) { it }` | `[2]`, the task ran twice | `[1]`, 
the task ran once |
   | `Promises.onError([task]) { }` | `[2]`, the task ran twice | `[1]`, the 
task ran once |
   | `Promises.onComplete([task.then { it + 1 }]) { it }` | `TimeoutException` 
after 5 seconds | `[2]`, the `then` callback ran once |
   | `task.then { it + 1 }.done` | still `false` after 5 seconds | `true` |
   
   The fix is two lines. They do what the RxJava 1 factory does: it reads 
`p.subject` here, and its constructor subscribes the subject itself, so the 
subject completes.
   - here: `return ((RxPromise<T>) p).subject as Observable<T>`
   - in the `RxPromise` constructor's observer: `subject.onComplete()` after 
`finished = true`
   
   Both are needed. With only the first, nothing completes the subject, so 
every list hangs. The module's own tests didn't finish in 10 minutes. With 
both, the four features and the module's 20 existing tests pass.
   
   <details><summary>The four features</summary>
   
   ```groovy
   import java.util.concurrent.TimeUnit
   import java.util.concurrent.atomic.AtomicInteger
   
   import spock.lang.Specification
   import spock.util.concurrent.PollingConditions
   
   import grails.async.Promise
   import grails.async.Promises
   
       void 'onComplete over a list runs each task once'() {
           given: 'a task that counts its runs'
               AtomicInteger runs = new AtomicInteger()
               Promise<Integer> task = Promises.createPromise { 
runs.incrementAndGet() }
   
           when: 'the list completes'
               List result = Promises.onComplete([task]) { it }.get(5, 
TimeUnit.SECONDS)
   
           then: 'the task ran once and the callback received its value'
               task.get() == 1
               result == [1]
               runs.get() == 1
       }
   
       void 'onError over a list runs each task once'() {
           given: 'a task that counts its runs'
               AtomicInteger runs = new AtomicInteger()
               Promise<Integer> task = Promises.createPromise { 
runs.incrementAndGet() }
   
           when: 'the list resolves without an error'
               List result = Promises.onError([task]) { }.get(5, 
TimeUnit.SECONDS)
   
           then: 'the task ran once and the list holds its value'
               task.get() == 1
               result == [1]
               runs.get() == 1
       }
   
       void 'onComplete over a list completes when a promise in it was chained 
with then'() {
           given: 'a promise chained from a task, with a callback that counts 
its runs'
               AtomicInteger callbacks = new AtomicInteger()
               Promise<Integer> chained = Promises.createPromise { 1 }.then { 
callbacks.incrementAndGet(); it + 1 }
   
           when: 'the list completes'
               List result = Promises.onComplete([chained]) { it }.get(5, 
TimeUnit.SECONDS)
   
           then: 'the callback received the chained value and the then callback 
ran once'
               result == [2]
               callbacks.get() == 1
       }
   
       void 'a promise chained with then is done once it resolves'() {
           when: 'a task is chained with then'
               Promise<Integer> chained = Promises.createPromise { 1 }.then { 
it + 1 }
   
           then: 'it resolves and reports done'
               chained.get(5, TimeUnit.SECONDS) == 2
               new PollingConditions(timeout: 5, delay: 0.2).eventually {
                   assert chained.done
               }
       }
   ```
   
   `task.get()` sits before the `runs` check so that both runs have finished by 
then. Otherwise the original run could still be in flight.
   
   </details>



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