[
https://issues.apache.org/jira/browse/GROOVY-12323?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-12323:
-------------------------------
Description:
{{ChannelSelect}} (GROOVY-12320) selects among *receives*: input guards. A
process therefore cannot say "I will send {{ping}}, *or* take {{pong}} if my
peer sends first" — the _mixed choice_ of the session-type and CSP literature.
occam banned output guards because arbitrating them needs a commit protocol
(Buckley & Silberschatz, _An effective implementation for the generalized
input-output construct of CSP_, TOPLAS 1983); JCSP's {{Alternative}} kept the
same restriction. GROOVY-12320 already built half of the machinery a modern
runtime needs to lift it: the claim-based receive ({{receiveIfUnclaimed}}).
This proposes the other half: a send offer that can be claimed or retired, so a
select over mixed offers commits *exactly one*.
The observations behind this are runnable:
[MixedChoiceRepro.groovy|https://github.com/paulk-asert/groovy-verify/blob/main/repro/MixedChoiceRepro.groovy],
identical on 6.0.0-beta-3 and on a 6.0.0-SNAPSHOT carrying GROOVY-12320.
h2. Proposed API
{code:groovy}
import static groovy.concurrent.ChannelSelect.*
def r = await offers(send(ping, i), receive(pong)).select()
if (r.index == 0) { /* my open committed: continue the ping branch */ }
else { /* my peer's open won: r.value is the pong opener */ }
{code}
* {{send(chan, value)}} — an offer to transfer {{value}} into {{chan}}. (No
{{Supplier<V>}} form in v1: the commit happens under the channel lock, so a
supplier would run user code under that lock or be pre-evaluated, defeating its
purpose — the value is required up front.)
* {{receive(chan)}} — today's input guard, unchanged.
* {{select()}} commits exactly one offer of this select; committing any offer
atomically retires the others.
* {{Result}} defines its send-commit shape explicitly: {{getValue()}} (the sent
value) and a branch-kind accessor ({{isSend()}}), rather than callers inferring
from the index.
* A send offer to a *closed* channel fails that branch and counts toward the
all-closed {{ChannelClosedException}} fast-fail, mirroring the receive
behaviour.
* Policies compose: {{fair()}} / {{random()}} order the scan over offers
exactly as GROOVY-12320 does over branches.
* A committed send behaves exactly like {{chan.send(v)}}; a retired one has NO
effect on the channel — no buffered residue, mirroring the claim-based
receive's "losers untouched".
h2. The real work: a revertible claim
GROOVY-12320 built the easy half, and this should be said plainly. A claimable
_receive_ never needs pairing — the claim is tested when a value is provably
present under a single channel lock. A send offer meeting a receive offer is a
genuine TWO-party commit across two selects' claims, and the current
irreversible boolean claim ({{Winner.claim}}, an {{AtomicBoolean}}) cannot
express it: claim yourself first and a failed CAS on the peer leaves you
committed with no transfer (and no guaranteed rescan — plain receivers withdraw
lock-free from the {{ConcurrentLinkedDeque}}); claim the peer first and a
failed CAS on yourself has committed the peer to a transfer that never happens.
The claim must become a three-state machine — OPEN → PENDING(owner) →
COMMITTED, with PENDING revertible and pending acquisition ordered/tie-broken
against livelock between symmetric peers: Buckley–Silberschatz's actual
protocol. Every existing claim site then speaks that machine (the receive path,
{{deliverToWaitingReceiver}}, {{drainBufferToReceivers}}, and
{{Winner.cancel}}, whose CAS-based timeout/cancel race is currently
load-bearing — the "holding the claim, this cannot fail" invariant).
{{waitingSenders}} (a plain {{ArrayDeque}} whose removal takes the channel
lock) needs the same lock-free-withdrawal treatment as receivers, or a losing
send offer withdrawn from inside a winning channel's delivery hits exactly the
cross-channel-lock deadlock the class comment warns about.
The alternative discipline is Go's — lock all member channels in a global order
during the scan, so the active party self-commits and one CAS on the parked
peer suffices — but this class deliberately rejected multi-channel locking
(that is why {{waitingReceivers}} became a {{ConcurrentLinkedDeque}}).
Contained to two classes either way; it needs stress testing under real
contention, not just unit tests.
h2. The coherence caveat, stated up front
Arbitration restores SESSION coherence only where a send cannot complete
unilaterally — that is, over RENDEZVOUS (capacity-0) channels, which
{{DefaultAsyncChannel(0)}} already supports (a capacity-0 {{send}} returns a
pending promise that completes exactly when a receiver takes it). Run the mixed
choice over buffered channels with the proposed API and both send offers find
buffer space, commit under only their own select's claim, and the collision of
experiment 1 reproduces exactly, through the new feature. Each select's claim
arbitrates within that select; coherence BETWEEN two selects comes only from
the rendezvous itself. Go has the identical property — its mixed-choice idioms
use unbuffered channels. Send offers on buffered channels remain meaningful (a
space-driven select: "send when room frees, or take from the other branch") —
they just do not give cross-select session coherence, and the docs should say
so, or the collision will be filed as a bug against the new feature.
h2. Why the buffered workaround is not one
With buffered channels each peer can just {{send}} its opener unconditionally —
but then _both_ sends succeed, and each peer reads the other's opener as "your
choice": one session, two peers on different branches, each sure of its own
(experiment 1; the session-type literature's classic coherence failure). Making
one peer the designated opener works (experiment 2), but it is priority, not a
race. Two polite peers that only offer to receive never start at all
(experiment 3). A one-line CAS shows the semantics wanted: 1000 racing trials,
exactly one branch committed in every one, zero double-commits, zero
non-commits (experiment 4) — that claim, run inside the select over the
channel's own machinery, is this proposal.
h2. Observed (both runtimes, MixedChoiceRepro.groovy)
# Both peers open: both buffered sends succeed; left continues down the PONG
branch while right continues down the PING branch — the collision.
# One initiator: the mixed choice degenerates to that peer's choice — works
today.
# Both polite: {{select().orTimeoutMillis(500)}} times out — nobody opens.
# The CAS claim: 1000/1000 trials commit exactly one branch.
# A capacity-0 (rendezvous) send pends until a receiver takes it, while a
buffered send completes alone — where cross-select coherence must come from.
h2. Relation to GROOVY-12320 — and to the class's own stated model
Same design centre: selection must not disturb what it does not take.
GROOVY-12320 made the _receive_ side claimable (losers untouched, all-closed
fails fast, {{fair()}}/{{random()}}); send offers are its completion — the
harder half. And the precedent is closer to home than occam:
{{ChannelSelect}}'s own javadoc cites Go's {{select}} as its inspiration, and
Go's {{select}} supports send cases — so this completes parity with the class's
stated model, not just with the generalized CSP alternative. The occam/JCSP ban
on output guards was motivated by DISTRIBUTED commit cost (Buckley–Silberschatz
1983); in shared memory that cost does not apply, and Go proved the construct
tractable there. With it, a compile-time session checker can certify the racing
mixed choice (over rendezvous channels) instead of refusing it.
Timing is worth weighing: {{ChannelSelect}} is {{@since 6.0.0}} and still in
beta, so landing the API surface before GA avoids a 6.x addition — against
which sits the protocol risk, since the claim state-machine touches invariants
already shipped in beta; if it slips past GA it remains additive and safe in
6.x.
> ChannelSelect: add claimable send offers (output guards)
> --------------------------------------------------------
>
> Key: GROOVY-12323
> URL: https://issues.apache.org/jira/browse/GROOVY-12323
> Project: Groovy
> Issue Type: Improvement
> Reporter: Paul King
> Priority: Major
>
> {{ChannelSelect}} (GROOVY-12320) selects among *receives*: input guards. A
> process therefore cannot say "I will send {{ping}}, *or* take {{pong}} if my
> peer sends first" — the _mixed choice_ of the session-type and CSP
> literature. occam banned output guards because arbitrating them needs a
> commit protocol (Buckley & Silberschatz, _An effective implementation for the
> generalized input-output construct of CSP_, TOPLAS 1983); JCSP's
> {{Alternative}} kept the same restriction. GROOVY-12320 already built half of
> the machinery a modern runtime needs to lift it: the claim-based receive
> ({{receiveIfUnclaimed}}). This proposes the other half: a send offer that can
> be claimed or retired, so a select over mixed offers commits *exactly one*.
> The observations behind this are runnable:
> [MixedChoiceRepro.groovy|https://github.com/paulk-asert/groovy-verify/blob/main/repro/MixedChoiceRepro.groovy],
> identical on 6.0.0-beta-3 and on a 6.0.0-SNAPSHOT carrying GROOVY-12320.
> h2. Proposed API
> {code:groovy}
> import static groovy.concurrent.ChannelSelect.*
> def r = await offers(send(ping, i), receive(pong)).select()
> if (r.index == 0) { /* my open committed: continue the ping branch */ }
> else { /* my peer's open won: r.value is the pong opener */ }
> {code}
> * {{send(chan, value)}} — an offer to transfer {{value}} into {{chan}}. (No
> {{Supplier<V>}} form in v1: the commit happens under the channel lock, so a
> supplier would run user code under that lock or be pre-evaluated, defeating
> its purpose — the value is required up front.)
> * {{receive(chan)}} — today's input guard, unchanged.
> * {{select()}} commits exactly one offer of this select; committing any offer
> atomically retires the others.
> * {{Result}} defines its send-commit shape explicitly: {{getValue()}} (the
> sent value) and a branch-kind accessor ({{isSend()}}), rather than callers
> inferring from the index.
> * A send offer to a *closed* channel fails that branch and counts toward the
> all-closed {{ChannelClosedException}} fast-fail, mirroring the receive
> behaviour.
> * Policies compose: {{fair()}} / {{random()}} order the scan over offers
> exactly as GROOVY-12320 does over branches.
> * A committed send behaves exactly like {{chan.send(v)}}; a retired one has
> NO effect on the channel — no buffered residue, mirroring the claim-based
> receive's "losers untouched".
> h2. The real work: a revertible claim
> GROOVY-12320 built the easy half, and this should be said plainly. A
> claimable _receive_ never needs pairing — the claim is tested when a value is
> provably present under a single channel lock. A send offer meeting a receive
> offer is a genuine TWO-party commit across two selects' claims, and the
> current irreversible boolean claim ({{Winner.claim}}, an {{AtomicBoolean}})
> cannot express it: claim yourself first and a failed CAS on the peer leaves
> you committed with no transfer (and no guaranteed rescan — plain receivers
> withdraw lock-free from the {{ConcurrentLinkedDeque}}); claim the peer first
> and a failed CAS on yourself has committed the peer to a transfer that never
> happens. The claim must become a three-state machine — OPEN → PENDING(owner)
> → COMMITTED, with PENDING revertible and pending acquisition
> ordered/tie-broken against livelock between symmetric peers:
> Buckley–Silberschatz's actual protocol. Every existing claim site then speaks
> that machine (the receive path, {{deliverToWaitingReceiver}},
> {{drainBufferToReceivers}}, and {{Winner.cancel}}, whose CAS-based
> timeout/cancel race is currently load-bearing — the "holding the claim, this
> cannot fail" invariant). {{waitingSenders}} (a plain {{ArrayDeque}} whose
> removal takes the channel lock) needs the same lock-free-withdrawal treatment
> as receivers, or a losing send offer withdrawn from inside a winning
> channel's delivery hits exactly the cross-channel-lock deadlock the class
> comment warns about.
> The alternative discipline is Go's — lock all member channels in a global
> order during the scan, so the active party self-commits and one CAS on the
> parked peer suffices — but this class deliberately rejected multi-channel
> locking (that is why {{waitingReceivers}} became a
> {{ConcurrentLinkedDeque}}). Contained to two classes either way; it needs
> stress testing under real contention, not just unit tests.
> h2. The coherence caveat, stated up front
> Arbitration restores SESSION coherence only where a send cannot complete
> unilaterally — that is, over RENDEZVOUS (capacity-0) channels, which
> {{DefaultAsyncChannel(0)}} already supports (a capacity-0 {{send}} returns a
> pending promise that completes exactly when a receiver takes it). Run the
> mixed choice over buffered channels with the proposed API and both send
> offers find buffer space, commit under only their own select's claim, and the
> collision of experiment 1 reproduces exactly, through the new feature. Each
> select's claim arbitrates within that select; coherence BETWEEN two selects
> comes only from the rendezvous itself. Go has the identical property — its
> mixed-choice idioms use unbuffered channels. Send offers on buffered channels
> remain meaningful (a space-driven select: "send when room frees, or take from
> the other branch") — they just do not give cross-select session coherence,
> and the docs should say so, or the collision will be filed as a bug against
> the new feature.
> h2. Why the buffered workaround is not one
> With buffered channels each peer can just {{send}} its opener unconditionally
> — but then _both_ sends succeed, and each peer reads the other's opener as
> "your choice": one session, two peers on different branches, each sure of its
> own (experiment 1; the session-type literature's classic coherence failure).
> Making one peer the designated opener works (experiment 2), but it is
> priority, not a race. Two polite peers that only offer to receive never start
> at all (experiment 3). A one-line CAS shows the semantics wanted: 1000 racing
> trials, exactly one branch committed in every one, zero double-commits, zero
> non-commits (experiment 4) — that claim, run inside the select over the
> channel's own machinery, is this proposal.
> h2. Observed (both runtimes, MixedChoiceRepro.groovy)
> # Both peers open: both buffered sends succeed; left continues down the PONG
> branch while right continues down the PING branch — the collision.
> # One initiator: the mixed choice degenerates to that peer's choice — works
> today.
> # Both polite: {{select().orTimeoutMillis(500)}} times out — nobody opens.
> # The CAS claim: 1000/1000 trials commit exactly one branch.
> # A capacity-0 (rendezvous) send pends until a receiver takes it, while a
> buffered send completes alone — where cross-select coherence must come from.
> h2. Relation to GROOVY-12320 — and to the class's own stated model
> Same design centre: selection must not disturb what it does not take.
> GROOVY-12320 made the _receive_ side claimable (losers untouched, all-closed
> fails fast, {{fair()}}/{{random()}}); send offers are its completion — the
> harder half. And the precedent is closer to home than occam:
> {{ChannelSelect}}'s own javadoc cites Go's {{select}} as its inspiration, and
> Go's {{select}} supports send cases — so this completes parity with the
> class's stated model, not just with the generalized CSP alternative. The
> occam/JCSP ban on output guards was motivated by DISTRIBUTED commit cost
> (Buckley–Silberschatz 1983); in shared memory that cost does not apply, and
> Go proved the construct tractable there. With it, a compile-time session
> checker can certify the racing mixed choice (over rendezvous channels)
> instead of refusing it.
> Timing is worth weighing: {{ChannelSelect}} is {{@since 6.0.0}} and still in
> beta, so landing the API surface before GA avoids a 6.x addition — against
> which sits the protocol risk, since the claim state-machine touches
> invariants already shipped in beta; if it slips past GA it remains additive
> and safe in 6.x.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)