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

Paul King updated GROOVY-12343:
-------------------------------
    Description: 
h2. Summary

A {{ChannelSelect}} cannot have a timeout branch. Every comparable system has 
one — it is how "wait for
work, but not forever" is written — and so, already, does the OTHER half of 
this library: an actor can
schedule a message to itself, a select cannot wait on a deadline.

There is a workaround, and it is worth being precise about how far it goes, 
because it goes further than it
first appears. What follows proposes the feature in two layers — a timer 
channel, which needs no change to
{{ChannelSelect}} at all, and a timer offer on top of it, which is what makes a 
per-round deadline compose
with a held select.

h2. What a user writes today

{{Awaitable.orTimeout}} wraps the whole select:

{code:groovy}
try {
    def r = await alt.select().orTimeoutMillis(100)
    ...
} catch (TimeoutException e) {
    ... // the timeout branch
}
{code}

This is *safe*. {{AsyncSupport.completeOnTimeout}}/{{orTimeout}} cancel the 
source when the deadline fires,
and {{Winner.cancel}} is {{claim.tryCommitCancel() && super.cancel(…)}}, so the 
cancellation wins the claim
and no offer commits. GROOVY-12320's protocol already makes the outer timeout 
correct — nothing is consumed
and lost. That is worth saying plainly, since it is the first thing a reviewer 
will worry about.

What it does not give:

* *Exceptions for expected control flow.* A timeout that is part of the design 
is signalled by
  {{TimeoutException}} rather than by a branch.
* *No fallback form.* {{completeOnTimeout}} needs a fallback value, and 
{{ChannelSelect.Result}}'s
  constructor is package-private — a caller cannot construct one. So only the 
throwing form is usable here.
* *One deadline for the whole select.* Different deadlines on different 
branches need nested selects or
  extra tasks.
* *No guarded timeout.* The deadline sits outside the select, so {{Offer.when 
\{ … \}}} (GROOVY-12326)
  cannot reach it. "Time out only while the buffer is empty" — never 
mid-transaction — is not expressible.
* *No composition with the choice policy.* The timeout is not a branch, so it 
is outside {{fair()}} /
  {{random()}} rotation and outside the {{select(boolean...)}} mask.

h2. What it enables

Ordinary, and common:

* wait for work, and on silence emit a heartbeat, retry, log, or shut down;
* serve requests while a periodic tick flushes a batch, emits metrics, or polls 
for shutdown.

Both are achievable today with try/catch. The two that are genuinely awkward 
are per-branch deadlines and
the guarded timeout above.

h2. The library is already inconsistent about this

The strongest argument is not what other ecosystems do; it is that 
{{groovy.concurrent}} has already decided
this question once, on the other side of the library. {{ActorContext}} carries:

{code:java}
Cancellable scheduleOnce(T message, java.time.Duration delay)
Cancellable scheduleAtFixedRate(T message, java.time.Duration initial, 
java.time.Duration period)
{code}

So an actor can say "deliver me this in 100ms" and interleave it with the 
messages it is already handling —
which is the actor spelling of exactly the shape a timer branch gives a select. 
The CSP half has no
equivalent. {{AsyncChannel.after(…)}} is therefore not a new capability for the 
library so much as parity
between its two halves, and the absence is more likely to read as an oversight 
than as a decision.

The same asymmetry shows in the bounded forms: {{ActorOptions}} has 
{{withBoundedMailbox(n, Overflow)}} and
{{withStashBound(n, StashOverflow)}}, so the actor side is already explicit 
about time and about bounds,
while the channel side is explicit about neither.

h2. Prior art — everyone has this

|| System || How the timeout appears || Timer is… ||
| Go | {{case <-time.After(d):}} | a channel |
| Rust (crossbeam-channel) | {{recv(after(timeout)) -> _ =>}} | a channel 
({{after(Duration) -> Receiver<Instant>}}) |
| Clojure core.async | {{(alts! \[ch (timeout 1000)\])}} | a channel |
| Kotlin coroutines | {{select \{ onTimeout(ms) \{ … \} \}}} | a select clause |
| Erlang | {{receive … after Timeout ->}} | language syntax |
| Ada | {{select … or delay 5.0;}} | language syntax |
| JCSP | {{CSTimer}} in the {{Guard\[\]}} | a guard kind |
| *Groovy 6 today* | *— (outer wrapper only)* | *—* |

Kotlin is the closest precedent for Groovy's situation: it has BOTH the outer 
wrapper
({{withTimeout}}/{{withTimeoutOrNull}}) and the branch ({{onTimeout}}). Having 
the wrapper is not treated as
making the branch redundant.

Note the split in that table. Go, Rust and Clojure make *the timer a channel*; 
Kotlin makes it *a select
clause*. Both shapes are in wide use, which is the first hint that they are 
layers rather than rivals — and
it shapes the proposal below.

h2. The proposal, in two layers

These two shapes are naturally presented as competing options. They are not 
alternatives: the second is
implementable on top of the first, and each covers a case the other handles 
badly. The suggestion is to take
layer 1 now, and layer 2 when the ergonomics ask for it.

h3. Layer 1 — a timer CHANNEL (the primitive)

{code:java}
public static AsyncChannel<java.time.Instant> after(long millis)
public static AsyncChannel<java.time.Instant> after(java.time.Duration duration)
{code}

A capacity-1 channel that delivers one element — the instant it fired — after 
the given delay.

{code:groovy}
def alt = ChannelSelect.from(work, AsyncChannel.after(100))
def r = await alt.select()
if (r.index == 1) { ... timed out ... }
{code}

It needs *no change to {{ChannelSelect}} at all* — a channel is already 
offerable — and composes with
everything there for free: {{from(…)}} and {{offers(receive(…))}}, {{Offer.when 
\{ … \}}} for the guarded
timeout, {{fair()}} / {{random()}}, the {{select(boolean...)}} mask, and 
{{Result.getChannel()}}, which
identifies the timer like any other branch rather than having to return null. 
It is also useful *outside* a
select — {{await AsyncChannel.after(50).first()}} as a delay, or as one arm of 
{{Awaitable.any}} — where a
select-only construct would not reach. And it is the design three of the four 
comparable ecosystems chose.

*Implementation.* One scheduled task on the scheduler {{AsyncSupport}} already 
uses for {{orTimeout}},
feeding a capacity-1 channel. The single new concern is cancellation: when a 
select withdraws its losing
branches the timer's {{ScheduledFuture}} should be cancelled too, or an 
abandoned select leaves a task
pending until it fires.

h3. Where layer 1 is awkward — the per-round deadline

This is what argues for a second layer, and it is a consequence of GROOVY-12320 
rather than a matter of
taste.

"Wait for work, but not more than 100ms _this round_" is the commonest select 
timeout there is. With a timer
channel alone it comes out as:

{code:groovy}
while (true) {
    def r = await ChannelSelect.from(work, AsyncChannel.after(100)).select()   
// rebuilt every round
    ...
}
{code}

The deadline must be fresh each iteration, so the select is rebuilt each 
iteration — and a rebuilt select is
a new instance with no memory. Since GROOVY-12320 the instance is exactly where 
the choice policy's state
lives: a held {{fair()}} rotates from the last winner, and a select constructed 
inside the loop rotates from
nothing every time. So the natural spelling of a per-round deadline silently 
forfeits fairness, which is a
poor trade to make by accident.

It is the same consideration that made GROOVY-12326's guard a 
{{BooleanSupplier}} rather than a
{{boolean}} — consulted at every select, so the instance can still be held. A 
deadline that re-arms per
select rather than being constructed per select is that idea applied to time.

h3. Layer 2 — a timer OFFER (the ergonomics)

{code:java}
public static ChannelSelect.Offer after(long millis)
{code}

{code:groovy}
def alt = ChannelSelect.offers(ChannelSelect.receive(work),
                               ChannelSelect.after(100)).fair()      // held 
once
while (true) {
    def r = await alt.select()        // the deadline re-arms; the rotation 
survives
    ...
}
{code}

Symmetric with the existing {{send(…)}} / {{receive(…)}} offers, and closest to 
JCSP's {{CSTimer}} guard.
Semantically it is a receive offer on a timer channel that the select creates 
and cancels per round — so
layer 1 is a reasonable implementation of it. The sugar is thin, and it is the 
sugar that lets the instance
be held.

The cost is spread wider than layer 1. {{Offer}} currently always has a 
channel, so it gains a nullable one
plus a delay (or a subtype); {{Result.getChannel()}} must return null for a 
timer win, and say so; the
registration loop in {{select(int\[\])}} gains a third path beside 
{{sendIfUnclaimed}} /
{{receiveIfUnclaimed}} — though that path is largely written already, since a 
timer can compete for the
claim exactly as a non-claimable foreign channel does, via 
{{winner.claim.tryCommit(future)}}.

*A third way, for completeness.* A resettable timer channel — {{reset(long 
millis)}} on the channel layer 1
returns — would also solve the per-round case, and is how Go answers the same 
caveat for {{time.After}} in a
loop. It trades a new offer kind for a mutable channel. Worth weighing against 
layer 2 rather than
overlooked.

*And one question layer 2 settles that is wider than timers.* It asks whether 
an {{Offer}} may be something
other than a channel operation. If it may, the same mechanism admits a barrier 
branch — JCSP's
{{AltingBarrier}}, a phase synchronisation that can lose a race to a channel — 
which is the other member of
the guard family {{groovy.concurrent}} has no port for. Layer 1 neither opens 
that door nor closes it. If
the answer is "offers stay channel operations", that is a perfectly good 
answer; it is just better given
deliberately than by default.

h2. Open questions

* *When does the clock start?* At creation, as in Go and crossbeam, is the 
obvious answer for layer 1 — and
  it is what makes a fresh timer per loop iteration the natural (and, per 
above, lossy) spelling. Layer 2
  and the resettable form are the two answers to that; which one lands decides 
whether layer 1 needs a
  {{reset}} at all.
* *Periodic ticks.* Go has {{time.Tick}} and crossbeam {{tick(d)}} — a channel 
that fires repeatedly. That
  is the natural companion for "every 100ms, flush", and is probably a separate 
issue rather than part of
  this one.
* *Element type.* {{Instant}} matches crossbeam and is more useful than 
{{null}} or a token; Go sends the
  time too.

h2. Provenance

Found while porting Jon Kerridge's _Using Concurrency and Parallelism 
Effectively_ onto
{{groovy.concurrent}} with a static verifier — the same exercise behind 
GROOVY-12320, GROOVY-12323,
GROOVY-12324 and GROOVY-12326. Three of the book's chapters put a timer in an 
{{ALT}} (c05's scaling device,
c14's hand-eye test, c17's sampling {{Sniffer}}), and none of them ports: the 
timeout is part of the choice
in each, not a wrapper around it. With input, output and boolean guards now all 
present — GROOVY-12323,
GROOVY-12324 and GROOVY-12326 respectively — the timer is the guard the gallery 
misses most often. The
remaining one, {{AltingBarrier}}, is noted under layer 2 above; it is not asked 
for here, only kept in view
so the offer question is answered once rather than twice.


  was:
h2. Summary

A {{ChannelSelect}} cannot have a timeout branch. Every comparable system has 
one — it is how "wait for
work, but not forever" is written — and so, already, does the OTHER half of 
this library: an actor can
schedule a message to itself, a select cannot wait on a deadline.

There is a workaround, and it is worth being precise about how far it goes, 
because it goes further than it
first appears.

h2. What a user writes today

{{Awaitable.orTimeout}} wraps the whole select:

{code:groovy}
try {
    def r = await alt.select().orTimeoutMillis(100)
    ...
} catch (TimeoutException e) {
    ... // the timeout branch
}
{code}

