Skip to content

BufferedMessageHandler breaks per-session ordering (concurrency + command ordering semantics) #165

@Andy963

Description

@Andy963

Summary

src/bub/channels/handler.py’s BufferedMessageHandler can break per-session ordering guarantees in two ways:

  1. Concurrency bug: the same session_id may have overlapping _handler(...) executions.
  2. Ordering/contract bug: ,command messages may bypass normal batching/ordering rules (depending on desired contract), leading to “later command replies before earlier normal messages”.

Both issues can corrupt session-level semantics (tape/state/output ordering).


Expected behavior (contract)

For a given session_id:

  • Turns should be processed serially: downstream self._handler(...) calls must not overlap.
  • Debounce/max-wait batching should apply only to the relevant non-command message group, without being accidentally affected by later messages.
  • Output order should be consistent with input order (at least within the same session).
  • Session-level state/tape should be updated deterministically without race conditions.

Problem 1: Overlapping processing for the same session (concurrency)

In the original implementation, _in_processing can be cleared before the awaited handler completes. While await self._handler(...) is still running, new messages may observe _in_processing is None and start another _process() task.

Impact

  • Out-of-order tape writes
  • Stale state reads / state overwrite races
  • Replies returned in unexpected order
  • Hard-to-reproduce timing-dependent failures

Typical symptom
Send message A (slow to process), then quickly send message B in the same session; B’s turn may start before A finishes.


Problem 2: Command ordering semantics (“command may cut in line”)

The command path (messages whose content.startswith(",")) may be handled differently from normal messages. If commands bypass the serialized/batched lane, they can run while a previous normal turn is still in progress, producing the common user-visible behavior:

A later ,command gets a response before an earlier normal message.

Whether this is a bug depends on the intended contract. If the intended behavior is strict arrival-order serialization for all messages, then commands must be serialized too. If commands are intentionally allowed to “cut in line”, this should be documented explicitly and downstream code must tolerate out-of-order effects.


Reproduction ideas

  1. Concurrency overlap

    • Send a slow normal message in a session.
    • Immediately send another normal message.
    • Observe overlapping handler execution (via logs) and/or out-of-order state/tape/output effects.
  2. Command cut-in

    • Send a slow normal message.
    • Immediately send a ,command.
    • If commands are not serialized, the command response may arrive first.

Root cause (high level)

The handler mixes multiple pieces of state (_event, _timer, _in_processing, pending queue) in a way that can:

  • prematurely release the “processing” guard, enabling re-entrancy, and/or
  • allow special message types (commands) to bypass ordering/batching invariants.

Suggested fix direction

Refactor BufferedMessageHandler into a single per-session worker loop with a clear invariant:

  • Exactly one worker task per session.
  • The worker processes a queue in arrival order.
  • Non-command messages are batched using debounce/max-wait without being influenced by later queue items.
  • Commands are either:
    • serialized through the same queue (strict ordering), or
    • explicitly documented as “out-of-order allowed” (if that is the intended design).

Based on my understanding, the design is likely not intended to be asynchronous. If this behavior is actually by design, please close this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions