[ 
https://issues.apache.org/jira/browse/GROOVY-12323?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Paul King resolved GROOVY-12323.
--------------------------------
    Fix Version/s: 6.0.0-beta-4
       Resolution: Fixed

> 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
>            Assignee: Paul King
>            Priority: Major
>             Fix For: 6.0.0-beta-4
>
>
> {{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)

Reply via email to