Skip to content

Refactor: propagate df.break() via a typed NodeError variant instead of a JSON sentinel #148

@pinodeca

Description

@pinodeca

Background

PR #140 fixes #132 by adding explicit is_break_signal checks inside execute_join_node and execute_race_node. That's the right point fix, but the underlying pattern is fragile and will keep producing the same class of bug.

Today, df.break() is encoded as an in-band JSON sentinel ({"__break__": true, "value": ...}) carried inside the same Result<String, String> that every node returns. Every compound node (~>, if, join, race, and any future construct like try/catch, with_timeout, compensate, …) must remember to:

  1. Call is_break_signal(&child_result) after awaiting each child.
  2. Early-return before doing its own post-processing (storing result_name, merging branch results, etc.).

The compiler offers no help. #132 happened because exactly this step was forgotten in two places. The next compound node added will likely reintroduce it.

There's also a minor collision risk: a user query returning JSON like {"__break__": true} would be misinterpreted as a break signal.

Proposal

Encode break as a typed error variant and let ? handle propagation:

enum NodeError {
    Break(String),      // unwinds until the nearest loop catches it
    Failure(String),    // real failure, propagates to orchestration result
}
type NodeResult = Result<String, NodeError>;

Then:

  • execute_break_node returns Err(NodeError::Break(value)) instead of the JSON sentinel.
  • execute_then_node, execute_join_node, execute_race_node, execute_if_node become child().await? — propagation is automatic, no explicit is_break_signal checks.
  • execute_loop_node is the only place that catches it:
    match execute_body(...).await {
        Ok(v) => { /* normal: check while-condition / continue_as_new */ }
        Err(NodeError::Break(v)) => return Ok(v),
        Err(e @ NodeError::Failure(_)) => return Err(e),
    }
  • SubtreeEnvelope gains a typed field so a break inside execute_subtree is re-raised as Err(Break(..)) in the parent rather than smuggled through the JSON result string.

Benefits

  1. The bug class disappears. Forgetting to propagate break becomes a type error — ? handles it.
  2. No sentinel collision — user payloads cannot impersonate control flow.
  3. extract_break_value / BREAK_SENTINEL JSON-munging goes away. The value travels as a plain String field on the enum (no double serialization).
  4. JOIN multi-break semantics become explicit. Today the first-in-iteration-order break silently wins; with a typed variant we can pick semantics deliberately.
  5. "df.break() outside a loop" becomes easy to detect. It surfaces as a single uncaught NodeError::Break at the orchestration boundary, which we can convert to a clean error instead of returning a JSON sentinel as the function's final result (the current behavior).

Costs

  • Mechanical refactor across every node handler signature in src/orchestrations/execute_function_graph.rs.
  • Small change to SubtreeEnvelope and the JOIN/RACE sub-orchestration boundary to carry the typed control-flow signal back to the parent.
  • Need to update unit/E2E tests that may rely on the sentinel format (currently none surface it publicly, but worth verifying).

Acceptance

  • All existing E2E tests pass, including tests/e2e/sql/22_break_in_join_race.sql from Fix df.break() silently ignored inside JOIN/RACE branches #140.
  • is_break_signal, extract_break_value, and BREAK_SENTINEL are removed.
  • New test: df.break() used at the top level of df.start() (no enclosing loop) produces a clear, actionable error rather than completing with a sentinel as the result.
  • New test: df.break() inside a nested compound (e.g., df.if(..., df.break('x'), ...) inside a JOIN inside a loop) correctly exits the outer loop.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions