[
https://issues.apache.org/jira/browse/GROOVY-12343?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18110944#comment-18110944
]
ASF GitHub Bot commented on GROOVY-12343:
-----------------------------------------
testlens-app[bot] commented on PR #2867:
URL: https://github.com/apache/groovy/pull/2867#issuecomment-5520250513
## π¨ TestLens detected 3 failed tests π¨
Here is what you can do:
1) Inspect the test failures carefully.
2) If you are convinced that some of the tests are flaky, you can mute them
below.
3) Finally, trigger a rerun by checking the rerun checkbox.
### Test Summary
#### [Build and test / lts \(17, windows-latest,
1\)](https://github.com/apache/groovy/actions/runs/33712847695/job/100516565254?pr=2867)
> :groovy-groovysh:test
| Test | Runs | Flakiness |
|---|---|--:|
| GroovyPosixCommandsTest >
headAndGrepAndWcStripControlCharactersFromNames\(\) | β | 19% π΄ |
| GroovyPosixCommandsTest > lsStripsControlCharactersFromFileNames\(\) | β |
19% π΄ |
| GroovyPosixCommandsTest > lsStripsControlCharactersFromSymlinkTargets\(\)
| β | 19% π΄ |
π·οΈ Commit: a3c74e466e2a25d88508bc95fafec4e4b051bb91
βΆοΈ Tests: 61223 executed
π‘ Checks: 8/29 completed
### Test Failures
<details><summary><strong>GroovyPosixCommandsTest >
headAndGrepAndWcStripControlCharactersFromNames()</strong>
(:groovy-groovysh:test in <a
href="https://github.com/apache/groovy/actions/runs/33712847695/job/100516565254?pr=2867">Build
and test / lts (17, windows-latest, 1)</a>)</summary>
```
java.nio.file.InvalidPathException: Illegal char <> at index 2:
ok[2Ktrap.txt
at
java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182)
at
java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
at
java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92)
at
java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:232)
at java.base/java.nio.file.Path.resolve(Path.java:515)
at
java.base/jdk.internal.reflect.GeneratedMethodAccessor31.invoke(Unknown Source)
at
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at
org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:391)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:298)
at
org.codehaus.groovy.vmplugin.v8.IndyInterface.invokeColdReflective(IndyInterface.java:648)
at
org.apache.groovy.groovysh.jline.GroovyPosixCommandsTest.headAndGrepAndWcStripControlCharactersFromNames(GroovyPosixCommandsTest.groovy:123)
```
</details>
<details><summary><strong>GroovyPosixCommandsTest >
lsStripsControlCharactersFromFileNames()</strong> (:groovy-groovysh:test in <a
href="https://github.com/apache/groovy/actions/runs/33712847695/job/100516565254?pr=2867">Build
and test / lts (17, windows-latest, 1)</a>)</summary>
```
java.nio.file.InvalidPathException: Illegal char <> at index 2:
ok[2Ktrap.txt
at
java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182)
at
java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
at
java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92)
at
java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:232)
at java.base/java.nio.file.Path.resolve(Path.java:515)
at
java.base/jdk.internal.reflect.GeneratedMethodAccessor31.invoke(Unknown Source)
at
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at
org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:391)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:298)
at
org.codehaus.groovy.vmplugin.v8.IndyInterface.invokeColdReflective(IndyInterface.java:648)
at
org.apache.groovy.groovysh.jline.GroovyPosixCommandsTest.lsStripsControlCharactersFromFileNames(GroovyPosixCommandsTest.groovy:97)
```
</details>
<details><summary><strong>GroovyPosixCommandsTest >
lsStripsControlCharactersFromSymlinkTargets()</strong> (:groovy-groovysh:test
in <a
href="https://github.com/apache/groovy/actions/runs/33712847695/job/100516565254?pr=2867">Build
and test / lts (17, windows-latest, 1)</a>)</summary>
```
java.nio.file.InvalidPathException: Illegal char <> at index 2:
ok[2Ktrap.txt
at
java.base/sun.nio.fs.WindowsPathParser.normalize(WindowsPathParser.java:182)
at
java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:153)
at
java.base/sun.nio.fs.WindowsPathParser.parse(WindowsPathParser.java:77)
at java.base/sun.nio.fs.WindowsPath.parse(WindowsPath.java:92)
at
java.base/sun.nio.fs.WindowsFileSystem.getPath(WindowsFileSystem.java:232)
at java.base/java.nio.file.Path.resolve(Path.java:515)
at
java.base/jdk.internal.reflect.GeneratedMethodAccessor31.invoke(Unknown Source)
at
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at
org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:391)
at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:298)
at
org.codehaus.groovy.vmplugin.v8.IndyInterface.invokeColdReflective(IndyInterface.java:648)
at
org.apache.groovy.groovysh.jline.GroovyPosixCommandsTest.lsStripsControlCharactersFromSymlinkTargets(GroovyPosixCommandsTest.groovy:108)
```
</details>
### Rerun Controls
> [!NOTE]
> Checks are currently running using the configuration below.
Select tests to mute in this pull request:
π² GroovyPosixCommandsTest >
headAndGrepAndWcStripControlCharactersFromNames\(\) <!
> 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)