This is *safe*. {{AsyncSupport.completeOnTimeout}}/{{orTimeout}} cancel the 
source when the deadline fires,
and {{Winner.cancel}} is {{claim.tryCommitCancel() && super.cancel(…)}}, so the 
cancellation wins the claim
and no offer commits. GROOVY-12320's protocol already makes the outer timeout 
correct — nothing is consumed
and lost. That is worth saying plainly, since it is the first thing a reviewer 
will worry about.

What it does not give:

* *Exceptions for expected control flow.* A timeout that is part of the design 
is signalled by
  {{TimeoutException}} rather than by a branch.
* *No fallback form.* {{completeOnTimeout}} needs a fallback value, and 
{{ChannelSelect.Result}}'s
  constructor is package-private — a caller cannot construct one. So only the 
throwing form is usable here.
* *One deadline for the whole select.* Different deadlines on different 
branches need nested selects or
  extra tasks.
* *No guarded timeout.* The deadline sits outside the select, so {{Offer.when 
\{ … \}}} (GROOVY-12326)
  cannot reach it. "Time out only while the buffer is empty" — never 
mid-transaction — is not expressible.
* *No composition with the choice policy.* The timeout is not a branch, so it 
is outside {{fair()}} /
  {{random()}} rotation and outside the {{select(boolean...)}} mask.

h2. What it enables

Ordinary, and common:

* wait for work, and on silence emit a heartbeat, retry, log, or shut down;
* serve requests while a periodic tick flushes a batch, emits metrics, or polls 
for shutdown.

Both are achievable today with try/catch. The two that are genuinely awkward 
are per-branch deadlines and
the guarded timeout above.

h2. The library is already inconsistent about this

The strongest argument is not what other ecosystems do; it is that 
{{groovy.concurrent}} has already decided
this question once, on the other side of the library. {{ActorContext}} carries:

{code:java}
Cancellable scheduleOnce(T message, java.time.Duration delay)
Cancellable scheduleAtFixedRate(T message, java.time.Duration initial, 
java.time.Duration period)
{code}

So an actor can say "deliver me this in 100ms" and interleave it with the 
messages it is already handling —
which is the actor spelling of exactly the shape a timer branch gives a select. 
The CSP half has no
equivalent. {{AsyncChannel.after(…)}} is therefore not a new capability for the 
library so much as parity
between its two halves, and the absence is more likely to read as an oversight 
than as a decision.

The same asymmetry shows in the bounded forms: {{ActorOptions}} has 
{{withBoundedMailbox(n, Overflow)}} and
{{withStashBound(n, StashOverflow)}}, so the actor side is already explicit 
about time and about bounds,
while the channel side is explicit about neither.

h2. Prior art — everyone has this

|| System || How the timeout appears || Timer is… ||
| Go | {{case <-time.After(d):}} | a channel |
| Rust (crossbeam-channel) | {{recv(after(timeout)) -> _ =>}} | a channel 
({{after(Duration) -> Receiver<Instant>}}) |
| Clojure core.async | {{(alts! \[ch (timeout 1000)\])}} | a channel |
| Kotlin coroutines | {{select \{ onTimeout(ms) \{ … \} \}}} | a select clause |
| Erlang | {{receive … after Timeout ->}} | language syntax |
| Ada | {{select … or delay 5.0;}} | language syntax |
| JCSP | {{CSTimer}} in the {{Guard\[\]}} | a guard kind |
| *Groovy 6 today* | *— (outer wrapper only)* | *—* |

Kotlin is the closest precedent for Groovy's situation: it has BOTH the outer 
wrapper
({{withTimeout}}/{{withTimeoutOrNull}}) and the branch ({{onTimeout}}). Having 
the wrapper is not treated as
making the branch redundant.

Note what Go, Rust and Clojure share: *the timer is a channel*, not a special 
branch kind. That shapes the
proposal below.

h2. Option A (preferred) — a timer CHANNEL

{code:java}
public static AsyncChannel<java.time.Instant> after(long millis)
public static AsyncChannel<java.time.Instant> after(java.time.Duration duration)
{code}

A capacity-1 channel that delivers one element — the instant it fired — after 
the given delay.

{code:groovy}
def alt = ChannelSelect.from(work, AsyncChannel.after(100))
def r = await alt.select()
if (r.index == 1) { ... timed out ... }
{code}

*Why this one.* It needs *no change to {{ChannelSelect}} at all*, and it 
composes with everything already
there for free: {{from(…)}} and {{offers(receive(…))}}, {{Offer.when \{ … \}}} 
for the guarded timeout,
{{fair()}} / {{random()}} rotation, the {{select(boolean...)}} mask, and 
{{Result.getChannel()}} (which
identifies the timer like any other branch, rather than having to return null). 
It is also useful *outside*
a select — {{await AsyncChannel.after(50).first()}} as a delay, or as one arm 
of {{Awaitable.any}} — where a
select-only offer would not reach. And it is the design three of the four 
comparable ecosystems chose.

*Implementation.* A channel whose element is produced by one scheduled task, on 
the same scheduler
{{AsyncSupport}} already uses for {{orTimeout}}. The single new concern is 
*cancellation*: when a select
withdraws its losing branches the timer's {{ScheduledFuture}} should be 
cancelled too, or an abandoned
select leaves a task pending until it fires. That is one thing to get right, 
against five for Option B.

h2. Option B — a timer OFFER

{code:java}
public static ChannelSelect.Offer after(long millis)
{code}

Symmetric with the existing {{send(…)}} / {{receive(…)}} offers, and closest to 
JCSP's {{CSTimer}} guard.

The cost is spread wider. {{Offer}} currently always has a channel, so it gains 
a nullable one plus a delay
(or a subtype); {{Result.getChannel()}} must return null for a timer win, and 
say so; the registration loop
in {{select(int\[\])}} gains a third path beside {{sendIfUnclaimed}} / 
{{receiveIfUnclaimed}} — though that
path is largely written already, since a timer can compete for the claim 
exactly as a non-claimable foreign
channel does, via {{winner.claim.tryCommit(future)}}. Withdrawal must still 
cancel the scheduled task.

Option B is defensible if the symmetry with {{send}}/{{receive}} matters more 
than the reuse; it is listed
so the choice is explicit rather than implied.

h2. Open questions

* *When does the clock start?* At creation, as in Go and crossbeam, is the 
obvious answer — and it means a
  fresh timer per loop iteration, so a long-running loop allocates one 
scheduled task per round. Go carries
  the same caveat for {{time.After}} in a loop and answers it with a resettable 
{{Timer}}. Worth deciding
  whether Groovy wants a resettable form now or later.
* *Periodic ticks.* Go has {{time.Tick}} and crossbeam {{tick(d)}} — a channel 
that fires repeatedly. That
  is the natural companion for "every 100ms, flush", and is probably a separate 
issue rather than part of
  this one.
* *Element type.* {{Instant}} matches crossbeam and is more useful than 
{{null}} or a token; Go sends the
  time too.

h2. Provenance

Found while porting Jon Kerridge's _Using Concurrency and Parallelism 
Effectively_ onto
{{groovy.concurrent}} with a static verifier — the same exercise behind 
GROOVY-12320, GROOVY-12323,
GROOVY-12324 and GROOVY-12326. Three of the book's chapters put a timer in an 
{{ALT}} (c05's scaling device,
c14's hand-eye test, c17's sampling {{Sniffer}}), and none of them ports: the 
timeout is part of the choice
in each, not a wrapper around it. With input, output and boolean guards now all 
present, the timer is the
one member of JCSP's guard family Groovy's select still lacks.



