Skip to content

Add non-blocking Channel.tryReceive to ReceiveChannel #307

Description

@rcardin

This was generated by AI during triage.

Problem Statement

Channel.ReceiveChannel[T] exposes only a blocking receive() (suspends until an element is available, raising ChannelClosed when drained/closed) and cancel(). There is no non-blocking variant: a caller cannot ask "give me an element if one is immediately available, otherwise tell me the channel is empty" without parking the calling fiber.

This blocks consumers that need to poll a channel — drain-what's-there loops, select/poll-style multiplexing, and the kyo-compat binding's CChannel.poll, which requires a non-blocking take.

Agent Brief

Category: enhancement
Summary: Add a non-blocking tryReceive() to ReceiveChannel, returning the next element if immediately available, None if empty-but-open, and raising ChannelClosed if drained/closed/cancelled.

Current behavior:
ReceiveChannel[T] offers receive()(using Raise[ChannelClosed]): T, which blocks (via the channel's lock/condition) until an element is available or the channel is closed/cancelled, at which point it raises ChannelClosed. There is no way to attempt a receive without suspending.

Desired behavior:
A new method on ReceiveChannel[T]:

def tryReceive()(using Raise[ChannelClosed]): Option[T]

Behavior, by channel state, with no blocking and no waiting on conditions:

State Result
An element is immediately available Some(value) (element removed)
Empty, still open None
Empty and closed (drained), or cancelled Raise(ChannelClosed)

Naming is tryReceive (not poll): it is consistent with the existing receive() and the Kotlin-coroutines lineage this Channel API follows; poll is the underlying java.util.concurrent name the API deliberately wraps. The kyo-compat CChannel.poll maps onto tryReceive at the binding layer.

Must obey the project error-handling philosophy: no thrown exceptions / require; signal closed via Raise[ChannelClosed], exactly like receive().

Per channel-type semantics (all three implementations must be covered):

  • Unbounded — take the head if present; otherwise None (or ChannelClosed if empty+closed).
  • Bounded — same, and after taking an element it must signal any sender parked on a full buffer, just as receive() does (overflow/back-pressure behavior must remain correct).
  • Rendezvous — succeeds with Some only if a sender is already parked with an item ready at the moment of the call; otherwise None (no handshake is initiated). This is the expected rendezvous caveat — document it in the Scaladoc.

Key interfaces:

  • Channel.ReceiveChannel[T] — gains tryReceive()(using Raise[ChannelClosed]): Option[T].
  • All concrete channel implementations (unbounded, bounded, rendezvous) — implement it, reusing the existing lock so it is atomic w.r.t. concurrent send/receive/close/cancel. It must not invoke the blocking await/condition path.

Acceptance criteria:

  • tryReceive() returns Some(v) when an element is immediately available and removes it, identical to what receive() would have returned.
  • tryReceive() returns None on an empty, open channel and does not block the caller.
  • tryReceive() raises ChannelClosed on an empty channel that is closed (fully drained) or cancelled.
  • On a closed channel that still has buffered elements, tryReceive() drains the remaining elements (Some) before it raises ChannelClosed — same drain-then-close ordering as receive().
  • Bounded: a successful tryReceive() unblocks a sender parked on a full buffer.
  • Rendezvous: tryReceive() returns Some only when a sender is already waiting with an item; returns None otherwise without initiating a handshake.
  • No exceptions thrown for invalid/closed states — closure is signalled only through Raise[ChannelClosed].
  • Tests added to the channel spec covering: available, empty-open, empty-closed, drain-then-close, bounded sender-unblock, rendezvous-no-partner — for the relevant channel types.
  • Scaladoc following project conventions (description, @return, the rendezvous caveat, a {{{ }}} usage example).

Out of scope:

  • A non-blocking trySend (symmetric counterpart) — separate issue if wanted.
  • Any change to the blocking receive() contract or to overflow strategies.
  • Timed/Async-aware receive variants.
  • The kyo-compat binding itself (this only provides the core primitive it will consume).

Further Notes

Motivated by the kyo-compat binding GAP analysis: CChannel.poll was the one open question in the otherwise-non-gap Channel mapping. This is an independent yaes-data primitive, decoupled from the Async.unsupervised epic (#302).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentFully specified, ready for an AFK agentyaes-dataYAES Data module

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions