[
https://issues.apache.org/jira/browse/GROOVY-12459?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Daniel Sun updated GROOVY-12459:
--------------------------------
Description:
h2. Context
{{AsyncChannel}} is the channel used with {{async}} and {{for await}}. A
producer sends 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 keeps that close and lets it carry the producer failure.
The code that fails, and the code that reads, each write their own {{try}} /
{{catch}}.
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 the consuming code never 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. {{defer}} runs on the way out of the task, including when the task
throws. A {{defer}} block that calls {{channel.close()}} closes the channel
with no cause, so {{for await}} ends normally and the exception stays on the
handle.
h2. Change
The producer catches the failure and closes with it. Values already sent are
still delivered. {{receive}} and {{send}} then fail with
{{ChannelClosedException}}, and that failure is the cause. {{for await}} throws
the failure itself after those values, including a checked exception or an
{{Error}}. The reader writes the {{try}} / {{catch}}, as with C# {{await
foreach}}, JavaScript {{for await}}, and Swift {{for try await}}.
{code:groovy}
def channel = AsyncChannel.create(2)
async {
try {
for (int i = 1; i <= 3; i++) {
someCall()
await channel.send(i * i)
}
channel.close()
} catch (Exception failure) {
channel.close(failure)
}
}
def seen = []
try {
for await (value in channel) {
seen << value
}
} catch (Exception failure) {
// failure is the exception thrown by someCall()
}
{code}
{{close(failure)}} returns nothing. {{null}} is a normal close. The first close
wins.
||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)}} on a channel that retains it|{{receive}} and {{send}} fail
with {{ChannelClosedException}} whose cause is {{failure}}. {{for await}} and
{{ChannelSelect}} throw {{failure}} after the values already sent.|
|Every selected channel closed normally|{{ChannelSelect}} fails with a
{{ChannelClosedException}} whose message is "all channels are closed".|
|A transfer already committed when another channel closes|That transfer stays
the result. A withdrawn branch, and {{CancellationException}}, are not closes.|
{{filter}}, {{map}}, {{merge}}, {{split}}, and {{tap}} carry a source failure,
or a failure thrown by the callback, through to their output. Closing one side
of a {{split}} stops the split and closes the other side normally. A failure
close of one side is not copied onto the other.
{{BroadcastChannel.close(failure)}} closes every subscriber the same way. {{for
await}} on a subscriber throws that failure, and the publisher view reports it
with {{onError}}.
{{getCloseCause()}} reports the retained failure, or {{null}} when the channel
is open, was closed normally, or does not retain a cause.
h2. Compatibility
{{close(Throwable)}} and {{getCloseCause()}} are default methods, added after
the 6.0.0 channel API. A class compiled against that API does not need to be
recompiled and does not throw {{AbstractMethodError}}.
* The default {{close(Throwable)}} calls {{close()}} and does not retain the
cause. {{for await}} on that channel ends. It does not throw the failure.
* The channel from {{AsyncChannel.create}} does retain the cause. {{close()}}
and {{close(Throwable)}} share one close, so neither calls the other. The cause
is stored before the closed flag, under the same lock.
* An implementation of {{close()}} must also override {{close(Throwable)}} if
its {{close()}} calls that method. The default calls {{close()}} again.
* 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}}.
was:
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}}.
> 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
> Priority: Major
>
> h2. Context
> {{AsyncChannel}} is the channel used with {{async}} and {{for await}}. A
> producer sends 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 keeps that close and lets it carry the producer failure.
> The code that fails, and the code that reads, each write their own {{try}} /
> {{catch}}.
> 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 the consuming code never 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. {{defer}} runs on the way out of the task, including when the task
> throws. A {{defer}} block that calls {{channel.close()}} closes the channel
> with no cause, so {{for await}} ends normally and the exception stays on the
> handle.
> h2. Change
> The producer catches the failure and closes with it. Values already sent are
> still delivered. {{receive}} and {{send}} then fail with
> {{ChannelClosedException}}, and that failure is the cause. {{for await}}
> throws the failure itself after those values, including a checked exception
> or an {{Error}}. The reader writes the {{try}} / {{catch}}, as with C#
> {{await foreach}}, JavaScript {{for await}}, and Swift {{for try await}}.
> {code:groovy}
> def channel = AsyncChannel.create(2)
> async {
> try {
> for (int i = 1; i <= 3; i++) {
> someCall()
> await channel.send(i * i)
> }
> channel.close()
> } catch (Exception failure) {
> channel.close(failure)
> }
> }
> def seen = []
> try {
> for await (value in channel) {
> seen << value
> }
> } catch (Exception failure) {
> // failure is the exception thrown by someCall()
> }
> {code}
> {{close(failure)}} returns nothing. {{null}} is a normal close. The first
> close wins.
> ||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)}} on a channel that retains it|{{receive}} and {{send}}
> fail with {{ChannelClosedException}} whose cause is {{failure}}. {{for
> await}} and {{ChannelSelect}} throw {{failure}} after the values already
> sent.|
> |Every selected channel closed normally|{{ChannelSelect}} fails with a
> {{ChannelClosedException}} whose message is "all channels are closed".|
> |A transfer already committed when another channel closes|That transfer stays
> the result. A withdrawn branch, and {{CancellationException}}, are not
> closes.|
> {{filter}}, {{map}}, {{merge}}, {{split}}, and {{tap}} carry a source
> failure, or a failure thrown by the callback, through to their output.
> Closing one side of a {{split}} stops the split and closes the other side
> normally. A failure close of one side is not copied onto the other.
> {{BroadcastChannel.close(failure)}} closes every subscriber the same way.
> {{for await}} on a subscriber throws that failure, and the publisher view
> reports it with {{onError}}.
> {{getCloseCause()}} reports the retained failure, or {{null}} when the
> channel is open, was closed normally, or does not retain a cause.
> h2. Compatibility
> {{close(Throwable)}} and {{getCloseCause()}} are default methods, added after
> the 6.0.0 channel API. A class compiled against that API does not need to be
> recompiled and does not throw {{AbstractMethodError}}.
> * The default {{close(Throwable)}} calls {{close()}} and does not retain the
> cause. {{for await}} on that channel ends. It does not throw the failure.
> * The channel from {{AsyncChannel.create}} does retain the cause. {{close()}}
> and {{close(Throwable)}} share one close, so neither calls the other. The
> cause is stored before the closed flag, under the same lock.
> * An implementation of {{close()}} must also override {{close(Throwable)}} if
> its {{close()}} calls that method. The default calls {{close()}} again.
> * 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)