> Provide AsyncChannel.after
> --------------------------
>
>                 Key: GROOVY-12343
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12343
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>
> h2. Summary
> A {{ChannelSelect}} cannot have a timeout branch. Every comparable system has 
> one — it is how "wait for
> work, but not forever" is written — and so, already, does the OTHER half of 
> this library: an actor can
> schedule a message to itself, a select cannot wait on a deadline.
> There is a workaround, and it is worth being precise about how far it goes, 
> because it goes further than it
> first appears. What follows proposes the feature in two layers — a timer 
> channel, which needs no change to
> {{ChannelSelect}} at all, and a timer offer on top of it, which is what makes 
> a per-round deadline compose
> with a held select.
> h2. What a user writes today
> {{Awaitable.orTimeout}} wraps the whole select:
> {code:groovy}
> try {
>     def r = await alt.select().orTimeoutMillis(100)
>     ...
> } catch (TimeoutException e) {
>     ... // the timeout branch
> }
> {code}
> This is *safe*. {{AsyncSupport.completeOnTimeout}}/{{orTimeout}} cancel the 
> source when the deadline fires,
> and {{Winner.cancel}} is {{claim.tryCommitCancel() && super.cancel(…)}}, so 
> the cancellation wins the claim
> and no offer commits. GROOVY-12320's protocol already makes the outer timeout 
> correct — nothing is consumed
> and lost. That is worth saying plainly, since it is the first thing a 
> reviewer will worry about.
> What it does not give:
> * *Exceptions for expected control flow.* A timeout that is part of the 
> design is signalled by
>   {{TimeoutException}} rather than by a branch.
> * *No fallback form.* {{completeOnTimeout}} needs a fallback value, and 
> {{ChannelSelect.Result}}'s
>   constructor is package-private — a caller cannot construct one. So only the 
> throwing form is usable here.
> * *One deadline for the whole select.* Different deadlines on different 
> branches need nested selects or
>   extra tasks.
> * *No guarded timeout.* The deadline sits outside the select, so {{Offer.when 
> \{ … \}}} (GROOVY-12326)
>   cannot reach it. "Time out only while the buffer is empty" — never 
> mid-transaction — is not expressible.
> * *No composition with the choice policy.* The timeout is not a branch, so it 
> is outside {{fair()}} /
>   {{random()}} rotation and outside the {{select(boolean...)}} mask.
> h2. What it enables
> Ordinary, and common:
> * wait for work, and on silence emit a heartbeat, retry, log, or shut down;
> * serve requests while a periodic tick flushes a batch, emits metrics, or 
> polls for shutdown.
> Both are achievable today with try/catch. The two that are genuinely awkward 
> are per-branch deadlines and
> the guarded timeout above.
> h2. The library is already inconsistent about this
> The strongest argument is not what other ecosystems do; it is that 
> {{groovy.concurrent}} has already decided
> this question once, on the other side of the library. {{ActorContext}} 
> carries:
> {code:java}
> Cancellable scheduleOnce(T message, java.time.Duration delay)
> Cancellable scheduleAtFixedRate(T message, java.time.Duration initial, 
> java.time.Duration period)
> {code}
> So an actor can say "deliver me this in 100ms" and interleave it with the 
> messages it is already handling —
> which is the actor spelling of exactly the shape a timer branch gives a 
> select. The CSP half has no
> equivalent. {{AsyncChannel.after(…)}} is therefore not a new capability for 
> the library so much as parity
> between its two halves, and the absence is more likely to read as an 
> oversight than as a decision.
> The same asymmetry shows in the bounded forms: {{ActorOptions}} has 
> {{withBoundedMailbox(n, Overflow)}} and
> {{withStashBound(n, StashOverflow)}}, so the actor side is already explicit 
> about time and about bounds,
> while the channel side is explicit about neither.
> h2. Prior art — everyone has this
> || System || How the timeout appears || Timer is… ||
> | Go | {{case <-time.After(d):}} | a channel |
> | Rust (crossbeam-channel) | {{recv(after(timeout)) -> _ =>}} | a channel 
> ({{after(Duration) -> Receiver<Instant>}}) |
> | Clojure core.async | {{(alts! \[ch (timeout 1000)\])}} | a channel |
> | Kotlin coroutines | {{select \{ onTimeout(ms) \{ … \} \}}} | a select 
> clause |
> | Erlang | {{receive … after Timeout ->}} | language syntax |
> | Ada | {{select … or delay 5.0;}} | language syntax |
> | JCSP | {{CSTimer}} in the {{Guard\[\]}} | a guard kind |
> | *Groovy 6 today* | *— (outer wrapper only)* | *—* |
> Kotlin is the closest precedent for Groovy's situation: it has BOTH the outer 
> wrapper
> ({{withTimeout}}/{{withTimeoutOrNull}}) and the branch ({{onTimeout}}). 
> Having the wrapper is not treated as
> making the branch redundant.
> Note the split in that table. Go, Rust and Clojure make *the timer a 
> channel*; Kotlin makes it *a select
> clause*. Both shapes are in wide use, which is the first hint that they are 
> layers rather than rivals — and
> it shapes the proposal below.
> h2. The proposal, in two layers
> These two shapes are naturally presented as competing options. They are not 
> alternatives: the second is
> implementable on top of the first, and each covers a case the other handles 
> badly. The suggestion is to take
> layer 1 now, and layer 2 when the ergonomics ask for it.
> h3. Layer 1 — a timer CHANNEL (the primitive)
> {code:java}
> public static AsyncChannel<java.time.Instant> after(long millis)
> public static AsyncChannel<java.time.Instant> after(java.time.Duration 
> duration)
> {code}
> A capacity-1 channel that delivers one element — the instant it fired — after 
> the given delay.
> {code:groovy}
> def alt = ChannelSelect.from(work, AsyncChannel.after(100))
> def r = await alt.select()
> if (r.index == 1) { ... timed out ... }
> {code}
> It needs *no change to {{ChannelSelect}} at all* — a channel is already 
> offerable — and composes with
> everything there for free: {{from(…)}} and {{offers(receive(…))}}, 
> {{Offer.when \{ … \}}} for the guarded
> timeout, {{fair()}} / {{random()}}, the {{select(boolean...)}} mask, and 
> {{Result.getChannel()}}, which
> identifies the timer like any other branch rather than having to return null. 
> It is also useful *outside* a
> select — {{await AsyncChannel.after(50).first()}} as a delay, or as one arm 
> of {{Awaitable.any}} — where a
> select-only construct would not reach. And it is the design three of the four 
> comparable ecosystems chose.
> *Implementation.* One scheduled task on the scheduler {{AsyncSupport}} 
> already uses for {{orTimeout}},
> feeding a capacity-1 channel. The single new concern is cancellation: when a 
> select withdraws its losing
> branches the timer's {{ScheduledFuture}} should be cancelled too, or an 
> abandoned select leaves a task
> pending until it fires.
> h3. Where layer 1 is awkward — the per-round deadline
> This is what argues for a second layer, and it is a consequence of 
> GROOVY-12320 rather than a matter of
> taste.
> "Wait for work, but not more than 100ms _this round_" is the commonest select 
> timeout there is. With a timer
> channel alone it comes out as:
> {code:groovy}
> while (true) {
>     def r = await ChannelSelect.from(work, AsyncChannel.after(100)).select()  
>  // rebuilt every round
>     ...
> }
> {code}
> The deadline must be fresh each iteration, so the select is rebuilt each 
> iteration — and a rebuilt select is
> a new instance with no memory. Since GROOVY-12320 the instance is exactly 
> where the choice policy's state
> lives: a held {{fair()}} rotates from the last winner, and a select 
> constructed inside the loop rotates from
> nothing every time. So the natural spelling of a per-round deadline silently 
> forfeits fairness, which is a
> poor trade to make by accident.
> It is the same consideration that made GROOVY-12326's guard a 
> {{BooleanSupplier}} rather than a
> {{boolean}} — consulted at every select, so the instance can still be held. A 
> deadline that re-arms per
> select rather than being constructed per select is that idea applied to time.
> h3. Layer 2 — a timer OFFER (the ergonomics)
> {code:java}
> public static ChannelSelect.Offer after(long millis)
> {code}
> {code:groovy}
> def alt = ChannelSelect.offers(ChannelSelect.receive(work),
>                                ChannelSelect.after(100)).fair()      // held 
> once
> while (true) {
>     def r = await alt.select()        // the deadline re-arms; the rotation 
> survives
>     ...
> }
> {code}
> Symmetric with the existing {{send(…)}} / {{receive(…)}} offers, and closest 
> to JCSP's {{CSTimer}} guard.
> Semantically it is a receive offer on a timer channel that the select creates 
> and cancels per round — so
> layer 1 is a reasonable implementation of it. The sugar is thin, and it is 
> the sugar that lets the instance
> be held.
> The cost is spread wider than layer 1. {{Offer}} currently always has a 
> channel, so it gains a nullable one
> plus a delay (or a subtype); {{Result.getChannel()}} must return null for a 
> timer win, and say so; the
> registration loop in {{select(int\[\])}} gains a third path beside 
> {{sendIfUnclaimed}} /
> {{receiveIfUnclaimed}} — though that path is largely written already, since a 
> timer can compete for the
> claim exactly as a non-claimable foreign channel does, via 
> {{winner.claim.tryCommit(future)}}.
> *A third way, for completeness.* A resettable timer channel — {{reset(long 
> millis)}} on the channel layer 1
> returns — would also solve the per-round case, and is how Go answers the same 
> caveat for {{time.After}} in a
> loop. It trades a new offer kind for a mutable channel. Worth weighing 
> against layer 2 rather than
> overlooked.
> *And one question layer 2 settles that is wider than timers.* It asks whether 
> an {{Offer}} may be something
> other than a channel operation. If it may, the same mechanism admits a 
> barrier branch — JCSP's
> {{AltingBarrier}}, a phase synchronisation that can lose a race to a channel 
> — which is the other member of
> the guard family {{groovy.concurrent}} has no port for. Layer 1 neither opens 
> that door nor closes it. If
> the answer is "offers stay channel operations", that is a perfectly good 
> answer; it is just better given
> deliberately than by default.
> h2. Open questions
> * *When does the clock start?* At creation, as in Go and crossbeam, is the 
> obvious answer for layer 1 — and
>   it is what makes a fresh timer per loop iteration the natural (and, per 
> above, lossy) spelling. Layer 2
>   and the resettable form are the two answers to that; which one lands 
> decides whether layer 1 needs a
>   {{reset}} at all.
> * *Periodic ticks.* Go has {{time.Tick}} and crossbeam {{tick(d)}} — a 
> channel that fires repeatedly. That
>   is the natural companion for "every 100ms, flush", and is probably a 
> separate issue rather than part of
>   this one.
> * *Element type.* {{Instant}} matches crossbeam and is more useful than 
> {{null}} or a token; Go sends the
>   time too.
> h2. Provenance
> Found while porting Jon Kerridge's _Using Concurrency and Parallelism 
> Effectively_ onto
> {{groovy.concurrent}} with a static verifier — the same exercise behind 
> GROOVY-12320, GROOVY-12323,
> GROOVY-12324 and GROOVY-12326. Three of the book's chapters put a timer in an 
> {{ALT}} (c05's scaling device,
> c14's hand-eye test, c17's sampling {{Sniffer}}), and none of them ports: the 
> timeout is part of the choice
> in each, not a wrapper around it. With input, output and boolean guards now 
> all present — GROOVY-12323,
> GROOVY-12324 and GROOVY-12326 respectively — the timer is the guard the 
> gallery misses most often. The
> remaining one, {{AltingBarrier}}, is noted under layer 2 above; it is not 
> asked for here, only kept in view
> so the offer question is answered once rather than twice.



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

Reply via email to