Daniel Sun created GROOVY-12459:
-----------------------------------

             Summary: A failed AsyncChannel producer never closes the channel, 
so for await hangs and the exception is dropped
                 Key: GROOVY-12459
                 URL: https://issues.apache.org/jira/browse/GROOVY-12459
             Project: Groovy
          Issue Type: Bug
            Reporter: Daniel Sun


h2. Context

{{AsyncChannel}} is the channel used with {{async}} and {{for await}}. A 
producer often sends a few values and then calls {{close()}} so the consumer 
can finish. That shape shipped with the channel API. Native async/await is 
GROOVY-9381. Later channel work includes GROOVY-12320 ({{ChannelSelect}}) and 
GROOVY-12343 ({{AsyncChannel.after}}).

The proposed change teaches the channel to carry a producer failure on the same 
close signal the rest of the API already understands.

h2. Problem

{code:groovy}
def channel = AsyncChannel.create(2)

async {
    for (int i = 1; i <= 3; i++) {
        someCall()
        channel.send(i * i)
    }
    channel.close()
}

for await (value in channel) {
    println value
}
{code}

If {{someCall()}} throws, both of these happen:

* {{channel.close()}} never runs, so {{for await}} waits forever.
* The exception stays on the {{Awaitable}} returned by {{async}}. The example 
discards that handle, so nothing in the consuming code sees the failure.

A {{try}} / {{finally}} inside the task, followed by {{await}} on the handle, 
fixes both symptoms, but the consumer is waiting on the channel, not on the 
handle. A resource that exists only inside the task does not have this problem: 
{{defer}} already runs on the way out, and {{await}} on the handle reports the 
exception.

h2. Change

{{receive}} and {{send}} still end with {{ChannelClosedException}}. A retained 
failure is that exception's cause, not a second exception type. Buffered values 
are delivered first. The first close wins; a later close does not replace it.

||What happened||What the consumer sees||
|{{close()}}, or {{close(null)}}|{{ChannelClosedException}} with no cause. 
{{for await}} ends. {{ChannelSelect}} still treats this as a normal close.|
|{{close(failure)}} when the channel retains it|{{ChannelClosedException}} 
whose cause is {{failure}}. {{for await}} and {{ChannelSelect}} throw 
{{failure}} after values already sent.|
|A withdrawn {{ChannelSelect}} branch|Unchanged. It is not a close, so it stays 
silent.|

{{produce}} is the helper for the example above. It runs the body and then 
closes the channel. On success the body just returns. On failure it calls 
{{close(failure)}}.

* When that call returns {{true}}, receivers observe the failure and the 
returned {{Awaitable}} completes with {{null}}.
* When it returns {{false}}, the channel is still closed, so the consumer does 
not hang, and the {{Awaitable}} completes with the body exception.

{code:groovy}
def channel = AsyncChannel.create(2)

channel.produce { out ->
    for (int i = 1; i <= 3; i++) {
        someCall()
        out.send(i * i)
    }
}

for await (value in channel) {
    println value
}
{code}

{{filter}}, {{map}}, {{merge}}, {{split}}, and {{tap}} keep a normal upstream 
close as a normal close. An upstream failure is passed on with the same cause, 
so a later {{for await}} still sees it.

{{getCloseCause()}} only reports a failure the channel actually retained. The 
default is {{null}}.

h2. Compatibility

The new methods are default methods, added after the 6.0.0 channel API, so a 
class compiled against that API does not need to be recompiled and does not 
throw {{AbstractMethodError}}.

* {{close(null)}} delegates to {{close()}}. The return value still means this 
call closed the channel.
* {{close(failure)}} on that older class calls {{close()}} and returns 
{{false}}. The failure is not stored. {{produce}} then completes its 
{{Awaitable}} with the body exception.
* {{DefaultAsyncChannel}} stores the cause under the same lock as the closed 
flag and returns {{true}} only when receivers will observe it. {{close()}} and 
{{close(Throwable)}} both enter one private close, so neither calls the other.

A resource that lives only inside an {{async}} task is unchanged: release it 
with {{defer}}, and observe the task with {{await}} or with {{scope.async}} 
inside {{AsyncScope.withScope}}.




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to